中级shell工具
1、在文件中查找字符串
你需要在一个或多个文件中查找所有出现过的某个字符串。
grep 命令可以搜索文件,查找指定的字符串。
$ grep printf *.c
both.c: printf("Std Out message.\n", argv[0], argc-1);
both.c: fprintf(stderr, "Std Error message.\n", argv[0], argc-1);
good.c: printf("%s: %d args.\n", argv[0], argc-1);
somio.c: // we'll use printf to tell us what we
somio.c: printf("open: fd=%d\n", iod[i]);
$
在这个例子中,我们搜索的文件全都位于当前目录下。因此,我们只使用了简单的 shell 模式 *.c 来匹配以 .c 结束的文件,并没有在文件名前再添加路径。
但并非所有待搜索的文件都老老实实地待在当前目录下。但因为 shell 并不在意你输入多少路径名,所以我们也可以这么写:
grep printf ../lib/*.c ../server/*.c ../cmd/*.c */*.c
如果待搜索的文件不止一个,grep 会在输出前加上文件名以及冒号,然后是该文件中包含 grep 搜索内容的文本。
搜索会匹配指定的字符串,所以含有“fprintf”的行会被返回,因为“fprintf”包含“printf”。
grep 的第一个(非选项)参数可以是一个简单的字符串,正如本例中所示,也可以是更复杂的正则表达式(regexp)。正则表达式不同于 shell 的模式匹配,尽管两者有时看起来差不多。模式匹配的功能无比强大,你可能会发现自己对其的依赖已经到了开始将“grep”作为动词的程度,只想随时随地都能用到它,比如“希望能在我的书桌上 grep 到你想要的论文”。
你可以用命令行选项控制 grep 的输出形式。如果不想看到特定文件名,指定 -h 选项即可。
$ grep -h printf *.c
printf("Std Out message.\n", argv[0], argc-1);
fprintf(stderr, "Std Error message.\n", argv[0], argc-1);
printf("%s: %d args.\n", argv[0], argc-1);
// we'll use printf to tell us what we
printf("open: fd=%d\n", iod[i]);
$
如果不想看到文件中包含指定字符串的那些行,只想知道匹配到了几次,可以使用 -c 选项。
$ grep -c printf *.c
both.c:2
good.c:1
somio.c:2
$
常见的错误是忘记指定 grep 的输入,例如grep myvar。这种情况下,grep 会认为你要从STDIN 提供输入,而你以为它会读取文件,于是grep 就干等着,无所事事。(其实它是在等待着你从键盘输入。)当你要搜索大量数据,预计会花费很长时间时,很难分辨出这种尴尬的情况。
2、只显示包含搜索结果的文件名
你需要找出包含特定字符串的文件,但是不想看到其所在的文本行,只用输出文件名即可。
用 grep 的 -l 选项仅显示文件名即可。
$ grep -l printf *.c
both.c
good.c
somio.c
$
如果在一个文件中找到了多次匹配,grep 仍然只输出该文件名一次。如果没有找到匹配,则什么都不输出。
由于这些文件包含了你要查找的字符串,如果想据此构建一个待处理文件的列表,选项 -h 就能派上用场了。将grep 命令放进 $(),然后就可以在命令行上使用这些文件名了。
例如,要想删除包含字符串“This file is obsolete”的文件,可以使用以下这样的 shell 命令组合。
rm -i $(grep -l 'This file is obsolete' * )
我们给 rm 加上了 -i 选项,以便在删除每个文件前都先询问你。考虑到该命令组合的威力,这显然是一种更安全的处理方式。
bash 对 * 进行扩展,以匹配当前目录(但并不会深入子目录)下的所有文件并将其作为 grep 的参数。grep 生成包含指定字符串的文件列表。最后,rm 会删除该列表中的所有文件。
3、了解搜索是否成功
你想要知道某个文件是否包含指定的字符串,而且只关心“有”还是“没有”。
使用 grep 的 -q(quiet)选项。或者,考虑到最大的可移植性,简单将输出重定向到 /dev/null 丢弃。不管用哪种方法,你想知道的答案就在 bash 的返回状态变量 $? 中,因此可以像下面这样将其用于 if 语句:
$ if grep -q findme bigdata.file ; then echo yes ; else echo nope ; fi
nope
$
在 shell 脚本中,通常并不需要输出搜索结果;你只是想知道有没有找到匹配,以便脚本选择相应的分支。
和大多数 Unix/Linux 命令一样,返回值 0 表示命令成功执行完毕。在这个例子中,成功的意思就是在指定文件(这里只搜索了一个文件)至少找到了一处匹配。返回值保存在 shell 变量 $? 中,随后可以在 if 语句中用到。
如果我们在 grep -q 之后列出多个文件名,那么 grep 会在找到第一处匹配后就停止搜索。它并不会搜索所有文件,因为你只是想知道有没有找到匹配。要是真想搜遍所有文件,(为什么这么做?)那就别用 -q 选项了。
$ if grep findme bigdata.file > /dev/null ; then echo yes ; else echo nope ; fi
nope
$
/dev/null 是一个特殊设备(位桶),重定向到此处的输出全都会被丢弃。
如果你希望自己编写的脚本能够适用于 Unix/Linux 系统上的各种 grep 版本,即便某些版本不支持 -q 选项,/dev/null 同样能够发挥作用。
4、不区分大小写搜索
你想要在日志文件中不区分大小写地搜索字符串(如“error”),以匹配该字符串的所有出现。
用 grep 的 -i 选项忽略大小写。
grep -i error logfile.msgs
不区分大小写的搜索能够找出包含“ERROR”、“error”、“Error”的日志消息,“ErrOR”和“eRrOr”这样的也不例外。该选项在查找大小写混合的单词时尤其管用,包括可能在句首或电子邮件地址中大写的那些单词。
5、在管道中进行搜索
你要搜索的文本并不在文件中,而是在某个命令或命令管道的输出中。
将输出导入 grep 即可。
some pipeline | of commands | grep
如果没有提供文件名,grep 会从标准输入读取。大多数经过良好设计可用于 shell 脚本编程的实用工具会这么做。这也是它们能够作为 shell 脚本的构建块(buildingblock)发挥作用的原因之一。
如果还想用 grep 搜索上一个命令中的错误信息,那么要确保在管道之前将该命令的错误输出重定向到标准输出。
gcc bigbadcode.c 2>&1 | grep -i error
此命令尝试编译一些假想的代码。将输出通过管道(|)传给 grep 之前,我们先重定向命令的标准错误到标准输出(2>&1),然后由 grep 搜索字符串 error,不区分其大小写(-i)。
对 grep 的结果再执行 grep 也不是不可能的事。为什么要这么做?这是为了进一步缩小搜索范围。假设你想要找出Bob Johnson 的电子邮件地址。
$ grep -i johnson mail/*
... too much output to think about; there are lots of Johnsons in the world ...
$ !! | grep -i robert
grep -i johnson mail/* | grep -i robert
... more manageable output ...
$ !! | grep -i "the bluesman"
grep -i johnson mail/* | grep -i robert | grep -i "the bluesman"
Robert M. Johnson, The Bluesman <rmj@noplace.org>
你可以将第一个 grep 命令再输入一遍,但这个示例还展示了历史运算符 !! 的威力(参见 18.2 节)。!! 可以重复上一个命令,无须重新输入。然后就像示例那样在 !! 之后添加新的命令。shell 会显示所执行的命令,因此你会看到 !! 替换后的结果。
你可以按照这种方法简单快速地构建一个比较长的 grep管道,查看中间步骤的结果,并决定如何用其他 grep 表达式改善搜索过程。也可以用单个 grep 配合精巧的正则表达式来完成相同任务,不过我们发现以增量方式构建管道要来得更加容易。
6、缩减搜索结果
搜索返回的结果不符合预期,其中包括许多并不需要的内容。
将结果通过管道传给 grep -v 并用表达式描述出你不想看到的内容。
假设你想在日志文件中找出整个 12 月的日志消息。你知道日志文件用字母缩写 Dec 代表 12 月,但不敢肯定总是如此,为了确保找出所有的日志消息,输入下列命令:
grep -i dec logfile
得到的结果却如下所示:
...
error on Jan 01: not a decimal number
error on Feb 13: base converted to Decimal
warning on Mar 22: using only decimal numbers
error on Dec 16 : the actual message you wanted
error on Jan 01: not a decimal number
...
一种快而糙的解决方案是,将第一次得到的结果通过管道传给另一个 grep,由后者过滤掉所有的“decimal”。
grep -i dec logfile | grep -vi decimal
将多个 grep 串联在一起(因为前所未见、出乎意料的匹配会不断出现),逐步过滤搜索结果,直至满意,这种做法并不鲜见。
grep -i dec logfile | grep -vi decimal | grep -vi decimate
在这种解决方案中,“糙”是指它可能会漏掉一些你本想留下的 12 月的日志消息:如果消息中含有单词“decimal”,那么就会被 grep -v 过滤。
如果谨慎使用,-v 选项非常方便,你只需要记住该排除什么就行了。
对于这个例子,更好的解决方案是使用威力更为强大的正则表达式来匹配月份和日期,查找“Dec”以及紧随其后的一个空格和两个数字。
grep 'Dec [0-9][0-9]' logfile
不过,这种写法往往行不通,因为 syslog 会用一个空格填充单数字日期。要想解决这个问题,可以在第一个字符集中加上一个空格。
grep 'Dec [0-9 ][0-9]' logfile
由于存在内嵌空格,同时为了避免 shell 解释方括号,我们在方括号周围加上了单引号。在任何可能会对 shell 造成困惑的地方使用单引号是一个好习惯。我们可以使用反斜线转义空格,如下所示:
grep Dec\ [0-9\ ][0-9] logfile
但是这种写法不太容易看出待搜索的字符串在哪里结束,文件名从哪里开始。
7、搜索更复杂的模式
grep 中的正则表达式提供了更为强大的模式匹配功能,能够满足大部分需求。
正则表达式描述了待匹配字符串的模式。字母字符(或者对于 shell 没有特殊含义的其他字符)只匹配自身。“A”匹配 A,“B”匹配 B,这没什么好说的。另一个重要的规则是按位置组合字母,如 AB匹配“AB”。这看起来也是显而易见的。但是,正则表达式还定义了其他一些特殊字符,它们既可以单独使用,也可以与其他字符结合,从而形成更为复杂的模式。
第一个特殊字符是点号(.),它可以匹配任意单个字符。因此,… 可以匹配任意 4 个字符;A. 匹配“A”以及紧随其后的任意单个字符;.A. 匹配任意单个字符,然后是“A”,接着是任意单个字符(未必和匹配到的第一个字符相同)。
星号()匹配上一个字符的 0 次或多次出现,因此,A匹配 0 个或多个“A”字符,.* 匹配 0 个或多个任意字符(如“abcdefg”、“aaaabc”、“sdfgf ;lkjhj”,甚至是空行)。
那么 …* 是什么意思?它匹配任意单个字符以及紧随其后的0 个或多个任意字符(也就是一个或多个字符,但不能是空行)。
说到文本行,脱字符 ^ 匹配文本行的行首位置,而美元符号 $ 匹配文本行的行尾位置,因此 ^$ 匹配空行(行首紧接行尾,两者之间什么都没有)。
如果想要匹配字面上的点号、脱字符、美元符号或者其他特殊字符,该怎么办呢?在特殊字符前加上反斜线(\)即可。ion. 匹配字母“ion”以及紧随其后的任意单个字符,但是 ion. 则匹配“ion”以及点号(例如,在句末或者其他以尾部点号出现的地方)。
方括号中的一组字符(如 [abc])匹配其中任意某个字符(如“a”、“b”或“c”)。如果方括号内的第一个字符是脱字符,则匹配的是不在该字符组中的任意字符。
举例来说,[AaEeIiOoUu] 匹配任意的元音字母,[^AaEeIiOoUu] 则匹配元音字母以外的任意字符。后一种情况不同于匹配辅音字母,因为 [^AaEeIiOoUu] 也能匹配标点符号和其他既非元音也非辅音的特殊字符。
另一个要介绍的是被称为“区间表达式”的重复机制,写作 {n,m},其中,n 是重复的最小次数,m 是重复的最大次数。如果写作 {n},则表示“只重复 n 次”;写作 {n,},则表示“至少重复 n 次”。
例如,正则表达式 A{5} 匹配连续 5 个“A”,而 A{5,}至少匹配连续 5 个“A”。
8、搜索SSN
你需要一个能够匹配社会保险号(social securitynumber,SSN)的正则表达式。
美国的社会保险号由 9 位数字组成,通常划分为 3 部分:先是 3 位数字,然后是 2 位数字,最后是 4 位数字(如123-45-6789)。有时候也可以不使用连字符,因此要在正则表达式中令连字符成为可选字符。
grep '[0-9]\{3\}-\{0,1\}[0-9]\{2\}-\{0,1\}[0-9]\{4\}' datafile
这种正则表达式经常被戏称为只写表达式,意思是很难或压根无法阅读。这里我们将其一一分解来帮助你理解。一般而言,只要是涉及正则表达式的 bash 脚本,一定要在正则表达式附近加上注释,解释清楚要匹配的内容。
在正则表达式中加入一些额外的空格能够改善可读性,让视觉效果更加轻松,但这同时也会改变正则表达式的含义,变成了需要在这些位置上匹配空格字符。暂时忽略这一点,先在其中插入一些空格,方便阅读。
[0-9]\{3\} -\{0,1\} [0-9]\{2\} -\{0,1\} [0-9]\{4\}
第一部分匹配 3 个任意数字。第二部分匹配 0 次或 1 次连字符。第三部分匹配 2 个任意数字。第四部分匹配 0 次或1 次连字符。最后一部分匹配 4 个任意数字。
9、搜索压缩文件
你需要搜索压缩文件。是不是非得先解压缩?
如果系统中提供了 zgrep、zcat 或 gzcat,那就不必了。
zgrep 就是能够理解各类压缩文件和未压缩文件(具体类型视系统而定)的 grep。通常来说,在 Linux 系统中搜索syslog 消息时就得用上它,因为日志轮替机制不会压缩当前日志文件(这样才不会影响正常使用),但会用 gzip压缩归档日志。
zgrep 'search term' /var/log/messages*
zcat 就是能够理解各类压缩文件和未压缩文件(具体类型视系统而定)的 cat。它比 zgrep 理解的格式还要多,而且很多系统已经默认安装了。zcat 还可用于恢复受损的压缩文件,因为它会尽可能输出所有内容,而不像 gunzip或其他工具那样出错。
zcat /var/log/messages.1.gz
gzcat 类似于 zcat,二者的区别在于商业软件与自由软件之分,以及向后兼容性。
10、保留部分输出
你需要用某种方法保留部分输出,丢弃其余输出。
以下代码会打印出所有输入行的第一个字段。
awk '{print $1}' myinput.file
字段之间以空白字符分隔。实用工具 awk 从命令行上指定的文件中读取数据,如果没有指定文件,则从标准输入读取。因此,你也可以将输入重定向到文件,如下所示:
awk '{print $1}' < myinput.file
或者通过管道传入:
cat myinput.file | awk '{print $1}'
awk 的用法多变。最简单的用法就是从输入中打印出所选的一个或多个字段。
字段之间以空白字符分隔(也可以用 -F 选项指定分隔字符),编号从 1 开始。字段 $0 代表整个输入行。
awk 是一门完备的编程语言。awk 脚本可以变得极为复杂。这个实例仅仅是个开始而已。
11、仅保留部分输入行
你只想保留部分输入行,例如第一个和最后一个字段。举例来说,你希望 ls 只列出文件名和权限,不需要 ls -l 所提供的其他信息。可惜的是,ls 并没有相应的选项能够按照这种方式限制输出。
可以通过管道将 ls 的输出传给 awk,并从中挑选出你需要的字段。
$ ls -l | awk '{print $1, $NF}'
total 151130
-rw-r--r-- add.1
drwxr-xr-x art
drwxr-xr-x bin
-rw-r--r-- BuddyIcon.png
drwxr-xr-x CDs
drwxr-xr-x downloads
drwxr-sr-x eclipse
...
$
思考 ls -l 命令的输出。其形式如下所示:
drwxr-xr-x 2 username group 176 2006-10-28 20:09 bin
对于 awk 而言,解析这种输出易如反掌(在 awk 中,默认的字段分隔符为空白字符)。
在输出文件名时,我们用了点小技巧。在 awk 中,各种字段是用美元符号和字段编号来引用的(如$1、$2、$3),而且 awk 还有一个内建变量 NF,其中保存着当前行中的字段总数,$NF 总是引用最后一个字段。(例如,ls 的输出行共有 8 个字段,因此变量 NF 的值就是 8,$NF 指向的就是输入行中的第 8 个字段,在这个例子中就是文件名)。
记住,读取 awk 变量时不需要使用 $(这一点和 bash 变量不同)。NF 本身就是一个有效的变量引用。在其之前加上 $ 就将其含义从“当前行的字段总数”改成了“当前行的最后一个字段”。
12、颠倒每行的单词
你想按照逆序输出输入行中的单词。
$ awk '{
> for (i=NF; i>=0; i--) {
> printf "%s ", $i;
> }
> printf "\n"
> }' <filename>
字符 > 不用你输入,shell 会输出该字符来提醒你还没有敲完命令(shell 在查找能配对的单引号)。由于 awk 程序位于单引号中,因此 bash shell 允许我们输入多行代码,同时使用 > 作为辅助提示符,直到我们给出与先前匹配的结束单引号。考虑到可读性,我们在程序中加入了空白字符,不过也完全可以写成一行。
$ awk '{for (i=NF; i>=0; i--) {printf "%s ", $i;} printf "\n" }'<filename>
awk 语言的 for 循环语法和 C 语言中的非常相似。前者甚至还支持用 printf 进行格式化输出,这自然也是从 C 语言(以及 bash)中学来的。我们用 for 循环从最后一个字段开始倒着处理到第一个字段,同时输出每个字段的内容。
我们特意没有在第一个 printf 处加上 \n,这是因为想将多个字段输出在同一行中。for 循环结束后,输出换行符来结束输出行。
对于 $i 的引用,awk 与 bash 的含义大相径庭。在 bash中,$i 表示要获取保存在变量 i 中的值。但在 awk 中,和大多数编程语言一样,要想引用变量 i 的值,写作 i 即可。那么 $i 在 awk 中是什么意思呢?变量 i 的值被解析成一个数字,然后“美元符号–数字”的表达式会被理解为引用某个输入字段(或单词),即第 i 个字段。因此,随着 i 从最后一个字段倒计到第一个字段,for 循环也就以逆序的方式输出了各个字段的内容。
13、汇总数字列表
你需要汇总数字列表,其中有些数字并未出现在行中。
用 awk 先过滤出待汇总的字段,然后再做汇总。这里我们要对 ls -l 命令输出的文件大小进行汇总。
ls -l | awk '{sum += $5}; END {print sum}'
我们要汇总 ls -l 输出的第 5 个字段。ls -l 的输出如下所示:
-rw-r--r-- 1 albing users 267 2005-09-26 21:26 lilmax
各个字段分别为:权限、链接、所有者、所属组、大小(以字节为单位)、最后一次修改日期、最后一次修改时间,以及文件名。我们只对文件大小感兴趣,因此在 awk程序中用 $5 来引用该字段。
我们在花括号({})里放置了两段 awk 代码,注意,awk程序中可以有多个代码段(或代码块)。前有关键词 END的代码块仅在程序其他部分完成后运行一次。同样,你可以在代码块前添加 BEGIN 关键词并提供读取输入前运行的代码。BEGIN 代码块可用于初始化变量,这里我们用它来初始化 sum,不过 awk 能够确保变量内容一开始为空。
如果查看 ls -l 命令的输出,你会注意到第一行是文件总量信息,与其他行的预期格式不一致。
我们有两种处理方法。第一种方法是,假装该行不存在,之前的解决方案用到的就是这个方法。因为用不着的这行并没有第 5 个字段,所以 $5 为空,汇总结果不会受到影响。
更负责的做法,即第二种方法是跳过这一行。将 ls -l 命令的输出传给 awk 之前,先使用 grep。
ls -l | grep -v '^total' | awk '{sum += $5}; END {print sum}'
或者在 awk 中完成类似操作。
ls -l | awk '/^total/{next} {sum += $5}; END {print sum}'
^total 是一个正则表达式,意思是“位于行首的字母 t-o-t-a-l”(开头的 ^ 将搜索锚定在行首)。对于匹配该正则表达式的输入行,执行与之关联的代码块。第二个代码块(sum)前面什么都没有,这意味着 awk 会对所有输入行执行该代码块(不管是否匹配正则表达式)。
针对“total”进行特殊处理的目的是想要将其从汇总过程中排除。因此,在 ^total 对应的代码块中,我们使用了next 命令,它会停止处理该输入行,并从下一行输入重新开始。因为下一行并不是以“total”起始,所以 awk 将对其执行第二个代码块。我们也可以用 getline 代替 next命令。getline 并不会重新匹配开头的模式,而是继续往后匹配。注意,在 awk 编程中,代码块的顺序很重要。
14、用awk统计字符串出现次数
你需要统计多个不同字符串出现的次数,其中包括一些事先不知道具体是什么的字符串。也就是说,你要统计的并不是已经确定好的一组字符串的出现次数。相反,你会在数据中碰到一些未知字符串并对其进行统计。
可以用 awk 的关联数组(其他语言中也称为散列或字典)来搞定。
在这个例子中,我们要统计系统中各个用户都拥有多少文件。ls -l 输出的第三个字段就是用户名,因此使用该字段($3)作为数组索引,并增加数组元素。
#!/usr/bin/awk -f
# 实例文件:asar.awk
# Awk关联数组
# 用法:ls -lR /usr/local | asar.awk
NF > 7 {
user[$3]++
}
END {
for (i in user) {
printf "%s owns %d files\n", i, user[i]
}
}
这里我们调用 awk 的方式有点不一样。由于这个 awk 脚本有些复杂,我们将它放在了单独的文件中。-f 选项告诉awk 从文件中读取脚本,不过也可以在脚本中使用shebang 行 3#!/usr/bin/awk。
$ ls -lR /usr/local | awk -f asar.awk
bin owns 68 files
albing owns 1801 files
root owns 13755 files
man owns 11491 files
$
我们用条件 NF > 7 来限制执行部分 awk 脚本,以此过滤不包含文件名的那些行,此类行出现在 ls -lR 的输出中是为了提高可读性,其中包括用于分隔不同目录的空行以及每个子目录中的文件总量信息。这种行包含的字段(单词)不多。左花括号前的 NF > 7 并没有出现在斜线中,这表明它并非正则表达式。它其实就是一个条件表达式,和if 语句中使用的差不多,用于判断条件的真假。NF 是一个特殊的内建变量,其中保存着当前输入行的字段总数。因此,如果某个输入行的字段(单词)数超过了 7 个,则由花括号中的语句进行处理。
最重要的是下面这行。
user[$3]++
这里用户名(如 bin)被用作数组索引。之所以称为关联数组,是因为使用散列表(或类似机制)将每个独一的字符串与数值关联了起来。这些事情已经由 awk 在幕后帮你做好了;字符串比较或查找之类的都不用你操心。
建立好关联数组之后,似乎不那么容易获得数组元素的值了。为此,awk 提供了一种特殊形式的 for 循环。与数字形式的 for(i=0; i<max; i++) 不同,它有专用于关联数组的语法。
for (i in user)
在该语句中,变量 i 连续从数组 user 的索引中获得值(没有什么特定顺序)。在示例中,这意味着 i 在每次循环中都会得到一个值(bin、albing、root、man)。如果以前还没见识过关联数组,希望这次能让你大开眼界,印象深刻。关联数组是 awk(以及 Perl)的一个强大特性。
15、用bash统计字符串出现次数
你需要统计多个字符串出现的次数,其中包括一些事先不知道具体是什么的字符串。也就是说,你要统计的并不是已经确定好的一组字符串的出现次数。相反,你会在数据中碰到一些未知字符串并需要对其进行统计。
如果使用的是 bash 4.0 或更高版本,那么可以用关联数组(其他语言中也称为散列或字典)来搞定。
在这个例子中,我们要统计系统中的各个用户都拥有多少文件。ls -l 输出的第 3 个字段就是用户名,因此我们用该字段($3)作为数组索引,并增加数组元素。
# 实例文件:cnt_owner
# 使用bash统计文件所有者
# 将“ls -l”的输出通过管道传入该脚本
declare -A AACOUNT
while read -a LSL
do
# 只考虑包含7个及以上字段的行
if (( ${#LSL[*]} > 7 )) # 数组大小
then
NDX=${LSL[3]} # 字符串赋值
(( AACOUNT[${NDX}] += 1 )) # 算术递增
fi
done
for VALS in "${!AACOUNT[@]}" # 各个元素的索引
do
echo $VALS "owns" ${AACOUNT[$VALS]} "files"
done
我们可以像下面这样调用该脚本并获得结果。
$ ls -lR /usr/local | bash cnt_owner.sh
bin owns 68 files
root owns 13755 files
man owns 11491 files
albing owns 1801 files
$
read -a LSL 每次读取一行,将其中的各个单词(由空白字符分隔)分别赋给数组 LSL 的各元素。通过检查数组大小,我们可以确定读入了多少个单词,以此过滤不包含文件名的那些行。它们属于 ls -lR 输出的一部分,通常是为了提高可读性,因为其中包含分隔不同目录的空行和各个子目录文件总量的统计。对于我们的脚本而言,这都是些无用信息,好在这种行并不像其他行那样包含太多的字段(单词),不难分辨。
只有那些至少有 7 个单词的行,我们才从中提取第 3 个单词(文件所有者)并将其作为关联数组的索引。像 LSL 这样的标准数组,数组元素通过整数索引来引用。对于关联数组,索引可以是字符串。
为了输出结果,我们需要遍历数组索引。${AACOUNT[@]} 会生成数组所有元素值的列表,如果加上惊叹号变成 ${!AACOUNT[@]},则得到的是该数组所有索引的列表。
注意,输出并不遵循特定的顺序(这与散列算法的实现有关)。要想按照用户名或文件数量排序,可以通过管道将结果传给 sort 命令。
16、用便捷的直方图展示数据
你需要为一些数据生成快捷的屏幕直方图。
使用 14 节讨论过的 awk 关联数组(参见下例)。
#!/usr/bin/awk -f
# 实例文件:hist.awk
# 用Awk生成直方图
# 用法:ls -lR /usr/local | hist.awk
function max(arr, big)
{
big = 0;
for (i in user)
{
if (user[i] > big) { big=user[i];}
}
return big
}
NF > 7 {
user[$3]++
}
END {
# 进行缩放
maxm = max(user);
for (i in user) {
#printf "%s owns %d files\n", i, user[i]
scaled = 60 * user[i] / maxm ;
printf "%-10.10s [%8d]:", i, user[i]
for (i=0; i<scaled; i++) {
printf "#";
}
printf "\n";
}
}
用 14 节中的输入来运行该脚本,可以得到:
$ ls -lR /usr/local | awk -f hist.awk
bin [ 68]:#
albing [ 1801]:#######
root [ 13755]:##################################################
man [ 11491]:##########################################
$
我们可以将 max 对应的那部分代码放进 END 代码块的开头,但这里是想告诉你也可以在 awk 中定义函数。这里用到了一个比较复杂的 printf 语句。字符串格式%-10.10s 会将字符串左对齐并填充至 10 个字符宽,同时限制长度为 10 个字符,多出的字符会被全部截断。整数格式 %8d 确保输出的整数宽度为 8 个字符。通过使用相同的屏幕空间(无论用户名的长度或整数大小是多少),可以确保每个直方图的起始位置相同
与 awk 中的所有算术运算一样,缩放计算使用浮点数完成,除非调用内建的 int() 函数来明确截断结果。我们没有这么做,这意味着 for 循环至少会执行一次,这样一来,最少量的数据也会显示一个 # 标记。
for(i in user) 循环返回的数据并不遵循特定顺序,可能是基于底层散列表的某些便利排序(convenientordering)。要想按照文件数量或用户名排序显示直方 图,那就得加入一些排序处理。一种方法是将脚本拆成两部分,将第一部分的输出发送给 sort 命令,然后通过管道将排序后的结果传给第二部分,以输出直方图。
17、用bash轻松实现直方图
你想在不借助外部程序的情况下用 bash 计算并绘制直方图。有没有可能?
当然没问题,这多亏了关联数组。bash 4.0 及更高版本都可使用。可以将15 节中的代码作为基础,唯一的不同在于输出部分。首先,遍历所有的数组元素值来找出其中的最大值,然后以此对直方图输出进行缩放以适应页面大小。
BIG=0
for VALS in "${!UCOUNT[@]}"
do
if (( UCOUNT[$VALS] > BIG )) ; then BIG=${UCOUNT[$VALS]} ; fi
done
有了最大值(保存在 BIG 中)后,为每个数组元素输出一行。
#
# 输出直方图
#
for VALS in "${!UCOUNT[@]}"
do
printf "%-9.9s [%7d]:" $VALS ${UCOUNT[$VALS]}
# 缩放至最大值(BIG);注:整数
SCALED=$(( ( (59 * UCOUNT[$VALS]) / BIG) +1 ))
for ((i=0; i<SCALED; i++)) {
printf "#"
}
printf "\n"
done
与 5 节中一样,${!UCOUNT[@]} 至关重要。可以通过它得到数组的索引值列表(这个示例为数组UCOUNT)。for 循环每次获得一个值,并用它作为数组索引来获得该用户的文件数量。
我们将直方图缩放至 59 个字符宽度,然后再多加 1 个字符宽度,这是为了令任何非 0 值 4 都能在直方图上至少生成一个标记。这在 awk 版本(参见 16 节)中不是什么问题,因为 awk 使用的是浮点运算,但 bash 版本使用的是整数运算,执行除法后,过小的值会变成 0。
18、显示匹配短语之后的文本段落
你要在文档中搜索某个短语,并希望找到该短语后还能显示之后的文本段落。
假设在一个简单的文本文件中,段落就是空行之间的所有文本,因此空行的出现就意味着段落的结束。有鉴于此,我们来看看下面这个短小精悍的 awk 脚本。
$ cat para.awk
/keyphrase/ { flag=1 }
flag == 1 { print }
/^$/ { flag=0 }
$ awk -f para.awk < searchthis.txt
这里只有 3 个简单的代码块。当输入行匹配正则表达式(这里是单词“keyphrase”)时,调用第一个代码块。只要“keyphrase”出现在输入行内的任何位置,就算匹配,然后执行该代码块。第一个代码块只干一件事:设置标志变量。
对于所有的输入行,第二个代码块都会被调用,因为其左花括号之前没有正则表达式。即便是匹配“keyphrase”的输入行,也会调用该代码块(如果不想这样,可以在第一个代码块中使用 next 语句)。第二个代码块所做的是:仅当标志变量设置时,打印整个输入行。
第三个代码块有对应的正则表达式,如果匹配,则重置(关闭)标志变量。这里的正则表达式使用了两个带有特殊含义的字符:脱字符(^),如果用作正则表达式的第一个字符,就匹配行首位置;美元符号(KaTeX parse error: Expected group after '^' at position 38: …配行尾位置。因此,正则表达式 ^̲ 匹配的是一个空行,行首和行尾之间没有任何字符。
我们可以对空行使用一个稍微复杂的正则表达式,使其也能够处理只包含空白字符的行,而不只是彻头彻尾的空行。修改后的第 3 行代码如下所示:
/^[[:blank:]]*$/ { flag=0 }
Perl 程序员会喜欢本节中讨论的这种问题和解决方案,但我们是用 awk 搞定的,因为 Perl(差不多)超出了本书的讨论范围。如果了解 Perl,那么不用犹豫,你可以直接使用。如果不了解,那就全靠 awk 了。
820



被折叠的 条评论
为什么被折叠?



