2016年9月

解析器是一种超级有用的软件库。从概念上简单的说,它们的实现很有挑战性,并且在计算机科学中经常被认为是黑魔法。在这个系列的博文中,我会向你们展示为什么你不需要成为哈利波特就能够精通解析器这种魔法。但是为了以防万一带上你的魔杖吧!

我们将探索一种叫做 Ohm 的新的开源库,它使得搭建解析器很简单并且易于重用。在这个系列里,我们使用 Ohm 去识别数字,构建一个计算器等等。在这个系列的最后你将已经用不到 200 行的代码发明了一种完整的编程语言。这个强大的工具将让你能够做到一些你可能过去认为不可能的事情。

为什么解析器很困难?

解析器非常有用。在很多时候你可能需要一个解析器。或许有一种你需要处理的新的文件格式,但还没有人为它写了一个库;又或许你发现了一种古老格式的文件,但是已有的解析器不能在你的平台上构建。我已经看到这样的事发生无数次。 Code 在或者不在, Data 就在那里,不增不减。

从根本上来说,解析器很简单:只是把一个数据结构转化成另一个。所以你会不会觉得你要是邓布利多校长就好了?

解析器历来是出奇地难写,所面临的挑战是绝大多数现有的工具都很老,并且需要一定的晦涩难懂的计算机科学知识。如果你在大学里上过编译器课程,那么课本里也许还有从上世纪七十年传下来的技术。幸运的是,解析器技术从那时候起已经提高了很多。

典型的,解析器是通过使用一种叫作 形式语法 formal grammar 的特殊语法来定义你想要解析的东西来创造的,然后你需要把它放入像 BisonYacc 的工具中,这些工具能够产生一堆 C 代码,这些代码你需要修改或者链接到你实际写入的编程语言中。另外的选择是用你更喜欢的语言亲自动手写一个解析器,这很慢且很容易出错,在你能够真正使用它之前还有许多额外的工作。

想像一下,是否你关于你想要解析的东西的语法描述也是解析器?如果你能够只是直接运行这些语法,然后仅在你需要的地方增加一些 挂钩 hook 呢?那就是 Ohm 所可以做到的事。

Ohm 简介

Ohm 是一种新的解析系统。它类似于你可能已经在课本里面看到过的语法,但是它更强大,使用起来更简单。通过 Ohm, 你能够使用一种灵活的语法在一个 .ohm 文件中来写你自己的格式定义,然后使用你的宿主语言把语义加入到里面。在这篇博文里,我们将用 JavaScript 作为宿主语言。

Ohm 建立于一个为创造更简单、更灵活的解析器的多年研究基础之上。VPRI 的 STEPS program (pdf) 使用 Ohm 的前身 Ometa 为许多特殊的任务创造了专门的语言(比如一个有 400 行代码的平行制图描绘器)。

Ohm 有许多有趣的特点和符号,但是相比于全部解释它们,我认为我们只需要深入其中并构建一些东西就行了。

解析整数

让我们来解析一些数字。这看起来会很简单,只需在一个文本串中寻找毗邻的数字,但是让我们尝试去处理所有形式的数字:整数和浮点数、十六进制数和八进制数、科学计数、负数。解析数字很简单,正确解析却很难。

亲自构建这个代码将会很困难,会有很多问题,会伴随有许多特殊的情况,比如有时会相互矛盾。正则表达式或许可以做的这一点,但是它会非常丑陋而难以维护。让我们用 Ohm 来试试。

用 Ohm 构建的解析器涉及三个部分: 语法 grammar 语义 semantics 测试 tests 。我通常挑选问题的一部分为它写测试,然后构建足够的语法和语义来使测试通过。然后我再挑选问题的另一部分,增加更多的测试、更新语法和语义,从而确保所有的测试能够继续通过。即使我们有了新的强大的工具,写解析器从概念上来说依旧很复杂。测试是用一种合理的方式来构建解析器的唯一方法。现在,让我们开始工作。

我们将从整数开始。一个整数由一系列相互毗邻的数字组成。让我们把下面的内容放入一个叫做 grammar.ohm 的文件中:

CoolNums {
   // just a basic integer
   Number = digit+
}

这创造了一条匹配一个或多个数字(digit)叫作 Number 的单一规则。 意味着一个或更多,就在正则表达式中一样。当有一个或更多的数字时,这个规则将会匹配它们,如果没有数字或者有一些不是数字的东西将不会匹配。“数字(digit)”的定义是从 0 到 9 其中的一个字符。digit 也是像 Number 一样的规则,但是它是 Ohm 的其中一条构建规则因此我们不需要去定义它。如果我们想的话可以推翻它,但在这时候这没有任何意义,毕竟我们不打算去发明一种新的数。

现在,我们可以读入这个语法并用 Ohm 库来运行它。

把它放入 test1.js:

var ohm = require('ohm-js');
var fs = require('fs');
var assert = require('assert');
var grammar = ohm.grammar(fs.readFileSync('src/blog_numbers/syntax1.ohm').toString());

Ohm.grammar 调用将读入该文件并解析成一个语法对象。现在我们可以增加一些语义。把下面内容增加到你的 JavaScript 文件中:

var sem = grammar.createSemantics().addOperation('toJS', {
    Number: function(a) {
        return parseInt(this.sourceString,10);
    }
});

这通过 toJS 操作创造了一个叫作 sem 的语法集。这些语义本质上是一些对应到语法中每个规则的函数。每个函数当与之相匹配的语法规则被解析时将会被调用。上面的 Number 函数将会在语法中的 Number 规则被解析时被调用。 语法 grammar 定义了在语言中这些代码是什么, 语义 semantics 定义了当这些代码被解析时应该做什么。

语义函数能够做我们想做的任何事,比如打印出故障信息、创建对象,或者在任何子节点上递归调用 toJS。此时我们仅仅想把匹配的文本转换成真正的 JavaScript 整数。

所有的语义函数有一个内含的 this 对象,带有一些有用的属性。其 source 属性代表了输入文本中和这个节点相匹配的部分。this.sourceString 是一个匹配输入的串,调用内置在 JavaScript 中的 parseInt 函数会把这个串转换成一个数。传给 parseInt10 这个参数告诉 JavaScript 我们输入的是一个以 10 为基底(10 进制)的数。如果少了这个参数, JavaScript 也会假定以 10 为基底,但是我们把它包含在里面因为后面我们将支持以 16 为基底的数,所以使之明确比较好。

既然我们有一些语法,让我们来实际解析一些东西看一看我们的解析器是否能够工作。如何知道我们的解析器可以工作?通过测试,许多许多的测试,每一个可能的边缘情况都需要一个测试。

使用标准的断言 assert API,以下这个测试函数能够匹配一些输入并运用我们的语义把它转换成一个数,然后把这个数和我们期望的输入进行比较。

   function test(input, answer) {
     var match = grammar.match(input);
     if(match.failed()) return console.log("input failed to match " + input + match.message);     
     var result = sem(match).toJS();
     assert.deepEqual(result,answer);
     console.log('success = ', result, answer);
    }

就是如此。现在我们能够为各种不同的数写一堆测试。如果匹配失败我们的脚本将会抛出一个例外。否则就打印成功信息。让我们尝试一下,把下面这些内容加入到脚本中:

    test("123",123);
    test("999",999);
    test("abc",999);

然后用 node test1.js 运行脚本。

你的输出应该是这样:

success =  123 123
success =  999 999
input failed to match abcLine 1, col 1:
> 1 | abc
      ^
Expected a digit

真酷。正如预期的那样,前两个成功了,第三个失败了。更好的是,Ohm 自动给了我们一个很棒的错误信息指出匹配失败。

浮点数

我们的解析器工作了,但是它做的工作不是很有趣。让我们把它扩展成既能解析整数又能解析浮点数。改变 grammar.ohm 文件使它看起来像下面这样:

CoolNums {
  // just a basic integer
  Number = float | int
  int    = digit+
  float  = digit+ "." digit+
}

这把 Number 规则改变成指向一个浮点数(float)或者一个整数(int)。这个 | 代表着“或”。我们把这个读成“一个 Number 由一个浮点数或者一个整数构成。”然后整数(int)定义成 digit+,浮点数(float)定义成 digit+ 后面跟着一个句号然后再跟着另一个 digit+。这意味着在句号前和句号后都至少要有一个数字。如果一个数中没有一个句号那么它就不是一个浮点数,因此就是一个整数。

现在,让我们再次看一下我们的语义功能。由于我们现在有了新的规则所以我们需要新的功能函数:一个作为整数的,一个作为浮点数的。

var sem = grammar.createSemantics().addOperation('toJS', {
    Number: function(a) {
        return a.toJS();
    },
    int: function(a) {
        console.log("doing int", this.sourceString);
        return parseInt(this.sourceString,10);
    },
    float: function(a,b,c) {
        console.log("doing float", this.sourceString);
        return parseFloat(this.sourceString);
    }
});

这里有两件事情需要注意。首先,整数(int)、浮点数(float)和数(Number)都有相匹配的语法规则和函数。然而,针对 Number 的功能不再有任何意义。它接收子节点 a 然后返回该子节点的 toJS 结果。换句话说,Number 规则简单的返回相匹配的子规则。由于这是在 Ohm 中任何规则的默认行为,因此实际上我们不用去考虑 Number 的作用,Ohm 会替我们做好这件事。

其次,整数(int)有一个参数 a,然而浮点数有三个:abc。这是由于规则的 实参数量 arity 决定的。 实参数量 arity 意味着一个规则里面有多少参数。如果我们回过头去看语法,浮点数(float)的规则是:

  float  = digit+ "." digit+

浮点数规则通过三个部分来定义:第一个 digit+.、以及第二个 digit+。这三个部分都会作为参数传递给浮点数的功能函数。因此浮点数必须有三个参数,否则 Ohm 库会给出一个错误。在这种情况下我们不用在意参数,因为我们仅仅直接攫取了输入串,但是我们仍然需要参数列在那里来避免编译器错误。后面我们将实际使用其中一些参数。

现在我们可以为新的浮点数支持添加更多的测试。

test("123",123);
test("999",999);
//test("abc",999);
test('123.456',123.456);
test('0.123',0.123);
test('.123',0.123);

注意最后一个测试将会失败。一个浮点数必须以一个数开始,即使它就是个 0,.123 不是有效的,实际上真正的 JavaScript 语言也有相同的规则。

十六进制数

现在我们已经有了整数和浮点数,但是还有一些其它的数的语法最好可以支持:十六进制数和科学计数。十六进制数是以 16 为基底的整数。十六进制数的数字能从 0 到 9 和从 A 到 F。十六进制数经常用在计算机科学中,当用二进制数据工作时,你可以仅仅使用两个数字表示 0 到 255 的数。

在绝大多数源自 C 的编程语言(包括 JavaScript),十六进制数通过在前面加上 0x 来向编译器表明后面跟的是一个十六进制数。为了让我们的解析器支持十六进制数,我们只需要添加另一条规则。

  Number = hex | float | int
  int    = digit+
  float  = digit+ "." digit+
  hex    = "0x" hexDigit+
  hexDigit := "0".."9" | "a".."f" | "A".."F"

我实际上已经增加了两条规则。十六进制数(hex)表明它是一个 0x 后面一个或多个十六进制数字(hexDigits)的串。一个十六进制数字(hexDigit)是从 0 到 9,或从 a 到 f,或 A 到 F(包括大写和小写的情况)的一个字符。我也修改了 Number 规则来识别十六进制数作为另外一种可能的情况。现在我们只需要另一条针对十六进制数的功能规则。

    hex: function(a,b) {
        return parseInt(this.sourceString,16);
    }

注意到,在这种情况下,我们把 16 作为基底传递给 parseInt,因为我们希望 JavaScript 知道这是一个十六进制数。

我略过了一些很重要需要注意的事。hexDigit 的规则像下面这样:

  hexDigit := "0".."9" | "a".."f" | "A".."F"

注意我使用的是 := 而不是 =。在 Ohm 中,:= 是当你需要推翻一条规则的时候使用。这表明 Ohm 已经有了一条针对 hexDigit 的默认规则,就像 digitspace 等一堆其他的东西。如果我使用了 =, Ohm 将会报告一个错误。这是一个检查,从而避免我无意识的推翻一个规则。由于新的 hexDigit 规则和 Ohm 的构建规则一样,所以我们可以把它注释掉,然后让 Ohm 自己来实现它。我留下这个规则只是因为这样我们可以看到它实际上是如何进行的。

现在,我们可以添加更多的测试然后看到十六进制数真的能工作:

test('0x456',0x456);
test('0xFF',255);

科学计数

最后,让我们来支持科学计数。科学计数是针对非常大或非常小的数的,比如 1.8×10^3。在大多数编程语言中,科学计数法表示的数会写成这样:1.8e3 表示 18000,或者 1.8e-3 表示 .018。让我们增加另外一对规则来支持这个指数表示:

    float  = digit+ "." digit+ exp?
    exp    = "e" "-"? digit+

上面在浮点数规则末尾增加了一个指数(exp)规则和一个 ?? 表示没有或有一个,所以指数(exp)是可选的,但是不能超过一个。增加指数(exp)规则也改变了浮点数规则的实参数量,所以我们需要为浮点数功能增加另一个参数,即使我们不使用它。

    float: function(a,b,c,d) {
        console.log("doing float", this.sourceString);
        return parseFloat(this.sourceString);
    },

现在我们的测试可以通过了:

test('4.8e10',4.8e10);
test('4.8e-10',4.8e-10);

结论

Ohm 是构建解析器的一个很棒的工具,因为它易于上手,并且你可以递增的增加规则。Ohm 也还有其他我今天没有写到的很棒的特点,比如调试观察仪和子类化。

到目前为止,我们已经使用 Ohm 来把字符串翻译成 JavaScript 数,并且 Ohm 经常用于把一种表示方式转化成另外一种。然而,Ohm 还有更多的用途。通过放入不同的语义功能集,你可以使用 Ohm 来真正处理和计算东西。一个单独的语法可以被许多不同的语义使用,这是 Ohm 的魔法之一。

在这个系列的下一篇文章中,我将向你们展示如何像真正的计算机一样计算像 (4.85 + 5 * (238 - 68)/2) 这样的数学表达式,不仅仅是解析数。

额外的挑战:你能够扩展语法来支持八进制数吗?这些以 8 为基底的数能够只用 0 到 7 这几个数字来表示,前面加上一个数字 0 或者字母 o。看看针对下面这些测试情况是够正确。下次我将给出答案。

test('0o77',7*8+7);
test('0o23',0o23);

via: https://www.pubnub.com/blog/2016-08-30-javascript-parser-ohm-makes-creating-a-programming-language-easy/

作者:Josh Marinacci 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

今日关注

经过半年的努力开发,别名为“ 卡尔斯鲁厄 Karlsruhe ”的 GNOME 3.22 正式发布了!

“GNOME Software 可以安装和更新 Flatpak 软件包,GNOME Builder 则可以创建它们,而桌面则为沙盒化的应用提供了可移植的实现。对于核心的 GNOME 应用的改进包括 GNOME Files 中的批量改名、GNOME Photos 中的分享支持,并更新了 GNOME Software 的外观。” Matthias Clasen 在发布公告中说到。

此外,Nautilus 内置了对压缩文件的支持;GNOME Calendar 中新的周视图、支持警告、事件拖放等;重新打造的 GNOME Software 支持更新固件、重新设计的键盘设置和崭新的 GNOME Control Center 以及 dconf Editor。更多变化可以参考官方的发布备注

Application Launching Screenshot

GNU Image Manipulation Program Screenshot

Map with Directions Screenshot

Weather Screenshot

Online Account Types

图文摘要

运行在 VirtualBox 中的 Ubuntu

即将发布的 Ubuntu 16.10 默认安装的还是 Unity 7。Canonical 表示他们在努力改进 Unity 7 中的 低级图形模式 low graphics mode ,特别是当 Ubuntu 运行在虚拟机中时,低级图形模式可以减少桌面元素的动画效果,比如启动器、桌面菜单、窗口切换器等,以及减少窗口阴影、淡入淡出效果。这样可以让你运行在 VirtualBox 中的 Ubuntu 显示的更快。同样,这些改进也能提升远程的 Ubuntu 图形界面的访问速度。

如果要在 16.10 发布前就体验一下低级图形模式,因为 16.04 LTS 的 Unity 7 已经有了低级图形模式,所以你可以在 16.04 LTS 中的 ~/.config/upstart/lowgfx.conf 加入如下行:

start on starting unity7
pre-start script
    initctl set-env -g UNITY_LOW_GFX_MODE=1
end script

来提前体验一下。

Wayland & Weston 1.12.0 compositor

Wayland 显示服务器 1.12.0 和 Weston compositor 1.12.0 发布。

APT 1.3 发布

经历了 12 个开发版之后,APT 开发团队宣布发布了 APT 1.3,最值得注意的是,在“Signed-By”功能中支持多个指纹,这样可以让软件包发布更加安全。另外,也对 EDSP (External Dependency Solver Protocol) 协议规范做了多处改进。

对于许多许多开源软件的粉丝和支持者来说,LibreOffice 是 Microsoft Office 最好的替代品,在最近的一些发布版本中可以看到它明显有了巨大的改进。然而,初始启动的体验仍然距离期望有所距离。有一些方法可以缩短 LibreOffice 的启动时间并改善它的整体性能。

在下面的段落里,我将会展示一些实用性的步骤,你可以通过它们来改善 LibreOffice 的加载时间和响应能力。

1. 增加每个对象和图像缓存的内存占用

这将可以通过分配更多的内存资源给图像缓存和对象来加快程序的加载时间。

  1. 启动 LibreOffice Writer (或者 Calc)。
  2. 点击菜单栏上的 “工具 -> 选项” 或者按键盘上的快捷键“Alt + F12”。
  3. 点击 LibreOffice 下面的“内存”然后增加“用于 LibreOffice” 到 128MB。
  4. 同样的增加“每个对象的内存占用”到 20MB。
  5. 点击确定来保存你的修改。

注意:你可以根据自己机器的性能把数值设置得比建议值的高一些或低一些。最好通过亲自体验来看看什么值能够让机器达到最佳性能。

2.启用 LibreOffice 的 快速启动器 QuickStarter

如果你的机器上有足够大的内存,比如 4GB 或者更大,你可以启用“系统托盘快速启动器”,从而让 LibreOffice 的一部分保持在内存中,在打开新文件时能够快速反应。

在启用这个选择以后,你会清楚的看到在打开新文件时它的性能有了很大的提高。

  1. 通过点击“工具 -> 选项”来打开选项对话框。
  2. 在 “LibreOffice” 下面的侧边栏选择“内存”。
  3. 勾选“启用系统托盘快速启动器”复选框。
  4. 点击“确定”来保存修改。

一旦这个选项启用以后,你将会在你的系统托盘看到 LibreOffice 图标,以及可以打开任何类型的文件的选项。

3. 禁用 Java 运行环境

另一个加快 LibreOffice 加载时间和响应能力的简单方法是禁用 Java。

  1. 同时按下“Alt + F12”打开选项对话框。
  2. 在侧边栏里,选择“Libreoffice”,然后选择“高级”。
  3. 取消勾选“使用 Java 运行环境”选项。
  4. 点击“确定”来关闭对话框。

如果你只使用 Writer 和 Calc,那么关闭 Java 不会影响你正常使用,但如果你需要使用 LibreOffice Base 和一些其他的特性,那么你可能需要重新启用它。在那种情况,将会弹出一个框询问你是否希望再次打开它。

4. 减少使用撤销步骤

默认情况下,LibreOffice 允许你撤销一个文件的多达 100 个改变。绝大多数用户不需要这么多,所以在内存中保留这么多撤销步骤是对资源的巨大浪费。

我建议减少撤销步骤到 20 次以下来为其他东西释放内存,但是这个部分需要根据你自己的需求来确定。

  1. 通过点击 “工具 -> 选项”来打开选项对话框。
  2. 在 “LibreOffice” 下面的侧边栏,选择“内存”。
  3. 在“撤销”下面把步骤数目改成最适合你的值。
  4. 点击“确定”来保存修改。

假如你这些技巧为加速你的 LibreOffice 套件的加载时间提供了帮助,请在评论里告诉我们。同样,请分享你知道的任何其他技巧来给其他人带来帮助。


via: https://www.maketecheasier.com/speed-up-libreoffice/

作者:Ayo Isaiah 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Tar(Tape ARchive,磁带归档的缩写,LCTT 译注:最初设计用于将文件打包到磁带上,现在我们大都使用它来实现备份某个分区或者某些重要的目录)是类 Unix 系统中使用最广泛的命令,用于归档多个文件或目录到单个归档文件中,并且归档文件可以进一步使用 gzip 或者 bzip2 等技术进行压缩。换言之,tar 命令也可以用于备份:先是归档多个文件和目录到一个单独的 tar 文件或归档文件,然后在需要之时将 tar 文件中的文件和目录释放出来。

本文将介绍 tar 的 17 个实用示例。

tar 命令语法如下:

# tar <选项> <文件>

下面列举 tar 命令中一些常用的选项:

--delete : 从归档文件 (而非磁带) 中删除

-r, --append : 将文件追加到归档文件中

-t, --list : 列出归档文件中包含的内容

--test-label : 测试归档文件卷标并退出

-u, --update : 将已更新的文件追加到归档文件中

-x, --extract, --get : 释放归档文件中文件及目录

-C, --directory=DIR : 执行归档动作前变更工作目录到 DIR

-f, --file=ARCHIVE : 指定 (将要创建或已存在的) 归档文件名

-j, --bip2 : 对归档文件使用 bzip2 压缩

-J, --xz : 对归档文件使用 xz 压缩

-p, --preserve-permissions : 保留原文件的访问权限

-v, --verbose : 显示命令整个执行过程

-z, gzip : 对归档文件使用 gzip 压缩

注 : 在 tar 命令选项中的连接符 - 是可选的(LCTT 译注:不用 - 也没事。这在 GNU 软件里面很罕见,大概是由于 tar 命令更多受到古老的 UNIX 风格影响)。

示例 1:创建一个 tar 归档文件

现在来创建一个 tar 文件,将 /etc/ 目录和 /root/anaconda-ks.cfg 文件打包进去。

[root@linuxtechi ~]# tar -cvf myarchive.tar /etc /root/anaconda-ks.cfg

以上命令会在当前目录创建一个名为 "myarchive" 的 tar 文件,内含 /etc/ 目录和 /root/anaconda-ks.cfg 文件。

其中,-c 选项表示要创建 tar 文件,-v 选项用于输出 tar 的详细过程到屏幕上,-f 选项则是指定归档文件名称。

[root@linuxtechi ~]# ls -l myarchive.tar
-rw-r--r--. 1 root root 22947840 Sep 7 00:24 myarchive.tar
[root@linuxtechi ~]#

示例 2:列出归档文件中的内容

在 tar 命令中使用 –t 选项可以不用释放其中的文件就可以快速列出文件中包含的内容。

[root@linuxtechi ~]# tar -tvf myarchive.tar

列出 tar 文件中的指定的文件和目录。下列命令尝试查看 anaconda-ks.cfg 文件是否存在于 tar 文件中。

[root@linuxtechi ~]# tar -tvf myarchive.tar root/anaconda-ks.cfg
-rw------- root/root 953 2016-08-24 01:33 root/anaconda-ks.cfg
[root@linuxtechi ~]#

示例 3:追加文件到归档(tar)文件中

-r 选项用于向已有的 tar 文件中追加文件。下面来将 /etc/fstab 添加到 data.tar 中。

[root@linuxtechi ~]# tar -rvf data.tar /etc/fstab

注:在压缩过的 tar 文件中无法进行追加文件操作。

示例 4:从 tar 文件中释放文件以及目录

-x 选项用于释放出 tar 文件中的文件和目录。下面来释放上边创建的 tar 文件中的内容。

[root@linuxtechi ~]# tar -xvf myarchive.tar

这个命令会在当前目录中释放出 myarchive.tar 文件中的内容。

示例 5:释放 tar 文件到指定目录

假如你想要释放 tar 文件中的内容到指定的文件夹或者目录,使用 -C 选项后边加上指定的文件的路径。

[root@linuxtechi ~]# tar -xvf myarchive.tar -C /tmp/

示例 6:释放 tar 文件中的指定文件或目录

假设你只要释放 tar 文件中的 anaconda-ks.cfg 到 /tmp 目录。

语法如下:

# tar –xvf {tar-file } {file-to-be-extracted } -C {path-where-to-extract}

[root@linuxtechi tmp]# tar -xvf /root/myarchive.tar root/anaconda-ks.cfg -C /tmp/
root/anaconda-ks.cfg
[root@linuxtechi tmp]# ls -l /tmp/root/anaconda-ks.cfg
-rw-------. 1 root root 953 Aug 24 01:33 /tmp/root/anaconda-ks.cfg
[root@linuxtechi tmp]#

示例 7:创建并压缩归档文件(.tar.gz 或 .tgz)

假设我们需要打包 /etc 和 /opt 文件夹,并用 gzip 工具将其压缩。可以在 tar 命令中使用 -z 选项来实现。这种 tar 文件的扩展名可以是 .tar.gz 或者 .tgz。

[root@linuxtechi ~]# tar -zcpvf myarchive.tar.gz /etc/ /opt/

[root@linuxtechi ~]# tar -zcpvf myarchive.tgz /etc/ /opt/

示例 8:创建并压缩归档文件(.tar.bz2 或 .tbz2)

假设我们需要打包 /etc 和 /opt 文件夹,并使用 bzip2 压缩。可以在 tar 命令中使用 -j 选项来实现。这种 tar 文件的扩展名可以是 .tar.bz2 或者 .tbz。

[root@linuxtechi ~]# tar -jcpvf myarchive.tar.bz2 /etc/ /opt/

[root@linuxtechi ~]# tar -jcpvf myarchive.tbz2 /etc/ /opt/

示例 9:排除指定文件或类型后创建 tar 文件

创建 tar 文件时在 tar 命令中使用 –exclude 选项来排除指定文件或者类型。假设在创建压缩的 tar 文件时要排除 .html 文件。

[root@linuxtechi ~]# tar -zcpvf myarchive.tgz /etc/ /opt/ --exclude=*.html

示例 10:列出 .tar.gz 或 .tgz 文件中的内容

使用 -t 选项可以查看 .tar.gz 或 .tgz 文件中内容。如下:

[root@linuxtechi ~]# tar -tvf myarchive.tgz  | more
.............................................
drwxr-xr-x root/root         0 2016-09-07 08:41 etc/
-rw-r--r-- root/root       541 2016-08-24 01:23 etc/fstab
-rw------- root/root         0 2016-08-24 01:23 etc/crypttab
lrwxrwxrwx root/root         0 2016-08-24 01:23 etc/mtab -> /proc/self/mounts
-rw-r--r-- root/root       149 2016-09-07 08:41 etc/resolv.conf
drwxr-xr-x root/root         0 2016-09-06 03:55 etc/pki/
drwxr-xr-x root/root         0 2016-09-06 03:15 etc/pki/rpm-gpg/
-rw-r--r-- root/root      1690 2015-12-09 04:59 etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
-rw-r--r-- root/root      1004 2015-12-09 04:59 etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-Debug-7
-rw-r--r-- root/root      1690 2015-12-09 04:59 etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-Testing-7
-rw-r--r-- root/root      3140 2015-09-15 06:53 etc/pki/rpm-gpg/RPM-GPG-KEY-foreman
..........................................................

示例 11:列出 .tar.bz2 或 .tbz2 文件中的内容

使用 -t 选项可以查看 .tar.bz2 或 .tbz2 文件中内容。如下:

[root@linuxtechi ~]# tar -tvf myarchive.tbz2  | more
........................................................
rwxr-xr-x root/root         0 2016-08-24 01:25 etc/pki/java/
lrwxrwxrwx root/root         0 2016-08-24 01:25 etc/pki/java/cacerts -> /etc/pki/ca-trust/extracted/java/cacerts
drwxr-xr-x root/root         0 2016-09-06 02:54 etc/pki/nssdb/
-rw-r--r-- root/root     65536 2010-01-12 15:09 etc/pki/nssdb/cert8.db
-rw-r--r-- root/root      9216 2016-09-06 02:54 etc/pki/nssdb/cert9.db
-rw-r--r-- root/root     16384 2010-01-12 16:21 etc/pki/nssdb/key3.db
-rw-r--r-- root/root     11264 2016-09-06 02:54 etc/pki/nssdb/key4.db
-rw-r--r-- root/root       451 2015-10-21 09:42 etc/pki/nssdb/pkcs11.txt
-rw-r--r-- root/root     16384 2010-01-12 15:45 etc/pki/nssdb/secmod.db
drwxr-xr-x root/root         0 2016-08-24 01:26 etc/pki/CA/
drwxr-xr-x root/root         0 2015-06-29 08:48 etc/pki/CA/certs/
drwxr-xr-x root/root         0 2015-06-29 08:48 etc/pki/CA/crl/
drwxr-xr-x root/root         0 2015-06-29 08:48 etc/pki/CA/newcerts/
drwx------ root/root         0 2015-06-29 08:48 etc/pki/CA/private/
drwx------ root/root         0 2015-11-20 06:34 etc/pki/rsyslog/
drwxr-xr-x root/root         0 2016-09-06 03:44 etc/pki/pulp/
..............................................................

示例 12:解压 .tar.gz 或 .tgz 文件

使用 -x-z 选项来解压 .tar.gz 或 .tgz 文件。如下:

[root@linuxtechi ~]# tar -zxpvf myarchive.tgz -C /tmp/

以上命令将 tar 文件解压到 /tmp 目录。

注:现今的 tar 命令会在执行解压动作前自动检查文件的压缩类型,这意味着我们在使用 tar 命令是可以不用指定文件的压缩类型。如下:

[root@linuxtechi ~]# tar -xpvf myarchive.tgz -C /tmp/

示例 13:解压 .tar.bz2 或 .tbz2 文件

使用 -j-x 选项来解压 .tar.bz2 或 .tbz2 文件。如下:

[root@linuxtechi ~]# tar -jxpvf myarchive.tbz2 -C /tmp/

[root@linuxtechi ~]# tar xpvf myarchive.tbz2 -C /tmp/

示例 14:使用 tar 命令进行定时备份

总有一些实时场景需要我们对指定的文件和目录进行打包,已达到日常备份的目的。假设需要每天备份整个 /opt 目录,可以创建一个带 tar 命令的 cron 任务来完成。如下:

[root@linuxtechi ~]# tar -zcvf optbackup-$(date +%Y-%m-%d).tgz /opt/

为以上命令创建一个 cron 任务即可。

示例 15:使用 -T 及 -X 创建压缩归档文件

想像这样一个场景:把想要归档和压缩的文件及目录记录到到一个文件,然后把这个文件当做 tar 命令的传入参数来完成归档任务;而有时候则是需要排除上面提到的这个文件里面记录的特定路径后进行归档和压缩。

在 tar 命令中使用 -T 选项来指定该输入文件,使用 -X 选项来指定包含要排除的文件列表。

假设要归档 /etc、/opt、/home 目录,并排除 /etc/sysconfig/kdump 和 /etc/sysconfig/foreman 文件,可以创建 /root/tar-include 和 /root/tar-exclude 然后分别输入以下内容:

[root@linuxtechi ~]# cat /root/tar-include
/etc
/opt
/home
[root@linuxtechi ~]#
[root@linuxtechi ~]# cat /root/tar-exclude
/etc/sysconfig/kdump
/etc/sysconfig/foreman
[root@linuxtechi ~]#

运行以下命令来创建一个压缩归档文件。

[root@linuxtechi ~]# tar zcpvf mybackup-$(date +%Y-%m-%d).tgz -T /root/tar-include -X /root/tar-exclude

示例 16:查看 .tar、.tgz 和 .tbz2 文件的大小

使用如下命令来查看 (压缩) tar 文件的体积。

[root@linuxtechi ~]# tar -czf - data.tar | wc -c
427
[root@linuxtechi ~]# tar -czf - mybackup-2016-09-09.tgz | wc -c
37956009
[root@linuxtechi ~]# tar -czf - myarchive.tbz2 | wc -c
30835317
[root@linuxtechi ~]#

示例 17:分割体积庞大的 tar 文件为多份小文件

类 Unix 系统中使用 split 命令来将大体积文件分割成小体积文件。大体积的 tar 当然也可以使用这个命令来进行分割。

假设需要将 "mybackup-2016-09-09.tgz" 分割成每份 6 MB 的小文件。

Syntax :  split -b <Size-in-MB> <tar-file-name>.<extension> “prefix-name”
[root@linuxtechi ~]# split -b 6M mybackup-2016-09-09.tgz mybackup-parts

以上命令会在当前目录分割 mybackup-2016-09-09.tgz 文件成为多个 6 MB 的小文件,文件名为 mybackup-partsaa ~ mybackup-partsag。如果在要在分割文件后以数字而非字母来区分,可以在以上的 split 命令使用 -d 选项。

[root@linuxtechi ~]# ls -l mybackup-parts*
-rw-r--r--. 1 root root 6291456 Sep 10 03:05 mybackup-partsaa
-rw-r--r--. 1 root root 6291456 Sep 10 03:05 mybackup-partsab
-rw-r--r--. 1 root root 6291456 Sep 10 03:05 mybackup-partsac
-rw-r--r--. 1 root root 6291456 Sep 10 03:05 mybackup-partsad
-rw-r--r--. 1 root root 6291456 Sep 10 03:05 mybackup-partsae
-rw-r--r--. 1 root root 6291456 Sep 10 03:05 mybackup-partsaf
-rw-r--r--. 1 root root 637219  Sep 10 03:05 mybackup-partsag
[root@linuxtechi ~]#

然后通过网络将这些分割文件转移到其他服务器,就可以合并成为一个单独的 tar 文件了,如下:

[root@linuxtechi ~]# cat mybackup-partsa* > mybackup-2016-09-09.tgz
[root@linuxtechi ~]#

文毕,希望你喜欢 tar 命令的这几个不同的示例。随时评论并分享你的心得。


via: http://www.linuxtechi.com/17-tar-command-examples-in-linux/

作者:Pradeep Kumar 译者:GHLandy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

摘要:有抱负的 Linux 系统管理员和 Linux 狂热者必须知道的、最重要的、而且基础的 Linux 网络命令合集。

在 It’s FOSS 我们并非每天都谈论 Linux 的“命令行方面”。基本上,我更专注于 Linux 的桌面端。但你们读者中的一些人在内部调查(仅面向 It's FOSS newsletter 订阅者)中指出,你们也想学些命令行技巧。速查表也受大部分读者所喜欢和支持。

为此,我编辑了一个 Linux 中基础网络命令的列表。它并不是一个教你如何使用这些命令的教程,而是一个命令合集和他们的简短解释。所以,如果你已经使用过这些命令,你可以用它来快速记住命令。

你可以把这个网页添加为书签以便快速查阅,或输出一个 PDF 版本以便离线使用。

当我还是通信系统工程专业的学生的时候我就有这个 Linux 网络命令的列表了。它帮助我在计算机网络课程获得了高分。希望它也能以同样的方式帮助你。

Linux 基础网络命令列表

我在计算机网络课程上使用 FreeBSD,不过这些 UNIX 命令应该也能在 Linux 上同样工作。

连通性

  • ping <host>:发送 ICMP echo 消息(一个包)到主机。这可能会不停地发送直到你按下 Control-C。Ping 的通意味着一个包从你的机器通过 ICMP 发送出去,并在 IP 层回显。Ping 告诉你另一个主机是否在运行。
  • telnet <host> [port]:与主机在指定的端口通信。默认的 telnet 端口是 23。按 Control-] 以退出 telnet。其它一些常用的端口是:

    • 7 —— echo 端口
    • 25 —— SMTP,用于发送邮件
    • 79 —— Finger (LCTT 译注:维基百科 - Finger protocal,不过举例 Finger 恐怕不合时宜,倒不如试试 80?),提供该网络下其它用户的信息。

ARP

ARP 用于将 IP 地址转换为以太网地址。root 用户可以添加和删除 ARP 记录。当 ARP 记录被污染或者错误时,删除它们会有用。root 显式添加的 ARP 记录是永久的 —— 代理设置的也是。ARP 表保存在内核中,动态地被操作。ARP 记录会被缓存,通常在 20 分钟后失效并被删除。

  • arp -a:打印 ARP 表。
  • arp -s <ip_address> <mac_address> [pub]:添加一条记录到表中。
  • arp -a -d:删除 ARP 表中的所有记录。

路由

  • netstat -r:打印路由表。路由表保存在内核中,用于 IP 层把包路由到非本地网络。
  • route add:route 命令用于向路由表添加静态(手动指定而非动态)路由路径。所有从该 PC 到那个 IP/子网的流量都会经由指定的网关 IP。它也可以用来设置一个默认路由。例如,在 IP/子网处使用 0.0.0.0,就可以发送所有包到特定的网关。
  • routed:控制动态路由的 BSD 守护程序。开机时启动。它运行 RIP 路由协议。只有 root 用户可用。没有 root 权限你不能运行它。
  • gated:gated 是另一个使用 RIP 协议的路由守护进程。它同时支持 OSPF、EGP 和 RIP 协议。只有 root 用户可用。
  • traceroute:用于跟踪 IP 包的路由。它每次发送包时都把跳数加 1,从而使得从源地址到目的地之间的所有网关都会返回消息。
  • netstat -rnf inet:显示 IPv4 的路由表。
  • sysctl net.inet.ip.forwarding=1:启用包转发(把主机变为路由器)。
  • route add|delete [-net|-host] <destination> <gateway>:(如 route add 192.168.20.0/24 192.168.30.4)添加一条路由。
  • route flush:删除所有路由。
  • route add -net 0.0.0.0 192.168.10.2:添加一条默认路由。
  • routed -Pripv2 -Pno_rdisc -d [-s|-q]:运行 routed 守护进程,使用 RIPv2 协议,不启用 ICMP 自动发现,在前台运行,供给模式或安静模式。
  • route add 224.0.0.0/4 127.0.0.1:为本地地址定义多播路由。(LCTT 译注:原文存疑)
  • rtquery -n <host>(LCTT 译注:增加了 host 参数):查询指定主机上的 RIP 守护进程(手动更新路由表)。

其它

  • nslookup:向 DNS 服务器查询,将 IP 转为名称,或反之。例如,nslookup facebook.com 会给出 facebook.com 的 IP。
  • ftp <host> [port](LCTT 译注:原文中 water 应是笔误):传输文件到指定主机。通常可以使用 登录名 "anonymous" , 密码 "guest" 来登录。
  • rlogin -l <host>(LCTT 译注:添加了 host 参数):使用类似 telnet 的虚拟终端登录到主机。

重要文件

  • /etc/hosts:域名到 IP 地址的映射。
  • /etc/networks:网络名称到 IP 地址的映射。
  • /etc/protocols:协议名称到协议编号的映射。
  • /etc/services:TCP/UDP 服务名称到端口号的映射。

工具和网络性能分析

  • ifconfig <interface> <address> [up]:启动接口。
  • ifconfig <interface> [down|delete]:停止接口。
  • ethereal &:在后台打开 ethereal 而非前台。
  • tcpdump -i -vvv:抓取和分析包的工具。
  • netstat -w [seconds] -I [interface]:显示网络设置和统计信息。
  • udpmt -p [port] -s [bytes] target_host:发送 UDP 流量。
  • udptarget -p [port]:接收 UDP 流量。
  • tcpmt -p [port] -s [bytes] target_host:发送 TCP 流量。
  • tcptarget -p [port]:接收 TCP 流量。

交换机

  • ifconfig sl0 srcIP dstIP:配置一个串行接口(在此前先执行 slattach -l /dev/ttyd0,此后执行 sysctl net.inet.ip.forwarding=1
  • telnet 192.168.0.254:从子网中的一台主机访问交换机。
  • sh rushow running-configuration:查看当前配置。
  • configure terminal:进入配置模式。
  • exit:退出当前模式。(LCTT 译注:原文存疑)

VLAN

  • vlan n:创建一个 ID 为 n 的 VLAN。
  • no vlan N:删除 ID 为 n 的 VLAN。
  • untagged Y:添加端口 Y 到 VLAN n。
  • ifconfig vlan0 create:创建 vlan0 接口。
  • ifconfig vlan0 vlan_ID vlandev em0:把 em0 加入到 vlan0 接口(LCTT 译注:原文存疑),并设置标记为 ID。
  • ifconfig vlan0 [up]:启用虚拟接口。
  • tagged Y:为当前 VLAN 的端口 Y 添加标记帧支持。

UDP/TCP

  • socklab udp:使用 UDP 协议运行 socklab
  • sock:创建一个 UDP 套接字,等效于输入 sock udpbind
  • sendto <Socket ID> <hostname> <port #>:发送数据包。
  • recvfrom <Socket ID> <byte #>:从套接字接收数据。
  • socklab tcp:使用 TCP 协议运行 socklab
  • passive:创建一个被动模式的套接字,等效于 socklabsock tcpbindlisten
  • accept:接受进来的连接(可以在发起进来的连接之前或之后执行)。
  • connect <hostname> <port #>:等效于 socklabsock tcpbindconnect
  • close:关闭连接。
  • read <byte #>:从套接字中读取 n 字节。
  • write:(例如,write ciaowrite #10)向套接字写入 "ciao" 或 10 个字节。

NAT/防火墙

  • rm /etc/resolv.conf:禁止地址解析,保证你的过滤和防火墙规则正确工作。
  • ipnat -f file_name:将过滤规则写入文件。
  • ipnat -l:显示活动的规则列表。
  • ipnat -C -F:重新初始化规则表。
  • map em0 192.168.1.0/24 -> 195.221.227.57/32 em0:将 IP 地址映射到接口。
  • map em0 192.168.1.0/24 -> 195.221.227.57/32 portmap tcp/udp 20000:50000:带端口号的映射。
  • ipf -f file_name:将过滤规则写入文件。
  • ipf -F -a:重置规则表。
  • ipfstat -I:当与 -s 选项合用时列出活动的状态条目(LCTT 译注:原文存疑)。

希望这份基础的 Linux 网络命令合集对你有用。欢迎各种问题和建议。


via: https://itsfoss.com/basic-linux-networking-commands

作者:Abhishek Prakash 译者:bianjp 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

现在我们将要学习如何搭建 git 服务器,如何编写自定义的 Git 钩子来在特定的事件触发相应的动作(例如通知),或者是发布你的代码到一个站点。

直到现在,我们主要讨论的还是以一个使用者的身份与 Git 进行交互。这篇文章中我将讨论 Git 的管理,并且设计一个灵活的 Git 框架。你可能会觉得这听起来是 “高阶 Git 技术” 或者 “只有狂热粉才能阅读”的一句委婉的说法,但是事实是这里面的每个任务都不需要很深的知识或者其他特殊的训练,就能基本理解 Git 的工作原理,有可能需要一丁点关于 Linux 的知识。

共享 Git 服务器

创建你自己的共享 Git 服务器意外地简单,而且在很多情况下,遇到的这点麻烦是完全值得的。不仅仅是因为它保证你有权限查看自己的代码,它还可以通过扩展为 Git 的使用敞开了一扇大门,例如个人 Git 钩子、无限制的数据存储、和持续集成与分发(CI & CD)。

如果你知道如何使用 Git 和 SSH,那么你已经知道怎么创建一个 Git 服务器了。Git 的设计方式,就是让你在创建或者 clone 一个仓库的时候,就完成了一半服务器的搭建。然后允许用 SSH 访问仓库,而且任何有权限访问的人都可以使用你的仓库作为 clone 的新仓库的基础。

但是,这是一个小的 点对点环境 ad-hoc 。按照一些方案你可以创建一些带有同样的功能的设计优良的 Git 服务器,同时有更好的拓展性。

首要之事:确认你的用户们,现在的用户以及之后的用户都要考虑。如果你是唯一的用户那么没有任何改动的必要。但是如果你试图邀请其他的代码贡献者使用,那么你应该允许一个专门的分享系统用户给你的开发者们。

假定你有一个可用的服务器(如果没有,这不成问题,Git 会帮忙解决,CentOS 的 树莓派 3 是个不错的开始),然后第一步就是只允许使用 SSH 密钥认证的 SSH 登录。这比使用密码登录安全得多,因为这可以免于暴力破解,也可以通过直接删除用户密钥而禁用用户。

一旦你启用了 SSH 密钥认证,创建 gituser 用户。这是给你的所有授权的用户们的公共用户:

$ su -c 'adduser gituser'

然后切换到刚创建的 gituser 用户,创建一个 ~/.ssh 的框架,并设置好合适的权限。这很重要,如果权限设置得太开放会使自己所保护的 SSH 没有意义。

$ su - gituser
$ mkdir .ssh && chmod 700 .ssh
$ touch .ssh/authorized_keys
$ chmod 600 .ssh/authorized_keys

authorized_keys 文件里包含所有你的开发者们的 SSH 公钥,你开放权限允许他们可以在你的 Git 项目上工作。他们必须创建他们自己的 SSH 密钥对然后把他们的公钥给你。复制公钥到 gituser 用户下的 authorized_keys 文件中。例如,为一个叫 Bob 的开发者,执行以下命令:

$ cat ~/path/to/id_rsa.bob.pub >> /home/gituser/.ssh/authorized_keys

只要开发者 Bob 有私钥并且把相对应的公钥给你,Bob 就可以用 gituser 用户访问服务器。

但是,你并不是想让你的开发者们能使用服务器,即使只是以 gituser 的身份访问。你只是想给他们访问 Git 仓库的权限。因为这个特殊的原因,Git 提供了一个限制的 shell,准确的说是 git-shell。以 root 身份执行以下命令,把 git-shell 添加到你的系统中,然后设置成 gituser 用户的默认 shell。

# grep git-shell /etc/shells || su -c "echo `which git-shell` >> /etc/shells"
# su -c 'usermod -s git-shell gituser'

现在 gituser 用户只能使用 SSH 来 push 或者 pull Git 仓库,并且无法使用任何一个可以登录的 shell。你应该把你自己添加到和 gituser 一样的组中,在我们的样例服务器中这个组的名字也是 gituser

举个例子:

# usermod -a -G gituser seth

仅剩下的一步就是创建一个 Git 仓库。因为没有人能在服务器上直接与 Git 交互(也就是说,你之后不能 SSH 到服务器然后直接操作这个仓库),所以创建一个空的仓库 。如果你想使用这个放在服务器上的仓库来完成工作,你可以从它的所在处 clone 下来,然后在你的 home 目录下进行工作。

严格地讲,你不是必须创建这个空的仓库;它和一个正常的仓库一样工作。但是,一个空的仓库没有工作分支(working tree) (也就是说,使用 checkout 并没有任何分支显示)。这很重要,因为不允许远程使用者们 push 到一个有效的分支上(如果你正在 dev 分支工作然后突然有人把一些变更 push 到你的工作分支,你会有怎么样的感受?)。因为一个空的仓库可以没有有效的分支,所以这不会成为一个问题。

你可以把这个仓库放到任何你想放的地方,只要你想要放开权限给用户和用户组,让他们可以在仓库下工作。千万不要保存目录到比如说一个用户的 home 目录下,因为那里有严格的权限限制。保存到一个常规的共享地址,例如 /opt 或者 /usr/local/share

以 root 身份创建一个空的仓库:

# git init --bare /opt/jupiter.git
# chown -R gituser:gituser /opt/jupiter.git
# chmod -R 770 /opt/jupiter.git

现在任何一个用户,只要他被认证为 gituser 或者在 gituser 组中,就可以从 jupiter.git 库中读取或者写入。在本地机器尝试以下操作:

$ git clone [email protected]:/opt/jupiter.git jupiter.clone
Cloning into 'jupiter.clone'...
Warning: you appear to have cloned an empty repository.

谨记:开发者们一定要把他们的 SSH 公钥加入到 gituser 用户下的 authorized_keys 文件里,或者说,如果他们有服务器上的用户(如果你给了他们用户),那么他们的用户必须属于 gituser 用户组。

Git 钩子

运行你自己的 Git 服务器最赞的一件事之一就是可以使用 Git 钩子。Git 托管服务有时提供一个钩子类的接口,但是他们并不会给你真正的 Git 钩子来让你访问文件系统。Git 钩子是一个脚本,它将在一个 Git 过程的某些点运行;钩子可以运行在当一个仓库即将接收一个 commit 时、或者接受一个 commit 之后,或者即将接收一次 push 时,或者一次 push 之后等等。

这是一个简单的系统:任何放在 .git/hooks 目录下的脚本、使用标准的命名体系,就可按设计好的时间运行。一个脚本是否应该被运行取决于它的名字; pre-push 脚本在 push 之前运行,post-receive 脚本在接受 commit 之后运行等等。这或多或少的可以从名字上看出来。

脚本可以用任何语言写;如果在你的系统上有可以执行的脚本语言,例如输出 ‘hello world’ ,那么你就可以这个语言来写 Git 钩子脚本。Git 默认带了一些例子,但是并不有启用。

想要动手试一个?这很简单。如果你没有现成的 Git 仓库,首先创建一个 Git 仓库:

$ mkdir jupiter
$ cd jupiter
$ git init .

然后写一个 “hello world” 的 Git 钩子。因为我为了支持老旧系统而使用 tsch,所以我仍然用它作为我的脚本语言,你可以自由的使用自己喜欢的语言(Bash,Python,Ruby,Perl,Rust,Swift,Go):

$ echo "#\!/bin/tcsh" > .git/hooks/post-commit
$ echo "echo 'POST-COMMIT SCRIPT TRIGGERED'" >> ~/jupiter/.git/hooks/post-commit
$ chmod +x ~/jupiter/.git/hooks/post-commit

现在测试它的输出:

$ echo "hello world" > foo.txt
$ git add foo.txt
$ git commit -m 'first commit'
! POST-COMMIT SCRIPT TRIGGERED
[master (root-commit) c8678e0] first commit
1 file changed, 1 insertion(+)
create mode 100644 foo.txt

现在你已经实现了:你的第一个有功能的 Git 钩子。

有名的 push-to-web 钩子

Git 钩子最流行的用法就是自动 push 更改的代码到一个正在使用中的产品级 Web 服务器目录下。这是摆脱 FTP 的很好的方式,对于正在使用的产品保留完整的版本控制,整合并自动化内容的发布。

如果操作正确,网站发布工作会像以前一样很好的完成,而且在某种程度上,很精准。Git 真的好棒。我不知道谁最初想到这个主意,但是我是从 Emacs 和 Git 方面的专家,IBM 的 Bill von Hagen 那里第一次听到它的。他的文章包含关于这个过程的权威介绍:Git 改变了分布式网页开发的游戏规则

Git 变量

每一个 Git 钩子都有一系列不同的变量对应触发钩子的不同 Git 行为。你需不需要这些变量,主要取决于你写的程序。如果你只是需要一个当某人 push 代码时候的通用邮件通知,那么你就不需要什么特殊的东西,甚至也不需要编写额外的脚本,因为已经有现成的适合你的样例脚本。如果你想在邮件里查看 commit 信息和 commit 的作者,那么你的脚本就会变得相对麻烦些。

Git 钩子并不是被用户直接执行,所以要弄清楚如何收集可能会混淆的重要信息。事实上,Git 钩子脚本类似于其他的脚本,像 BASH、Python、C++ 等等一样从标准输入读取参数。不同的是,我们不会给它提供这个输入,所以,你在使用的时候,需要知道可能的输入参数。

在写 Git 钩子之前,看一下 Git 在你的项目目录下 .git/hooks 目录中提供的一些例子。举个例子,在这个 pre-push.sample 文件里,注释部分说明了如下内容:

# $1 -- 即将 push 的远程仓库的名字
# $2 -- 即将 push 的远程仓库的 URL
# 如果 push 的时候,并没有一个命名的远程仓库,那么这两个参数将会一样。
#
# 提交的信息将以下列形式按行发送给标准输入
# <local ref> <local sha1> <remote ref> <remote sha1>

并不是所有的例子都是这么清晰,而且关于钩子获取变量的文档依旧缺乏(除非你去读 Git 的源码)。但是,如果你有疑问,你可以从线上其他用户的尝试中学习,或者你只是写一些基本的脚本,比如 echo $1, $2, $3 等等。

分支检测示例

我发现,对于生产环境来说有一个共同的需求,就是需要一个只有在特定分支被修改之后,才会触发事件的钩子。以下就是如何跟踪分支的示例。

首先,Git 钩子本身是不受版本控制的。 Git 并不会跟踪它自己的钩子,因为对于钩子来说,它是 Git 的一部分,而不是你仓库的一部分。所以,Git 钩子可以监控你的 Git 服务器上的一个空仓库的 commit 记录和 push 记录,而不是你本地仓库的一部分。

我们来写一个 post-receive(也就是说,在 commit 被接受之后触发)钩子。第一步就是需要确定分支名:

#!/bin/tcsh

foreach arg ( $< )
  set argv = ( $arg )
  set refname = $1
end

这个 for 循环用来读入第一个参数 $1 ,然后循环用第二个参数 $2 去覆盖它,然后用第三个参数 $3 再这样。在 Bash 中有一个更好的方法,使用 read 命令,并且把值放入数组里。但是,这里是 tcsh,并且变量的顺序可以预测的,所以,这个方法也是可行的。

当我们有了 commit 记录的 refname,我们就能使用 Git 去找到这个分支的供人看的名字:

set branch = `git rev-parse --symbolic --abbrev-ref $refname`
echo $branch #DEBUG

然后把这个分支名和我们想要触发的事件的分支名关键字进行比较:

if ( "$branch" == "master" ) then
  echo "Branch detected: master"
  git \
    --work-tree=/path/to/where/you/want/to/copy/stuff/to \
    checkout -f $branch || echo "master fail"
else if ( "$branch" == "dev" ) then
  echo "Branch detected: dev"
  Git \
    --work-tree=/path/to/where/you/want/to/copy/stuff/to \
    checkout -f $branch || echo "dev fail"
  else
    echo "Your push was successful."
    echo "Private branch detected. No action triggered."
endif

给这个脚本分配可执行权限:

$ chmod +x ~/jupiter/.git/hooks/post-receive

现在,当一个用户提交到服务器的 master 分支,那些代码就会被复制到一个生产环境的目录,提交到 dev 分支则会被复制到另外的地方,其他分支将不会触发这些操作。

同时,创造一个 pre-commit 脚本也很简单。比如,判断一个用户是否在他们不该 push 的分支上 push 代码,或者对 commit 信息进行解析等等。

Git 钩子也可以变得复杂,而且它们因为 Git 的工作流的抽象层次不同而变得难以理解,但是它们确实是一个强大的系统,让你能够在你的 Git 基础设施上针对所有的行为进行对应的操作。如果你是一个 Git 重度用户,或者一个全职 Git 管理员,那么 Git 钩子是值得学习的,只有当你熟悉这个过程,你才能真正掌握它。

在我们这个系列下一篇也是最后一篇文章中,我们将会学习如何使用 Git 来管理非文本的二进制数据,比如音频和图片。


via: https://opensource.com/life/16/8/how-construct-your-own-git-server-part-6

作者:Seth Kenlon 译者:maywanting 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出