4. 正则表达式

Regular Expressions

4.1 正则表达式

4.1.1 正则表达式简介

正则表达式就是用模板来表示一组字符。

与算术表达式类似,正则表达式也是通过 “用各种操作符连接较小的表达式” 而构建起来的。

基本的组件是用来匹配 单个字符 的表达式。包括所有字母及标点在内的大部分字符都可用来匹配自己。有特殊含义的 元字符 可以用 反斜线 来转义。

4.1.2 正则表达式元字符

一个正则表达式后面可以跟一个或多个 元字符

元字符:一个字符,并不仅仅表示它自身,而是有特殊的含义。

正则表达式操作符:

操作符 效果
. 匹配任何单个字符
? 最多匹配一次
+ 最少匹配一次
* 任意次匹配(零或更多次)
{N} 前面字符串匹配 N 次
{N,} 前面字符串匹配 N 或更多次
{N,M} 前面字符串匹配 N ~ M 次
- 若其不在列表的两头、不是列表中某范围的结束点,代表范围
^ 匹配行首空字符,或把字符串从范围排除
$ 匹配行尾空字符
\b 匹配字段外沿相邻空字符
\B 匹配字段外非相邻空字符
\< 匹配字段开头空字符
\> 匹配字段结尾空字符
各操作的优先级

处理优先级从高到低:

  • (R) 组合操作
  • R* 数量限定操作
  • R1R2 串接操作
  • R1 | R2 选择操作

为了避免括号,星号有最高优先级,接着是串接,接着是选择。如果没有歧义则可以省略括号。

因此 ab*c|d 会被解析为 ((a(b*))c)|d。解析流程:

  • 因为没有括号,所以首先处理数量限定操作 b*,得到 (b*)
  • 再处理串接操作 a(b*)c,得到 (a(b*)c)
  • 最后处理选择操作,得到 ((a(b*))c)|d
组合操作

括号 把字符强制组合在一起,组合操作具有最高优先级。

go!(go!)* 匹配 go!go!go!go!go!go! 等。

数量限定操作

+?* 等操作符来限定左侧字符串出现的次数。

booo* 匹配 booboooboooo 等。

串接操作

Concatenation

把两个正则表达式直接放在一起就是串接操作。相邻的字符默认为串接关系。

f 串接 o 得到 fo,只匹配 fo

f 串接 o* 得到 fo*,匹配 foforfoofool

选择操作

Alternation

两个正则表达式可以用 | 操作符连接起来,得到的表达式匹配它们的 或集

trick|treat 匹配 tricktreat

4.1.3 基本正则表达式与扩展正则表达式

POSIX 定义了两种正则表达式语法:基本正则表达式(BRE)和扩展正则表达式(ERE)。

在 BRE 中,这些元字符失去了其特殊功能,要想让它们再做为元字符使用,必须用反斜线转义:

?+{|()

BRE 定义的语法符号 包括:

. - 匹配任意一个字符。

[] - 字符集匹配,匹配方括号中定义的字符集之一。

[^] - 字符集否定匹配,匹配没有在方括号中定义的字符。

^ - 匹配开始位置。

$ - 匹配结束位置。

\(\) - 定义子表达式。

\n - 子表达式向前引用,n 为 1-9 之间的数字。 由于此功能已超出正则语义,需

        要在字符串中回溯,因此需要使用 NFA 算法进行匹配。

* - 任意次匹配(零次或多次匹配)。

\{m,n\} - 至少 m 次,至多 n 次匹配;{m} 表示 m 次精确匹配;{m,} 表示至少 m

        次匹配。

ERE 修改了 BRE 中的部分语法,并 增加了以下语法符号

? - 最多一次匹配(零次或一次匹配)。

+ - 至少一次匹配(一次或更多次匹配)。

| - 或运算,其左右操作数均可以为一个子表达式。

同时,ERE 在使用子表达式 () 和 次数匹配 {m,n} 语法符号时,不再需要转义符。 与此同时, ERE 也取消了非正则语义的子表达式向前引用能力。

BRE 和 ERE 共享同样的 POSIX 字符类定义。同时,它们还支持 字符类比较操作 “[. .]” 和字符类等效体 “[= =]” 操作,但很少被使用。

4.2 用 GREP 做范例

4.2.1 GREP 简介

对于给定的模板,grep 会在输入文件中查找匹配的 ,如果在某一行中发现了匹配内容,它默认会将该行的内容复制到标准输出,或用户指定的其它输出。

如果输入文件的 结尾 不是换行符,grep 会 自动加上一个换行符

因为换行符是分隔符之一,所以无法被匹配。

4.2.2 GREP 与正则表达式

4.2.2.1 行与字段锚点

锚点是指定义模板时,关键字的位置,如:

  • 行首:grep ^root /etc/passwd
  • 行尾:gep :$ /etc/passwd
  • 字段头:grep '\<PATH' file
  • 字段尾:grep 'bash\>' file
  • 单独字段:grep -w / /etc/fstab
4.2.2.2 字符类

一对方括号可以用于定义一个字符列表,用来匹配其中的任意 单个 字符。如果列表开头为 ^,表示 取反

如果方括号中的字符列表是一段连续的字符,即某个 范围 内的字符,为了简化,可以在首尾两个字符中间使用 - 来表示,如 [a-d]

字典顺序

但是,[a-d] 并不一定代表 [abcd]

如果系统语系是默认的 C,[a-d] 是代表 [abcd]

但有许多其它语系是按字典顺序来排列字符的,在那些语系中,[a-d] 代表 [aBbCcDd]

因此,如果希望得到预想的结果,建议把 LC-ALL 变量设置为 C

4.2.2.3 通配符
.

. 作为通配符时,用来匹配任意单个字符,因此 grep '\<c...r\>' 可以匹配 color

如果要匹配 . 本身,即句点,则要使用 grep -F 选项。

*

* 作为通配符时,用来匹配多个字符,如 grep '\<c.*h' . 匹配当前目录中以 c 开头 h 结尾的文件名。

如果要匹配 * 自身,即星号,可以用 单引号 将其 引用,如 grep '*' /etc/profile

4.3 用 bash 的功能来匹配

4.3.1 字符范围

除了 grep 和正则表达式,实际上在 shell 中可以直接使用 *?[ ]!^ 这些元字符进行匹配,而无需任何第三方程序。

如果要匹配它们自身,也需要将其 引用,如 '*'"*" 都可以。

同样可以用 方括号 来指定字符范围:ls -ld [a-cx-z]*

!^ 来取反。

4.3.2 字符类

方括号中还以指定字符类,语法为 [:CLASS:],其中 CLASS 是 POSIX 定义的标准,主要有以下值:

[:alnum:] 字母数字

[:alpha:] 字母

[:ascii:] ASCII 字符

[:blank:] 空格与制表符

[:cntrl:] 控制字符

[:digit:] 数字

[:graph:] 可见字符(非空白、非控制字符)

[:lower:] 小写字母

[:print:] 可见字符与空格

[:punct:] 标点

[:space:] 所有空白字符(换行符、空格、制表符)

[:upper:] 大写字母

[:word:] 字母、数字、下划线

[:xdigit:] 十六进制数字

如果 shell 的 extglob 选项被启用,还可以使用其它几个扩展的操作符。

5. SED

GDN sed 流编辑器。

5.1 SED 简介

SED,Stream EDitor,流编辑器 用于把从文件或管道读取的文本进行一些基本的转换,转换的结果被发送给 基本输出。SED 不会直接修改原始文件,可以用重定向将修改结果另存为文件。

sed 与 vi 等编辑器的区别在于,它可以 过滤 来自于管道的文本。你无需与其交互,因此 sed 又被称为批处理编辑器。它允许把多个文本编辑的命令保存到脚本中,大大简化了重复的编辑任务。它可以在大量文件中进行 批量的文本替换

sed 命令

sed 程序可以使用正则表达式,进行文本的 替换删除

sed SCRIPT INPUTFILE

sed 编辑命令

其编辑文本的命令与 vi 有些类似:

命令 作用
a\ 在当前行下面追加文本
c\ 修改当前行文本
d 删除匹配行
i\ 在当前行上面插入文本
p 打印匹配行
r 读取文件
s 查找并替换文本
w 写入文件
sed 选项
选项 作用
-e SCRIPT 脚本 来处理输入
-f 脚本文件 中的命令来处理输入
-n 不打印原文件
-V 查看 sed 版本
sed -e '/^foo/d' -f script2.sed input.txt > output.txt

5.2 交互式编辑

5.2.1 打印匹配行

sed -n '/erors/p' example

-n 确保不打印全部原文

p 打印匹配行

5.2.2 删除匹配行

sed '/erors/d' example

d 删除匹配行。即 取反,打印不匹配的行。

5.2.3 根据行号限定行

sed '2,4d' example

删除第 2 行 ~ 第 4 行。

sed '3,$d' example

删除第 3 行到末行,即仅保留前 2 行。

sed -n '/a text/,/This/p' example

匹配的起始行为 a text 所在行,结束行为 This 所在行。

5.2.4 查找并替换

sed 's/erors/errors/' example

查找 erors,替换为 errors,但仅针对每行找到的 第一个 字段。

sed 's/erors/errors/g' example

查找替换所有字段。

sed 's/^/> /' example

在每行开头插入 >

sed 's/$/EOL/' example

在每行结尾插入 EOL

sed -e 's/erors/errors/g' -e 's/last/final/g' example

同时进行多个查找与替换。

5.3 非交互式编辑

5.3.1 从文件中读取 sed 命令

可以把多个 sed 命令写入文件,然后用 sed -f file.sed 选项来执行。创建 sed 文件时,要确保:

  • 每行结尾不能有空白
  • 不能使用引用
  • 如果要插入或替换文本,除最后一行外,每行要以反斜线 \ 结束

5.3.2 输出文件另存为

需要借助输出重定向操作系统 > 来把输出另存为文件。

SCRIPT="/home/sandy/scripts/script.sed"
NAME="$1"
TEMPFILE="/var/tmp/sed.$PID.tmp"
sed "s/\n/^M/" $1 | sed -f $SCRIPT | sed "s/^M/\n/" > $TEMPFILE
mv $TEMPFILE $NAME

6. AWK

6.1 从 gawk 开始

6.1.1 gawk 是什么

awk 是一个 UNIX 程序,而 gawk 是其常用的 GNU 版本,它是另一个流编辑器。实际上 awk 程序通常是 gawk 的符号链接。

awk 的基本功能是按照 一个或多个模板,查找文件的匹配 其它匹配单位,可以针对匹配行进行特殊的操作。

通常的程序是 “程序化” 的,需要在程序中详细描述每一步它应该做什么,很难清楚地表达程序要处理的数据。

awk 中的众多程序比较特别,它们是 “数据驱动” 的,你可以先描述要处理的 数据是什么样的,然后告诉程序找到这些数据以后要 做什么*。

6.1.2 gawk 命令

运行 awk 时,需要指定一个 awk 程序来 处理匹配数据。该程序由一系列规则组成,其中可以包含函数定义、循环、条件等编程组件。每条规则指定 一个模板 用来查找,一个动作 用来处理匹配。

awk 有多种运行方式,通常使用 awk PROGRAM inputfile(s) 格式。

如果要对多个文件、进行多种修改,最好把 awk 命令写入脚本文件,以如下方式运行:

awk -f PROGRAM-FILE inputfile(s)

6.2 print 程序

6.2.1 打印所选字段

awkprint 命令用于将所选数据打印出来。

awk 从文件中读取一行时,会基于字段分隔符(FS)将该行分割成字段。FS 是一个 awk 的变量,默认为一个或多个空白字符。

变量 $1$2$3$4 等用于保存分割后的每个字段的值,$0 用于保存整行的内容。

ls -l | awk '{ print $5 "\t" $9 }'

6.2.2 格式化字段

ls -ldh * | grep -v total | \
awk '{ print "Size is " $5 " bytes for " $9 }'

要想让 awk 的输出更加易于阅读,还是需要人为地为格式化做些努力。如在适当的位置插入一些文字说明、空格、制表符等。

需要时还可以使用 sortheadtail 等来排序、截取。

df -h | sort -rnk 5 | head -3 | \
awk '{ print "Partition " $6 "\t: " $5 " used." }'
特殊格式化字符
字符 作用
\a 提示音
\n 换行符
\t 制表符

6.2.3 print 命令与正则表达式

把正则表达式放到一对斜线中,可以用来做模板来匹配。

awk 'EXPRESSION { PROGRAM }' file(s)

df -h | awk '/dev\/hd/ { print $6 "\t: " $5 }'

/dev\/hd/ 表达式中的斜线必须转义。

ls -l | awk '/\<(a|x).*\.conf$/ { print $9 }'

匹配以 a 或 x 开头的 .conf 文件。表达式中第一个 . 为通配符,第二个 . 为句点本身,所以被转义。

6.2.4 在输出中添加注释

要想在输出的前面或后面添加注释,需要使用 BEGINEND 来标记:

ls -l | \
awk 'BEGIN { print "Files found:\n" } /\<[a|x].*\.conf$/ { print $9 }'

ls -l | \
awk '/\<[a|x].*\.conf$/ { print $9 } END { print \
"these files are found." }'

6.2.5 gawk 脚本

awk 命令写入脚本文件,以便重用。awk 脚本可以包含 awk 声明,通过声明来 定义模板和动作

$ cat diskrep.awk
BEGIN { print "*** WARNING WARNING WARNING ***" }
/\<[8|9][0-9]%/ { print "Partition " $6 "\t: " $5 " full!" }
END { print "*** Give money for new disks URGENTLY! ***" }

匹配 80% ~ 99%

$ df -h | awk -f diskrep.awk
*** WARNING WARNING WARNING ***
Partition /usr  : 97% full!
*** Give money for new disks URGENTLY! ***

6.3 gawk 变量

awk 处理输入文件时,要用到几个变量,其中有的是可修改的,有的是只读的。

6.3.1 输入字段分隔符

Field Separator,FS

字段分隔符可以是单个字符,也可以是一个正则表达式,用来控制 awk 如何把输入分割成字段。

字段分隔符用内建变量 FS 表示,注意,它与兼容 POSIX 的 shell 所使用的 IFS 变量不是一回事。

字段分隔符的值可以用 = 来修改。通常对它的定义需要在 BEGIN 模板中进行:

$ awk 'BEGIN { FS=":" } { print $1 "\t" $5 }' /etc/passwd

如果写到脚本中:

$ cat printnames.awk
BEGIN { FS=":" }
{ print $1 "\t" $5 }

$ awk -f printnames.awk /etc/passwd

定义分隔符时一定要小心,否则容易出问题。

6.3.2 输出分隔符

6.3.2.1 输出字段分隔符

Output Field Separator,OFS,也是内建变量。通常为空格。

输出的字段通常用空格分隔,但只有在用逗号来分隔 print 的参数时才会使用:

$ cat test
record1         data1
record2         data2

$ awk '{ print $1 $2}' test
#      参数  $1 和 $2 之间没有逗号
record1data1
record2data2
#  输出就不会用空格,而是把 $1$2 做为一个参数
#  所以它们的输出是连在一起的

$ awk '{ print $1, $2}' test
#                ^  有逗号分隔
record1 data1
record2 data2
# 所以 $1 和 $2 被当作两个参数对待
# 在它们的输出之间自动添加空格
6.3.2.2 输出记录分隔符

Output Record Separator,ORS,内建变量。

一条 print 语句的整体输出称为一条输出记录。每个 print 命令都会产生一个输出记录,其后跟着一个字符串,即 ORS。该变量的默认值为换行符 \n,因此,每条 print 语句才会产生一个单独的行。

要想修改分割输出字段和输出记录的方式,可以通过为 OFSORS 变量赋值:

$ awk 'BEGIN { OFS=";" ; ORS="\n-->\n" } \
	{ print $1,$2}' test
record1;data1
-->
record2;data2
-->

如果 ORS 中没有换行符,全部输入就会变成一行。

6.3.3 记录数

Number of Records,NR,内建变量。

该变量用于保存 awk 处理的记录数量。每读取一行新的输入,就会加 1。可以在脚本结尾处,用来统计记录总数。

$ cat processed.awk
BEGIN { OFS="-" ; ORS="\n--> done\n" }
{ print "Record number " NR ":\t" $1,$2 }
END { print "Number of records processed: " NR }

6.3.4 自定义变量

除了内建变量,用户可以根据需要来自定义自己的变量。

如果 awk 遇到对一个新变量的引用,如果该变量之前没有定义,则会立即创建,并为其赋空值。

可以用 = 为变量赋值。

$ cat revenues
20021009        20021013        consultancy     BigComp         2500
20021015        20021020        training        EduComp         2000
20021112        20021123        appdev          SmartComp       10000
20021204        20021215        training        EduComp         5000

$ cat total.awk
{ total=total + $5 }
{ print "Send bill for " $5 " dollar to " $4 }
END { print "---------------------------------\nTotal revenue: " total }

$ awk -f total.awk test
Send bill for 2500 dollar to BigComp
Send bill for 2000 dollar to EduComp
Send bill for 10000 dollar to SmartComp
Send bill for 5000 dollar to EduComp
---------------------------------
Total revenue: 19500

也支持使用 VAR+= value 的自增格式。

6.3.5 printf 命令

要想达到更加精确地格式化,可以使用 printf 命令。

它可以针对每一个具体的字段进行个性化的格式设定。

7. 条件语句

7.1 if 简介

7.1.1 if 的一般用法

根据某命令的运行结果来进行下一个操作。

if TEST-COMMANDS; then CONSEQUENT-COMMANDS; fi

如果 TEST-COMMANDS 运行后返回的状态为 0,则会执行 CONSEQUENT-COMMANDS 中的命令。

CONSEQUENT-COMMANDS 经常会涉及到对数字或字符串的比较,当然也可以是任何其它命令。

通常用 一元表达式 来检查文件的状态,如果被检查的文件参数为 /dev/fd/N 形式,则会检查文件描述符 N。

标准输入、标准输出、标准错误及它们对应的描述符也可以用来测试。

7.1.1.1 常用的 if 表达式

TEST-COMMANDS 如果是条件测试,则需要用中括号 [ expressions ] 把表达式括起来。

针对文件的条件测试:

表达式 的测试结果
[ -a FILE ] 文件存在
[ -b FILE ] 文件存在,为块设备
[ -c FILE ] 文件存在,为字符设备
[ -d FILE ] 文件存在,为目录
[ -e FILE ] 文件存在
[ -f FILE ] 文件存在,普通文件
[ -g FILE ] 文件存在,SGID 位被设置
[ -h FILE ] 文件存在,符号链接
[ -k FILE ] 文件存在,SBIT 位被设置
[ -p FILE ] 文件存在,FIFO 文件
[ -r FILE ] 文件存在,可读
[ -s FILE ] 文件存在,大小大于零
[ -t FD ] 文件描述符被打开,指代某终端
[ -u FILE ] 文件存在,SUID 位被设置
[ -w FILE ] 文件存在,可写
[ -x FILE ] 文件存在,可执行
[ -O FILE ] 文件存在,所有者为 EUID
[ -G FILE ] 文件存在,所有者为 EGID
[ -L FILE ] 文件存在,符号链接
[ -N FILE ] 文件存在,最近读取后被修改过
[ -S FILE ] 文件存在,套接字
[ FILE1 -nt FILE2 ] FILE1 的修改时间比 FILE2 新,或 FILE1 存在而 FILE2 不存在
[ FILE1 -ot FILE2 ] FILE1 的修改时间比 FILE2 旧,或 FILE2 存在而 FILE1 不存在
[ FILE1 -ef FILE2 ] FILE1FILE2 指向同一个设备、同一个 inode 编号

针对选项、字符串、参数的条件测试:

表达式 的测试结果
[ -o OPTIONNAME ] shell 的 OPTIONNAME 选项已启用
[ -z STRING ] STRING 字符串的长度为 0
[ -n STRING ] or [ STRING ] STRING 字符串的长度不是 0
[ STRING1 == STRING2 ] 两个字符串相同。== 可用 = 代替
[ STRING1 != STRING2 ] 两个字符串不同
[ STRING1 < STRING2 ] 在当前语系中,STRING1 排序在 STRING2 的前面
[ STRING1 > STRING2 ] 在当前语系中,STRING1 排序在 STRING2 的后面
[ ARG1 OP ARG2 ] OP 代表操作符,可以是 -eq-ne-lt-le-gt-ge 之一。

可以用以下操作符来组合表达式:

表达式 的测试结果
[ ! EXPR ] EXPR 为假
[ ( EXPR ) ] 返回 EXPR 的值,用于覆盖操作符的普通优先级
[ EXPR1 -a EXPR2 ] EXPR1EXPR2 均为真
[ EXPR1 -o EXPR2 ] EXPR1EXPR2 为真

这种 [ ... ] 格式,或 test 内建命令可以对条件表达式进行测试。

7.1.1.2 then 后面的命令

then 后面的 CONSEQUENT-COMMANDS 可以是任何有效的 UNIX 命令、可执行程序、可执行脚本、shell 语句,当然除了 fi

一定要记住,thenfi 在 shell 中被当作单独的语句,因此,如果在命令行中同一行使用时,它们之间必须用分号 ; 来分隔。

在脚本中,if 语句的不同部分通常可以自动分隔。

7.1.1.3 检测文件

检测文件是否存在:

#!/bin/bash
if [ -f /var/log/messages ]
  then
    echo "/var/log/messages exists."
fi
7.1.1.4 检测 shell 选项

可以在启动脚本中添加:

if [ -o noclobber ]
  then
	echo "Your files are protected against accidental overwriting using redirection."
fi

7.1.2 if 的常见应用

7.1.2.1 测试退出状态

? 变量会保存上一个前台命令的退出状态。

直接测试 ? 的值:

grep $USER /etc/passwd
if [ $? -eq 0 ]

用表达式测试:

if ! grep $USER /etc/passwd
7.1.2.2 数字的比较
if [ "$num" -gt "150"
7.1.2.3 字符串比较
if [ "$(whoami)" != 'root' ]; then
[ "$(whoami)" != 'root' ] && ( echo you are using a non-privileged account; exit 1 )

比较语句中也可以使用正则表达式:

if [[ "$gender" == f* ]]

👉 test 命令

大部分程序员更喜欢用 test 内建命令来进行测试,其效果与方括号是一样的:

test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

👉 不能用 exit

因为在子 shell 中调用 exit 会导致无法传递变量。

因此,如果不希望 bash 产生子 shell,可以用 { },而不用 ( )

7.2 if 高级应用

7.2.1 if / then / else 结构

7.2.1.1 范例

该结构可以根据不同的测试结果,分别进行不同的操作。

if [[ "$gender" == "f*" ]]
then echo "Pleasure to meet you, Madame."
else echo "How come the lady hasn't got a drink yet?"
fi

单层方括号与双层方括号的区别:

双层 [[ ]] 可以防止变量值被执行 字段分割,还可以防止 路径扩展

7.2.1.2 检测命令行参数

在命令行中为变量赋值,比从脚本中赋值来的更加优雅。

通常使用位置参数 $1$2 等来完成,$# 表示命令行参数的总数,$0 表示脚本名称。

例脚本内容为:

weight="$1"
height="$2"
idealweight=$[$height - 110]

if [ $weight -le $idealweight ] ; then
  echo "You should eat a bit more fat."
else
  echo "You should eat a bit more fruit."
fi

调用脚本:

bash -x weight.sh 55 169
7.2.1.3 测试参数的数量

$# 表示命令行参数的总数。

if [ ! $# == 2 ]; then
  echo "Usage: $0 weight_in_kilos length_in_centimeters"
  exit
fi
7.2.1.4 测试文件是否存在

许多脚本中经常用到这个测试。

#!/bin/bash

FILENAME="$1"

echo "Properties for $FILENAME:"

if [[ -f $FILENAME ]]; then
  echo "Size is $(ls -lh $FILENAME | awk '{ print $5 }')"
  echo "Type is $(file $FILENAME | cut -d":" -f2 -)"
  echo "Inode number is $(ls -i $FILENAME | cut -d" " -f1 -)"
  echo "$(df -h $FILENAME | grep -v Mounted | awk '{ print "On",$1", \
which is mounted as the",$6,"partition."}')"
else
  echo "File does not exist."
fi

7.2.2 if / then / elif / else 结构

基本结构:

if TEST-COMMANDS; then
	CONSEQUENT-COMMANDS;
elif MORE-TEST-COMMANDS; then
	MORE-CONSEQUENT-COMMANDS;
else ALTERNATE-CONSEQUENT-COMMANDS;
fi

7.2.3 if 语句的嵌套

if 可以嵌套多层 if

7.2.4 布尔运算

如果条件很复杂,可以用布尔运算符来连接多个条件:

&& :表示

|| :表示

#!/bin/bash

year=`date +%Y`

if (( ("$year" % 400) == "0" )) || (( ("$year % 4 == "0") && ("$year" % 100 != "0") )); then
	echo "leap year"
else
	echo "not leap year"
fi

7.2.5 exit 语句

exit 语句会终止脚本的执行,经常用于用户发来的输入请求有误时,或某个语句没有成功运行,或发生其它错误时。

exit 可以接一个参数,该参数必须是整数,代表退出状态码,它会被传递给父进程,并保存到 $? 变量中。

exit 0 表示脚本运行成功,任何其他值都用于针对不同的错误进行不同的操作。如果 exit 命令没有跟参数,父 shell 会使用 $? 变量的当前值。

7.3 case 语句

7.3.1 简化条件

虽然可以使用嵌套的 if 语句,但如果不同的可能性太多,就会越来越不方便。对于更复杂的条件,建议使用 case 语句:

case EXPRESSION in
CASE1)
	COMMAND-LIST
	;;
CASE2)
	COMMAND-LIST
	;;
 ...
CASEN)
	COMMAND-LIST
	;;
esac

8. 编写交互式脚本

8.1 显示用户消息

8.1.1

有些脚本运行时不需要与用户进行任何交互,它们的优点:

  • 每次的运行结果可预知
  • 可在后台运行

然而有些脚本需要用户的输入,或需要给用户提供输出,它们的优点:

  • 可以做的更灵活
  • 脚本运行时可以有多种可能性
  • 脚本运行时可以汇报其运行进度

编写交互式脚本时,要尽可能地添加注释。如果脚本能够提示合适的消息,则对用户更友好,更容易调试。否则就等着接投诉电话吧。因此,需要等待时可以提示用户 “当前正在计算,请稍等”,如果计算时间会很久,可以考虑在脚本的输出中添加一个进度提示。

当提示用户输入时,也应该尽可能详细地说明对输入数据的要求,包括参数和用法等。

8.1.2 使用 echo

echo 为内建命令,它会将其参数输出,用空格分隔,在换行符处终止。返回状态永远是 0。

echo 可以用的选项:

  • -e :识别反斜线转义序列,如 \n\a
  • -n :行尾不加换行符,输出会紧接着下一个提示符

以下为 echo 可用的转义序列:

序列 作用
\a 警示字符,提示音
\b 回退字符
\c 取消输出中最后一个参数后面的换行符。 \c 序列后面的所有字符都将被忽略。
\e 转义符,用于转义控制字符
\f 馈页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\\ 反斜线
\0NNN 3 位 8 进制数字
\NNN 3 位二进制数字
\xHH 2 位十六进制数字

【 关于 \e 转义符 】 ASCII 码的前 32 个字符被保留,做为控制字符,也叫转义序列。这些控制字符不是为了输出可见信息,而是为了控制使用 ASCII 码的设备(打印机、显示器等),或为数据流提供元信息(如磁带中的数据流)。 转义序列通常可以通过 Ctrl 键与其它键的组合来 模拟。 第 27 (十进制)个转义符,即八进制的第 33 个(\033),十六进制的 0x1b,是转义序列符。不同的 shell、不同的语言和工具处理起来会有所不同。可以用 Ctrl - [ 来模拟,即 ^[。 在输出时,要想使用控制字符,必须在其前面加上 \e 转义符,这样像 echo -e 这样的命令才能识别。 常见的控制字符: 7,响铃字符,会发出响铃以警示用户。bell,BEL\a^G 8,覆盖显示前一个字符。backspace,BS\b^H 10,换行符。line feed,LF\n^J

8.2 捕捉用户的输入

8.2.1 read 命令

read 是内建命令,与 echo 相反,它要读取用户的输入。

read [options] NAME1 NAME2 ... NAMEN

read 命令会从命令行读取一行,或者从文件描述符中读取。读取到的第一个字段被分配给变量 NAME1,以此类推。

变量值中的 IFS 用于分割输入行。

反斜线用于转义或继续上行输入。

如果在 read 命令中没有指定 NAME1 这些变量,读取的行会赋予变量 REPLY

read 通常返回代码为 0。除非遇到 EOF 字符,或超时,或参数中的文件描述符无效。

read 命令的选项
选项 作用
-a ANAME 字段会被依次分配给数组变量 ANAME,索引从 0 开始。为该数组赋值之前,会先清空该数组。
-d DELIM 用来结束输入行的不再是换行符,而是 DELIM 中的第一个字符
-e readline 命令来读取输入行
-n NCHARS read 命令在读取到 NCHARS 这个字符后立即返回,而不会读完整行输入
-p PROMPT 在读取输入之前,先在终端上显示一个提示信息 PROMPT,只对从终端读取输入有效
-r 反斜线不再有转义和继续上一行的作用
-s 安静模式,如果从终端读取输入,键入的文字不会被回显
-t TIMEOUT 设置超时,指定时间内如果没有能够读取到一行输入就超时。仅适用于从终端和管道读取输入。
-u FD 从文件描述符 FD 中读取输入

8.2.2 用户输入的提示信息

在使用 read 命令来读取用户输入时,要确保用户明白你需要的数据是什么。

echo -n "Enter your name and press [ENTER]: "
read name
echo -n "Enter your gender and press [ENTER]: "
read -n 1 gender

8.2.3 重定向与文件描述符

8.2.3.1 一般用法

脚本中也可以发生重定向,如从文件中读取输入,或把输出发送给文件。

文件的输入和输出是由文件描述符完成的,最常用的是标准输入、标准输出、标准错误,分别为 0,1,2。

bash 也可以把网络主机上的 TCP 或 UDP 端口做为文件描述符 来使用。

执行某个命令时,以下步骤会依次发生:

  • 如果上一个命令的标准输出正通过管道被定向到当前命令的标准输入,/proc/<current_process_ID>/fd/0 会更新,指向与 /proc/<previous_process_ID/fd/1 相同的匿名管道。
  • 如果当前命令的标准输出通过管道被重定向到下一个命令的标准输入,/proc/<current_process_ID/fd/1 会更新,指向另一个匿名管道。
  • 针对当前命令的重定向是从左向右处理的
  • 一条命令后面的重定向 N>&MN<&M,其效果等同于用与 /proc/self/fd/M 相同的目标,来创建或更新符号链接 /proc/self/fd/N
  • 重定向 N> fileN< file ,其效果等同于用目标文件创建或更新符号链接 /proc/self/fd/N
  • N>&- 其效果等同于删除符号链接 /proc/self/fd/N
  • 只有到现在,当前的命令才执行

从命令行中执行脚本时,不会发生太多的改变,因为子 shell 进程使用的文件描述符与父 shell 相同。如果父进程不存在,如通过 cron 来运行脚本时,标准的文件描述符是管道或其它临时文件,除非使用重定向。

8.2.3.2 对错误进行重定向

重定向标准错误时,一定要记住,顺序至关重要

ls -l * 2> /var/tmp/unaccessible-in-spool

把错误重定向到文件。

ls -l * > /var/tmp/spoollist 2>&1

把标准输出和标准错误都重定向到文件:

先把标准输出重定向到文件,再把标准错误重定向到标准输出。

ls -l * 2 >& 1 > /var/tmp/spoollist

先把标准错误重定向到当前的标准输出,再把当前的标准输出重定向到文件。因此最终只有标准输出被重定向到文件。

为了方便,经常把错误重定向到 /dev/null

&> FILE 等效于 > FILE 2>&1,使用起来更简单。

8.2.4 文件输入与输出

8.2.4.1 使用文件描述符

/dev/fd 目录包含了所有的描述符,如 0,1,2 等。打开文件 /dev/fd/N 相当于复制文件描述符 N。如果当前系统提供 /dev/stdin/dev/stdout/dev/stderr,你会看到它们只是指向 /dev/fd/0/dev/fd/1/dev/fd/2 的符号链接。

/dev/fd 中的文件主要是在 shell 中使用,通过该机制,以路径为参数的那些程序可以像对待其它路径一样来操作这些描述符。 如果 /dev/fd 在当前系统不可用,可以用 - 来代替标准输入或标准输出。

filter body.txt.gz | cat header.txt - footer.txt | lp
filter body.txt | cat header.txt /dev/fd/0 footer.txt | lp

以上两条命令是等效的,将三个文档打印出来。

8.2.4.2 readexec
为文件分配描述符

exec 命令可用来替换当前的 shell,或更改当前 shell 的描述符。

例如,可以用来给文件分配一个描述符:

exec fdN> file 给文件分配描述符,用于输出

exec fdN< file 给文件分配描述符,用于输入

然后就可以在 shell 中直接使用描述符了:

exec 4> result.txt

filter body.txt | cat header.txt /dev/fd/0 footer.txt >& 4

给文件 result.txt 分配描述符 4,用于输出,然后把三个文件的输出及错误全部保存到该文件。

描述符 5 可能会造成问题,因为 bash 用 exec 创建子 shell 时,其子进程会继承描述符 5,最好保留这个描述符不要使用。

读取脚本

该脚本会把重要的配置文件制作索引,将它们放到一个备份文件中。

CONFIG=/var/tmp/sysconfig.out
rm "$CONFIG" 2>/dev/null

echo "Output will be saved in $CONFIG."

exec 7<&0
# 创建描述符 7,用于输入,与描述符 0 的目标相同
# 相当于把描述符 0 的值先保存到 7 里

exec < /etc/passwd
# 更新描述符 0 为 /etc/passwd 文件,此时标准输入为一个文件

read rootpasswd
# 从标准输入读取数据,相当于读取 passwd 文件的第一行内容

echo "Your root account info:" >> $CONFIG
# 把一行说明信息追加到配置文件中

echo $rootpasswd >> "$CONFIG"
# 把从 passwd 读取到的第一行数据追加到配置文件

exec 0<&7 7<&-
# 把原来保存在描述符 7 里的值还给 0,恢复之前的描述符 0
# 删除描述符 7

echo -n "Enter comment or [ENTER] for no comment: "
read comment; echo $comment >> "$CONFIG"
# 此时 read 就会从命令行读取了
8.2.4.3 关闭文件描述符

因为子进程会继承打开文件的描述符,所以不再需要时要及时将其关闭:

exec 7<&-
8.2.4.4 Here 文档

编写脚本时,调用的程序或脚本经常需要输入。shell 可以通过 here 文档来读取输入,直到某行只包含关键字符串,且其后面没有空白字符。到此行读取的所有内容将一起做为标准输入。

使用 here 文档就可以免于调用单独的文件了,而且比用一堆 echo 也简单一些。

虽然名字叫 here 文档,实际上它是脚本中的一种 结构 而已。

# Start here document
cat << BROWSERS
mozilla
links
lynx
konqueror
opera
netscape
BROWSERS
# End here document

……….