Post

ch4 执行命令

4.1 运行程序

一些 sh 用户的常见做法是创建个人 bin 目录,这类似于保存可执行文件的系统目录 /bin 和 /usr/bin。你可以将自己喜欢的 shell 脚本和其他定制或私有命令放入个人的 bin 目录中(如果创建于主目录,则路径为~/bin)。然后将该目录加入 $PATH,甚至可以放在目录列表的最前面(PATH=~/bin:$PATH)。这样一来,你既可以拥有自己偏好的定制工具,也不存在误执行陌生人命令的安全风险。

4.2 依次执行多个命令

  1. 手动不停的输入
  2. 命令写入文件,然后 sh 执行这个文件中的命令
  3. 依次执行每个命令
    1. 如果你想运行各个程序,不管之前的程序是否成功运行,只需要用分号将其隔开:long ; medium ; short
    2. 如果只想在上一个程序成功运行的情况下运行下一个程序,并且所有的程序都正确设置了退出码,那么可以用 && 将其隔开:long && medium && short

4.3 同时执行多个命令

执行 3 个命令,但这些命令相互独立,不必等待前一个命令运行结束。

  1. 可以在命令末尾添加一个 & 符号,在后台运行该命令。
1
2
3
4
5
$ long &
[1] 4592
$ medium &
[2] 4593
$ short
  1. 在单个命令行中完成所有操作。
1
2
3
$ long & medium & short
[1] 4592
[2] 4593

后台的意思:

在“后台”(Linux 其实没有这么个地方)运行某个命令时,真正的意思是我们断开了键盘输入与命令之间的联系,shell 在显示命令行提示符并接受更多命令输入前不会再等着该命令完成。命令输出(除非我们采取明确的操作来改变这种行为)仍然会出现在屏幕上,因此,示例中 3 个命令的输出会在屏幕上交错出现。

作业号或进程 ID 可用于对作业实施有限的控制。例如,我们可以用kill %1(因为作业号为 1)或者指定进程 ID(kill 4592)来“杀死”long 作业,二者能够实现相同的结果。

你也可以用作业号重新连接到后台作业。例如,可以用 fg %1 将long 作业放回前台。如果后台只有一个作业在运行,甚至都不用指定作业号,只使用 fg 即可。如果执行某个命令,但发觉其完成时间比预想的更长,那么可以用 Ctrl-Z 暂停该命令,返回到提示符下。接着输入 bg 来恢复作业,并在后台继续运行。这么做的效果相当于事前在命令尾部加上 & 符号。

4.4 了解命令是否成功运行

1
2
3
4
5
6
7
8
$ somecommand
# it works...
$ echo $?
0
$ badcommand
# it fails...
$ echo $?
1

shell 变量 $? 中保存着命令的退出状态,其取值范围为 0~255。

在编写 shell 脚本时,良好的做法是:如果一切正常,脚本退出时就返回 0;如果运行过程中出错,则返回非 0 值。我们推荐只使用0~127 作为返回值,因为 shell 用 128+N 代表 被信号N“杀死”。另外,如果使用的值大于 255 或小于 0,则会出现值回绕。可以用 exit 语句(如 exit 1 或 exit 0)返回退出状态。但要注意,读取命令退出状态的机会只有一次

1
2
3
4
5
6
7
$ badcommand
# 运行失败……
$ STAT=$?
$ echo $STAT
1
$ echo $STAT
1

将退出状态值保存在变量 $STAT 中,以便随后检查。

1
2
3
somecommand
...
if (( $? )) ; then echo failed ; else echo OK; fi

4.5 仅当一个命令运行成功后才执行下一个命令

用命令的退出状态($?)配合 if 语句来实现

1
2
3
4
cd mytmp
if (( $? == 0 )); then rm * ; fi
## 更好的写法
if cd mytmp; then rm * ; fi

Q: 那么变量 $? 中的值是怎么来的?

  1. C 语言程序员会将其作为提供给 exit() 函数的参数值,例如,exit(4); 会返回 4。
  2. 对于 shell 而言,退出码 0 代表成功,非 0 则代表失败。

4.6 减少 if 语句的数量

在 sh 中使用 && 运算符,根据条件执行命令。为一个短路运算。

1
cd mytmp && rm *

用 && 分隔两个命令,以此告诉 sh 先执行第一个命令,如果该命令成功(退出状态为 0),再执行第二个命令。这非常类似于用 if 语句检查第一个命令的退出状态,从而判断是否执行第二个命令。

1
2
cd mytmp
if (( $? == 0 )); then rm * ; fi

要想彻底检查错误,但又不想到处出现 if 语句,可以设置 -e 标记,这样的话,只要脚本中有任何命令(排除在 while 循环和 if 语句中,因为其本身就要用到退出状态)出现错误(退出状态为非0),sh 就会退出。

1
2
3
set -e
cd mytmp
rm *

设置了 -e 标记之后,shell 会在命令失败时退出。如果本例中的cd 命令失败,脚本则直接退出,不再执行 rm * 命令。但不推荐在交互式 shell 中这么做,因为如果 shell 退出,那么终端窗口也会随之消失。

4.7 无人值守下运行耗时作业

在后台运行作业并在该作业完成前退出 shell,那就需要对作业使用 nohup。

1
2
$ nohup long &
nohup: appending output to `nohup.out'

将作业置入后台时(&),它仍旧是 Bash shell 的子进程。如果退出 shell 的某个实例,bash 就会向其所有子进程发送 hangup 信号。这就是作业运行不了多久的原因。只要退出 bash,后台作业就会被“杀死”。(嗨,你要离开了。它是怎么知道的呢?)nohup 命令只是设置子进程忽略 hangup 信号。你仍可以用 kill 命令“杀死”作业,因为 kill 发送的是 SIGTERM 信号,而非 SIGHUP 信号。但有了 nohup,作业就不会在退出 sh 时被无意间“杀死”。

nohup 给出的那句关于追加输出的消息只是为了提高自身的实用性。因为你有可能发出 nohup 命令后就退出 shell,输出信息也就无处可去了,也就是说,终端中的 sh 会话已经结束,作业无法再向 STDOUT 写入。更重要的是,向不存在的位置写入信息会产生错误。因此,nohup 会替你重定向输出,将其追加(不是覆盖,而是添加到文件现有内容的末尾)到当前目录下的 nohup.out 文件中。你也可以明确地在命令行上指定将输出重定向到其他地方,nohup 足够聪明,能够发现你已经另有安排,也就不会再使用 nohup.out 了。

4.8 出现故障时显示错误消息

一些 shell 程序员的惯用做法是配合使用 和命令来输出调试 / 错误消息
1
2
cmd || printf "%b" "cmd failed. You're on your own\n"
cmd || printf "%b" "FAILED.\n" ; exit 1
exit 无论如何都不会执行! 仅作用于前两个命令。如果只想在cmd 出错时执行 exit,那么要将其与 printf 分组到一起,以便两者被视为一个单元。语法如下所示:
1
cmd || { printf "%b" "FAILED.\n" ; exit 1 ; }

最后一个命令必须以分号结尾,闭合花括号与其中的内容之间要用空白字符分隔。

&& 告诉 shell 如果第一个表达式为假,则不再评估第二个表达式,与此类似,|| 告诉 shell 如果第一个表达式为真(成功),则不再评估第二个表达式。

&& 一样,|| 可以追溯到逻辑运算和 C 语言,如果 A OR B 中的表达式 A 为真,则最终结果就为真,无须评估表达式 B。

在 shell 中,如果第一个表达式返回0(成功),则跳过剩下的表达式,继续往下执行。仅当第一个表达式返回非 0 值(命令出错)时,才必须评估第二个表达式中的命令。

4.9 执行变量中的命令

不仅可以将变量内容用于参数,还可以用于命令本身。

1
2
3
4
5
FN=/tmp/x.x
PROG=echo
$PROG $FN
PROG=cat
$PROG $FN

可以将命令名保存在变量($PROG)中,然后在需要命令名的地方引用该变量,sh 会使用变量($PROG)的值作为要执行的命令。

sh 解析命令行,用相应的值替换其中的变量,最后将替换后的结果作为最终命令,就像是我们一字不差地敲入那样。

4.10 执行目录中的所有脚本

你想要执行一系列脚本,但是脚本清单并不固定,新脚本不停地加入其中,可你也不想总是修改清单。

将要执行的脚本全都放进一个目录,让 sh 逐一执行。不用保存脚本清单,用该目录的内容作为清单即可。以下脚本会执行特定目录中的所有可执行文件。

1
2
3
4
5
6
7
for SCRIPT in /path/to/scripts/dir/*
do
    if [ -f "$SCRIPT" -a -x "$SCRIPT" ]
    then
        $SCRIPT
    fi
done

变量 $SCRIPT 会依次获得通配符 * 匹配到的各个文件名,该通配符能够匹配指定目录下的所有内容(文件名以点号开头的隐藏文件除外)。如果匹配到的是文件(由 -f 测试)且具有执行权限(由 -x 测试),那么 shell 会尝试执行此脚本。

This post is licensed under CC BY 4.0 by the author.