资讯详情

Shell编程实例——中级shell工具(一)

📅 2026/9/16 23:31:27 | 华诺云谱 👁 阅读
Shell编程实例——中级shell工具(一)
中级shell工具1、在文件中查找字符串2、只显示包含搜索结果的文件名3、了解搜索是否成功4、不区分大小写搜索5、在管道中进行搜索6、缩减搜索结果7、搜索更复杂的模式8、搜索SSN9、搜索压缩文件10、保留部分输出11、仅保留部分输入行12、颠倒每行的单词13、汇总数字列表14、用awk统计字符串出现次数15、用bash统计字符串出现次数16、用便捷的直方图展示数据17、用bash轻松实现直方图18、显示匹配短语之后的文本段落1、在文件中查找字符串你需要在一个或多个文件中查找所有出现过的某个字符串。grep 命令可以搜索文件查找指定的字符串。$grepprintf*.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: // well useprintfto tell us what we somio.c: printf(open: fd%d\n, iod[i]);$在这个例子中我们搜索的文件全都位于当前目录下。因此我们只使用了简单的 shell 模式*.c来匹配以.c结束的文件并没有在文件名前再添加路径。但并非所有待搜索的文件都老老实实地待在当前目录下。但因为 shell 并不在意你输入多少路径名所以我们也可以这么写grepprintf../lib/*.c../server/*.c../cmd/*.c */*.c如果待搜索的文件不止一个grep 会在输出前加上文件名以及冒号然后是该文件中包含 grep 搜索内容的文本。搜索会匹配指定的字符串所以含有“fprintf”的行会被返回因为“fprintf”包含“printf”​。grep 的第一个非选项参数可以是一个简单的字符串正如本例中所示也可以是更复杂的正则表达式regexp​。正则表达式不同于 shell 的模式匹配尽管两者有时看起来差不多。模式匹配的功能无比强大你可能会发现自己对其的依赖已经到了开始将“grep”作为动词的程度只想随时随地都能用到它比如“希望能在我的书桌上 grep 到你想要的论文”​。你可以用命令行选项控制 grep 的输出形式。如果不想看到特定文件名指定 -h 选项即可。$grep-hprintf*.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);// well useprintfto tell us what we printf(open: fd%d\n, iod[i]);$如果不想看到文件中包含指定字符串的那些行只想知道匹配到了几次可以使用 -c 选项。$grep-cprintf*.c both.c:2 good.c:1 somio.c:2 $常见的错误是忘记指定 grep 的输入例如grep myvar。这种情况下grep 会认为你要从STDIN 提供输入而你以为它会读取文件于是grep 就干等着无所事事。​其实它是在等待着你从键盘输入。​当你要搜索大量数据预计会花费很长时间时很难分辨出这种尴尬的情况。2、只显示包含搜索结果的文件名你需要找出包含特定字符串的文件但是不想看到其所在的文本行只用输出文件名即可。用 grep 的 -l 选项仅显示文件名即可。$grep-lprintf*.c both.c good.c somio.c $如果在一个文件中找到了多次匹配grep 仍然只输出该文件名一次。如果没有找到匹配则什么都不输出。由于这些文件包含了你要查找的字符串如果想据此构建一个待处理文件的列表选项 -h 就能派上用场了。将grep 命令放进 $()然后就可以在命令行上使用这些文件名了。例如要想删除包含字符串“This file is obsolete”的文件可以使用以下这样的 shell 命令组合。rm-i$(grep-lThis file is obsolete*)我们给 rm 加上了 -i 选项以便在删除每个文件前都先询问你。考虑到该命令组合的威力这显然是一种更安全的处理方式。bash 对 * 进行扩展以匹配当前目录但并不会深入子目录下的所有文件并将其作为 grep 的参数。grep 生成包含指定字符串的文件列表。最后rm 会删除该列表中的所有文件。3、了解搜索是否成功你想要知道某个文件是否包含指定的字符串而且只关心“有”还是“没有”​。使用 grep 的 -qquiet选项。或者考虑到最大的可移植性简单将输出重定向到 /dev/null 丢弃。不管用哪种方法你想知道的答案就在 bash 的返回状态变量 $? 中因此可以像下面这样将其用于 if 语句$ifgrep-qfindme bigdata.file;thenechoyes;elseechonope;finope $在 shell 脚本中通常并不需要输出搜索结果你只是想知道有没有找到匹配以便脚本选择相应的分支。和大多数 Unix/Linux 命令一样返回值 0 表示命令成功执行完毕。在这个例子中成功的意思就是在指定文件这里只搜索了一个文件至少找到了一处匹配。返回值保存在 shell 变量 $? 中随后可以在 if 语句中用到。如果我们在 grep -q 之后列出多个文件名那么 grep 会在找到第一处匹配后就停止搜索。它并不会搜索所有文件因为你只是想知道有没有找到匹配。要是真想搜遍所有文件​为什么这么做​那就别用 -q 选项了。$ifgrepfindme bigdata.file/dev/null;thenechoyes;elseechonope;finope $/dev/null 是一个特殊设备位桶​重定向到此处的输出全都会被丢弃。如果你希望自己编写的脚本能够适用于 Unix/Linux 系统上的各种 grep 版本即便某些版本不支持 -q 选项/dev/null 同样能够发挥作用。4、不区分大小写搜索你想要在日志文件中不区分大小写地搜索字符串如“error”​​以匹配该字符串的所有出现。用 grep 的 -i 选项忽略大小写。grep-ierror logfile.msgs不区分大小写的搜索能够找出包含“ERROR”​、​“error”​、​“Error”的日志消息​“ErrOR”和“eRrOr”这样的也不例外。该选项在查找大小写混合的单词时尤其管用包括可能在句首或电子邮件地址中大写的那些单词。5、在管道中进行搜索你要搜索的文本并不在文件中而是在某个命令或命令管道的输出中。将输出导入 grep 即可。some pipeline|of commands|grep如果没有提供文件名grep 会从标准输入读取。大多数经过良好设计可用于 shell 脚本编程的实用工具会这么做。这也是它们能够作为 shell 脚本的构建块buildingblock发挥作用的原因之一。如果还想用 grep 搜索上一个命令中的错误信息那么要确保在管道之前将该命令的错误输出重定向到标准输出。gcc bigbadcode.c21|grep-ierror此命令尝试编译一些假想的代码。将输出通过管道|传给 grep 之前我们先重定向命令的标准错误到标准输出21​然后由 grep 搜索字符串 error不区分其大小写-i​。对 grep 的结果再执行 grep 也不是不可能的事。为什么要这么做这是为了进一步缩小搜索范围。假设你想要找出Bob Johnson 的电子邮件地址。$grep-ijohnson mail/*... too much output to think about;there are lots of Johnsonsinthe world... $!!|grep-irobertgrep-ijohnson mail/*|grep-irobert...moremanageable output... $!!|grep-ithe bluesmangrep-ijohnson mail/*|grep-irobert|grep-ithe bluesmanRobert M. Johnson, The Bluesmanrmjnoplace.org你可以将第一个 grep 命令再输入一遍但这个示例还展示了历史运算符 !! 的威力参见 18.2 节​。!! 可以重复上一个命令无须重新输入。然后就像示例那样在 !! 之后添加新的命令。shell 会显示所执行的命令因此你会看到 !! 替换后的结果。你可以按照这种方法简单快速地构建一个比较长的 grep管道查看中间步骤的结果并决定如何用其他 grep 表达式改善搜索过程。也可以用单个 grep 配合精巧的正则表达式来完成相同任务不过我们发现以增量方式构建管道要来得更加容易。6、缩减搜索结果搜索返回的结果不符合预期其中包括许多并不需要的内容。将结果通过管道传给 grep -v 并用表达式描述出你不想看到的内容。假设你想在日志文件中找出整个 12 月的日志消息。你知道日志文件用字母缩写 Dec 代表 12 月但不敢肯定总是如此为了确保找出所有的日志消息输入下列命令grep-idec logfile得到的结果却如下所示... error on Jan 01: not a decimal number error on Feb13: base converted to Decimal warning on Mar22: using only decimal numbers error on Dec16:the actual message you wanted error on Jan 01: not a decimal number...一种快而糙的解决方案是将第一次得到的结果通过管道传给另一个 grep由后者过滤掉所有的“decimal”​。grep-idec logfile|grep-videcimal将多个 grep 串联在一起因为前所未见、出乎意料的匹配会不断出现​逐步过滤搜索结果直至满意这种做法并不鲜见。grep-idec logfile|grep-videcimal|grep-videcimate在这种解决方案中​“糙”是指它可能会漏掉一些你本想留下的 12 月的日志消息如果消息中含有单词“decimal”​那么就会被 grep -v 过滤。如果谨慎使用-v 选项非常方便你只需要记住该排除什么就行了。对于这个例子更好的解决方案是使用威力更为强大的正则表达式来匹配月份和日期查找“Dec”以及紧随其后的一个空格和两个数字。grepDec [0-9][0-9]logfile不过这种写法往往行不通因为 syslog 会用一个空格填充单数字日期。要想解决这个问题可以在第一个字符集中加上一个空格。grepDec [0-9 ][0-9]logfile由于存在内嵌空格同时为了避免 shell 解释方括号我们在方括号周围加上了单引号。在任何可能会对 shell 造成困惑的地方使用单引号是一个好习惯。我们可以使用反斜线转义空格如下所示grepDec\[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 securitynumberSSN的正则表达式。美国的社会保险号由 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压缩归档日志。zgrepsearch term/var/log/messages*zcat 就是能够理解各类压缩文件和未压缩文件具体类型视系统而定的 cat。它比 zgrep 理解的格式还要多而且很多系统已经默认安装了。zcat 还可用于恢复受损的压缩文件因为它会尽可能输出所有内容而不像 gunzip或其他工具那样出错。zcat /var/log/messages.1.gzgzcat 类似于 zcat二者的区别在于商业软件与自由软件之分以及向后兼容性。10、保留部分输出你需要用某种方法保留部分输出丢弃其余输出。以下代码会打印出所有输入行的第一个字段。awk{print $1}myinput.file字段之间以空白字符分隔。实用工具 awk 从命令行上指定的文件中读取数据如果没有指定文件则从标准输入读取。因此你也可以将输入重定向到文件如下所示awk{print $1}myinput.file或者通过管道传入catmyinput.file|awk{print $1}awk 的用法多变。最简单的用法就是从输入中打印出所选的一个或多个字段。字段之间以空白字符分隔也可以用 -F 选项指定分隔字符​编号从 1 开始。字段 $0 代表整个输入行。awk 是一门完备的编程语言。awk 脚本可以变得极为复杂。这个实例仅仅是个开始而已。11、仅保留部分输入行你只想保留部分输入行例如第一个和最后一个字段。举例来说你希望 ls 只列出文件名和权限不需要 ls -l 所提供的其他信息。可惜的是ls 并没有相应的选项能够按照这种方式限制输出。可以通过管道将 ls 的输出传给 awk并从中挑选出你需要的字段。$ls-l|awk{print $1, $NF}total151130-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-x2username group1762006-10-2820:09 bin对于 awk 而言解析这种输出易如反掌在 awk 中默认的字段分隔符为空白字符​。在输出文件名时我们用了点小技巧。在 awk 中各种字段是用美元符号和字段编号来引用的如$1、$2、$3​而且 awk 还有一个内建变量 NF其中保存着当前行中的字段总数$NF总是引用最后一个字段。​例如ls 的输出行共有 8 个字段因此变量 NF 的值就是 8$NF指向的就是输入行中的第 8 个字段在这个例子中就是文件名​。记住读取 awk 变量时不需要使用 $这一点和 bash 变量不同​。NF 本身就是一个有效的变量引用。在其之前加上 $ 就将其含义从“当前行的字段总数”改成了“当前行的最后一个字段”​。12、颠倒每行的单词你想按照逆序输出输入行中的单词。$awk{ for (iNF; i0; i--) { printf %s , $i; } printf \n }filename字符 不用你输入shell 会输出该字符来提醒你还没有敲完命令shell 在查找能配对的单引号​。由于 awk 程序位于单引号中因此 bash shell 允许我们输入多行代码同时使用 作为辅助提示符直到我们给出与先前匹配的结束单引号。考虑到可读性我们在程序中加入了空白字符不过也完全可以写成一行。$awk{for (iNF; i0; i--) {printf %s , $i;} printf \n }filenameawk 语言的 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--1albingusers2672005-09-2621: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.awkNF7{user[$3]}END{for(iinuser){printf%s owns %d files\n, i, user[i]}}这里我们调用 awk 的方式有点不一样。由于这个 awk 脚本有些复杂我们将它放在了单独的文件中。-f 选项告诉awk 从文件中读取脚本不过也可以在脚本中使用shebang 行 3#!/usr/bin/awk。$ls-lR/usr/local|awk-fasar.awk bin owns68files albing owns1801files root owns13755filesmanowns11491files $我们用条件 NF 7 来限制执行部分 awk 脚本以此过滤不包含文件名的那些行此类行出现在 ls -lR 的输出中是为了提高可读性其中包括用于分隔不同目录的空行以及每个子目录中的文件总量信息。这种行包含的字段单词不多。左花括号前的 NF 7 并没有出现在斜线中这表明它并非正则表达式。它其实就是一个条件表达式和if 语句中使用的差不多用于判断条件的真假。NF 是一个特殊的内建变量其中保存着当前输入行的字段总数。因此如果某个输入行的字段单词数超过了 7 个则由花括号中的语句进行处理。最重要的是下面这行。user[$3]这里用户名如 bin被用作数组索引。之所以称为关联数组是因为使用散列表或类似机制将每个独一的字符串与数值关联了起来。这些事情已经由 awk 在幕后帮你做好了字符串比较或查找之类的都不用你操心。建立好关联数组之后似乎不那么容易获得数组元素的值了。为此awk 提供了一种特殊形式的 for 循环。与数字形式的 for(i0; imax; i) 不同它有专用于关联数组的语法。for(iinuser)在该语句中变量 i 连续从数组 user 的索引中获得值没有什么特定顺序​。在示例中这意味着 i 在每次循环中都会得到一个值bin、albing、root、man​。如果以前还没见识过关联数组希望这次能让你大开眼界印象深刻。关联数组是 awk以及 Perl的一个强大特性。15、用bash统计字符串出现次数你需要统计多个字符串出现的次数其中包括一些事先不知道具体是什么的字符串。也就是说你要统计的并不是已经确定好的一组字符串的出现次数。相反你会在数据中碰到一些未知字符串并需要对其进行统计。如果使用的是 bash 4.0 或更高版本那么可以用关联数组其他语言中也称为散列或字典来搞定。在这个例子中我们要统计系统中的各个用户都拥有多少文件。ls -l 输出的第 3 个字段就是用户名因此我们用该字段$3作为数组索引并增加数组元素​。# 实例文件cnt_owner# 使用bash统计文件所有者# 将“ls -l”的输出通过管道传入该脚本declare-AAACOUNTwhileread-aLSLdo# 只考虑包含7个及以上字段的行if((${#LSL[*]}7))# 数组大小thenNDX${LSL[3]}# 字符串赋值((AACOUNT[${NDX}]1))# 算术递增fidoneforVALSin${!AACOUNT[]}# 各个元素的索引doecho$VALSowns${AACOUNT[$VALS]}filesdone我们可以像下面这样调用该脚本并获得结果。$ls-lR/usr/local|bashcnt_owner.sh bin owns68files root owns13755filesmanowns11491files albing owns1801files $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.awkfunctionmax(arr, big){big0;for(iinuser){if(user[i]big){biguser[i];}}returnbig}NF7{user[$3]}END{# 进行缩放maxmmax(user);for(iinuser){#printf %s owns %d files\n, i, user[i]scaled60* user[i]/ maxm;printf%-10.10s [%8d]:, i, user[i]for(i0;iscaled;i){printf#;}printf\n;}}用 14 节中的输入来运行该脚本可以得到$ls-lR/usr/local|awk-fhist.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 节中的代码作为基础唯一的不同在于输出部分。首先遍历所有的数组元素值来找出其中的最大值然后以此对直方图输出进行缩放以适应页面大小。BIG0forVALSin${!UCOUNT[]}doif((UCOUNT[$VALS]BIG));thenBIG${UCOUNT[$VALS]};fidone有了最大值保存在 BIG 中后为每个数组元素输出一行。## 输出直方图#forVALSin${!UCOUNT[]}doprintf%-9.9s [%7d]:$VALS${UCOUNT[$VALS]}# 缩放至最大值BIG注整数SCALED$((((59*UCOUNT[$VALS])/BIG)1))for((i0;iSCALED;i)){printf#}printf\ndone与 5 节中一样${!UCOUNT[]} 至关重要。可以通过它得到数组的索引值列表这个示例为数组UCOUNT​。for 循环每次获得一个值并用它作为数组索引来获得该用户的文件数量。我们将直方图缩放至 59 个字符宽度然后再多加 1 个字符宽度这是为了令任何非 0 值 4 都能在直方图上至少生成一个标记。这在 awk 版本参见 16 节中不是什么问题因为 awk 使用的是浮点运算但 bash 版本使用的是整数运算执行除法后过小的值会变成 0。18、显示匹配短语之后的文本段落你要在文档中搜索某个短语并希望找到该短语后还能显示之后的文本段落。假设在一个简单的文本文件中段落就是空行之间的所有文本因此空行的出现就意味着段落的结束。有鉴于此我们来看看下面这个短小精悍的 awk 脚本。$catpara.awk /keyphrase/{flag1}flag1{print}/^$/{flag0}$awk-fpara.awksearchthis.txt这里只有 3 个简单的代码块。当输入行匹配正则表达式这里是单词“keyphrase”​时调用第一个代码块。只要“keyphrase”出现在输入行内的任何位置就算匹配然后执行该代码块。第一个代码块只干一件事设置标志变量。对于所有的输入行第二个代码块都会被调用因为其左花括号之前没有正则表达式。即便是匹配“keyphrase”的输入行也会调用该代码块如果不想这样可以在第一个代码块中使用 next 语句​。第二个代码块所做的是仅当标志变量设置时打印整个输入行。第三个代码块有对应的正则表达式如果匹配则重置关闭标志变量。这里的正则表达式使用了两个带有特殊含义的字符脱字符^​如果用作正则表达式的第一个字符就匹配行首位置美元符号KaTeX parse error: Expected group after ^ at position 38: …配行尾位置。因此正则表达式 ^̲匹配的是一个空行行首和行尾之间没有任何字符。我们可以对空行使用一个稍微复杂的正则表达式使其也能够处理只包含空白字符的行而不只是彻头彻尾的空行。修改后的第 3 行代码如下所示/^[[:blank:]]*$/{flag0}Perl 程序员会喜欢本节中讨论的这种问题和解决方案但我们是用 awk 搞定的因为 Perl差不多超出了本书的讨论范围。如果了解 Perl那么不用犹豫你可以直接使用。如果不了解那就全靠 awk 了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。