Shell脚本学习指南(六)——输入/输出、文件与命令执行


前言

本篇博客将完成介绍Shell语言。首先讨论的是文件:如何以不同的方式处理输入/输出和产生文件名。接着是命令替换,也就是让你使用一个命令的输出作为命令行的参数。然后,我们继续将重点放在命令行上,讨论Shell提供的各类引用。最后,则是深入讨论命令执行顺序,并针对内建于Shell里的命令作介绍。

标准输入、标准输出与标准错误输出

标准输入/输出可能是软件工具设计原则里最基本的观念了。它的构想是:程序应有一个数据来源、数据出口、以及报告问题的地方。它们分别叫做标准输入、标准输出、标准错误输出。程序应该不知道也不在意其输入与输出背后是那一种设备,这些设备可能是磁盘文件、终端、磁带机、网络连接、或者甚至另一个执行中的程序!程序可以预期,在它启动的时候,这些标准位置都已打开,且已经准备好可以使用了。

有很多UNIX程序都遵循这一设计理念。默认情况下,它们会读取标准输入、写入标准输出,并将错误信息传递给标准错误输出。正如我们在前面第5章所见到的,这样的程序我们称它为过滤器,因为它们“过滤”数据流,每一个都会在数据流上执行某种运算,再通过管道,将它传递给下一个。

使用read读取行

read命令是将信息传递给Shell程序的重要方式之一:

在这里插入图片描述

read可以一次读取所有的值到多个变量里。这种情况下,在$IFS里的字符会分割输入行里的数据,使其成为各自独立的单词。例如:

printf "Enter name, rank, serial number:"
read name rank serno 

最典型的用法是处理/etc/passwd文件。其标准格式为7个以冒号分隔开的字段:用户名称、加密的密码、数值型用户ID、数值模型ID、全名、根目录与登录Shell。例如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IkSOZd1c-1670490114191)(file://C:\Users\g700382\AppData\Roaming\marktext\images\2022-12-05-14-40-23-image.png)]

你可以使用简单的循环逐行处理/etc/passwd:

while IFS=: read user pass uid gid fullname homedir Shell
do
    ...   #处理每个用户的行
done < /etc/passwd

这个循环并不是说“当IFS等于冒号时,便读取…”,而是通过IFS的设置,让read使用冒号作为字段的分隔字符,而并不影响循环体里使用的IFS值,也就是说,它只改变read所继承环境内的IFS值。这一点在前面以做过说明。

当遇到输入文件结尾时,read会以非0值退出。这个操作将终止while循环。

咋看之下可能会觉得将/etc/passwd的重定向防止与循环体的结尾有点奇怪,不过这是必需的,这样一来,read才会在每次循环的时候看到后续的行。如果循环写成这个样子:

#重定向的不正确使用
while IFS=: read user pass uid gid fullname homedir Shell < /etc/passwd
do
    ...  #处理每个用户的行
done

就永远不会终止了!每次循环时,Shell都会再打开/etc/passwd一次,且read只读取文件的第一行!

while read .... do ... done < file还有一种替代方式,即在管道里把cat和循环一起使用:

# 较容易读取,不过使用cat会损失一点效率
cat /etc/passwd |
    while IFS=: read user pass uid gid fullname homedir Shell
    do
        ....
    done

有一个常见的小技巧:任何命令都能用来将输入管道传送给read。当read用在循环中时,这一技巧格外有用。我们曾展示过下面这个简单的脚本,用来复制整个目录树:

find /home/tolstoy -type d -print | #寻找所有目录
    sed 's;/home/tolstoy/;/home/lt/;' | # 更改名称;留意使用的是分号定界符
        sed 's/^/mkdir /'    |  #插入mkdir命令
        sh -x  #以Shell的跟踪模式执行

上面的例子,其实可以很容易地完成,而且从Shell程序员的观点来看更加自然,也就是使用循环:

find /home/tolstoy -type d -print
    sed 's;/home/tolstoy;/home/lt/;'
       while read newdir
        do
            mkdir $newdir
        done

(我们注意到这个脚本并不完美:特别是它无法保留原始目录的所有权与使用权限)

如果输入单词多于变量,最后剩下的单词全部被指定给最后一个变量。理想的行为应转义这个法则:使用read搭配单一变量,将一整行的输入读取到该变量中。

很早以前,read默认的行为便是将输入行的结尾反斜杠看做续行的指示符。这样的一行会使得read舍弃反斜杠与换行字符的结合,且继续读取下一个输入行:

$ printf "Enter name, rank, serial number:" ; read name rank serno
Enter name, rank, serial number: Jones\
> Major \
> 123-45-6789
$ printf "Name: %s, Rank:%s, Serial number:%s\n" $name $rank $serno
Name: Johs, Rank: Major, Serial number: 123-45-6789

偶尔你还是会想要读取一整行的时候,而不管哪一行包含了什么。-r选项可以实现此目的(-r选项是特定于POSIX的,许多Bourne Shell不支持),当给定-r选项时,read不会将结尾的反斜杠视为特殊字符:

$read -r name rank serno
tolstoy \                      #只提供两个字段
$ echo $name $rank $serno   
tolstoy                        #$serno是空的

关于重定向

我们已经介绍且使用过基本的输出入重定向运算符:<、>、>>,以及|。本节,我们要看看还有那些运算符可以使用,并介绍文件描述符处理的重要话题。

额外的重定向运算符

这里是Shell提供的额外运算符:

  • 使用set -C搭配

    • POSIX Shell提供了防止文件意外截断的选项:执行set -C命令可打开Shell所谓的禁止覆盖选项,当它在打开状态下时,单纯的>重定向遇到目标文件已存在时,就会失败。>|运算符则可令noclobber选项失效。
  • 提供行内输入的<<与<<-

    • 使用program << delimiter,可以在Shell脚本正文内提供输入数据。这样的数据叫做嵌入文件。默认情况下,Shell可以在嵌入文件正文内做变量、命令和算术替换:
cd /home        #移到根目录的顶端
du -s * |       #产生原始磁盘用量
    sort -nr |  #以数字排序,最高的在第一个
    sed 10q  |  #在前10行之后就停止
    while read amount name
    do
        mail -s "disk usage warning" $name << EOF
Greetings. You are one of the top 10 consumers of disk space
on the system. Your home directory uses $amount disk blocks.

Please clean up unneeded files, as soon as possiable.

Thanks,
Your friendly neighborhood system administrator.
EOF
    done

这个范例将电子邮件发送给系统上前十名的“磁盘贪婪户”,要求它们清理自己的根目录(以我们的经验来说,这种信息多半是没有用,不过这么做会让系统管理人员觉得好过些)。

如果定界符以任何一种形式的引导括起来,Shell便不会处理输入的内文:

$ i=5                                        #设置变量
$ cat << 'E'OF                               #定界符已被引用
>This is the value of i: $i                  # 尝试变量参照
>Here is a command substitution: $(echo hello, world) #命令替换
>EOF
This is the value of i: $i                   #冗长式地显示文字
Here is a command substitution: $(echo hello, world)

嵌入文件重定向器的第二种形式有一个负号结尾。这种情况下,所有开头的制表符(Tab)在传递给程序作为输入之前,都从嵌入文件与结束定界符中删除(注意:只有开头的制表符会被删除,开头的空格则不会删除)。这么做,让Shell脚本更易于阅读了,让我们来看看下面的例子通知程序修正版:

cd /home        #移到根目录的顶端
du -s * |       #产生原始磁盘用量
    sort -nr |  #以数字排序,最高的在第一个
    sed 10q  |  #在前10行之后就停止
    while read amount name
    do
        mail -s "disk usage warning" $name << -EOF
        Greetings. You are one of the top 10 consumers of disk space
        on the system. Your home directory uses $amount disk blocks.

        Please clean up unneeded files, as soon as possiable.

        Thanks,
        Your friendly neighborhood system administrator.
        EOF
    done
  • <>打开一个文件作为输入与输出之用

    • 使用program <> file,可供读取与写入操作。默认是在标准输入上打开file。一般来说,<以只读模式打开文件,而>以只写模式打开文件。<>运算符则是以读取与写入两种模式打开给定的文件。这交由program确定并充分利用;实际上,使用这个操作符并不需要太多的支持。

文件描述符处理

在系统内部,UNIX是以一个小的整数数字,称为文件描述符,表示每个进程的打开文件。数字由零开始,至多到系统定义的打开文件数目的限制。传统上,Shell允许你直接处理至多10个打开文件:文件描述符从0至9(POSIX标准将是否可以处理大于9的文件描述符,保留给自行定义。bash可以,但ksh则否)。

文件描述符0、1与2,各自对应到标准输入、标准输出以及标准错误输出。如前所述,每个程序都是从附加到终端的这些文件描述符开始(不管是真的终端还是虚拟终端,例如X window)。到目前为止,最常见的操作便是变更三个文件描述符其中一个的位置,不过也可能处理其他的变动。首先来看的是:将程序的输出传送到一个文件,并将其错误无信息传输到另一个文件:

make 1> results 2> ERRS

上面的命令是将make的标准输出传给results,并将标准错误输出传给ERRS(make不会知道这之间的差异:它不知道也不关心,也并未传送输出或错误信息到终端)。将错误信息捕捉一个单独的文件里是一种很实用的做法,你之后可以使用分页程序查阅它们,或使用编辑器修正问题。否则,大量输出的错误信息会快速的卷过屏幕画面,你会很难找到需要的信息。另一种不同的方式就更利落了,直接舍弃错误信息;

make 1> results 2> /dev/null

1>results里的1其实没有必要,供输出重定向的默认文件描述符是标准输出:也就是文件描述符1。下个例子会将输出与错误信息送给相同的文件:

make > results 2>&1

重定向> results让文件描述符1(标准输出)作为文件results,接下来的重定向2>&1有两部分。2>重定向文件描述符2,也就是标准错误输出。而&1是Shell语法:无论文件描述符1在哪里。本例中,文件描述符1是results文件,所以哪里就是文件描述符2要附加的地方。需要特别留意的一点是:命令行上,这4个字符2>&1必须连在一起,中间不能有任何空格。

在此,顺序格外重要:Shell处理重定向时,由左至右。来看看此例:

make 2>&1 >results

上述命令,Shell会先传送标准错误信息到文件描述符1,这时仍为终端,然后文件描述符1(标准输出)被改为results。更进一步,Shell会在文件描述符重定向之前处理管道,使得我们得以将标准输出与标准错误输出都传递到相同的管道:

make 2>&1 | ...

最后要介绍的是可用来改变Shell本身I/O设置的exec命令。使用时,如果只有I/O重定向而没有任何参数时,exec会改变Shell的文件描述符:

exec 2> /tmp/$0.log    #重定向Shell本身的标准错误输出
exec 3> /some/file     #打开新文件描述符3
...
read name runk serno <3  #从该文件读取

在这里插入图片描述

搭配上参数,exec还能起到另一个作用,即在当前Shell下执行指定的程序。换句话说,就是Shell在其当前进程中启动新程序。例如,想使用Shell做选项处理但大部分仍要由一些其他程序来完成时,你可以用这个方式:

while [ $# -gt 1 ]    #循环遍历参数
do
    case $1 in
    -f)               # code for -f here
        ;;
    -q)               # code for -q here
        ;;
    ...
    *)    break ;;    #没有选项,中断循环
    esac
    shift              #移到下一个参数
done
exec real-app -q "$qargs" -f "$fargs" "$@"  #执行程序
echo real-app failed, get help! 1>&2        #紧急信息

使用此法时,exec为单向操作。也就是说:控制权不可能会回到脚本。唯一的例外只有在新程序无法被调用时,在该情况下,我们会希望有"紧急"代码,可显示信息,再完成其他可能的清楚工作。

printf的完整介绍

前面我们介绍过printf命令,下面我们将完整地介绍它。

printf命令的完整语法有两个部分:

Printf format-string [arguments...]

第一部分为描述符规格的字符串,它的最佳提供方式是放在引号内的字符串常数。第二部分为参数列表,例如字符串或变量值的列表,该列表需与格式规格相对应。格式字符串结合要以字面意义输出的文本,它使用的规格是描述如何在printf命令行上格式化一连串的参数。一般字符都按照字面上的意义输出。转义序列会被解释(与echo相似),然后输出为相对应的字符。格式指示符是以%字符开头且由已定义的字母集之一作为结尾,用来控制接下来相对应参数的输出。printf的转义序列见下表:

在这里插入图片描述

printf的转义序列

序列 说明
\a 警告字符,通常为ASCII的BEL字符
\b 后退
\c 抑制(不显示)输出结果中任何结尾的换行字符;而且,任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符,都被忽略
\f 换页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\\ 一个字面上的反斜杠字符
\ddd 表示1到3位数八进制值的字符。仅在格式字符串中有效
\0ddd 表示1到3位的八进制值字符

printf对转义序列的处理可能会让人觉得混淆。默认情况下,转义序列只在格式字符串中会被特别对待,也就是说,出现参数字符串里的转义序列不会被解释:

$ printf "a string, no processing: <%s>\n" "A\nB"
a string, no processing: <A\nB>
#当你使用%b格式指示符时,printf会解释参数字符串里的转义序列
$ printf "a string, with processing: <%b>\n" "A\nB"
a string, with processing:<A
B>

无论是在格式字符串内还是使用%b所打印的参数字符串里,大部分的转义序列都是被相同对待。无论如何,\c\0ddd只有搭配%b使用才有效,而\ddd只有在格式字符串里才会被解释。

现在应大致可以作结论,格式指示符为printf提供了强大的功能和灵活性。格式规格字母间下表

项目 说明
%b 相对应的参数被视为含有要处理的转义序列之字符串(对比上表)
%c ASCII字符。显示相对应参数的第一个字符
%d,%i 十进制整数
%e 浮点格式
%E 浮点格式
%f 浮点格式
%g %e或%f转换,看哪一个较短,则删除结尾的零
%G %E或%f转换,看哪一个较短,则删除结尾的零
%o 不带正负号的八进制值
%s 字符串
%u 不带正负号的十六进制值
%x 不带正负号的十六进制值。使用a至f表示10至15
%X 不带正负号的十六进制值。使用A至F表示10至15
%% 字面意义的%

根据POSIX标准:浮点格式%e、%E、%f、%g与%G是不需要被支持。这是因为awk支持浮点运算,且有它自己的printf语句。这样,Shell程序中需要将浮点数值进行格式化的打印时,可使用小型awk程序实现。然而,内建于bash、ksh93和zsh中的printf命令都支持浮点格式。

printf命令可用来指定输出字段的宽度以及进行对齐操作。为实现此目的,接在%后面的格式表达式可采用三个可选用的修饰符以及前置的格式指示符:

%flags width.precision format-specifier

输出字段的width为数字值。指定字段宽度时,字段的内容默认为向右对齐,如果你希望文字向左靠,必须指定-标志。这样:“%-20s”会在一个有20个字符宽度的字段里,输出一个向左对齐的字符串。如果字符串少于20个字符,则子段将以空白填满。下面的例子里,|是输出,以表示字段的实际宽度。第一个例子为向右对齐文字:

$ printf "|%10s|\n" hello
|    hello|

precision修饰符是可选用的。对十进制或浮点数而言,它可以控制数字出现于结果中的位数。对字符串而言,它控制将要打印的字符串的最大字符数。具体的含义会因格式指示符有不同。

精度的意义

转换 精度含义
%e, %E 要打印的最小位数。当值的位数少于此数字时,会在小数点后面补零,默认精度为6.而精度为0则表示不显示小数点
%f 小数点右边的位数
%g,%G 有效位数的最大数目
%s 要打印字符的最大数目

下面是几个精度的例子:

$ printf "%.5d\n" 15
00015
$ printf "%.10s\n" "a very long string"
a very lon
$ printf "%.2f\n" 123.4567
123.45

C函数库的printf()函数允许通过参数列表里的额外数值动态地指定宽度及精度。

POSIX标准不支持此功能,相反地,它会建议你在格式字符串里使用Shell变量值

举例如下:

$ width=5 prec=6 myvar=42.123456
$ printf "|%$(width).$(prec)G|\n" $myvar  #POSIX
|42.1235|
$ printf "|%*。*G|\n" 5 6 $myvar  #ksh93 与bash
|42.1235|

最后要介绍的是:在字段宽度与精度前放置一个或多个标志的用法。我们已介绍过使用-标志可以让字符串向左对齐。万恒的标志列表如下所示:

字符 意义
- 将字符里已格式化的值向左对齐
空白(space) 将正值前置一个空格,在负值前置一个负号
+ 总是在数值之前放置一个正号或负号,即便是正值也是
# 下列形式选择其一:%o有一个前置的O;%g与%G为没有结尾的零。
0 以零填补输出,而非空白。这仅发生在字段宽度大于转换后的情况下。在C语言里,该标志应用到所有输出格式,即使是非数字的值也一样。对于printf命令而言,它仅应用到数值格式

再举例说明:

$ printf "|%-10s| |%10s|\n" hello world #字符串向左、向右看齐
|helo    | |    world|
$ printf "|% d| |% d|\n" 15 -15 #空白标志
| 15| |-15|
$ printf "%+d %+d\n"    15 -15 # +标志
+15 -15
$ printf "%x %#x" 15 -15 # #标志
f 0xf
$ printf "%05d\n" 15  # 0标志
00015

对于转换指示符%b、%c与%s而言,相对应的参数都视为字符串。否则,它们会被解释为C语言的数字常数(开头的0为八进制,以及开头的0x与0X为十六进制)。更进一步来说,如果参数的第一个字符为单引号或双引号,则相应的数值是字符串的第二个字符的ASCII值:

$ printf "%s is %d\n" a "'a"
a is 97

当参数多于指示符时,格式指示符会根据需要再利用。这种做法在参数列表长度未知时很方便,例如来自通配符表达式。如果留在格式字符串里剩下的指示符比参数多时,如果是数值转换,则遗漏的值会被看成是零,如果是字符串转换,则被视为空字符串(虽然可以这么用,但比较好的方式应该是确认你提供的参数数目,与格式字符串所预期的数目是一样的)。如果printf无法进行格式的转换,它便会返回一个非零的退出状态。

波浪号展开与通配符

Shell有两种与文件名为相关的展开。第一个是波浪号展开,另一个则有很多叫法,有人称之为通配符展开式,有人则称之为全局展开或路径展开

波浪号展开

如果命令行字符串的第一个字符为波浪号(~),或者变量指定(例如PATH或ADPATH变量)的值里任何未被引号括起来的冒号之后的第一个字符为波浪号(~)时,Shell便会执行波浪号展开。

波浪号展开的目的,是要将用户根据目录的符号型表达方式,改为实际的目录路径。可以采用直接或间接的方式指定执行此程序的用户,如未明白指定,则为当前的用户:

$ vi ~/.profile    #与vi $HOME/.profile相同
$ vi ~tolstoy/.profile #编辑用户tolstoy的.profile文件

以第一个例子来看,Shell将~换成$HOME,也就是当前用户的根目录。第二个例子,则是Shell在系统的密码数据库里,寻找用户tolstoy,再将~tolstoy置换为tolstoy的根目录。

使用波浪号展开有两个好处。第一,它是一种简洁的概念表达方式,让查阅Shell脚本的人更清楚脚本在做的事。第二,它可以避免在程序里把路径直接编码。先看这个脚本片段:

printf "Enter username: "  #显示提示信息
read user                  #读取指定的用户名
vi /home/$user/.profile    #编辑该用户的.profile文件

前面的程序假设所有用户的根目录都在/home之下。如果这有任何变动(例如,用户子目录根据部门存放在部门目录的子目录下),那么这个脚本就得重写。但如果使用波浪号展开,就能避免重写的情况:

printf "Enter username: "
read user
vi ~$user/.profile
...

这样一来,无论用户根目录在哪里,程序都可以正常工作。

使用通配符

寻找文件名里的特殊字符,也是Shell提供的服务之一。当它找到这类字符时,会将它们视为要匹配的模式,也就是,一组文件的规格,它们的文件名称都匹配于某个给定的模式。Shell会将命令行上的模式,置换为符合模式的一组排序过的文件名。

如果接触过MS-DOS下的简单命令行环境,你可能会觉得*.*这样的通配符很熟悉,它指的是当前目录下所有文件名。UNIX Shell的通配符也类似,只不过功能更强大。基本的通配符见下表。

通配符 匹配
任何的单一字符
* 任何的字符字符串
[set] 任何在set里的字符
[!set] 任何不在set里的字符

通配符匹配于任何的单一字符,所以如果你的目录里含有whizprog.c、whizprog.log与whizprog.o这三个文件,与表达式whizprog.?匹配为whizprog.c与whizprog.o,但whizprog.log则不匹配。

星号(*)是一个功能强大而且广为使用的通配符;它匹配于任何字符组成的字符串。表达式whizprog.*符合前面列出的所有三个文件;网页设计人员也可以使用*.html表达式匹配他们的输入文件。

剩下的通配符就是set结构了。set是一组字符列表(例如abc)、一段内含的范围(例如a-z),或者是这两者的结合。如果希望破折号(dash)也是列表的一部分,只要把它放在第一个或最后一个就可以了。参考下表:

表达式 匹配的单一字符
[abc] a、b或c
[.,;] 局点、逗点或分号
[-_] 破折号或下划线
[a-c] a、b或c
[a-z] 任何一个小写字母
[!0-9] 任何一个非数字字符
[0-9!] 任何一个数字或惊叹号
[a-zA-Z] 任何一个小写或大写的字母
[a-zA-Z0-9_-] 任何一个字母、任何一个数字、下划线或破折号

范围表示法固然方便,但你不应该对包含在范围内的字符有太多的假设。比较安全的方式是:分别指定所有大写字母、小写字母、数字,或任意的子范围。不要想在标点符号字符上指定范围,或者在混用字母大小写上使用,想[a-Z]与[A-Z]这样的用法,都不保证一定能确切地匹配出包括所有想要的字母,而没有其他不想要的字符。更大的问题在于:这样的范围在不同类型的计算机之间无法提供完全的可移植性。

另一个问题是:现行系统支持各种不同的系统语言环境,用来描述本地字符集的工作方式。很多国家的默认local字符集与纯粹ASCII的字符集是不同的。为解决这些问题,POSIX标准提出了方括号表达式,用来表示字母、数字、标点符号及其他类型的字符,并且具有可移植性。

习惯上,当执行通配符展开时,UNIX Shell会忽略文件名开头为一个点号的文件。像这样的“点号文件”通常用做程序配置文件或启动文件。像是Shell的$HOME/.profile、ex/vi编辑器的$HOME/.exrc,以及bash与gdb使用的GNU的readline程序库的$HOME/.inputrc.

要是看到这类文件,需在模式前面明确地提供一个点号,例如:

echo .*    #显示隐藏文件

你可以使用-a(显示全部)选项,让ls列出隐藏文件:

在这里插入图片描述

命令替换

命令替换是指Shell执行命令并将命令替换部分替换为执行该命令后的结果。这听起来雨点饶舌,不过实际上相当简单。

命令替换的形式有两种。第一种是使用反引号——或称重音符(``)的方式,将要执行的命令框起来:

for i in `cd /old/code/dir; echo *.c`  #产生/old/code/dir下的文件列表
do
    diff -c /old/code/dir/$i $i | more #循环处理,在分页程序下比较旧版与新版的异同
done

这个Shell一开始执行cd /old/code/dir; echo *.c产生的输出结果(文件列表)接着会成为for循环里所使用的列表。

反引号形式长久以来一直是供命令替换使用的方法,而且POSIX也支持它,因此许多已存在的Shell脚本都使用它。不过,所有简单的简答的用法很快变成复杂的,特别是内嵌的命令替换及使用双引号时,都需要小心地转义反斜杠字符:

$ echo outer `echo inner1 \`echo inner2\` inner1` outer
outer innner1 inner2 inner1 outer

这个例子举得有点牵强,不过正说明了必须使用反引号的原因。命令执行顺序如下:

  1. 执行echo innner2,其输出为单词inner2会放置到下一个要被执行的命令中。

  2. 执行echo innner1 inner2 inner1,其输出单词会放置到下一个要执行的命令中。

  3. 最后,执行echo outer inner1 innner2 innner1 outer

使用双引号,情况更糟:

$ echo "outer + `echo innner -\`echo \"nested quote\"here\`-innner`+ouuter"
outer + inner - nested quote here- inner+ outer

为了更清楚明白,我们使用负号括住内部的命令替换,而使用正号框柱外部的命令替换。简而言之,就是更为混乱。

由于使用嵌套的命令替换,有没有引号,很快地就变得很难阅读,所以POSIX Shell采用Korn Shell里的一个功能。不用反引号的用法,改为将命令括在$(...)里。因为这种架构使用不同的开始定界符与结束定界符,所以看起来容易多了。以先前的例子来看,使用新语法重写如下:

$ echo outer $(echo inner1 $(echo inner2) inner1) outer
outer inner1 inner2 inner1 outer
$ echo "outer +$(echo inner -$(echo "nested quote" here)- inner)+ outer"
outer +inner -nested quote here- inner+ outer

这样是不是好看多了?不过特别留意的是:内嵌的双引号不在需要转义。这种风格已广泛建议用在新的开发上,我们也主要使用这种方法。

这边要看的是先前介绍过的,使用for循环比较不同的两个目录下的文件版本,以新语法重写如下:

for i in $(cd /old/code/dir ; echo *.c)
do
    diff -c /old/code/dir/$i  $i
done | more

这里的不同之处在于使用$(...)命令替换,以及将整个循环的输出,通过管道送到more屏幕分页程序。

为head命令使用sed

前面介绍使用过sedhead命令来显示文件的前n行。真实的head命令可以加上选项,以指定要显示多少行,例如head -n 10 /etc/passwd。传统的POSIX版本之前的head可指明行数作为选项,且许多UNIX的长期用户也习惯于使用此法执行head

使用命令替换与sed,我们对Shell脚本稍作修改,使其与原始head版本的工作方式相同。

# head --打印前n行
#
# 语法: head -N file
count=$(echo $1 | sed 's/^-//' | sed 's/$/q/')
shift
sed $count "$@"

当我们以head -10 foo.xml调用这个脚本时,sed最终是以sed 10qfoo.xml被引用。

创建邮件列表

不同的UNIX Shell的新版本不断地出现,而且分散在各站点的用户可以从/etc/Shells所列的Shell中选择自己的登录Shell。这样,如果以email通知用户有新版本的Shell更新且已安装的功能,这对系统管理而言会是很不错的事。

为了实现这个目的,我们必须先以登录Shell来识别用户,且产生邮件列表供安装程序用来公告新的Shell版本。由于每封通知信息内容都不尽相同,我们也不是要建立一个直接传送邮件的脚本,只是建立一个可用来寄送邮件的地址列表。邮件列表格式会因邮件用户端程序而有所不同,所以我们可以做一个合理的假设:最后完成的,只是一个以逗点分割电子邮件地址的列表,一行一个或多个地址,而最后一个地址之后是否有逗点则不重要。

这种情况下,较合理的方式应该是听过密码文件处理,为每个登录Shell建立一个输出文件,文件中每一行的用户名称都以逗点结束。

脚本本身结合了变量与命令替换,read命令以及while循环,整个脚本执行的代码不到10行!

#! /bin/sh
# passwd-to-mailing-list
#
# 产生使用特定 Shell 的所有用户邮寄列表
#
# 语法:
# passwd-to-mailing-list < /etc/passwd
# ypcat passwd | passwd-to-mailing-list
# niscat passwd.org_dir | passwd-to-mailing-list


# 此操作或许有些过度小心
rm -f /tmp/*.mailing-list
# 从标准输入读取:
while IFS=: read user passwd uid gid name home Shell
do
    Shell=$(Shell:-/bin/sh) #空的 Shell 字段意思指 /bin/sh
    file="/tmp/$(echo $Shell | sed -e 's;^/;;' -e 's;/;-;-').mailing-list"
    echo $user, >>$file
done

每次读取密码文件的记录时,程序都会根据Shell的文件名产生文件名。sed命令会删除前置/字符,并将后续的每个/改成-连字号。这段脚本会建立/tmp/bin-bash.mailing-list。这样形式的文件名。每个用户的名称与结尾的逗点都通过>>附加到特定的文件中。执行这个脚本后,会得到以下结果:

$ cat /tmp/bin-bash.mailing-list
dorothy,
ben,
jhancock,
tj,

我们可以让这个建立邮件列表的程序更广泛地应用。例如,如果系统的进程统计是打开的,要为系统里的每个程序做一份邮件列表就很容易了,只要从进程统计记录中取出程序名称和执行过程的用户姓名即可。注意,访问统计文件必须拥有root权限。每个厂商提供的统计软件都不一样,不过它们累积的数据的种类都差不多,所以只要稍作微调即可。GNU的统计摘要工具:sa可产生类似下面这样的报告。

# sa -u
...
jones    0.01 cpu    377k    mem     0 io gcc
...

也就是说,我们有一个以空白隔开的字段,第一个字段为用户名称而且最后一个字段为程序名称。这让我们可以简单地过滤该输出,使其看起来像是密码文件数据类型,再将其通过管道传递给我们的邮件列表程序来处理:

sa -u | awk '{ print $1 "::::::" $8}' | sort -u | passwd-to-mailing-list

(sort命令是用来排序数据; -u选项则删除重复的行。)这个UNIX过滤程序与管道以及简单的数据标记,最漂亮的地方在于简洁。我们不必编写新的邮件列表产生程序来处理统计数据:只需要一个简单的awk步骤以及一个sort,就可以让数据看起来就像我们可以处理的那样!

简易数学:expr

expr命令是UNIX命令里少数几个设计的不是那么严谨又很难用的一个。虽然经过POSIX标准化,但我们非常不希望你在新程序里使用它,因为还有更多其他程序与工具做的比它更好。在Shell脚本的编写上,expr主要用于Shell的算术运算,所以我们把重点放在这就好。如果你真有那么强的求知欲,可以参考expr(1)手册页了解更详尽的使用方法。

expr的语法很麻烦:运算数与运算符必须是单个的命令行参数;因此我们建议你在这里大量使用空格间隔他们。很多expr的运算符同时也是Shell的meta字符,所以必须谨慎使用引号。

expr被设置用来命令替换之内。这样,它会通过打印的方式把值返回到标准输出,而并非通过使用退出码(也就是Shell内的$?

expr运算符

在这里插入图片描述

在新的代码里,你可以使用test或$((...))进行这里的所有运算。正则表达式的匹配于提取,也可搭配sed或是Shell的case语句来完成。

这里的一个简单算术运算的例子。在真实的脚本里,循环体会做一些较有意义的操作,而不只是把循环变量的值显示出来:

$ i=1        #初始化计数器
$ while [ "$i" -le 5 ] #循环测试
>do
>    echo i is $i
>    i = `expr $i + 1`
>done
i is 1  
i is 2  
i is 3  
i is 4  
i is 5
$ echo $i
6  

这类算术运算,已经给出了你可能遇到的expr的使用方式的99%。我们故意在这里使用test(别名用法为[...])以及反引号的命令替换,因为这是expr的传统用法。在新的代码里,使用Shell的内建算术替换应该会更好:

$ i=1        #初始化计数器
$ while [ "$i" -le 5 ] #循环测试
>do
>    echo i is $i
>    i = $((i+1))
>done
i is 1  
i is 2  
i is 3  
i is 4  
i is 5
$ echo $i
6  

无论expr的价值如何,它支持32位的算术运算,也支持64位的算术运算——在很多系统上都可以,因此,几乎不会有计算器溢出(overflow)的问题。

引用

引用时用来防止Shell将某些你想要的东西解释成不同的意义。举例来说,如果你要命令接受含有meta字符的参数,如*?,就必须将这个meta字符用引号引用起来。或更典型的情况是:你希望将某些可能被Shell视为个别参数的东西保持为单个参数,这时你就必须将其引用。这里是三种引用的方式:

  • 反斜杠转义

    • 字符前置反斜杠(\),用来告知Shell该字符即为其字面上的意义。这是引用单一字符最简单的方式:
$ echo here is a real star: \* and a real question mark: \?
here is a real star: * and a real question mark: ?
  • 单引号

    • 单引号('...')强制Shell将一对引号之间的所有字符都看做其字面上的意义。Shell脚本会删除这两个引号,只单独留下被括起来的完整文字内容:
$ echo 'here are some metacharacters: *? [abc] ` $ \'
here are some metacharacters: * ? [abc] ` $ \

不可以在一个单引号引用的字符串里再做内嵌一个单引号。即便是反斜杠,在单引号里也没有特殊意义(某些系统里,像echo 'A\tB这样的命令看起来像是Shell特别地处理反斜杠,其实不然,这是echo命令本身有特殊的处理方式)。

如需混用单引号与双引号,你可以小心地使用反斜杠转义以及不同引号字符串的连接做到:

$ echo 'He said, "How'\''s tricks?"'
He said, "How's tricks?"

不管你怎么处理,这种结合方式永远都是很那阅读的。

  • 双引号

    • 双引号("...")就像单引号一样,将括起来的文字视为单一字符串。只不过,双引号会确切地处理括起来文字中转义字符和变量、算术、命令和替换:
$ x="I am x"
$ echo "\$x is \"$x\".Here is some output: '$(echo Hello World)'"
$x is "I am x". Here is some output: 'Hello World'

在双引号里,字符$、"、与\,如需用到字面上的意义,都必须前置\。任何其他字符前面的反斜杠是不带有特殊意义的。序列\-newline会完全地被删除,就好像是用在脚本的正文中一样。

请注意,如范例所示,单引号被括在双引号里时就无特殊意义了,它们不比成对,也无须转义。

一般来说,使用单引号的时机是你希望完全不处理的地方。否则,当你希望将多个单词视为单一字符串,但又需要Shell为你做一些事情的时候,请使用双引号,例如,将一个变量值与另一个变量值连接在一起,你就可以这么做:

oldvar="$oldvar $newvar"  #将newvar的值附加到oldvar变量

执行顺序与eval

我们曾经提到过的各类展开与替换都以定义好的次序完成。POSIX标准更提供了很多琐碎的细节。在这里,我们站在Shell程序设计人员的层面来看这些必须了解的东西。这里的解释,省略了许多小细节:例如复合命令的中间与结尾、特殊字符等。

Shell从标准输入或脚本中读取的每一行称为管道;它包含了一个或多个命令,这些命令被零或多个管道字符(|)隔开。事实上还有很多特殊符号可用来分隔单个的命令;分号(;)、管道(|)、&、逻辑AND(&&),还有逻辑OR(||)。对于每一个读取的管道,Shell都会将命令分割,为管道设置I/O,并且对每一个命令一次执行下面操作:

  1. 将命令分割成token,是以固定的一组meta字符分隔,有空格、制表符、换行字符、;、(,)、<、>、|与&。token的种类包括单词、关键字、输出入重定向器,以及分号。这是微妙的,但是变量、命令还有算术替换,都可以在Shell执行token认定时候被执行。

  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的根目录。波浪号替换会发生在下面的位置

  5. 在命令行里,作为单词的第一个未引用字符

  6. 在变量赋值中=之后以及变量赋值中的任何:之后

  7. 形式${variable op word}的变量替换里的word部分。

  8. 将任何开头为$符号的表达式、执行参数(变量)替换。

  9. 将任何形式为$(string),执行命令替换。

  10. 执行形式$((string))的算术表达式。

  11. 从参数、命令与算术替换中取出结果行的部分,再一次将它们切分为单词。这次它使用$IFS里的字符作为定界符,而不是使用步骤1的meta字符。通常,在IFS里连续多个重复的输入字符是作为单一定界符,这是你所期待的。这只有对空白字符而言是真的。对于非空白字符,则不是这样的。举例来说,当读取以冒号分割字段的/etc/passwd文件,连续冒号锁定接的是一个空字段。

  12. 对于*、?以及一对[...]的任何出现次数,都执行文件名生成的操作,也就是通配符展开。

  13. 使用第一次单词作为一个命令;也就是,先作为一个特殊的内建命令,接着是作为函数,然后作为一般的内建命令,以及最后作为查找$PATH找到的第一个文件。

  14. 在完成I/O重定向与其他同类型事项之后,执行命令。

如下图所示,引用可用来避免执行程序的不同部分。eval命令可以让你再经过一次这一流程。执行两次命令行的处理,看来似乎有点奇怪,不过这却是相当好用的功能:它让你编写一个脚本,可在任务中建立命令字符串,再将它们传递给Shell执行。这么一来,你可以让脚本聪明地修改它们执行时的行为。

在这里插入图片描述

这个步骤看起来有点复杂;当命令行被处理时,每个步骤都是在Shell的内存里发生的;Shell不会真地把每个步骤的发生显示给你看。所以,你可以假象这是我们偷窥Shell内存里的情况,从而知道每个阶段的命令行是如何被准换的。我们从这个例子开始说:

$ mkdir /tmp/x            #创建临时性目录
$ cd /tmp/x               #切换到该目录
$ touch f1 f2             #建立文件
$ f=f y="a b"             #赋值两个变量
$ echo ~+/${f}[12] $y $(echo cmd subst) $((3+2)) > out #忙碌的命令

上述的执行步骤概要如下:

  1. 命令一开始会根据Shell语法而分割token。最重要的一点是:I/O重定向>out在这里是被识别的,并存储供稍后使用。流程继续处理下面这行,其中每个token的范围显示于命令下方的行上:在这里插入图片描述

  2. 检查第一个单词(echo)是否为关键字,例如if或for。在这里不是,所以命令行不变继续处理。

  3. 检查第一个单词(依然是echo)是否为别名。在这里并不是,所以命令行不变,继续往下处理。

  4. 扫描所有单词是否需要波浪号展开。在本例中,~+为ksh93与bash的扩展,等通过$PWD,也就是当前目录。token2将被修改,处理继续如下
    在这里插入图片描述

  5. 下一步是变量展开:token2与3都被修改。这会产生

在这里插入图片描述

  1. 再来要处理的是命令替换。注意,这里可递归引用列表里的所有步骤!在此例中,因为我们试图让所有的东西容易理解,因此命令替换修改了token4,结果如下:
    在这里插入图片描述

  2. 现在执行算术替换。修改的是token5,结果是:
    在这里插入图片描述

  3. 前面所有的展开产生的结果,都将再一次被扫描,看看是否有$IFS字符。如果有,则它们是作为分隔符,产生额外的单词。例如,两个字符$y原来是组成一个单词,但是展开式"a-空格-b",在此阶段被切分为两个单词;a与b。相同方式也应用与命令替换$(echo cmd subset)的结果上。先前的token 3变成了token3与4。先前的token4则变成的token5与6,结果为:
    在这里插入图片描述

  4. 最后的替换阶段是通配符展开。token2变成了token2与3,结果如下:
    在这里插入图片描述

  5. 这时,Shell已经准备好执行最后的命令了。它会去寻找echo。正好ksh93与bash里的echo都已内建到Shell中。

  6. Shell实际执行命令。首先执行>out的I/O重定向,再调用内部的echo版本,显示最后的参数。

最后的结果:

$ cat out
/tmp/x/f1 /tmp/x/f2 a b cmd subst 5

eval语句

eval语句是在告知Shell取出eval的参数,并再次执行它们一次,使它们经过整个命令行的处理步骤。这里有个例子可以让你了解eval究竟是什么。

eval ls会传递字符串ls给Shell执行,所以Shell显示当前目录下的文件列表。这个例子太简单了:字符串ls上没有东西需要被送出让命令处理两个步骤。所以我们来看这个:

listpage="ls | more"
$ listpage

为什么Shell会把|more看作ls的参数,而不是直接产生一页页的文件列表呢?这是由于Shell执行变量时,管道字符出现在步骤5,也就是在它确实寻找管道字符后(在步骤1)。变量的展开一直要到步骤8才进行解析。结果,Shell把|more看做ls的参数,使得ls会试图在当前目录下寻找名为|more的文件!

现在,想想eval $listpage吧。在Shell到达最后一步时,会执行带有ls、|与more参数的eval。这会让Shell回到步骤1,具有一行包括了这个参数的命令。在步骤1发现|后,将该行分割为两个命令:lsmore。每个要被处理的命令都以一般方式执行,最后的结果是在当前目录下已分页的文件列表。

subShell与代码块

还有两个其他的结构,有时也很有用:subShell与代码块

subShell是一群被括在圆括号里的命令,这些命令会在另外的进程中执行。当你需要让一小组的命令在不同的目录下执行时,这种方法可以让你不必修改主脚本的目录,直接处理这种情况。例如,下面的管道是将某个目录树复制到另一个地方,在原始的V7 UNIX 的手册页里可以看到此例:

tar -cf - . | (cd /newdir; tar -xpf -)

左边的tar命令会产生当前目录的tar打包文件,将它传送给标准输出。这份打包文件按会通过管道传递给右边subShell里的命令。开头的cd命令会切换到新目录,也就是让打包文件在此目录下解开。然后,右边的tar将从打包文件中解开文件。请注意,执行此管道的Shell并未更改它的目录。

代码块概念上与subShell雷同,只不过它不会建立新进程。代码块里的命令以花括号({})括起来,且对主脚本的状态会造成影响(例如它的当前目录)。一般来说,花括号被视为Shell关键字:意即它们只有出现在命令的第一个符号时会被识别。实际上:这表示你必须将结束花括号放置在换行字符或分号之后,例如:

cd /some/directory || {
    
                                     #代码块开始
    echo could not change to /some/directory! >&2       #怎么了
    echo you lose! >&2                                 #挖苦信息
    exit 1                                              #终止整个脚本
}                                                       #代码块结束

I/O重定向也可套用到subShell(如先前的两个tar例子)与代码块里。在该情况下,所有的命令会从重定向来源读取它们的输入或传送它们的输出。下面简单说明了subShell与代码块之间的差异:
在这里插入图片描述

要用subShell还是代码块需要根据个人的喜好而定。主要差异在于代码块会与主脚本共享状态。这么一来,cd命令会对主脚本造成影响,也会对变量赋值产生影响。特别是,代码块里的exit会终止整个脚本。因此,如果你希望将一组命令括起来执行,而不影响主脚本,则应该使用subShell。否则,请使用代码块。

内建命令

Shell有为数不少的内建命令。指的是由Shell本身执行命令,而并非在另外的进程中执行外部程序。POSIX更进一步将它们区分为“特殊”内建命令以及“一般”内建命令。特殊内建命令以+标示。大部分这里列出的一般内建命令都必须内建于Shell中,以维持Shell正常运行(例如,read)。其他内建于Shell内的命令,则只是为了效率(例true与false)。在此标准下,也为了更有效率,而允许内建其他的命令,不过所有的一般内建命令都必须能够以单个的程序被访问,也就是可以被其他二进制程序直接执行。像test内建命令就是为了让Shell的执行更有效率。

命令 摘要
:(冒号) 不做任何事(只做参数的展开)
.(点号) 读取我呢间并与当前Shell中执行它的内容
alias(别名) 设置命令或命令行的捷径(交互式使用)
bg 将工作置于后台(交互式使用)
break 从for、while或until循环中退出
cd 改变工作目录
command 找出内建于外部命令;寻找内建命令而非同名函数
continue 跳到下一个for、while或until循环重复
eval 将参数当成命令行来处理
exec 给定的程序取代Shell或为Shell改变I/O
exit 退出Shell
export 建立环境变量
false 什么事也不做,指不成功的状态
fc 与命令历史一起运行(交互式使用)
fg 将后台工作置于前台(交互式使用)
getopts 处理命令行的选项
jobs 列出后台工作(交互式使用)
kill 传送信号
newgrp 以新的group ID启动新的Shell(已过时)
pwd 打印当前目录
read 从标准输入中读取一行
readonly 让变量为只读模式(无法指定的)
return 从包围函数中返回
set 设置选项或是位置参数
shift 移位命令行参数
times 针对Shell与其子代Shell,显示用户与系统CPU时间的累计
trap 设置信号捕捉程序
ture 什么事也不做,表示成功状态
umask 设置/显示文件权限掩码
unalias 删除别名定义(交互式使用)
unset 删除变量或函数的定义
wait 等待后台工作完成

注意:bash的source命令(来自于BSD C Shell)是等同于点号命令。

特殊内建命令与一般内建命令的差别在于Shell查找要执行的命令时。命令查找次序是先找特殊内建命令,再找Shell函数,接下来才是一般内建命令,最后则为$PATH内所列目录下找到的外部命令。这种查找顺序让定义Shell函数以扩展或覆盖一般Shell内建命令成为可能。

这一功能最常用于交互式Shell中。举例来说,当你希望Shell的提示号能包含当前目录路径的最后一个组成部分。最简单的实现方式,就是在每次你改变目录时,都让Shell改变PS1.你可以写一个自己专用的函数,如下所示:

# chdir ---在改变目录时也更新PS1的个人函数
chdir(){
    
    
    cd "$@"    #实际更改目录
    x=$(pwd)    #取得当前目录的名称,传给变量x
    PS1="${x##*/}\$"
}

这么做会有一个问题:你必须在Shell下输入chdir而不是cd,如果你突然忘了而输入cd你会在新的目录上,提示号就不会改变了。基于这个原因,你可以写一个名为cd的函数,然后Shell就会先找到你的函数,因为cd是一般内建命令:

#cd ---改变目录时更新 PS1的个人版本
#    (无法如实运行,见内文)
cd(){
    
    
    cd "$@"    #真的改变目录了吗?
    x=$(pwd)    #取得当前目录的名称,并传给变量x
    PS1="${x##*/}\$"    #截断前面的组成部分后,指定给PS1
}

这里有一点美中不足:Shell函数如何访问真正的cd命令?这里所显示的cd "$@"只会再次调用此函数,导致无限递归。我们需要的转义策略,告诉Shell要避开函数的查找并访问真正的命令。这正是内建命令command的工作

#cd ---改变目录时更新PS1的私人版本
cd(){
    
    
    command cd "$@"  #实际改变目录
    x=$(pwd)         #取得当前目录的名称,传递给变量x
    PS1="${x##*/}\$" #截断前面的组成部分,指定给PS1
}

在这里插入图片描述

POSIX标准为特殊内建命令提供两个附加特性:

  • 特殊内建工具语法上的错误,会导致Shell执行该工具时退出,然而当语法错误出现在一般内建命令时,并不会导致Shell执行该工具时退出。如果特殊内建工具遇到语法错误时不退出Shell,则它的退出值应该非零。

  • 以特殊内建命令所标明的变量指定,在内建命令完成之后仍会有影响;这种情况不会发生在一般内建命令或其他工具的情况下。

第二项需要解释一下,你可以在命令前面指定一个变量复制,且变量值在被指定命令的环境中不影响当前Shell内的变量或是接下来的命令:

PATH=/bin:/usr/bin:/usr/ucb awk '...'

然而,当这样的指定用于特殊内建命令时,即使用在特殊内建命令完成之后,仍然会有影响。

下表列出了到目前为止尚未介绍的命令。这些命令中的大部分对于Shell脚本而言为特殊情况或不相关情况,不过我们在这里还是做一些简介,让你了解他们在做什么以及使用他们的时机:

命令 介绍
alias、unalias 分别用于别名的定义和删除。当命令被读取时,Shell会展开别名定义。别名主要用于交互式Shell;例如alias ‘rm=rm -i’ ,指的是强制rm在执行时要进行确认。Shell不会递归的别名展开,因此此定义是合法的。
bg、fg、jobs、kill 这些命令用于工作控制,他是一个操作系统工具,可将工作移到后台执行,或由后台执行中移出。
fc 是“fix command”的缩写,该命令设计用来交互模式下使用。它管理Shell之前已存储的执行过的命令历史,允许交互式用户再次调用先前用过的命令,编辑它以及再重新执行它
times 该命令会打印由Shell及所有子进程所累积执行迄今的CPU时间。它对于日常的脚本编写不是那么有用。
umask 用来设置文件建立时的权限掩码

剩下的两个命令在脚本中是用得到的。第一个是用来等待后台程序完成的wait命令。如未加任何参数,wait会等待所有的后台工作完成;否则,每个参数可以是后台工作的进程编号,或是工作控制的工作规格。

最后,.点号也是非常重要的命令。它是用来读取与执行包含在各别文件中的命令。例如,当你有很多个Shell函数想要在多个脚本中使用时,正确的方式是将它们放在各自的库文件里,再以点号命令来读取它们:

.my_funcs  #在函数中读取

如指定的文件未含斜杠,则Shell会查找$PATH下的目录,以找到该文件。该文件无须是可执行的,只要是可读取的即可。

set命令

set命令可以做的事相当广泛。就连使用的选项语法也与众不同,这是POSIX为保留历史的兼容性而存在的命令。也因为这样,这个命令有点难懂。

set命令最简单的工作就是以排序的方式显示所有Shell变量的名称与值。这是调用它时不加任何选项与参数时的行为。其输出采用Shell稍后可以重读的形式——包含适当的引号。这个想法出自Shell脚本有可能需要存储它的状态,在之后会通过.(点号)命令恢复它。

set的另一项任务是改变位置参数($1、$2等)。使用--的第一个参数来结束设置它自己的选项,则所有接下来的参数都会取代位置参数,即使它们是以正号或负号开头。

在这里插入图片描述

最后,set被用来打开或停用Shell选项,指的是改变Shell行为模式的内部设置。这里就是复杂的地方了,从历史来看,Shell选项是以单个字母来描述的,以负号打开并且以正号关闭。POSIX另加入了长选项,打开或关闭分别是用-o+o。每个单个字母选项都有相对应的长名称选项。下表列出了部分选项,并简短说明了它们的功能。

短选项 o形式 说明
-a allexport 输出所有后续被定义的变量
-b notify 立即显示工作完成的信息,而不是等待下一个提示号。供交互式使用
-C noclobber 不允许>重定向到已存在的文件。>|运算符可使此选项的设置无效。供交互式使用。
-e errexit 当命令以非零值状态退出时,则退出Shell
-f noglob 停用通配符展开
-h 当函数被定义(而非函数被执行)时,寻找并记住从函数体中被调用的命令位置。
-m monitor 打开工作控制。供交互式使用
-n noexec 读取命令且检查语法错误,但不要执行它们。交互式Shell被允许忽略此选项。
-u nounset 视未定义的变量为错误,而非null
-v verbose 在执行前先打印命令
-x xtrace 在执行前先显示命令
ignoreeof 不允许以Ctrl-D退出Shell
nolog 关闭函数定义的命令历史记录功能
vi 使用vi风格的命令行编辑。供交互式使用

你可能感到意外的地方是:set并非用来设置Shell变量。该工作是通过简单的variable=value指定实现的。

特殊变量$-是一个字符串,表示当前已打开的Shell选项。每个选项的短选项字母会出现在字符串中(假设该选项是打开的话)。这可被用来测试选项设置,像这样:

case $- in
*C*)    ...  #启用noclobber选项
        ;;
esac

猜你喜欢

转载自blog.csdn.net/m0_56145255/article/details/128240210
今日推荐