shell脚本学习指南[四](Arnold Robbins & Nelson H.F. Beebe著)
本文导语: 回忆起一件事情:之前用linux寻找中文输入法的时候,在百度输入了fcitx,然后结果上边有个,您要找的是不是: 讽刺腾讯 。本来一直记不住这个输入法名字,不过以后哥就记住这个输入法的名字是怎么拼了,感谢百度。 第九...
回忆起一件事情:之前用linux寻找中文输入法的时候,在百度输入了fcitx,然后结果上边有个,您要找的是不是: 讽刺腾讯 。本来一直记不住这个输入法名字,不过以后哥就记住这个输入法的名字是怎么拼了,感谢百度。
第九章awk的惊人表现
awk的调用可以定义变量、提供程序并且指定输入文件,语法:
awk [ -F fs ] [ -v var=value ... ] 'program' [ -- ] [ var=value ... ] [file(s) ]
awk [ -F fs ] [ -v var=value ... ] -f programfile [ -- ] [ var=value ... ] [ file(s) ]
短程序通常直接在命令行上提供,而比较长的程序则委托-f选项指定,可以重复使用此选项。如果命令行未指定文件名,则awk会从标准输入读取。 -- 是特殊选项,指出awk本身已经没有更进一步的命令行选项。任何接下来的选项都可被你的程序使用。
-F选项是用来重新定义默认字段分隔字符,且一般惯例将它作为第一个命令选项。紧接-F选项后的fs参数是一个正则表达式或是被提供作为下一个参数。字段分隔字符也可以设置使用内建变量FS所指定。如:
awk -F 't' '{ ... }' files FS="[fv]" files
上边例子-F选项设置的值,应用到第一个文件组,而由FS指定的值,则应用到第二个组。初始化的-v选项必须放在命令行上直接给定的任何程序之前,他们会在程序启动前生效。在一命令行程序之后-v选项会被解释为一个文件名。在命令行上其他地方的初始化会在处理参数时完成,并且会带上文件名,如:
awk '{...}' Pass=1 *.tex Pass=2 *.tex
处理文件的列表两次,第一次Pass设为1,第二次为2。使用字符串值进行初始化无须用引号框起来,除非shell要求这样的引用以保护特殊字符或空白。
特殊文件名-(连字符)表示标准输入。大部分现代的awk实现(不包括POSIX)都认定特殊名称/dev/stdin为标准输入,即使主机操作系统不支持该文件名。同样:/dev/stderr与/dev/stdout可用于awk程序内,分别表示标准错误输出与标准输出。
一般awk命令模式或操作可省略一个,如果模式省略,则每条输入都被操作;如果操作省略,则默认操作为输出匹配模式的记录。虽然模式多半是数字或字符串表达式,不过awk以保留自BEGIN与END提供两种特殊模式。
与BEGIN关联的操作只会执行一次,在任何命令行文件或一般命令行赋值被处理之前,但是在任何开头的-v选项指定已完成之后。它大部分是用来处理程序所需要的任何特殊初始化工作。END操作也是只执行一次。用于所有输出数据已被处理完之后。BEGIN和END模式可以是任意顺序,可以存在awk程序内任何位置。当指定多个BEGIN或END模式,则他们将按照在awk程序里的顺序执行。
awk提供了标量与数组两种变量以保存数据、数字与字符串表达式,还提供了一些语句类型以处理数据:赋值、注释、条件、函数、输入、循环及输出。awk表达式许多功能与c语言相似。awk里注释是从#开始到行尾。跨行语句需要在结尾处加上反斜杠。
awk里的字符串常数是以引号定界,字符串可包含任何8bit的字符除了控制字符NUL以外。因为NUL在底层实现语言(C)里,扮演的是一个字符串中断字符的角色。awk字符串长度视内存而定。反斜杠转义序列允许非打印字符的表示。
awk提供了许多内建函数,可以在字符串上执行,之后再详细说,这会说两个length(string)返回string内的字符数。字符串的比较用的是传统的关系运算符:==、!=、=。比较不同长度的字符串,且其中一个字符串为另一个的初始子字符串时,较短的定义为小于较长的那个。在shell里字符串连接可以直接进行,不需要连接符号。
awk功能强大的地方大多来自于它对正则表达式的支持。有两个运算符:~(匹配)与!~(不匹配)让awk更容易使用正则表达式:"ABC" ~ "^[A-Z]+$"结果为真,正则表达式常量可以用引号或斜杠加以定界:/^[A-Z]+$/。注意如果有字面意义的符号,需要反斜杠来转义。
awk里的数字,都以双精度浮点值表示,如1/32 写成0.03125、3.125e-2等,awk里没有提供字符串转数字的函数,不过想做到也很简单,只要加个零到字符串里,如:s = "123" , n = 0 + s 。这样123便赋值给n了。一般"+123ABC"转化为123,而"ABC123"与""都转化为0。即使awk里所有的数值运算都是在浮点算术内完成,整数值还是可以表示的,只要值不太大,这个值限定在53位,即2^53即9千万亿的样子。awk的数值运算符没有位运算符,多一个指数运算符(^ 或 ** 或 **=,但是避免使用**和*=,它不是POSIX awk的一部分)它是右结合性的,且与赋值运算符是仅有的右结合性运算符。比如a^b^c^d运算顺序是a^(b^(c^d))。awk里的取余运算测试了 5 % 3 是2 ; 5 % -3 是2; -5 % 3 是-2; -5 % -3是-2;发现取余的结果取决于被取余的数的正负。还有一个内建函数:
int(x) 对x取整
rand 取 0到1之间的随机数
srand(x) 设置x为rand的新输入值
cos(x) 给出x的余弦值
sin(x) 给出x的正弦值
atan2(x,y) 给出y/x的正切值
exp(x) 给出e的x次幂
log(x) 给出x的常用对数值(基为e)
sqrt(x) 给出x的正平方根值
exit(x) 结束awk程序,若有x值,则返回x,否则返回0.
index(s,t) 返回t在s中的第一个开始位置,如t不是s的子串,则返回0]
length(x) 求x的长度(字符个数)
substr(s,x,y) 在字符串s中取得从x个字符开始的长度为y的子字符串.
awk内置字符串函数
gsub(r,s) 在整个$0中用s替代r
gsub(r,s,t) 在整个t中用s替代r
index(s,t) 返回s中字符串t的第一位置
length(s) 返回s长度
match(s,r) 测试s是否包含匹配r的字符串
split(s,a,fs) 在fs上将s分成序列a
sprint(fmt,exp) 返回经fmt格式化后的exp
sub(r,s) 用$0中最左边最长的子串代替s
substr(s,p) 返回字符串s中从p开始的后缀部分
substr(s,p,n) 返回字符串s中从p开始长度为n的后缀部分
awk提供许多内建变量,都是大写名称,时常用到的几个有:
FILENAME 当前输入文件的名称
FNR 当前输入文件的记录数
FS 字段分隔字符(正则表达式)(默认为:" ")
NF 当前记录的字段数
NR 在工作中的记录数
OFS 输出字段分隔字符(默认为:" ")
ORS 输出记录分隔字符(默认为:"n")
RS 输入记录分隔字符(仅用于gawk与mawk里的正则表达式)(默认为:"n")
awk允许的测试:
x==y x等于y?
x!=y x不等于y?
x>y x大于y?
x>=y x大于或等于y?
x x >= < showargs.awk
BEGIN{
print "ARGC = ",ARGC
for ( k = 0 ; k < ARGC ; k++)
print "ARGV[" k "] = [" ARGV[k] "]"
}
$ awk -v One=1 -v Two=2 -f showargs.awk Three=3 file1 Four=4 file2 file3
ARGC = 6
ARGV[0] = [awk]
ARGV[1] = [Three=3]
ARGV[2] = [file1]
ARGV[3] = [Four=4]
ARGV[4] = [file2]
ARGV[5] = [file3]
正如C/C++中,参数存储在数组项目0、1....、ARGC-1中,第0个项目是awk程序本身的名称。不过与-f 和 -v选项结合性的参数是不可使用的。同样的,任何命令行程序也不可使用:
$ awk 'BEGIN{for(k=0;k 72 { print FILENAME":"FNR":"$0}' file(s)
awk支持语句的连续执行。支持条件语句,if else 类似C语言,支持循环 while(){} 或do{} while()或for( ; ; ){] 类似c语言。还有一个for(key in array) { } 。
如 awk 'BEGIN { for( x=0; x:h2>:g
...
cd top level web site directory
find . -name '*.html' -o -name '*.htm' -type f |
while read file
do
echo $file
mv $file $file.save
sed -f $HOME/html2xhtml.sed < $file.save > $file
done
书中说了一小节寻找问题文件,意思是文件名里有特殊字符,可以实用find -print0 来解析,但是没搞明白说这些是干嘛用的。
然后介绍了一个执行命令xargs,是为了处理给脚本传参过长的问题,不如有时候我们会写寻找字符串的命令如下:
$ grep POSIX_OPEN_MAX /dev/null $(find /usr/include -type f | sort )
我们在后边一堆文件中寻找 POSIX_OPEN_MAX这样的一个字符串。如果后边find出来的文件很少,那很好,这条命令就会顺利执行,但是如果过长会给出提示:****:Argument list too long. 这样子。我们可以通过getconf ARG_MAX来查看你的系统允许的最大值是多少。上边这条命令有一个文件是空文件/dev/null,这是为了防止find没找到任何文件使grep进入从标准输入获取信息的空等状态,也为了使grep命令有多个文件参数而使结果可以显示文件名和出现的行数。
我们可以解决这样的一个参数过长的问题通过开始提到的xargs命令,如:
$ find /usr/include -type f | xargs grep POSIX_OPEN_MAX /dev/null
这里xargs如果未取得输入文件名,则会默认终止。GNU的xargs支持--null选项:可处理GNU find的-print0选项所产生的NUL结尾的文件名列表。xargs将每个这样的文件名作为一个完整参数,传递给它执行的命令,而没有shell(错误)解释问题或换行符号混淆的危险,然后是交给其后的命令处理它的参数。另外xargs的选项可以控制哪些参数需要被替换,还可以限制传递的参数个数等。
如果了解文件系统的空间信息,我们可以通过find和ls命令配合awk程序协助就可办到,比如:
$ find -ls | awk '{sum +=$7} END {printf("Total: %.0f bytesn",sum)}'
但并不好用,编码长不说还不知道可用空间。有两个好用的命令来解决这一需求:df和du。
df(disk free)提供单行摘要,一行显示一个加载的文件系统的已使用和可实用空间。显示单位具体看相应版本。可以实用-k强制实用kilobytes单位。还有一个选项-l 仅显示本地文件系统,排除网络加载的文件系统。还有-i选项,提供访问inode使用量。GNU的df还提供-h(human-readable)选项,方便阅读。还可以提供一个或多个文件系统名称或加载点来限制输出项目:$ df -lk /dev/sda6 /var 。
du会摘要文件系统的可用空间,但是不会告诉你某个特定的目录树需要多少空间,这是du(disk usage)的工作。不同系统可能有所不同,-k控制单位,-s显示摘要。
GNU版本提供-h,同df。du可以解决的一个常见问题是:找出哪个用户用掉最多的系统空间:$ du -s -k /home/users/* | sort -k1nr | less
假设用户目录全部放在/home/users下。
关于比较文件好用的两个命令cmp和diff。cmp直接后边跟两个文件参数即可,如果不同输出结果会指出第一个不同处的位置,相同没有任何输出。-s可以抑制输出,可以通过$?来查看离开状态码,非零表示不同。diff惯例是将旧文件作为第一个参数,不同的行会以前置左尖括号的方式,对应到左边文件,而前置右尖括号则指的是右边的文件。还有一个扩展是diff3,比较3个文件。
有时候需要修复不同的地方,patch命令提供了十分方便的做法:
$ echo test 1 > test.1
$ echo test 2 > test.2
$ diff -c test.[12] > test.dif
$ patch < test.dif
此时你查看test.1会发现里边的内容已经变为test 2了。patch会尽可能套用不同之处,然后报告失败的部分,由用户自行处理。虽然patch也可以处理一般的diff输出,但是常规都是处理diff -c选项的信息。
如果有你怀疑有很多文件有相同的内容,实用cmp或diff就十分麻烦。这时可以实用file checksum(文件校验和),取得近似线性的性能完成这一繁琐的工作。有很多工具可以提供,如:sum、cksum、checksum,消息摘要工具md5与md5sum,安全性三列(secure-hash)算法工具sha、sha1sum、sha256以及sha384。可惜的是:sum的实例在各个平台都不想同,使得他们的输出无法跨越不同unix版本进行文件校验和的比较。一般的会这样:
$ md5sum /bin/l?
57e35260b8e017f4696b8559ed4f32f2 /bin/ln
0f68a291df1a708e130e0f407a67e9ca /bin/ls
输出结果有32个十六进制数,等同128位,因此两个不同文件最后散列出相同签名的可能性非常低。了解这个后可以写一个简单脚本来实现我们之前的目标了。
#! /bin/sh -
# 根据他们的MD5校验和,显示某种程度上内容机会一直的文件名
#
# 语法:
# show-indentical-files files
IFS='
'
PATH=/usr/local/bin:/usr/bin:/bin
export PATH
md5sum "$@" /dev/null 2> /dev/null |
awk '{
count[$1]++
if( count[$1] ==1 ) first[$1]=$0
if( count[$1] ==2 ) print first[$1]
if( count[$1] >1 ) print $0
}' |
sort | awk '{
if ( last != $1 ) print ""
last = $1
}'
程序很简单,就不弄注释了吧。可以测试一下:
$ show-indentical-files /bin/*
发现好多命令都很能装啊,其实内容都一样的 - -!。
这里说一下数字签名验证,很有用。
软件发布的时候,一般会包含分发文件的校验和,这可以让你方便得知所下载的文件是否与原始文件匹配。不过单独的校验和不能提供验证(verification)工作:如果校验和被记录在你下载软件里的另一个文件中,则攻击者可以恶意的修改软件,然后只需要相应的修改校验和即可。
这个问题的解决方案是公钥加密(public-key cryptography)。在这种机制下,数据的安全保障来自两个相关密钥的存在:一个私密密钥,只有所有者知悉,以及一个公开密钥,任何人都可得知。两个密钥的其中一个用以加密,另一个则用于解密。公开密钥加密的安全性,依赖已知的公开密钥及可被该密钥解密的文本,以提供一条没有实际用途的信息但可被用来回复私密密钥。这一发明最大的突破是解决了一直以来密码学上极为严重的问题:在需要彼此沟通的对象之间,如何安全的交换加密密钥。
私密密钥与公开密钥是如何使用和运作的呢?假设Alice想对一个公开文件签名,她可以使用她的私密密钥(private key)为文件加密。之后Bob再使用Alice的公开密钥(public key)将签名后的文件解密,这么一来即可确信该文件为Alice所签名,而Alice无须泄漏其私密密钥,就能让文件得到信任。
如果Alice想传送一份只有Bob能读的信给他,她应以Bob的公开密钥为信件加密,之后Bob再使用它的私密密钥将信件解密。只要Bob妥善保管其私密密钥,Alice便可确信只有Bob能读取她的信件。
对整个信息加密其实是没有必要的:相对的,如果只有文件的校验和加密,它就等于有数字签名(digital signature)了。如果信息本身是公开的,这种方法便相当有用,不过还需要有方法验证它的真实性。要完整说明公开密钥加密机制,需要整本书才行,可参考《安全性与密码学》。
计算机越来越容易受到攻击,下载文件或软件要很注意安全。一般软件存档文件都并入了文件校验和信息的数字签名,如果不确定下载的东西是否安全,可以验证它。举例:
$ ls -l coreutils-5.0.tar*
-rw-rw-r-- 1 jones devel 6020616 Apr 2 2003 coreutils-5.0.tar.gz
-rw-rw-r-- 1 jones devel 65 Apr 2 2003 coreutils-5.0.tar.gz.sig
$gpg coreutils-5.0.tar.gz.sig #尝试验证此签名
gpg: Signature made Wed Apr 2 14:26:58 2003 MST using DSA key ID D333CBA1
gpg: Can't check signature: public key not found
验证失败,是因为我们还未将签名者的公开密钥加入gpg密钥环。我们可以在签名作者的个人网站找到公开密钥或者通过email询问。然而幸好使用数字签名的人多半会将他们的公开密钥注册到第三方(thrid-party)的公开密钥服务器,且该注册会自动地提供给其他的密钥服务器共享。
将密钥内容存储到临时文件如”temp.key",加到密钥环中:
$ gpg --import temp.key
然后就可以成功验证签名了。