摘要:本文是以英文版为基础整理的笔记,力求脱水更新完中级。空格被默认作各列的分隔符,也可以通过开关进行自定义。这样的数组也叫作关联数组,或称为映射,或者是哈希表。空行表示段落的结束会匹配空行。
Read Me
本文是以英文版
2018.01.21 更新完【中级】。内容包括工具、函数、中断及时间处理等进阶主题。
本系列其他两篇,与之互为参考
【基础】内容涵盖bash语法等知识点。传送门
【高级】内容涉及脚本安全、bash定制、参数设定等高阶内容。传送门
所有代码在本机测试通过
Debian GNU/Linux 9.2 (stretch)
GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
2018.01.21 更新 【四】工具.sed流处理 【六】日期与时间
约定格式# 注释:前导的$表示命令提示符 # 注释:无前导的第二+行表示输出 # 例如: $ 命令 参数1 参数2 参数3 # 行内注释 输出_行一 输出_行二 $ cmd par1 par1 par2 # in-line comments output_line1 output_line2四、工具
UNIX(Linux)喜欢小而美,不喜欢大而杂grep 搜索字符串
在当前路径的所有c后缀文件中,查找printf字符串
$ grep printf *.c both.c: printf("Std Out message. ", argv[0], argc-1); both.c: fprintf(stderr, "Std Error message. ", argv[0], argc-1); good.c: printf("%s: %d args. ", argv[0], argc-1); somio.c: // we"ll use printf to tell us what we somio.c: printf("open: fd=%d ", iod[i]);
当然,也可以像这样,指定不同的搜索路径
$ grep printf ../lib/*.c ../server/*.c ../cmd/*.c */*.c
搜索结果的默认输出格式为“文件名 冒号 匹配行”
可以通过-h开关隐藏(hide)文件名
$ grep -h printf *.c printf("Std Out message. ", argv[0], argc-1); fprintf(stderr, "Std Error message. ", argv[0], argc-1); printf("%s: %d args. ", argv[0], argc-1); // we"ll use printf to tell us what we printf("open: fd=%d ", iod[i]);
或者,不显示匹配行,而只是用-c开关进行对匹配次数进行计数(count)
$ grep -c printf *.c both.c:2 good.c:1 somio.c:2
或者,只是简单地列出(list)含搜索项的文件清单,可以用-l开关
$ grep -l printf *.c both.c good.c somio.c
文件清单可视为一个不包含重复项的集合,便于后续处理,比如
$ rm -i $(grep -l "This file is obsolete" * )
有时候,只需要知道是否满足匹配,而不关心具体的内容,可以使用-q静默(quiet)开关
$ grep -q findme bigdata.file $ if [ $? -eq 0 ] ; then echo yes ; else echo nope ; fi nope
也可以把输出重定向进/dev/null位桶,一样实现静默的效果。位桶(bit bucket)就相当于“位的垃圾桶”,一个有去无回的比特黑洞
$ grep findme bigdata.file >/dev/null $ if [ $? -eq 0 ] ; then echo yes ; else echo nope ; fi nope
经常,你更希望搜索时忽略(ignore)大小写,这时可以用-i开关
$ grep -i error logfile.msgs # 匹配ERROR, error, eRrOr..
很多时候,搜索范围并不是来自文件,而是管道
$ 命令1 | 命令2 | grep
举个例。将gcc编译的报错信息从标准错误(STDERR, 2)重定向到标准输出(STDOUT,1),再通过管道传给grep进行筛选
$ gcc bigbadcode.c 2>&1 | grep -i error
多个grep命令可以串联,以不断地缩小搜索范围
grep 关键字1 | grep 关键字2 | grep 关键字3
比如,与!!(复用上一条命令)组合使用,可以实现强大的增量式搜索
$ grep -i 李 专辑/* ... 世界上有太多人姓李 ... $ !! | grep -i 四 grep -i 李 专辑/* | grep -i 四 ... 叫李四的也不少 ... $ !! | grep -i "饶舌歌手" grep -i 李 专辑/* | grep -i 四 | grep -i "饶舌歌手" 李四, 饶舌歌手
-v开关用来反转(reverse)搜索关键字
$ grep -i dec logfile | grep -vi decimal | grep -vi decimate
按关键字"dec"匹配,但不要匹配"decimal",也不要匹配"decimate"。因为这里的dec意思是december
... 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 : 匹配这一行就对了 error on Jan 01: not a decimal number ...
像上边这样“要匹配这个,但不要包含那个”...是非常笨重的,就像在纯手工地对密码进行暴力破解。
仔细观察规律,匹配关键字的模式,才是正解。
$ grep "Dec [0-9][0-9]" logfile
0-9匹配dec后边的一位或两位数日期。如果日期是一位数,syslog会在数字后加个空格补齐格式。所以为了考虑进这种情况,改写如下,
$ grep "Dec [0-9 ][0-9]" logfile
对于包含空格等敏感字符的表达式,总是用单引号"..."对表达式进行包裹是个良好的习惯。这样可以避免很多不必要的语法歧义。
当然,用反斜杠对空格取消转义(escaping)也行。但考虑到可读性,还是建议用单引号对。
$ grep Dec [0-9 ][0-9] logfile
结合正则表达式(Re),可以实现更复杂的匹配。
正则表达式【简表】. # 任意一个字符 .... # 任意四个字符 A. # 大写A,跟一个任意字符 * # 零个或任意一个字符 A* # 零个或任意多个大写A .* # 零个或任意个任意字符,甚至可以是空行 ..* # 至少包含一个空行以外的任意字符 ^ # 行首 $ # 行尾 ^$ # 空行 # 保留各符号的本义 [字符集合] # 匹配方括号内的字符集合 [^字符集合] # 不匹配方括号内的字符集合 [AaEeIiOoUu] # 匹配大小写元音字母 [^AaEeIiOoUu] # 匹配不包括大小写元音的任意字母 {n,m} # 重复,最少n次,最多m次 {n} # 重复,正好n次 {n,} # 重复,至少n次 A{5} # AAAAA A{5,} # 至少5个大写A
举个实用的例子:匹配社保编号 SSN
$ grep "[0-9]{3}-{0,1}[0-9]{2}-{0,1}[0-9]{4}" datafile
这么长的正则,写的人很爽,读的人崩溃。所以也被戏称为Write Only.
为了写给人看,一定要加个注释的。
为了讲解清楚,来做个断句
[0-9]{3} # 先匹配任意三位数 -{0,1} # 零或一个横杠 [0-9]{2} # 再跟任意两位数 -{0,1} # 零或一个横杠 [0-9]{4} # 最后是任意四位数
还有一些z字头工具,可以直接对压缩文件进行字符串的查找和查看处理。比如zgrep, zcat, gzcat等。一般系统会预装有
$ zgrep "search term" /var/log/messages*
特别是zcat,会尽可能地去还原破损的压缩文件,而不像其他工具,对“文件损坏”只会一味的报错。
$ zcat /var/log/messages.1.gzawk 多带带飞
awk是一门语言,是perl的先祖,是一头怪兽,是一只多带带飞(chameleon)。
作为(最)强大的文本处理引擎,awk博大精深,一本书都讲不完。这里只能挑些最常用和基础的内容来讲。
首先,以下三种传文件给awk的方式等效:
$ awk "{print $1}" 输入文件 # 作为参数 $ awk "{print $1}" < 输入文件 # 重定向 $ cat 输入文件 | awk "{print $1}" # 管道
对于格式化的文本,比如ls -l的输出,awk对各列从1开始编号,依次递增。不是从0,因为$0表示整行。最后一列,记为NF。空格被默认作各列的分隔符,也可以通过-F开关进行自定义。
$1 | $2 | $3 | ... | $NF | |
---|---|---|---|---|---|
首列 | 第二列 | 第三列 | ... | 尾列 | |
$0 | 整行 |
$ ls -l total 4816 drwxr-xr-x 4 jimhs jimhs 4096 Nov 26 02:10 backup drwxr-xr-x 3 jimhs jimhs 4096 Nov 24 08:20 bash ... $ $ ls -l| awk "{print $1, $NF}" # 打印第一行和最后一行 total 4816 drwxr-xr-x backup drwxr-xr-x bash ...
注意到,第五列是文件大小,可以对其大小求和,并作为结果输出
$ ls -l | awk "{sum += $5} END {print sum}"
ls -l输出的第一行,是一个total汇总。也正因为该行并没有“第五列”,所以对上边的{sum += $5}没有影响。
但实际上,严格来讲,应该对这样的特例做预处理,即,删掉该行。
首先想到的:可以用之前介绍grep时的-v翻转开关,来去除含"total"的那行
$ ls -l | grep -v "^total" | awk "{sum += $5} END {print sum}"
另一种方法是:在awk脚本内,先用正则定位到total行(第一行),找到后立即执行紧跟的{getline}句块,因为getline用来接收新的输入行,这样就顺利跳过了total行,而进入了{sum += $5}句块。
$ ls -l | awk "/^total/{getline} {sum += $5} END {print sum}"
也就是说,作为awk脚本,各结构块摆放的顺序是相当重要的。
一个完整的awk脚本可以允许多个大括号{}包裹的结构。END前缀的结构体,表示待其他所有语句执行完后,执行一次。与之相对的,是BEGIN前缀,会在任何输入被读取之前执行,通常用来进行各种初始化。
作为可编程的语言,awk部分借用了c语言的语法。
可以像这样,将结构写成多行
$ awk "{ > for (i=NF; i>0; i--) { > printf "%s ", $i; > } > printf " " > }"
也可以把整个结构体塞进一行内
$ awk "{for (i=NF; i>0; i--) {printf "%s ", $i;} printf " " }"
以上脚本,将各列逆序输出:
drwxr-xr-x 4 jimhs jimhs 4096 Nov 26 02:10 backup 变成了 backup 02:10 26 Nov 4096 jimhs jimhs 4 drwxr-xr-x
对于复杂的脚本,可以多带带写成一个.awk后缀的文件
# # 文件名: asar.awk # NF > 7 { # 触发计数语句块的逻辑,即该行的项数要大于7 user[$3]++ # ls -l的第3个变量是用户名 } END { for (i in user) { printf "%s owns %d files ", i, user[i] } }
然后通过-f文件开关来引用(file)
$ ls -lR /usr/local | awk -f asar.awk bin owns 68 files albing owns 1801 files root owns 13755 files man owns 11491 files
这个脚本asar.awk,递归地遍历/usr/local路径,并统计各用户名下的文件数量。
注意:其中用于自增时计数的user[]数组,它的索引是$3,即用户名,而不是整数。这样的数组也叫作关联数组(associative arrays) ,或称为映射(map),或者是哈希表(hashes)。
至于怎么做的关联、映射、哈希,这些技术细节,awk都在幕后自行处理了。
这样的数组,肯定是无法用整数作为索引去遍历了。
所以,awk为此专门定制了一条优雅的for...in...的语法
for (i in user)
这里,i会去遍历整个关联数组user,本例是[bin , albing , man , root]。再强调一下,重点是会遍历“整个”。至于遍历“顺序”,你没法事先指定,也没必要关心。
下边的hist.awk脚本,在asar.awk的基础上,加了格式化输出和直方图的功能。也借这个稍复杂的例子,说明awk脚本中函数的定义和调用:
# # 文件名: 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 { # for scaling maxm = max(user); for (i in user) { #printf "%s owns %d files ", i, user[i] scaled = 60 * user[i] / maxm ; printf "%-10.10s [%8d]:", i, user[i] for (i=0; i本例中还用到了printf的格式化输出,这里不展开说明。
awk内的算术运算默认都是浮点型的,除非通过调用內建函数int(),显式指定为整型。
本例中做的是浮点运算,所以,只要变量scaled不为零,for循环体就至少会执行一次,类似下边的"bin"一行,虽然寥寥68个文件,也还是会显示一格#
$ ls -lR /usr/local | awk -f hist.awk bin [ 68]:# albing [ 1801]:####### root [ 13755]:################################################## man [ 11491]:##########################################至于各用户名输出时的排列顺序,如前所述,是由建立哈希时的内在机制决定的,你无法干预。
如果非要干预(比如希望按字典序,或文件数量)排列的话,可以这样实现:将脚本结构一分为二,将第一部分的输出先送给sort做排序,然后再通过管道送给打印直方图的第二部分。
最后,再通过一个小例子,结束awk的介绍。
这个简短的脚本,打印出包含关键字的段落:
$ cat para.awk /关键字/ { flag=1 } { if (flag == 1) { print $0 } } /^$/ { flag=0 } $ $ awk -f para.awk < 待搜索的文件段落(paragraph),是指两个空行之间所有的文本。空行表示段落的结束
/^$/会匹配空行。但是,对那些含有空格的“空行”,更精确的匹配是像这样:
/^[:blank:]*$/sed 流处理@2018.01.20
sed即流编辑器(stream editor)。
可以这么来简单区分,sed对文本是按行扫描。awk则是按列扫描。
sed本身就是一个庞大的话题,所以原著
并未过多涉及。 最近看
(第2版)的时候,发现书中引用的一个链接内容不错,中文翻得也不错。 所以放在此处,供有求知欲的读者参考。
并特此感谢原作者Eric Pement,及译者Joe Hong。
USEFUL ONE-LINE SCRIPTS FOR SED (Unix stream editor)
SED单行脚本快速参考(Unix 流编辑器)
cut uniq sort 切割 去重 排序处理格式化数据,经常涉及一系列组合操作:切割、去重、排序。先看个例子,统计系统里各个shell的频次
$ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ... $ $ cut -d":" -f7 /etc/passwd | sort | uniq -c | sort -rn 17 /bin/false # 禁止登录 16 /usr/sbin/nologin # 禁止登录 2 /bin/bash 1 /bin/sync将管道拆开,逐项来看:
cut -d":" -f7 /etc/passwd # 以冒号为分隔符,取第七个字段 sort # 预排序 uniq -c # 去重,合计归总 sort -rn # 由大到小,再次排序对于cut命令, 常用-d分隔符(delimiter)开关来做列向切割。tab制表符是默认的分隔符。切割后的各列通过-f域(field)开关来索引。这点与awk的$1...$NF类似。
$ cat ipaddr.list 10.0.0.20 # lanyard 192.168.0.2 # laptop 10.0.0.5 # mainframe 192.168.0.4 # office 10.0.0.2 # sluggish 192.168.0.12 # speedy-f2和$2等效,都能取到第二列
$ cut -d"#" -f2 < ipaddr.list $ $ awk -F"#" "{print $2}" < ipaddr.list对于列宽度固定的格式化数据,比如ps -l或ls -l的输出,可以用-c列索引(column)开关来定位。第一列索引号是1,依次递增。
$ ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1000 9148 9143 0 80 0 - 5322 - pts/0 00:00:00 bash 0 R 1000 9536 9148 0 80 0 - 7466 - pts/0 00:00:00 ps ...字符区间[12,15]是PID列,
$ ps -l | cut -c12-15 PID 9148 9536 ...也可以用开区间,例如用[67,)取CMD至末尾
$ ps -l | cut -c67- CMD bash ps ...怎么取出下边方括号内的数据列?
$ cat delimited_data Line [l1]. Line [l2]. Line [l3].当然,优雅的解法,肯定是用awk+正则
不过,用cut也行:先剪掉左括号,再剪掉右括号。简单直接。
$ cut -d"[" -f2 delimited_data | cut -d"]" -f1 l1 l2 l3回到本章最开始的例子,了解一下“去重”。
$ 预排序 | uniq -c | 再次排序uniq适用于预排序过的序列。-c开关意思是计数(count),将预排序后的各个相邻的重复项汇总计数。还有一个-d开关,用于列出重复项(duplicate)。
当uniq接收到两个文件作为参数时,第二个文件被用来接收输出,里边原有的内容会被覆盖掉。
$ uniq -d file.in file.out如果不需要计数,可以用sort的-u开关去重(unique):
cut -d":" -f7 /etc/passwd | sort -usort命令,有三个开关最为常用:
-r 逆序(reverse)
$ sort -r-f 混杂(fold),忽略大小写,即“将大小写混为一体”
$ sort -f # GNU长格式参数的等效写法: $ sort -–ignore-case-n 数字(number) 将排序对象视为数字
举个例子:对ip地址排序
$ cat ipaddr.list 10.0.0.20 # lanyard 192.168.0.2 # laptop 10.0.0.5 # mainframe 192.168.0.4 # office 10.0.0.2 # sluggish 192.168.0.12 # speedy用前边介绍过的cut,先去掉注释列
$ cut -d# -f1 ipaddr.list 10.0.0.20 192.168.0.2 10.0.0.5 192.168.0.4 10.0.0.2 192.168.0.12$ !! | sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n 10.0.0.2 10.0.0.5 10.0.0.20 192.168.0.2 192.168.0.4 192.168.0.12先用-t指定 域分隔符(field seperator),这里是点号。分隔出四个域。
-k 1,1n,用人话表达,就是"从第一个域(1)的首,直至(,)第一个域(1)的尾,按数字(n)排序"。后边的2、3、4以此类推。
这是新式的POSIX风格的写法。如果按旧式(已废止),要写成这样
$ sort -t. +0n -1 +1n -2 +2n -3 +3n -4一样丑。旧式写法就不多介绍了。
sort的排序行为,会受本地化设置(locale setting)的影响。所以,如果你发现排序行为跟预期不符,最好先检查一下该设置。
最后,再介绍一个概念,稳定排序(stable sort)。
现在, 我们只希望对第四个数域进行排序:
$ sort -t. -k4n ipaddr.list 10.0.0.2 # sluggish 192.168.0.2 # laptop 192.168.0.4 # office 10.0.0.5 # mainframe 192.168.0.12 # speedy 10.0.0.20 # lanyard对比原始的ip列表。可以看到,虽然laptop和sluggish行的第四个数是相等的,但排序后sluggish被提到了前边
$ cat ipaddr.list ... 192.168.0.2 # laptop ... 10.0.0.2 # sluggish ...这是因为,sort默认会进行last-resort comparison的操作:如果分不出大小,就用其他域值来辅助判断,进行终极的比较。
这种行为,可以通过-s开关(stable)禁用
$ sort -t. -s -k4n ipaddr.list 192.168.0.2 # laptop 10.0.0.2 # sluggish ...tr wc 转换 统计将分号全部替换成逗号
$ tr ";" "," <源文件 >目标文件这个是tr(translate)命令最原始的用法。分号和逗号是一对一的替换关系
也可以进行多对一的替换,逗号","会被展开成";:.!?"的长度
$ tr ";:.!?" "," <源文件 >目标文件一对多呢?这样写是没有意义的。";:.!?"长出来的部分都会被截断
$ tr "," ";:.!?" <源文件 >目标文件作为文字转换和替换工具,tr不如sed功能丰富,至少tr不支持正则表达式,所以限制了使用范围。
但是,tr也内置了一些能处理字符范围的语法。
比如,大小写的转换
$ tr "A-Z" "a-z" <源文件 >目标文件$ tr "[:upper:]" "[:lower:]" <源文件 >目标文件总之记住一点,保证替换和被替换目标长度(或范围)的一致。否则tr会自动去做补齐和截断,这可能并不是你所期望的。
ROT-13也称为回转13,诞生于古罗马。通过字母移位实现简单的加解密。
密文 = ROT13(明文)
明文 = ROT13(密文)$ cat /tmp/joke Q: Why did the chicken cross the road? A: To get to the other side.$ tr "A-Za-z" "N-ZA-Mn-za-m" < /tmp/joke D: Jul qvq gur puvpxra pebff gur ebnq? N: Gb trg gb gur bgure fvqr.$ !! | tr "A-Za-z" "N-ZA-Mn-za-m" Q: Why did the chicken cross the road? A: To get to the other side.DOS/Windows,一行结束的标志是"回车"+"换行",两个字符。Linux,只有一个字符,"换行"。
可以通过开关-d进行删除(delete)
$ tr -d " "linux文件 这样,所有的回车键都被删除了。包括行末和行内的。很少会有回车键出现在“行内”(inline),但这也是可能的。为了避免误删,可以考虑用更专业的转换工具,比如dos2unix或unix2dos
总结一下除了回车键之外的转义字符:
转义字符【简表】
转义符 描述 ooo 1-3个八进制数 反斜杠自身 a 哔 退格 f 换页 换行 回车 制表(水平) v 制表(垂直) wc用于字数统计(word count)
$ wc data_file 5 15 60 data_file # 统计行数 $ wc -l data_file 5 data_file # 统计词数 $ wc -w data_file 15 data_file # 统计字符(字节)数 $ wc -c data_file 60 data_file # 60字节,与ls的结果一致 $ ls -l data_file -rw-r--r-- 1 jp users 60B Dec 6 03:18 data_file如果希望将统计的结果作为变量,
这样是不行的
data_file_lines=$(wc -l "$data_file")因为你会得到"5 data_file",而不是数字5
可以用awk将5提取出来
data_file_lines=$(wc -l "$data_file" | awk "{print $1}")find locate slocate 查找如何在大海里捞针?所谓文件夹(folders),是图形用户界面(GUI)里的通俗叫法。更专业(BIGE)的名称,叫做子目录(subdirectories)
先从最基本的find开始
在当前路径(.)查找所有的mp3文件,然后移动到~/songs
$ find . -name "*.mp3" -print -exec mv "{}" ~/songs ;与前边介绍的各种单字符命令开关(-d, -n)不同,find使用谓语(predicates)来修饰各种行为,比如上边的-name,-print,-exec,对应名称,打印,执行。花括号用于接收找到的文件。
文件名有怪异(odd)字符怎么办?UNIX玩家眼里,任何非小写、非数字都是怪异的,比如大写、空格、各种标点、头上带音调的字母等等。
$ find . -name "*.mp3" -print0 | xargs -i -0 mv "{}" ~/songs-print0告诉find,用空字符