今天木有冷笑话,只有一个噩耗。噩耗是:今天木有冷笑话!!!
不要总想着冷笑话嘛,有点追求,听毛主席的话:好好学习,天天向上!
第七章输入输出、文件与命令执行
学C的应该了解标准输入输出和错误输出吧?感觉总打很多字进度太慢,所以一直在省略类似C的东西,也方便以后看这篇文章的人能够快速学完shell脚本(或者是快速看完这本书)。
读取行read命令是重要方式之一,它可以自标准输入读取行后,通过shell字段切割的功能(使用$IFS)进行切分,第一部分给第一个变量,第二部分给第二个,类推。如果切割单词多余变量,则剩下所有的给最后一个变量。如果输入行以反斜杠结尾,则read会丢弃反斜杠与换行符继续读取下行数据。它有一个选项 -r,它将忽视最后反斜杠当读入数据。使用read可能的一个错误是通过循环让read读取一个文件如:
while IFS=: read user pass uid gid fullname homedir shell < /etx/passwd ... 这个循环将一直下去并且每次read只读passwd的第一行。因为每次循环都重新打开了passwd文件读取内容。解决办法是: cat /etc/passwd | while IFS=: read ....... 这样通过管道解决读取文件问题。 这里有一个概念,文件描述符,一般这个文件描述符是由0-9这几个数字来描述的,所以传统上shell也就允许你最多打开十个文件。比如make 1> results 2> ERRS 。命令make的标准输出(文件描述符1)传给results,并将错误输出(文件描述符2)传给ERRS。设置完文件描述符后,如何引用呢?像这样:make > results 2>&1 。
1> results这里的1其实没必要,供输出重定向的默认文件描述符是标准输出,也就是文件描述符1,重定向 > results让文件描述符1作为文件results,接下来重定向2>&1有两部分,2>重定向文件描述2,也就是标准错误输出。而&1就是刚才我们的疑问,用来引用我们定义的文件描述符。特别注意:2>&1这样的四个字符一定要连着写。
再介绍一个用来改变shell本身I/O设置的exec命令。如果只有I/O重定向而没有任何参数时,exec会改变shell的文件描述符:
上例展示了如何关闭文件描述符。
exec还有一个功能就是在当前shell下执行指定的程序。
书中对printf做了完整的介绍,这里就不再介绍了,就是C里边的那些东西。
shell中有两种与文件名相关的展开:第一个是波浪号展开(~ tilde expansion),另一个叫法较多如通配符展开式(wildcard expansion)、全局展开(globbing)或路径展开(pathname expansion)。
如果命令行字符第一个字符为波浪号或者变量指定的值里任何未被引号括起来的冒号之后的第一个字符为波浪号时,shell便会执行波浪号展开。波浪号展开的目的,是要将用户根目录的符号型表示方式,改为实际的目录路径。
shell环境下的通配符展开,有几个基本的通配符:? , * , [set] [!set] ,前俩略过,第三个是匹配出现中括号集合中的字符,第四个取反义。比如可以查找 *.html 就知道处所有类似的文件。另外有一点注意的,在linux下文件名里的.号没有任何特殊意义,匹配所有文件时只需用一个*即可,不用像windows下那样*.*。
习惯上,当执行通配符展开时,shell会忽略文件名开头为一个点号的文件。像这样的点号文件(dot files)通常用作程序配置文件或启动文件。
命令替换,书上写的概念很绕口,其实就是一个命令的用法或者写法,例如:
echo outer `echo inner1 \`echo inner2 \` `
这样输出结果就是 outer inner1 inner2 类似命令嵌套,从最内层开始执行。注意是反单引号,键盘1左边与波浪号同键那个。但是这样嵌套多了之后会非常难以阅读,就出现了新的语法:
$ echo outer $(echo inner1 $(echo inner2) inner1) outer
这样输出结果就是outer inner1 inner2 inner1 outer。这样清晰多了。
书中教了一个expr命令,从提到这个命令,到接下来的两段都在说这个命令不好用,并且可以是由$(( )) ,test替代。但是可以了解一下,作用就是计算之后跟着的一个表达式比如:expr 1 + 1 。这里注意加号两边的空格,是必要的,书里貌似没说,郁闷半天才发现必须添加空格。。- -!
这里又提了引用,就是说用来防止shell将某些东西解释成你不想要的意义,比如你就是有就想要*,而不是需要一个通配符,这时候你需要转义(\) ,或者是单引号引起来(单引号引起来的内容转义符号也无效),或是双引号。混用的时候请小心。
书中详细说了一下命令的执行顺与,感觉很有必要细看一下,全都摘录一下。
shell从标准输入或脚本中读取的每一行成为管道;它包含了一个或多个命令,这些命令被零或多个管道字符隔开。事实上还有很多特殊符号可用来分隔单个的命令:分号;、管道|、&、逻辑AND&&、还有逻辑OR||。对于每一个读取的管道,shell都会将命令分割,为管道设置I/O,并且对每一个命令依次执行下面操作:
1、将命令分割成token,是以固定的一组meta字符分隔,有空格、制表符、换行字符、;、(、)、<、>、|、和&。token的种类包括单词、关键字、输出入重定向器,以及分号。这是微妙的,但是变量、命令还有算符替换,都可以在shell执行token认定的时候被执行。这就是为什么先前所举例子vi ~$user/.profile中波浪符号可以展开像预期的那样工作。
2、检查每个命令的第一个token,看看是否它是不带有引号或反斜杠的关键字。如果它是一个开放的关键字(if 或者 { (之类的),则这个命令其实是一个复合命令。shell为复合命令进行内部的设置,读取下一条命令,并再次启动进程。如果关键字非复合命令的开始符号(例如,它是控制结构的中间部分,像then、else或do,或是结尾部分,例如fi,done或逻辑运算符),则shell会发出语法错误的信号。
3、将每个命令的第一个单词与别名列表对照检查。如果匹配,它便代替别名的定义,并回到步骤1;否则,进行步骤4(别名是给交互式shell使用)。回到步骤1,允许让关键字的别名被定义:例如alias aslongas=while or alias procedure=function。注意,shell不会执行递归的别名展开,反而当别名展开为相同的命令时它会知道,并停止潜在的递归操作。可以通过引用要保护的单词的任何部分而禁止别名展开。
4、如果波浪号字符出现在单词的开头处,则将 波浪号替换成用户的跟目录$HOME,将~user替换成user的根目录。
波浪号替换会发生在下面的位置:
* 在命令行里,作为单词的第一个未引用字符
* 在变量赋值中的=之后以及变量赋值中的任何:之后
* 形式${ varibale op word } 的变量替换里的word部分
5、将任何开头为$符号的表达式,执行参数(变量)替换。
6、将任何形式为$(string)或 `string`的表达式,执行命令替换。
7、执行形式$((string))的算术表达式。
8、从参数、命令与算术替换中取出结果行的部分,再一次将它们切分为单词。这次它使用$IFS里的字符作为定界符,而不是使用步骤1的那组meta字符。通常,在IFS里连续多个重复的输入字符是作为单一定界符,这是你所期待的。这只有对空白字符而言是真的。对非空白字符,则不是这样的。举例说,当读取以冒号分隔字段的/etc/passwd文件时,两个连续冒号所界定的是一个空子段。
9、对于*、?以及一对[...]的任何出现次数,都执行文件名生成的操作,也就是通配符展开。
10、使用第一个单词作为一个命令,遵循查找次序,也就是,先作为个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,以及最后作为查找$PATH找到的第一个文件。
11、在完成I/O重定向与其他同类型事项之后,执行命令。
shell程序碰到一句命令,都会执行上边的一次流程。比如:
命令一开始会根据shell语法分割token,最重要一点是I/O重定向 > out 在这里是被识别的,并存储供稍后使用。最后这句echo被分为5个token,分别是: echo ,~+/${f}[12] , $y , $(echo cmd subst) ,$((3 + 2))这5个部分。
然后检查第一个单词echo是否为关键字,例如if或for,这里不是所以命令不变继续处理。
检查第一个单词依然是echo 是否为别名,这里不是,继续执行。
扫描所有单词是否需要波浪号展开,本例中,~+为ksh93与bash的扩展,等同于$PWD,也就是当前的目录。token 2将被修改变成如下:
echo /tmp/x/${f}[12] $y $(echo cmd subst) $((3 + 2))
下一步变量展开:token2与token3被修改变成:
echo /tmp/x/f[12] a b $(echo cmd subst) $((3 + 2))
再来要处理的是命令替换。注意,这里可递归引用列表里的所有步骤!在次例中,因为我们试图让所有的东西容易理解,因此命令替换修改了token4,结果如下:
echo /tmp/x/f[12] a b cmd subst $((3 + 2))
现在执行算术替换,结果如下:
echo /tmp/x/f[12] a b cmd subst 5
前面所有的展开产生的结果,都将再一次被扫描,看看是否有#IFS字符,如果有则它们是作为分隔符,产生

