Shell编程实例——内务及管理任务(一)

内务及管理任务

1、批量重命名文件

**你想要重命名多个文件,但是 mv .foo .bar 并不管用。或者说,你希望能够以任意方式重命名一批文件。

我们在前面展示了一个能够修改文件扩展名的简单循环。以下是一个 for 循环的例子。

bash 复制代码
for FN in *.bad
do
    mv "${FN}" "${FN%bad}bash"
done

要是想做一些更随意的修改呢?例如,假设你正在写一本书,希望每章的文件名都遵循某种格式,但这又不符合出版社规定的格式。你可以将文件命名为chNN=Title=Author.odt,然后用简单的 for 循环以及包含 cut 的命令替换进行重命名。

bash 复制代码
for i in *.odt; do mv "$i" "$(echo "$i" | cut -d'=' -f1,3)"; done

要坚持给文件名参数加上引号,以免其中存在空格。在该解决方案中,为了清晰地分辨都有哪些参数,我们在测试代码时使用了 echo 和尖括号(set -x 也可以)​。一旦确定一切正常,就可以删除尖括号,并将 echo 替换成 mv。

bash 复制代码
# 测试
$ for i in *.odt; do echo "<$i>" "<$(echo "$i" | cut -d'=' -f1,3)>"; done
<ch01=Beginning Shell Scripting=JP.odt><ch01=JP.odt>
<ch02=Standard Output=CA.odt><ch02=CA.odt>
<ch03=Standard Input=CA.odt><ch03=CA.odt>
<ch04=Executing Commands=CA.odt><ch04=CA.odt>
[...]

# 更多测试
$ set -x

$ for i in *.odt; do echo "<$i>" "<$(echo "$i" | cut -d'=' -f1,3)>"; done
++xtrace 1: echo ch01=Beginning Shell Scripting=JP.odt
++xtrace 1: cut -d= -f1,3
+xtrace 535: echo '<ch01=Beginning Shell Scripting=JP.odt>' '<ch01=JP.odt>'
<ch01=Beginning Shell Scripting=JP.odt><ch01=JP.odt>
++xtrace 1: echo ch02=Standard Output=CA.odt
++xtrace 1: cut -d= -f1,3
+xtrace 535: echo '<ch02=Standard Output=CA.odt>' '<ch02=CA.odt>'
<ch02=Standard Output=CA.odt><ch02=CA.odt>
++xtrace 1: echo ch03=Standard Input=CA.odt
++xtrace 1: cut -d= -f1,3
+xtrace 535: echo '<ch03=Standard Input=CA.odt>' '<ch03=CA.odt>'
<ch03=Standard Input=CA.odt><ch03=CA.odt>
++xtrace 1: echo ch04=Executing Commands=CA.odt
++xtrace 1: cut -d= -f1,3
+xtrace 535: echo '<ch04=Executing Commands=CA.odt>' '<ch04=CA.odt>'
<ch04=Executing Commands=CA.odt><ch04=CA.odt>

$ set +x
+xtrace 536: set +x

本书有很多类似于这样的 for 循环,因为用起来着实方便。棘手的地方在于将正确的值插入 mv 或 cp 的参数。本例以 = 为分隔符,而且只关心第一个字段,因此非常容易。

要想找出需要的值,可以用 ls(或 find)命令列出正在处理的文件,并通过管道将其传入合适的工具链(通常是cut、awk 或 sed)​。bash 的参数扩展在这里也非常方便。

bash 复制代码
ls *.odt | cut -d'=' -f1

好在本书中有实例可以告诉你提供正确参数值的详细方法;你只需要将其插入即可。确保先用 echo 测试,注意文件名中的空格或其他怪异字符,因为它们总是会给你惹麻烦。

别将脚本命名为 rename。我们在主流的Linux 发行版中至少已经发现两个 rename 命令,肯定还有不少别的。Red Hat 的 util-linux 软件包中就有一个 rename from_string to_string file_name 工具。Debian 及其衍生版的 Perl 软件包中包含 Larry Wall1 用 Perl 编写的 rename 及相关 renameutils 实用工具包。Solaris、HP-UN 和一些 BSD 发行版中还有 rename 系统调用,尽管不太容易被最终用户访问到。可以在你的系统中尝试打开 rename 手册页,看看会发现什么。

2、在Linux中使用GUN Texinfo和info

你在访问文档时遇到了麻烦,因为 Linux 中很多 GNU 工具的文档属于 Texinfo 文档,传统的手册页仅仅是存根(stub)而已,而默认的 info 程序对用户并不友好(而且你也不想再去学习一个用途单一的程序)​。

通过管道将 info 命令的输出传给分页程序(如 less)​,不过这样会丧失 info 的链接导航特性。

bash 复制代码
info bash | less

info 基本上是 Emacs info 阅读器的独立版本,因此,如果你是 Emacs 的粉丝,可能还有意义。但是,通过管道将 info 的输出传给 less 是使用现已熟悉的命令来查看文件的快捷方法。

Texinfo 背后的理念很好:从单一来源产生各种输出格式。这算不上新鲜,很多其他标记语言也是这么做的,甚至 5.2 节中就谈及过一种。既然如此,干吗不用 TeX 过滤man 的输出?可能是因为手册页遵循的是一种标准化、结构化且经过时间考验的格式,而 Texinfo 采用的形式更为自由。

如果你不喜欢 info,还有别的 Texinfo 浏览器和转换工具,如 pinfo、info2www、tkman,甚至是 info2man(先转换成 POD,再转换成手册页格式)​。

3、批量解压ZIP文件

*你想一次性解压目录下的多个 ZIP 文件,但 unzip .zip 没有效果。

将模式放入单引号即可,不同于其他大多数 Unix 命令,unzip 自己处理文件通配模式。

bash 复制代码
unzip '*.zip'

也可以用循环来解压各个文件。

bash 复制代码
for x in /path/to/date*/name/*.zip; do unzip "$x"; done

或者:

bash 复制代码
for x in $(ls /path/to/date*/name/*.zip 2>/dev/null); do unzip $x; done

和很多 Unix 命令(如 gzip 和 bzip2)不同,unzip 的最后一个参数并不是任意长度的文件列表。在处理命令unzip *.zip 时,shell 会扩展其中的通配符,因此(假设你有若干文件,文件名分别为 zipfile1.zip 到zipfile4.zip)​,unzip *.zip 会扩展为 unzip zipfile1.zipzipfile2.zip zipfile3.zip zipfile4.zip。该命令尝试从zipfile1.zip 中提取 zipfile2.zip、zipfile3.zip、zipfile4.zip。除非 zipfile1.zip 中真的包含这些文件,否则肯定失败。

解决方案中提出的第一种方法用单引号来避免 shell 扩展通配符。但是,这仅适用于只有一个通配符的情况。后两种方法的解决思路是,在 shell 完成通配符扩展或返回 ls命令的结果后,明确地为每个 ZIP 文件运行 unzip 命令。

之所以使用 ls,是因为 bash(以及 sh)的默认行为是原封不动地返回未匹配的模式。这意味着,如果没有文件能够匹配通配符模式,你就要尝试解压名为/path/to/date*/name/*.zip 的文件。ls 会简单地向STDOUT 返回空,返回 STDERR 的错误信息则会被直接丢 弃。你可以用 shopt -s nullglob 来设置 shell 选项,使未能匹配的文件名模式扩展成空串,而不再返回自身。

4、用screen恢复断开的会话

你在 SSH 上运行了一些耗时的进程,有可能还是通过广域网运行的,如果连接断开,会丢失大量的工作成果。或者是,你上班时启动了一项漫长的工作,但现在需要回家,晚些时候才能检查工作的完成情况。这可以使用 nohup运行进程,但等到连接恢复或返回家中时,发现无法重新连接到该进程。

安装并使用 GNU screen。

screen 的用法非常简单。输入 screen 或 screen -a。-a 可以启用 screen 的所有功能,代价则是牺牲一些重绘(进而是带宽)效率。不过说实话,在使用 -a 时,我们从来没注意到有什么差别。

输入上述命令后,看起来似乎什么都没发生,但现在你已经是在一个 screen 窗口中运行了。如果一切正常,echoSHLVL 返回的数字应该会大于 1(有关 SHLVL,参见16.2 节)​。要想测试的话,输入 ls -la,然后用 kill"杀死"终端(不要干净退出,因为这样会一并退出screen)​。再次登录后输入 screen -r,重新连接到screen。如果没有恢复到断开前的状态,尝试 screen -d -r。要是还不管用,输入 ps auwx | grep screen,查看screen 是否还在运行,并在 screen 的手册页中查阅排错 信息,但一般应该可以正常运行的。

启动 screen 时加入以下内容会更容易确定随后该重新连接到哪个会话。

bash 复制代码
screen -aS "$(whoami).$(date$$'$$ $$+$$%Y-%m-%d$$_$$%H:%M:%S%z$$'$$)

要想退出 screen 及会话,需要不停地输入 exit,直到所有会话全部结束。也可以输入 Ctrl-A Ctrl-\ 或者 Ctrl-Aquit 来退出 screen 本身(假定你没有修改过 Ctrl-A 的默认辅助键)​。

以下是官方站点对 screen 的描述。

screen 是一个全屏窗口管理器,它在多个进程(通常是交互式 shell)之间多路复用物理终端。每个虚拟终端都具备 DEC VT100 终端的功能,此外还提供了 ANSI X3.64(ISO 6429)和 ISO 2022 标准中的一些控制功能(例如,插入/删除行以及多字符集支持)​。每个虚拟终端都有自己的回滚历史记录缓冲区和允许用户在窗口之间移动文本的复制粘贴机制。

这意味着你可以在一个 SSH 终端中拥有多个会话(回想一下 i286/386 上的 DeskView)​。它还允许你通过 SSH 连入计算机、启动进程、断开终端后返回家中,然后重新连接,继续先前的进程。同时能够实现可用于培训、故障排除或协作的多人共享会话​。

警告

screen 在 Linux 上通常是默认安装的,但其他系统很少如此。screen 的二进制可执行文件必须以 SUIDroot 运行,以便能够写入适合的 /usr/dev 伪终端(PTY)​。这也可能是 screen 无法正常工作的原因(要想修复,需要以 root 身份执行命令 chmod u+s/usr/bin/screen)​。

另外,screen 会干扰 zmodem 等内联传输协议。较新的 screen 版本有专门针对此的配置选项,可参见其手册页。

配置

bash 命令行编辑默认采用的 Emacs 模式使用 Ctrl-A跳转到行首,而这同样也是 screen 的命令模式或辅助键 ,如果需要大量用到 Ctrl-A(就像我们一样)​,可以考虑将下列内容添加到 ~/.screenrc 文件。

bash 复制代码
# ~/.screenrc设置样例
# 将默认的C-a更改为C-n(用C-n发送字面^N)
escape ^Nn

# 关闭烦人的铃声
vbell off

# 接收到hangup信号时自动断开
autodetach on

# 使每个窗口中的shell成为登录shell
shell -$SHELL

某些键盘上能够看到辅助键,通常位于空格键旁边,当 与其他按键组合使用时,可执行特殊功能。它源于 19 世纪 60 年代的 Lisp 计算机的键盘,并在 Sun 计算机上得以沿用(键帽表面绘制有菱形)​。在现代计算机中,Windows 键(Microsoft Windows 系统)或 ⌘ 键(Apple 系统)可以实现辅助键的传统功能。

5、共享单个bash会话

你需要共享单个 bash 会话,以作培训或故障排除之用,但是身边围观的人太多,难以开展工作。或者你需要帮助的人身在他方,需要通过网络共享会话。

在多用户模式下使用 GNU screen。以下内容假定你没有按照 4 节中描述的方法更改默认的辅助键 Ctrl-A。如果已经更改,则可以使用新的辅助键(如 Ctrl-N)​。

作为演示方,执行下列操作。

  • 输入 screen -S session_name(不能有空格),例如,screen -S training。输入 Ctrl-A addacl usernam
  • 输入 Ctrl-A addacl usernames,列出可以访问显示器的账户(以逗号分隔,不能有空格!),例如,Ctrl-A addacl alice,bob,carol。注意,这允许完全的读/写访问权限。
  • 根据需要,使用 Ctrl-A chacl usernames permbits list 命令来调整权限。
  • 用 Ctrl-A multiuser on 启用多用户模式。

作为观众,执行下列操作。

  • 用 screen -x user/name 连接共享屏幕;例如,screen -x host/training。
  • 按下 Ctrl-A k 来关闭窗口,并结束会话。

对于多用户模式,/tmp/screens 必须存在且所有用户应该皆可读取及执行。

Red Hat(RHEL3)的 screen 从版本 3.9.15-8 至 4.0.1-1都有问题,如果要用到多用户模式,不要使用这些版本。版本 4.0.2-5 或后续版本应该没问题。一旦开始使用新版本的 screen, H O M E / . s c r e e n 中的现有 s c r e e n 套接字就失效了,再也无法使用。注销所有会话,使用新版本在 / t m p / s c r e e n s / S − HOME/.screen 中的现有 screen 套接字就失效了,再也无法使用。注销所有会话,使用新版本在/tmp/screens/S- HOME/.screen中的现有screen套接字就失效了,再也无法使用。注销所有会话,使用新版本在/tmp/screens/S−USER 中创建新的套接字,然后删除$HOME/.screen 目录。

6、记录整个会话或批量作业

你需要捕获整个会话或冗长的批量作业产生的所有输出。

解决这个问题的方法有很多种,取决于你的需求和环境。

题的方法有很多种,取决于你的需求和环境。

另一种简单的解决方案是修改作业,使其自身能够记录日志,或者将所有输出重定向到 tee 或文件。例如,以下做法也许可行。

bash 复制代码
long_noisy_job >& log_file
long_noisy_job 2>&1 | tee log_file

( long_noisy_job ) >& log_file
( long_noisy_job ) 2>&1 | tee log_file

这里的问题是,你可能无法修改该作业,或者作业本身要做的事情超出了这些方法的解决能力(例如,如果需要用户输入,它会在提示符出现之前一直干等着)​。之所以会这样,是因为 STDOUT 属于缓冲型,当还要有更多数据到来时,提示符会在缓冲区中等待显示,但由于程序正等待输入,此时根本不会有数据进入。

第三种解决方案是使用有趣的专有工具 script,你的系统可能已经自带了。运行 script,它会将所有的输出全部记录在指定的日志文件(称为 typescript)中,如果你想记录整个会话,也完全没有问题,启动 script,然后运行作业即可。但如果只是想捕获部分会话,是没有办法让代码启动 script,运行并记录一段时间,然后再停止 script的。script 无法脚本化,因为一旦运行,你就被置于子shell 中了(也就是说,无法实现 script file_to_log_tosome_command_to_run 这样的操作)​。

我们给出的最终解决方案是使用终端复用器 screen。借助screen,你可以在脚本内启用或关闭整个会话的日志记录功能。运行 screen 后,能够在脚本中执行下列操作。

bash 复制代码
# 设置日志文件并启用日志记录功能
screen -X logfile /path/to/logfile && screen -X log on

# 将你的命令放在这里

# 关闭日志记录功能
screen -X logfile 1     # 设置缓冲区冲刷间隔为1秒
sleep 3                 # 等待,避免文件被截断......
screen -X log off

我们建议你依次尝试这些解决方案,哪个能先满足你的需求,就用哪个。script 应该不会有什么问题,除非你有非常特殊的需求。但为了以防万一,了解 screen 的选项会带来不少便利。

7、注销时清除屏幕

你使用或管理的一些系统在注销时不会清除屏幕,你不希望自己的工作内容露出任何蛛丝马迹,以免造成信息泄露。

将 clear 命令放入 ~/.bash_logout。

bash 复制代码
# 实例文件:bash_logout

# bash_logout:在注销时执行

# 在注销时清除屏幕,避免信息泄露(如果尚未在别处设置退出陷阱)
[ -n "$PS1" ] && clear

或者设置陷阱,在 shell 终止时执行 clear。

bash 复制代码
# 设置陷阱,在注销时清除屏幕,避免信息泄露
#(如果尚未在~/.bash_logout中设置)
trap ' [ -n "$PS1" ] && clear ' 0

注意,如果使用了远程连接且客户端配备了回滚缓冲区,那么你的工作内容可能仍旧会显示在屏幕上。clear 对shell 的命令历史记录也没有效果。

设置陷阱来清除屏幕可能有点杀鸡用牛刀了,却能够涵盖~/.bash_logout 未被执行的错误场景。如果举棋不定,可以两种方法皆用,不过真要是打算这样的话,不妨再去了解一下 TEMPEST 和法拉第笼(Faraday cages)。

如果不测试 shell 是否为交互式,某些情况下会产生下列错误。

bash 复制代码
# 例如,tput引发的错误
No value for $TERM and no -T specified

# 例如,clear引发的错误
TERM environment variable not set.

8、获取用于数据恢复的文件元数据

*你想要创建文件清单以及相关文件的详细信息(例如,核实备份、重新创建目录等)​,以作归档之用;或是要执行大批量的 chmod -R 操作,需要准备对应的回撤计划,抑或要将 /etc/ 保存在不保留权限或所有权的版本控制系统中。**

配合一些 printf 格式使用 GNU find,如下例所示。

bash 复制代码
#!/usr/bin/env bash
# 实例文件:archive_meta-data

printf "%b" "Mode\tUser\tGroup\tBytes\tModified\tFileSpec\n" > archive_file
find / \( -path /proc -o -path /mnt -o -path /tmp -o -path /var/tmp \
  -o -path /var/cache -o -path /var/spool \) -prune \
  -o -type d -printf 'd%m\t%u\t%g\t%s\t%t\t%p/\n' \
  -o -type l -printf 'l%m\t%u\t%g\t%s\t%t\t%p -> %l\n' \
  -o         -printf '%m\t%u\t%g\t%s\t%t\t%p\n' >> archive_file

注意,-printf 表达式可用于 GNU 版本的find 命令中。

(-path /proc -o -path...) -prune 用于去除各种无关的目录。-type d 表示查找类型为目录。printf 格式以 d 作为前缀,然后是八进制模式的文件权限、用户名、用户组等。-type l 表示查找符号链接并显示每个链接的指向。有了这些,再加上其他脚本,你就可以从较高层面确定是否有改动,或是重建损坏的所有权或权限。注意,这并不能取代Tripwire、AIDE、Samhain 这种主打安全的程序。

9、为多个文件创建索引

你想为多个文件创建索引。

使用 find 命令,配合 head、grep 或其他能够解析文件注释或摘要信息的命令。

例如,如果你所有的 shell 脚本的第二行都遵循"名称---描述"这种格式,那就可以按照以下方法创建一个不错的索引。

bash 复制代码
for i in $(grep -El '#![[:space:]]?/bin/sh' *); do head -2 $i | tail -1; done

如前所述,这种技术取决于每个文件都具有能够解析出来的某种摘要信息(如注释)​。然后,我们寻找一种识别文件类型(本例为 shell 脚本)的方法,并获取每个文件的第二行。

如果文件不具有易于解析的摘要信息,你可以按照以下方式尝试手动处理输出,以创建索引。

bash 复制代码
for dir in $(find . -type d); do head -15 $dir/*; done

10、使用diff和patch

你总是记不起来如何使用 diff 创建之后可能要通过 patch应用的补丁。

如果要为单个文件生成一个简单的补丁,使用:

bash 复制代码
$ diff -u original_file modified_file > your_patch
$

如果要为类似目录结构中的多个文件创建补丁,使用:

bash 复制代码
$ cp -pR original_dirs/ modified_dirs/
$

# 在此做出改动

$ diff -Nru original_dirs/ modified_dirs/ > your_comprehensive_patch
$

谨慎起见,使用 -a 选项强制 diff 将所有文件视为 ASCII 编码,并将语言和时区设置为通用默认值,如下所示。

bash 复制代码
$ LC_ALL=C TZ=UTC diff -aNru original_dirs/ modified_dirs/ \
  > > your_comprehensive_patch
$

$ LC_ALL=C TZ=UTC diff -aNru original_dirs/ modified_dirs/
diff -aNru original_dirs/changed_file modified_dirs/changed_file
--- original_dirs/changed_file 2006-11-23 01:04:07.000000000 +0000
+++ modified_dirs/changed_file 2006-11-23 01:04:35.000000000 +0000
@@ -1,2 +1,2 @@
 This file is common to both dirs.
-But it changes from one to the other.
+But it changes from 1 to the other.
diff -aNru original_dirs/only_in_mods modified_dirs/only_in_mods
--- original_dirs/only_in_mods 1970-01-01 00:00:00.000000000 +0000
+++ modified_dirs/only_in_mods 2006-11-23 01:05:58.000000000 +0000
@@ -0,0 +1,2 @@
+While this file is only in the modified dirs.
+It also has two lines, this is the last.
diff -aNru original_dirs/only_in_orig modified_dirs/only_in_orig
--- original_dirs/only_in_orig 2006-11-23 01:05:18.000000000 +0000
+++ modified_dirs/only_in_orig 1970-01-01 00:00:00.000000000 +0000
@@ -1,2 +0,0 @@
-This file is only in the original dirs.
-It has two lines, this is the last.

要想应用补丁文件,使用 cd 切换到文件目录或目录树的父目录,然后使用 patch 命令。

bash 复制代码
$ cd /path/to/files
$ patch -Np1 < your_patch

patch 的 -N 选项会忽略陈旧或已经应用过的补丁。-pnumber 会删除前导目录的 number 层,以此允许补丁创建人员与补丁应用人员之间存在目录结构上的差异。通常使用 -p1 即可;如果不行,尝试 -p0,然后再尝试 -p2,以此类推。要么奏效,要么出错并询问你要做什么,这种情况下,取消操作并尝试别的方法,除非你确实知道自己在做什么。

diff 可以产生各种形式的输出,其中一些形式要更为实用。-u 选项产生的合并输出(unified output)通常被认为是最好的,因为与 patch 一起使用时,该形式既易于阅读又颇为稳健。它提供了变更附近的 3 行上下文,这不仅方便了阅读人员定位,而且即便要打补丁的文件与创建补丁的文件不同,patch 命令也可以正常工作。只要上下文是完整的,patch 通常就能弄清楚怎么做。使用 -c 选项的上下文输出类似于 -u 选项的输出,但是更加冗余,也不怎么易于阅读。-e 选项生成的 ed 脚本适合于古老 ed 编辑器使用。最后,diff 的默认输出与 ed 输出类似,但上下文更容易理解一点。

bash 复制代码
# 合并格式(首选)
$ diff -u original_file modified_file
--- original_file       2006-11-22 19:29:07.000000000 -0500
+++ modified_file       2006-11-22 19:29:47.000000000 -0500
@@ -1,9 +1,9 @@
-This is original_file, and this line is different.
+This is modified_file, and this line is different.
 This line is the same.
 So is this one.
 And this one.
 Ditto.
-But this one is different.
+But this 1 is different.
 However, not this line.
 And this is the last same, same, same.

# 上下文格式
$ diff -c original_file modified_file
*** original_file       Wed Nov 22 19:29:07 2006
--- modified_file       Wed Nov 22 19:29:47 2006
***************
*** 1,9 ****
! This is original_file, and this line is different.
  This line is the same.
  So is this one.
  And this one.
  Ditto.
! But this one is different.
  However, not this line.
  And this is the last same, same, same.

--- 1,9 ---
! This is modified_file, and this line is different.
  This line is the same.
  So is this one.
  And this one.
  Ditto.
! But this 1 is different.
  However,


# ed格式
$ diff -e original_file modified_file
6c
But this 1 is different.
.
1c
This is modified_file, and this line is different.
.


# 正常格式
$ diff original_file modified_file
1c1
< This is original_file, and this line is different.
---
> This is modified_file, and this line is different.
6c6
< But this one is different.
---
> But this 1 is different.

diff 的 -r 和 -N 参数虽然简单,但功能强大。-r 依旧表示对目录结构执行递归操作,而 -N 会使 diff 认为在一个目录结构中找到的文件也会以空文件的形式存在于另一个目录结构中。从理论上讲,这可以起到按需创建或删除文件的效果;但在实践中,并不是所有系统(尤其 Solaris)都支持 -N,而且最终可能会留下零字节文件。有些版本的patch 默认使用 -b 选项,这会遗留下大量的 .orig 文件,有些版本(尤其 Linux)没有其他版本(尤其 BSD)那么多的输出信息。不少 diff 版本(非 Solaris)也支持 -p 选项,该选项会尝试显示补丁所影响到的 C 函数。

克制住执行 diff -u prog.c.orig prog.c 这类操作的冲动。

patch 也许还会创建 .orig 文件,因此可能会引起各种混乱。也别尝试 diff -u prog/prog.c new/prog/prog.c 这类操作,因为 patch 会对路径中不等量的目录数感到非常困惑。

值得一提的还有另一个鲜为人知的工具 wdiff。wdiff 可以比较文件以检测单词的变化,这里所说的"单词"是指两侧均为空白字符的字符串。它能够处理不同的换行符并尝试使用 termcap 字符串来产生更具可读性的输出。如果逐行比较还不够细致,这个工具就能派上用场了,它类似于 Emacs 的单词差异(word diff)特性以及 git diff --word-diff 命令。注意,默认情况下,很少有系统自带wdiff。你可以从自由软件目录或系统的软件包管理器获取。以下是 wdiff 的输出示例。

bash 复制代码
$ wdiff original_file modified_file
This is [-original_file,-] {+modified_file,+} and this line is different.
This line is the same.
So is this one.
And this one.
Ditto.
But this [-one-] {+1+} is different.
However, not this line.
And this is the last same, same, same.
$

11、统计文件间存在多少差异

你想知道两个文件之间存在多少处差异。

统计 diff 输出中有多少个 hunk(改动过数据的区域)​。

bash 复制代码
$ diff -C0 original_file modified_file | grep -c "^\*\*\*\*\*"
2

$ diff -C0 original_file modified_file
*** original_file       Fri Nov 24 12:48:35 2006
--- modified_file       Fri Nov 24 12:48:43 2006
***************
*** 1 ****
! This is original_file, and this line is different.
--- 1 ---
! This is modified_file, and this line is different.
***************
*** 6 ****
! But this one is different.
--- 6 ---
! But this 1 is different.

如果只是想知道文件是否有所差异,并不关心有多少处差异,可以使用 cmp。该命令只要找到第一处差异,就立刻退出,在面对大文件时,这种做法能够节省时间。和 diff一样,如果文件一模一样,cmp 会一声不吭;如果存在差异,则报告第一处差异的位置。

bash 复制代码
$ cmp original_file modified_file
original_file modified_file differ: char 9, line 1

hunk 其实是技术术语,尽管某些地方也称其为 chunk。注意,从理论上讲,相同的文件在不同的机器或 diff 版本上得到的结果会略有不同,因为 hunk 的数量是由 diff 所使用的算法决定的。使用不同的 diff 输出格式时,得到的答案肯定也不一样,随后的示例会演示这一点。

我们发现零上下文(zero-context)的 diff 最方便用于此目的,使用 -C0 代替 -c 可以为 grep 产生较少的搜索行。diff 的合并输出倾向于将比预期更多的变更合并成一个hunk,从而生成更少的差异报告。

bash 复制代码
$ diff -u original_file modified_file | grep -c "^@@"
1

$ diff -u original_file modified_file
--- original_file       2006-11-24 12:48:35.000000000 -0500
+++ modified_file       2006-11-24 12:48:43.000000000 -0500
@@ -1,8 +1,8 @@
-This is original_file, and this line is different.
+This is modified_file, and this line is different.
 This line is the same.
 So is this one.
 And this one.
 Ditto.
-But this one is different.
+But this 1 is different.
 However, not this line.
 And this is the last same, same, same.

使用正常的或 ed 风格的 diff 输出也没问题,只不过 grep的搜索模式会更复杂些。虽然这个示例没有展现出来,但多行变更在普通 grep 输出中可能看起来类似于 2,3c2,3,因此,相较于使用 -C0,需要用到字符类和更多的输入。

bash 复制代码
$ diff -e original_file modified_file | egrep -c '^[[:digit:],]+[[:alpha:]]+'
2

$ diff original_file modified_file | egrep -c '^[[:digit:],]+[[:alpha:]]+'
2

$ diff original_file modified_file
1c1
< This is original_file, and this line is different.
---
> This is modified_file, and this line is different.
6c6
< But this one is different.
---
> But this 1 is different.

12、删除或重命名名称中包含特殊字符的文件

你需要删除或重命名的文件在创建时使用了特殊字符,这会造成 rm 或 mv 行为异常。典型的例子就是以连字符起始的文件(如 -f 或 --help)​,这会使得要执行的命令将这种文件名视为选项。

如果文件名以连字符起始,既可以用 -- 表示命令选项到此结束,也可以使用完整路径(/tmp/-f)或相对路径(./-f)​。要是文件名中还包含其他会被 shell 解释的特殊字符,如空格或星号,则使用 shell 的引用机制。如果使用了文件名补全(默认是按 Tab 键)​,则 shell 会自动帮你引用特殊字符。另外也可以将可能造成问题的文件名放进单引号。

bash 复制代码
$ ls
--help                          this is a *crazy* file name!
$ mv --help help
mv: unknown option -- -
usage: mv [-fiv] source target
       mv [-fiv] source ... directory

$ mv -- --help my_help

$ mv this\ is\ a\ \*crazy\*\ file\ name\! this_is_a_better_name
$ ls
my_help                         this_is_a_better_name

要想弄清楚 shell 扩展后执行的究竟是什么命令,可以将echo 放在命令之前。

bash 复制代码
$ rm *
rm: unknown option -- -
usage: rm [-f|-i] [-dPRrvW] file ...

$ echo rm *
rm --help this is a *crazy* file name!

你也可以在目录中创建一个名为 -i 的文件,以免 rm * 一声不吭地就删除所有的文件。

bash 复制代码
$ mkdir del-test ; cd $_

$ > -i

$ touch important_file

$ ll
total 0
-rw-r--r-- 1 jp jp 0 Jun 12 22:28 -i
-rw-r--r-- 1 jp jp 0 Jun 12 22:28 important_file

$ rm *
rm: remove regular empty file 'important_file'? n

13、将数据追加到文件开头

你想将数据追加到现有文件的开头,例如,在排序后添加标题。

在子 shell 中使用 cat。

bash 复制代码
temp_file="temp.$RANDOM$RANDOM$$"
(echo 'static header line1'; cat data_file) > $temp_file \
  && cat $temp_file > data_file
rm $temp_file
unset temp_file

你也可以使用流编辑器 sed。追加静态文本时要注意,反斜线转义序列在 GNU sed 中会被扩展,但在其他有些版本中则不会。另外,在有些 shell 中,行尾的反斜线可能需要写成两个。

bash 复制代码
# 适用于任意版本的sed,如Solaris 10 /usr/bin/sed
$ sed -e '1i\
> static header line1
> ' data_file
static header line1
1 foo
2 bar
3 baz

$ sed -e '1i\
> static header line1\
> static header line2
> ' data_file
static header line1
static header line2
1 foo
2 bar
3 baz


# GNU sed
$ sed -e '1istatic header line1\nstatic header line2' data_file
static header line1
static header line2
1 foo
2 bar
3 baz

追加到现有文件内容之前。

bash 复制代码
$ sed -e '$r data_file' header_file
Header Line1
Header Line2
1 foo
2 bar
3 baz

这看起来是一个非爱即厌的解决方案。人们要么喜欢用cat,要么喜欢用 sed,但不会两者皆爱。cat 版本可能更快更简单;sed 版本显然更加灵活。

你也可以将 sed 脚本保存在文件中,这样就不必写成命令行了。当然,惯常的做法是将脚本输出重定向成一个新文件,就像 sed -e '$r data' header > new_file 一样,但要注意,这会改变文件的 i 节点(inode)​,也许还会改变其他的文件属性,如权限或所有权。要想保留除 i 节点之外的其他内容不变,可以使用 -i 选项进行就地编辑(in-place editing)​(如果你的 sed 版本支持)​。但不要将 -i选项与之前展示的那种在文件开头追加标题文件的方法一起使用,否则会改变标题文件!另外要注意,Perl 也有类似的 -i 选项,同样会写入新文件,但对这个例子来说,Perl 本身的工作方式与 sed 完全不同。

bash 复制代码
# 显示i节点
$ ls -i data_file
509951 data_file

$ sed -i -e '1istatic header line1\nstatic header line2' data_file

$ cat data_file
static header line1
static header line2
1 foo
2 bar
3 baz
# 验证i节点已经改变
$ ls -i data_file
509954 data_file

要想保持一切不变(或者你使用的 sed 没有 -i 选项,又或者你想使用之前介绍的文件追加方法)​:

bash 复制代码
# 显示i节点
$ ls -i data_file
509951 data_file

# $RANDOM仅适用于bash;在其他系统中可以使用mktemp
$ temp_file=$RANDOM$RANDOM

$ sed -e '$r data_file' header_file > $temp_file

# 仅当源文件存在且不为空时才使用cat!
$ [ -s "$temp_file" ] && cat $temp_file > data

$ unset temp_file

$ cat data_file
Header Line1
Header Line2
1 foo
2 bar
3 baz
# 核实i节点并未改变
$ ls -i data_file
509951 data

将标题文件追加到数据文件开头是相当违反直觉的操作。如果尝试在第一行将 header_file 文件读入 data_file 文件,则会得到下列结果。

bash 复制代码
$ sed -e '1r header_file' data_file
1 foo
Header Line1
Header Line2
2 bar
3 baz

因此,我们只需要简单地将数据追加到标题文件尾部,然后将输出写入另一个文件就行了。还是那句话,别用 sed -i,否则会改变标题文件。

另一种方法是用 cat 从 STDIN 中读取 here-document 或here-string。注意,here-string 仅在 bash 2.05b 或更高版本中可用,而且不会执行反斜线转义序列扩展,但同时也避开了所有 sed 版本存在的问题。

bash 复制代码
# 使用here-document
$ cat - data_file <<EoH
> Header line1
> Header line2
> EoH
Header line1
Header line2
1 foo
2 bar
3 baz


# 使用bash 2.05b或更高版本中的here-string,其中的反斜线转义序列不会被扩展
$ cat - data_file <<<'Header Line1'
Header Line1
1 foo
2 bar
3 baz
相关推荐
byte轻骑兵1 小时前
【BlueZ 】input 模块:蓝牙鼠标/键盘等输入设备的基础适配逻辑
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
wdfk_prog2 小时前
ROS教程10:从 gtest 到 rostest——Unit Test、Node 集成测试、rosbag 与 rqt 验证闭环
运维·缓存·docker·容器·ros
吴声子夜歌2 小时前
Shell编程实例——高级脚本编程(一)
linux·运维·网络·shell
Huangjin007_3 小时前
【Linux 系统篇(二十一)】进程(九):进程等待 wait/waitpid、status状态参数
linux·运维·服务器
螺蛳粉 螺蛳粉4 小时前
mysql的备份与恢复
linux·运维·数据库·mysql
ao-weilai4 小时前
Linux网络编程:Socket UDP
linux·服务器·网络·c++
可乐鸡翅yeah_4 小时前
混合内容 Mixed‑Content 安全策略,HLS HTTPS 页面加载 HTTP 资源排错实战
运维·ffmpeg·音视频·媒体·m3u8
爱吃香菜的初学者4 小时前
九.Linux——文件操作
linux
园长的牧歌4 小时前
Ubuntu 打开应用在导航栏没有图标是个齿轮
linux·ubuntu