@H_
502_0@
Read Me
- 本文是以英文版<bash cookbook> 为基础整理的笔记,力求脱水
- 基础部分传送门
- 本篇为中级内容,对应原著第七章开始,讲解各种工具的使用
- 12.02 更新 grep,awk
- 翻得不好的地方欢迎交流,也期待您的点赞支持,谢谢 ^^
约定格式
# 注释:前导的$表示命令提示符
# 注释:无前导的第二+行表示输出
# 例如:
$ 命令 参数1 参数2 参数3 # 行内注释
输出_行一
输出_行二
$ cmd par1 par1 par2 # in-line comments
output_line1
output_line2
四、工具
@H_502_0@UNIX(Linux)喜欢小而美,不喜欢大而杂
grep 搜索字符串
@H_
502_0@在当前路径的所有c后缀
文件中,查找printf字符串
$ grep printf *.c
both.c: printf("Std Out message.\n",argv[0],argc-1);
both.c: fprintf(stderr,"Std Error message.\n",argc-1);
good.c: printf("%s: %d args.\n",argc-1);
somio.c: // we'll use printf to tell us what we
somio.c: printf("open: fd=%d\n",iod[i]);
$
@H_
502_0@当然,也可以像这样,指定不同的
搜索路径
$ grep printf ../lib/*.c ../server/*.c ../cmd/*.c */*.c
@H_
502_0@
搜索结果的默认
输出格式为“
文件名 冒号 匹配行”
@H_
502_0@可以通过
-h
开关隐藏(
hide)
文件名
$ grep -h printf *.c
printf("Std Out message.\n",argc-1);
fprintf(stderr,argc-1);
printf("%s: %d args.\n",argc-1);
// we'll use printf to tell us what we
printf("open: fd=%d\n",iod[i]);
$
@H_
502_0@或者,
不显示匹配行,而只是用
-c
开关进行对匹配
次数进行计数(
count)
$ grep -c printf *.c
both.c:2
good.c:1
somio.c:2
$
@H_
502_0@或者,只是简单地列出(
list)含
搜索项的
文件清单,可以用
-l
开关
$ grep -l printf *.c
both.c
good.c
somio.c
@H_
502_0@
文件清单可视为一个不包含重复项的集合,便于后续处理,比如
$ rm -i $(grep -l 'This file is obsolete' * )
@H_
502_0@有时候,只需要知道是否满足匹配,而不关心具体的
内容,可以使用
-q
静默(
quiet)开关
$ grep -q findme bigdata.file
$ if [ $? -eq 0 ] ; then echo yes ; else echo nope ; fi
nope
$
@H_
502_0@也可以把
输出重定向进/dev/null位桶,一样实现静默的
效果。位桶(
bit bucket)就相当于“位的
垃圾桶”,一个有去无回的比特黑洞
$ grep findme bigdata.file >/dev/null
$ if [ $? -eq 0 ] ; then echo yes ; else echo nope ; fi
nope
$
@H_
502_0@经常,你更希望
搜索时忽略(
ignore)大小写,这时可以用
-i
开关
$ grep -i error logfile.msgs # 匹配ERROR,error,eRrOr..
@H_
502_0@很多时候,
搜索范围并不是来自
文件,而是管道
$ 命令1 | 命令2 | grep
@H_
502_0@举个例。将gcc编译的报错信息从标准
错误(
STDERR,2)
重定向到标准
输出(
STDOUT,1),再通过管道传给grep进行筛选
$ gcc bigbadcode.c 2>&1 | grep -i error
@H_
502_0@多个grep命令可以串联,以不断地缩小
搜索范围
grep 关键字1 | grep 关键字2 | grep 关键字3
@H_
502_0@比如,与
!!
(复用上一条命令)组合使用,可以实现强大的增量式
搜索
$ grep -i 李 专辑/*
... 世界上有太多人姓李 ...
$ !! | grep -i 四
grep -i 李 专辑/* | grep -i 四
... 叫李四的也不少 ...
$ !! | grep -i "饶舌歌手"
grep -i 李 专辑/* | grep -i 四 | grep -i "饶舌歌手"
李四,饶舌歌手 <lsi@noplace.org>
@H_
502_0@
-v
开关用来反转(
reverse)
搜索关键字
$ grep -i dec logfile | grep -vi decimal | grep -vi decimate
@H_
502_0@按关键字'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
...
@H_
502_0@像上边这样“要匹配这个,但不要包含那个”...是非常笨重的,就像在纯手工地对密码进行暴力破解。
@H_
502_0@仔细观察规律,匹配关键字的模式,才是正解。
$ grep 'Dec [0-9][0-9]' logfile
@H_
502_0@0-9匹配dec后边的一位或两位数日期。如果日期是一位数,syslog会在数字后加个空格补齐格式。所以为了考虑进这种情况,改写如下,
$ grep 'Dec [0-9 ][0-9]' logfile
@H_
502_0@对于包含空格等敏感字符的表达式,总是用单引号'...'对表达式进行包裹是个良好的习惯。这样可以避免很多不必要的语法歧义。
@H_
502_0@当然,用反斜杠
\
对空格取消转义(
escaping)也行。但考虑到可读性,还是建议用单引号对。
$ grep Dec\ [0-9\ ][0-9] logfile
@H_
502_0@结合正则表达式(Re),可以实现更复杂的匹配。
常用的正则表达式【简表】
. # 任意一个字符
.... # 任意四个字符
A. # 大写A,跟一个任意字符
* # 零个或任意一个字符
A* # 零个或任意多个大写A
.* # 零个或任意个任意字符,甚至可以是空行
..* # 至少包含一个空行以外的任意字符
^ # 行首
$ # 行尾
^$ # 空行
\ # 保留各符号的本义
[字符集合] # 匹配方括号内的字符集合
[^字符集合] # 不匹配方括号内的字符集合
[AaEeIiOoUu] # 匹配大小写元音字母
[^AaEeIiOoUu] # 匹配不包括大小写元音的任意字母
\{n,m\} # 重复,最少n次,最多m次
\{n\} # 重复,正好n次
\ {n,\} # 重复,至少n次
A\{5\} # AAAAA
A\{5,\} # 至少5个大写A
@H_
502_0@举个实用的例子:匹配社保编号 SSN
$ grep '[0-9]\{3\}-\{0,1\}[0-9]\{2\}-\{0,1\}[0-9]\{4\}' datafile
@H_
502_0@这么长的正则,写的人很爽,读的人崩溃。所以也被戏称为
Write Only.
@H_
502_0@为了写给人看,一定要加个注释的。
@H_
502_0@为了讲解清楚,来做个断句
[0-9]\{3\} # 先匹配任意三位数
-\{0,1\} # 零或一个横杠
[0-9]\{2\} # 再跟任意两位数
-\{0,1\} # 零或一个横杠
[0-9]\{4\} # 最后是任意四位数
@H_
502_0@还有一些z字头工具,可以直接对压缩
文件进行字符串的查找和查看处理。比如zgrep,zcat,gzcat等。一般系统会预装有
$ zgrep 'search term' /var/log/messages*
@H_
502_0@特别是zcat,会尽可能地去还原破损的压缩
文件,而不像其他工具,对“
文件损坏”只会一味的报错。
$ zcat /var/log/messages.1.gz
awk 变色龙
@H_502_0@awk是一门语言,是perl的先祖,是一头怪兽,是一只变色龙(chameleon)。
@H_
502_0@作为(最)强大的文本处理引擎,awk博大精深,一本书都讲不完。这里只能挑些最常用和基础的
内容来讲。
@H_
502_0@首先,以下三种传
文件给awk的方式等效:
$ awk '{print $1}' 输入文件 # 作为参数
$ awk '{print $1}' < 输入文件 # 重定向
$ cat 输入文件 | awk '{print $1}' # 管道
@H_
502_0@对于格式化的文本,比如
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
$
@H_
502_0@注意到,第五列是
文件大小,可以对其大小求和,并作为结果
输出
$ ls -l | awk '{sum += $5} END {print sum}'
@H_
502_0@
ls -l
输出的第一行,是一个total汇总。也正因为该行并没有“第五列”,所以对上边的{sum += $5}没有影响。
@H_
502_0@但实际上,严格来讲,应该对这样的特例做预处理,即,删掉该行。
@H_
502_0@首先想到的:可以用之前介绍grep时的
-v
翻转开关,来
去除含'total'的那行
$ ls -l | grep -v '^total' | awk '{sum += $5} END {print sum}'
@H_
502_0@另一种
方法是:在awk脚本内,先用正则定位到total行(第一行),找到后立即执行紧跟的{getline}句块,因为getline用来接收新的输入行,这样就顺利跳过了total行,而进入了{sum += $5}句块。
$ ls -l | awk '/^total/{getline} {sum += $5} END {print sum}'
@H_
502_0@也就是说,作为awk脚本,各结构块摆放的顺序是相当重要的。
@H_
502_0@一个完整的awk脚本可以允许多个大括号{}包裹的结构。END前缀的结构体,表示待其他所有语句执行完后,执行一次。与之相对的,是BEGIN前缀,会在任何输入被读取之前执行,通常用来进行各种初始化。
@H_
502_0@作为可编程的语言,awk部分借用了c语言的语法。
@H_
502_0@可以像这样,将结构写成多行
$ awk '{
> for (i=NF; i>0; i--) {
> printf "%s ",$i;
> }
> printf "\n"
> }'
@H_
502_0@也可以把整个结构体塞进一行内
$ awk '{for (i=NF; i>0; i--) {printf "%s ",$i;} printf "\n" }'
@H_
502_0@以上脚本,将各列逆序
输出:
drwxr-xr-x 4 jimhs jimhs 4096 Nov 26 02:10 backup
变成了
backup 02:10 26 Nov 4096 jimhs jimhs 4 drwxr-xr-x
@H_
502_0@对于复杂的脚本,可以单独写成一个.awk后缀的
文件
#
# 文件名: asar.awk
#
NF > 7 { # 触发计数语句块的逻辑,即该行的项数要大于7
user[$3]++ # ls -l的第3个变量是用户名
}
END {
for (i in user)
{
printf "%s owns %d files\n",i,user[i]
}
}
@H_
502_0@然后通过
-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
$
@H_
502_0@这个脚本asar.awk,递归地遍历/usr/local路径,并
统计各
用户名下的
文件数量。
@H_
502_0@注意:其中用于自增时计数的user[]数组,它的索引是$3,即
用户名,而不是整数。这样的数组也叫作
关联数组(associative arrays) ,或称为
映射(map),或者是
哈希表(hashes)。
@H_
502_0@至于怎么做的关联、映射、哈希,这些技术细节,awk都在幕后自行处理了。
@H_
502_0@这样的数组,肯定是无法用整数作为索引去遍历了。
@H_
502_0@所以,awk为此专门定制了一条优雅的for...in...的语法
for (i in user)
@H_
502_0@这里,i会去遍历整个关联数组user,本例是[bin,albing,man,root]。再强调一下,重点是会遍历“整个”。至于遍历“顺序”,你没法事先指定,也没必要关心。
@H_
502_0@下边的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\n",user[i]
scaled = 60 * user[i] / maxm ;
printf "%-10.10s [%8d]:",user[i]
for (i=0; i<scaled; i++) {
printf "#";
}
printf "\n";
}
}
@H_
502_0@本例中还用到了printf的格式化
输出,这里不展开说明。
@H_
502_0@awk内的算术运算默认都是浮点型的,除非通过
调用內建
函数int(),
显示指定为整型。
@H_
502_0@本例中做的是浮点运算,所以,只要变量scaled不为零,for循环体就至少会执行一次,类似下边的"bin"一行,虽然寥寥68个
文件,也还是会
显示一格#
$ ls -lR /usr/local | awk -f hist.awk
bin [ 68]:#
albing [ 1801]:#######
root [ 13755]:##################################################
man [ 11491]:##########################################
$
@H_
502_0@至于各
用户名输出时的排列顺序,如前所述,是由建立哈希时的内在机制决定的,你无法干预。
@H_
502_0@如果非要干预(比如希望按字典序,或
文件数量)排列的话,可以这样实现:将脚本结构一分为二,将第一部分的
输出先送给sort做排序,然后再通过管道送给打印直方图的第二部分。
@H_
502_0@最后,再通过一个小例子,结束awk的介绍。
@H_
502_0@这个简短的脚本,打印出包含关键字的段落:
$ cat para.awk
/关键字/ { flag=1 }
{ if (flag == 1) { print $0 } }
/^$/ { flag=0 }
$
$ awk -f para.awk < 待搜索的文件
@H_
502_0@段落(paragraph),是指两个空行之间所有的文本。空行表示段落的结束
@H_
502_0@
/^$/
会匹配空行。但是,对那些含有空格的“空行”,更精确的匹配是像这样:
/^[:blank:]*$/