2. 第二部分 shell 脚本编程基础
2.11 构建基础脚本
2.11.2 创建 shell 脚本文件
- 其中输入命令。 在创建 shell 脚本文件时,必须在文件的第一行指定要使用的 shell,格式如下: #!/bin/bash 在普通的 shell 脚本中,#用作注释行。shell 并不会处理 shell 脚本中的注释行。然而,shell 脚本文件的第一行是个例外,#后面的惊叹号会告诉 shell 用哪个 shell 来运行脚本。(是的,可以使用 bash shell,然后使用另一个 shell 来运行你的脚本。)
- 通过 chmod 命令(参见第 7 章)赋予文件属主执行文件的权限后,就可以直接
./script.sh来运行脚本了。
2.11.3 显示消息
- 如果在 echo 命令后面加上字符串,那么 echo 命令就会显示出这个字符串
- echo输出默认句末带一个换行符
\n,可以使用-n选项取消换行符
2.11.4 使用变量
2.11.4.1 环境变量
- 反斜线允许 shell 脚本按照字面意义解释$,而不是引用变量
- 通过${variable}形式引用的变量。花括号通常用于帮助界定$后的变量名。
2.11.4.2 用户自定义变量
- 使用等号为变量赋值。在变量、等号和值之间不能出现空格
- shell 脚本会以字符串形式存储所有的变量值,脚本中的各个命令可以自行决定变量值的数据类型。
- shell 脚本中定义的变量在脚本的整个生命周期里会一直保持着它们的值,在脚本结束时会被删除。
- 引用变量值时要加$,对变量赋值时则不用加$
2.11.4.3 命令替换
- 将命令输出赋给变量:
var=$(command)或者: var=`command` 注意,赋值号和命令替换 符之间没有空格
2.11.5 重定向输入和输出
2.11.5.1 输出重定向
- 最基本的重定向会将命令的输出发送至文件。bash shell 使用“>”来实现该操作
- 将命令输出追加到已有文件中:>>
2.11.5.2 输入重定向
- 输入重定向会将文件的内容作为命令的输入。使用“<”来实现该操作
command < inputfile
- 一种简单的记忆方法是,在命令行中,命令总是在左侧,而重定向运算符“指向”数据流动的方向。小于号说明数据正在从输入文件流向命令。
- 还有另外一种输入重定向的方法,称为内联输入重定向。这种方法 无须使用文件进行重定向,只需在命令行中指定用于输入重定向的数据即可。
- 内联输入重定向的格式如下:
command << delimiter
- 其中,delimiter 是一个用户定义的字符串,用于标识输入重定向的结束。
- 内联输入重定向的工作原理是,shell 会将命令行中所有位于 delimiter 之前的文本作为命令的输入。
例如:
1 2 3 4 5
| $ wc << EOF > test string 1 > test string 2 > test string 3 > EOF
|
1 2 3
| cat >> $outfile << EOF INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('$lname', '$fname', '$address', '$city', '$state', '$zip'); EOF
|
2.11.6 管道
- 将一个命令的输出作为另一个命令的输入
command1 | command2
- 实际上,Linux 系统会同时运行这两个命令, 在系统内部将二者连接起来。当第一个命令产生输出时,它会被立即传给第二个命令。数据传输 不会用到任何中间文件或缓冲区。
- 管道可以串联的命令数量没有限制。可以持续地将命令输出通过管道传给其他命令来细化操作。
2.11.7 执行数学运算
2.11.7.2 使用括号
- 要将数学运算结果赋给变量,使用
var=$((expression))
- bash shell 的数学运算符只支持整数运算, z shell(zsh)提供了完整的浮点数操作。
2.11.8 退出脚本
2.11.8.1 查看退出状态码
- Linux 提供了专门的变量$?来保存最后一个已执行命令的退出状态码。
- 按照惯例,对于成功结束的命令,其退出状态码是 0。对于因错误而结束的命令,其退出状态码是一个正整数。
2.1.8.2 exit 命令
- exit 命令允许在脚本结束时指定一个退出状态码
- 退出状态码必须是 0 到 255 之间的整数。如果指定了超出这个范围的值,则会自动对256取模。
2.12 结构化命令
2.12.1 使用 if-then 语句
1 2 3
| if command; then commands fi
|
2.12.2 if-then-else
1 2 3 4 5
| if command; then commands else commands fi
|
2.12.3 if-then-elif-else
1 2 3 4 5 6 7 8
| if command; then commands elif command; then commands (省略可能的多个elif语句) else commands fi
|
2.12.4 test 命令
1 2 3
| if [ condition ]; then commands fi
|
2.12.4.1 数值比较
| 比较 |
描述 |
| n1 -eq n2 |
检查 n1 是否等于 n2 |
| n1 -ge n2 |
检查 n1 是否大于或等于 n2 |
| n1 -gt n2 |
检查 n1 是否大于 n2 |
| n1 -le n2 |
检查 n1 是否小于或等于 n2 |
| n1 -lt n2 |
检查 n1 是否小于 n2 |
| n1 -ne n2 |
检查 n1 是否不等于 n2 |
2.12.4.2 字符串比较
| 比较 |
描述 |
| -z string |
检查字符串是否为空 |
| -n string |
检查字符串是否不为空 |
| string1 = string2 |
检查字符串是否相等 |
| string1 != string2 |
检查字符串是否不相等 |
| string1 < string2 |
检查字符串是否小于 string2 |
| string1 > string2 |
检查字符串是否大于 string2 |
测试表达式使用标准的数学比较符号来表示字符串比较,而用文本代码来表示数值比较。
2.12.4.3 文件比较
| 比较 |
描述 |
| -e filename |
检查文件是否存在 |
| -f filename |
检查文件是否存在且是一个普通文件 |
| -d filename |
检查文件是否存在且是一个目录 |
| -r filename |
检查文件是否存在且可读 |
| -w filename |
检查文件是否存在且可写 |
| -x filename |
检查文件是否存在且可执行 |
| -s filename |
检查文件是否存在且非空 |
| filename1 -nt filename2 |
检查 filename1 是否比 filename2 新 |
| filename1 -ot filename2 |
检查 filename1 是否比 filename2 旧 |
2.12.5 复合条件测试
- if-then 语句允许使用布尔逻辑将测试条件组合起来。可以使用以下两种布尔运算符:
- [ condition1 ] && [ condition2 ]
- [ condition1 ] || [ condition2 ]
2.12.6 if-then 的高级特性
2.12.6.1 使用单括号
1 2 3
| if (command); then commands fi
|
- 单括号中的命令会在子 shell 中执行,而不是在当前 shell 中执行。
- 单括号中的命令可以使用分号分隔多个命令。
- 单括号中的命令可以使用反引号或美元符号引用变量。
2.12.6.2 使用双括号
1 2 3
| if ((expression)); then commands fi
|
| 符号 |
描述 |
| val++ |
后增 |
| val– |
后减 |
| ++val |
先增 |
| –val |
先减 |
| ! |
逻辑求反 |
| ~ |
位求反 |
| ** |
幂运算 |
| << |
左位移 |
| >> |
右位移 |
| & |
位布尔 AND |
| | |
位布尔 OR |
| && |
逻辑 AND |
| || |
逻辑 OR |
- 双括号命令既可以在 if 语句中使用,也可以在脚本中的普通命令里用来赋值
- 双括号中表达式的大于号和小于号不用转义。这是双括号命令又一个优越性的体现。
2.12.6.3 使用双方括号
1 2 3
| if [[ expression ]]; then commands fi
|
- 提供了 test 命令所不具备的另一个特性——模式匹配。
- 双等号(==)会将右侧的字符串视为一个模式并应用模式匹配规则。
2.12.7 case 命令
- 有了 case 命令,就无须再写大量的 elif 语句来检查同一个变量的值了。case 命令会采 用列表格式来检查变量的多个值:
1 2 3 4 5 6 7 8 9 10 11
| case variable in pattern1) commands ;; pattern2) commands ;; *) commands ;; esac
|
2.13 更多的结构化命令
2.13.1 for 命令
1 2 3
| for variable in list; do commands done
|
2.13.1.2 读取列表中的复杂值
- for 循环假定各个值之间是 以空格分隔的。如果某个值含有空格, 则必须将其放入双引号内。
2.13.1.5 更改字段分隔符
- IFS 环境变量定义了 bash shell 用作字段分隔符的一系列字符。在默认情况下,bash shell 会将下列字符视为字段分隔符。
- 修改 IFS 环境变量可以改变 bash shell 用作字段分隔符的字符。修改 IFS 的值,使其只能识别换行符:
IFS=$'\n'
在 Bash 里,$’…’ 是 ANSI-C 风格的字符串,会识别 C 语言风格的转义符
$’\n’ → 真正的换行符
$’\t’ → 制表符(Tab)
$’\x41’ → 字符 A
在处理代码量较大的脚本时,可能在一个地方需要修改 IFS 的值,然后再将其恢复原状,而脚本的其他地方则继续沿用 IFS 的默认值。一种安全的做法是在修改 IFS 之前保存原来的 IFS 值,之后再恢复它。这种技术可以像下面这样来实现:
IFS.OLD=$IFS IFS=$'\n'
<在代码中使用新的 IFS 值>
IFS=$IFS.OLD
这就保证了在脚本的后续操作中使用的是 IFS 的默认值。
2.13.1.6 使用通配符读取目录
- 用 for 命令来自动遍历目录中的文件。
- 在文件名或路径名中使用 通配符,这会强制 shell 使用文件名通配符匹配(file globbing)。
1 2 3
| for file in /path/*; do commands done
|
在 Linux 中,目录名和文件名中包含空格是完全合法的。要应对这种情况,应该将$file 变 量放入双引号内。否则,遇到含有空格的目录名或文件名时会产生错误
1 2 3
| for file in /path/* /path2/*; do commands done
|
2.13.2 C 语言风格的 for 命令
- bash 中仿 C 语言的 for 循环的基本格式如下:
1 2 3
| for (( variable assignment ; condition ; iteration process )); do commands done
|
2.13.3 while 命令
2.13.3.1 while 的基本格式
1 2 3
| while [ condition ]; do commands done
|
2.13.7 循环控制
2.13.7.1 break 命令
1 2 3 4 5 6
| for var in list; do commands if [ condition ]; then break fi done
|
2.13.7.2 continue 命令
1 2 3 4 5 6
| for var in list; do commands if [ condition ]; then continue fi done
|
2.13.8 处理循环的输出
- 对循环的输出使用管道或进行重定向。通过在 done 命令之后 添加一个处理命令来实现
1 2 3
| for var in list; do commands done > output.txt
|
2.14 处理用户输入
2.14.1 传递参数
2.14.1.1 读取参数
- bash shell 会将所有的命令行参数都指派给称作位置参数(positional parameter)的特殊变量。 这也包括 shell 脚本名称。
- 位置变量的名称都是标准数字:$0 对应脚本名,$1 对应第一个命令行参数,$2 对应第二个命令行参数,以此类推。
2.14.1.2 读取脚本名
2.14.2 特殊参数变量
2.14.2.1 参数统计
- $#含有脚本运行时携带的命令行参数的个数,不包含脚本名。
${!#} 代表了最后一个位置变量
2.14.2.2 获取所有的数据
- $*变量和$@变量可以轻松访问所有参数,它们各自包含了所有的命令行参数
- $*变量会将所有的命令行参数视为一个单词。这个单词含有命令行中出现的每一个参数。 基本上,$*变量会将这些参数视为一个整体,而不是一系列个体。
- $@变量会将所有的命令行参数视为同一字符串中的多个独立的单词,以便你能遍历并处理全部参数。这通常使用 for 命令完成。
2.14.3 移动参数
shift 命令会将所有的位置参数向左移动。每个位置参数的变量名都会减 1。$1 会变成$0,$2 会变成$1,以此类推。被删除的参数会被丢弃。
shift 命令可以接受一个可选的参数,用于指定要左移几位。默认情况下,它会将所有的位置参数向左移动一个位置。
2.14.4.3 使用 getopts 命令
getopts optstring variable
- getopts 命令要用到两个环境变量。如果选项需要加带参数值,那么 OPTARG 环境变量保存的就是这个值。OPTIND 环境变量保存着参数列表中 getopts 正在处理的参数位置。这样在处理完当前选项之后就能继续处理其他命令行参数了。
使用举例:
1 2 3 4 5 6 7 8 9 10
| while getopts :ab: opt; do case $opt in a) echo "Option a" ;; b) echo "Option b with value $OPTARG" ;; \?) echo "Invalid option: -$opt" ;; esac done
|
- 在解析命令行选项时,getopts 命令会移 除起始的连字符,所以在 case 语句中不用连字符。
- 选项字符串中的冒号(:)表示该选项需要一个参数值。
2.14.6 获取用户输入
2.14.6.1 基本的读取
- read 命令从标准输入(键盘)或另一个文件描述符中接受输入。获取输入后,read 命令会 将数据存入变量。
read variable
- read 命令也提供了-p 选项,允许直接指定提示符:
read -p "Enter your name: " name
2.14.6.2 超时
- 用-t 选项来指定一个计时器。-t 选项会指定 read 命令等待输入的 秒数。如果计时器超时,则 read 命令会返回非 0 退出状态码
- 用-n 选项来指定 read 命令读取的字符数,read会在接收到设定个数的字符后自动退出。
2.14.6.3 无显示读取
- -s 选项可以避免在 read 命令中输入的数据出现在屏幕上(其实数据还是会被显示,只不 过 read 命令将文本颜色设成了跟背景色一样)
2.14.6.4 从文件中读取
- 每次调用 read 命令都会从指定文件中读取一行文本。 当文件中没有内容可读时,read 命令会退出并返回非 0 退出状态码。
1 2 3
| while read line; do echo $line done < input.txt
|
2.15 呈现数据
2.15.1 理解输入和输出
| 文件描述符 |
缩写 |
描述 |
| 0 |
STDIN |
标准输入 |
| 1 |
STDOUT |
标准输出 |
| 2 |
STDERR |
标准错误 |
- shell 对于错误消息的处理是跟普通输出分开的。只使用“>”重定向只能重定向标准输出,而不能重定向标准错误。
- 在默认情况下,STDERR 和 STDOUT 指向同一个地方————显示器。
2.15.1.2 重定向错误
- 只重定向错误
command 2> &1 2与“>”必须紧挨着,否则无法正常工作
- 重定向错误消息和正常输出
- 如果想重定向错误消息和正常输出,则必须使用两个重定向符号。你需要在重定向符号之前 放上需要重定向的文件描述符,然后让它们指向用于保存数据的输出文件
- 将 STDERR 和 STDOUT 的输出重定向到同一个文件:
&>
2.15.2 在脚本中重定向输出
- 有意在脚本中生成错误消息,可以将单独的一行输出重定向到 STDERR:
>&2
- 在重定向到文件描述符时,必须在文件 描述符索引值之前加一个&
- 可以理解为:0、1、2为文件描述符,而&1、&2、&3为文件描述符所指代的具体的文件/设备名。
2.15.2.2 永久重定向
- exec 命令会告诉 shell 在脚本执行期间重定向某个特定文件描述符:
exec 1> output.txt。还可以用来创建新的文件描述符。exec 3> testfile
2.15.3 在脚本中重定向输入
- exec 命令允许将 STDIN 重定向为文件:
exec 0< input.txt
- 只要脚本需要输入, 这个重定向就会起作用:
1 2 3 4 5 6
| exec 0< testfile count=1 while read line; do echo "Line #$count: $line" count=$[ $count + 1 ] done
|
2.15.4 创建自己的重定向
2.15.4.2 重定向文件描述符
1 2 3 4 5
| exec 3>&1 exec 1>test14out ... exec 1>&3
|
2.15.4.5 关闭文件描述符
- 要关闭文件描述符,只需将其重定向到特殊符号&-即可:
exec 3>&-
2.15.5 列出打开的文件描述符
- lsof 命令可以列出当前系统中打开的所有文件描述符。
- 最常用的选项包括-p 和-d,前者允许指定进程 ID(PID),后者允许指定要显示的文件描述符编号(多个编号之间以逗号分隔)。
- $$: 当前进程的 PID
- lsof的默认输出:
| 列 |
描述 |
| COMMAND |
进程对应的命令名的前 9 个字符 |
| PID |
进程的 PID |
| USER |
进程属主的登录名 |
| FD |
文件描述符编号以及访问类型(r 代表读,w 代表写,u 代表读/写) |
| TYPE |
文件的类型(CHR 代表字符型,BLK 代表块型,DIR 代表目录,REG 代表常规文件) |
| DEVICE |
设备号(主设备号和从设备号) |
| SIZE |
如果有的话,表示文件的大小 |
| NODE |
本地文件的节点号 |
| NAME |
文件名 |
2.15.6 抑制命令输出
- 重定向到 /dev/null 可以抑制命令的输出
- 也可以在输入重定向中将/dev/null 作为输入文件,快速清除现有文件中的数据:
cat /dev/null > testfile
2.15.7 使用临时文件
2.15.7.1 创建本地临时文件
- mktemp 会在本地目录中创建一个文件。在使用 mktemp 命令时,只需指定 一个文件名模板即可。模板可以包含任意文本字符,同时在文件名末尾要加上 6 个 X。
- mktemp 命令会任意地将 6 个 X 替换为同等数量的字符,以保证文件名在目录中是唯一的。 你可以创建多个临时文件,并确保每个文件名都不重复
- mktemp 命令的输出正是它所创建的文件名。
2.15.7.2 在/tmp 目录中创建临时文件
- -t 选项会强制 mktemp 命令在系统的临时目录中创建文件。在使用这个特性时,mktemp 命令会返回所创建的临时文件的完整路径名,而不只是文件名
2.15.7.3 创建临时目录
- -d 选项会告诉 mktemp 命令创建一个临时目录。你可以根据需要使用该目录,比如在其中 创建其他的临时文件
2.15.8 记录消息
- tee 命令就像是连接管道的 T 型接头,它能将来自 STDIN 的数据同时送往两处。一处是 STDOUT,另一处是 tee 命令行所指定的文件名(这个文件不需要先前存在)
- 配合管道命令来重定向命令输出:
command | tee testfile
- tee 命令的 -a 选项会将输出追加到文件中,而不是覆盖文件内容
2.16 脚本控制
2.16.1 处理信号
2.16.1.1 重温 Linux 信号
| 信号值 |
信号名 |
描述 |
| 1 |
SIGHUP |
挂起(hang up)进程 |
| 2 |
SIGINT |
中断(interrupt)进程 |
| 3 |
SIGQUIT |
停止(stop)进程 |
| 9 |
SIGKILL |
无条件终止(terminate)进程 |
| 15 |
SIGTERM |
尽可能终止进程 |
| 18 |
SIGCONT |
继续运行停止的进程 |
| 19 |
SIGSTOP |
无条件停止,但不终止进程 |
| 20 |
SIGTSTP |
停止或暂停(pause),但不终止进程 |
2.16.2 以后台模式运行脚本
2.16.2.1 后台运行脚本
- 以后台模式运行 shell 脚本非常简单,只需在脚本名后面加上&即可
2.16.3 在非控制台下运行脚本
- 有时候,即便退出了终端会话,你也想在终端会话中启动 shell 脚本,让脚本一直以后台模 式运行到结束。这可以用 nohup 命令来实现
- nohup 命令能阻断发给特定进程的 SIGHUP 信号。当退出终端会话时,这可以避免进程退出
nohup command
2.16.4 作业控制
2.16.4.1 查看作业
- jobs 是作业控制中的关键命令,该命令允许用户查看 shell 当前正在处理的作业
- 可以使用 jobs 命令的-l 选项(小写字母 l)查看作业的 PID
2.16.4.2 重启已停止的作业
- 暂停: ^Z
- 要以后台模式重启作业,可以使用 bg 命令
- 如果存在多个作业,则需要在 bg 命令后加上作业号
2.16.6 定时运行作业
2.16.6.1 使用 at 命令调度作业
at [-f filename] time。适用于计划运行少数几次。
2.16.6.2 调度需要定期运行的脚本
- Linux 系统使用 cron 程序调度需要定期执行的作业。cron 在后台运行,并会检查一个特殊的 表(cron 时间表),从中获知已安排执行的作业
- cron 时间表通过一种特别的格式指定作业何时运行,其格式如下:
minutepasthour hourofday dayofmonth month dayofweek command
- cron 时间表允许使用特定值、取值范围(比如 1-5)或者通配符(*)来指定各个字段。
- 如果想在每天的 10:15 运行一个命令,可以使用如下 cron 时间表字段:
15 10 * * * command
- 列出已有的 cron 时间表:
crontab -l
- 向cron 时间表添加字段:
crontab -e, 然后添加15 10 * * * command
- 保存并退出编辑器后, cron 会在指定的时间运行命令