执行环境

Shell 脚本执行环境

在写脚本时,在一开始(Shebang 之后)就加上这一句,或者它的缩略版:

$ set -xeuo pipefail

这能避免很多问题,更重要的是能让很多隐藏的问题暴露出来。

异常处理

在 Shell 脚本中,通过插入 set -e 可以设置在有命令失败时候退出,我们还可以通过添加 DEBUG 以及 EXIT 注解来在脚本退出前执行某些命令,通过该语句可以输出最末执行的存在问题的语句:

# 脚本执行配置: https://intoli.com/blog/exit-on-errors-in-bash-scripts/
set -e
# keep track of the last executed command
trap 'last_command=$current_command; current_command=$BASH_COMMAND;' DEBUG
# echo an error message before exiting
trap '
err_code=$?;
err_command=${last_command};
if [ "$err_code" != "0" ]; then
  echo "\"${err_command}\" 命令异常退出 $err_code."
fi
' EXIT

我们也可以指定在某个语句失败的时候输出错误:

exit_on_error() {
    exit_code=$1
    last_command=${@:2}
    if [ $exit_code -ne 0 ]; then
        >&2 echo "\"${last_command}\" command failed with exit code ${exit_code}."
        exit $exit_code
    fi
}

# enable !! command completion
set -o history -o histexpand

-u 参数则是试图使用未定义的变量,就立即退出。如果在 bash 里使用一个未定义的变量,默认是会展开成一个空串。有时这种行为会导致问题,比如:rm -rf $MYDIR/data,如果 MYDIR 变量因为某种原因没有赋值,这条命令就会变成 rm -rf /data,使用 -u 可以避免这种情况。

但有时候在已经设置了 -u 后,某些地方还是希望能把未定义变量展开为空串,可以这样写:${SOME_VAR:-}。还有一种情况是在管道执行中,我们可以设置如果管道的某个命令出错则直接抛出异常:

set -o pipefail

set -o pipefail 会在某个管道(譬如 curl -s https://sipb.mit.edu/ | grep foo)中的任意命令出错时候返回整体错误,而不是仅当最后一个命令异常时候才抛出异常。这样和 -e 参数搭配使用的时候,会在任意管道中的命令出错时候抛出异常。

防止重叠运行

在一些场景中,我们通常不希望一个脚本有多个实例在同时运行。比如用 crontab 周期性运行脚本时,有时不希望上一个轮次还没运行完,下一个轮次就开始运行了。这时可以用 flock 命令来解决。flock 通过文件锁的方式来保证独占运行,并且还有一个好处是进程退出时,文件锁也会自动释放,不需要额外处理。

用法 1:假设你的入口脚本是 myscript.sh,可以新建一个脚本,通过 flock 来运行它:

# flock --wait 超时时间   -e 锁文件   -c "要执行的命令"
# 例如:
flock  --wait 5  -e "lock_myscript"  -c "bash myscript.sh"

用法 2:也可以在原有脚本里使用 flock。可以把文件打开为一个文件描述符,然后使用 flock 对它上锁(flock 可以接受文件描述符参数)。

exec 123<>lock_myscript   # 把lock_myscript打开为文件描述符123
flock  --wait 5  123 || { echo 'cannot get lock, exit'; exit 1; }

脚本调试

调试模式

我们可以在执行脚本的时候添加 x 参数:

$ bash -x script-name
$ bash -xv script-name

#!/bin/bash -x
echo "Hello ${LOGNAME}"
echo "Today is $(date)"
echo "Users currently on the machine, and their processes:"
w

set 指令

Bash shell 提供了调试选项,可以使用 set 命令打开或关闭它。

  • set -x:在执行命令时显示命令及其参数。
  • set -v:显示读取的外壳程序输入行。
  • set -n:读取命令,但不执行。这可用于检查 shell 脚本中的语法错误。
#!/bin/bash
### Turn on debug mode ###
set -x

# Run shell commands
echo "Hello $(LOGNAME)"
echo "Today is $(date)"
echo "Users currently on the machine, and their processes:"

### Turn OFF debug mode ###
set +x

# Add more commands without debug mode

另一个例子如下:

#!/bin/bash
set -n # only read command but do not execute them
set -o noexec
echo "This is a test"
# no file is created as bash will only read commands but do not executes them
>/tmp/debug.txt

连续管道日志

有时候我们会用到把好多条命令用管道串在一起的情况。如 cmd1 | cmd2 | cmd3 | ... 这样会让问题变得难以排查,因为中间数据我们都看不到。如果改成这样的格式:

cmd1 > out1.dat
cat out1 | cmd2 > out2.dat
cat out2 | cmd3 > out3.dat

性能又不太好,因为这样 cmd1, cmd2, cmd3 是串行运行的,这时可以用 tee 命令:

cmd1 | tee out1.dat | cmd2 | tee out2.dat | cmd3 > out3.dat

其他技巧

意外退出时杀掉所有子进程

我们的脚本通常会启动好多子脚本和子进程,当父脚本意外退出时,子进程其实并不会退出,而是继续运行着。如果脚本是周期性运行的,有可能发生一些意想不到的问题。在 StackOverflow 上找到的一个方法,原理就是利用 trap 命令在脚本退出时 kill 掉它整个进程组。把下面的代码加在脚本开头区,实测管用:

trap "trap - SIGTERM && kill -- -\$\$" SIGINT SIGTERM EXIT

不过如果父进程是用 SIGKILL (kill -9) 杀掉的,就不行了。因为 SIGKILL 时,进程是没有机会运行任何代码的。

timeout 限制运行时间

有时候需要对命令设置一个超时时间。这时可以使用 timeout 命令,用法很简单:

timeout 600s some_command arg1 arg2

命令在超时时间内运行结束时,返回码为 0,否则会返回一个非零返回码。

timeout 在超时时默认会发送 TERM 信号,也可以用 -s 参数让它发送其它信号。

后台执行

Linux 支持并行或串行执行多个进程。您总是在 Linux 系统上以运行 bash 作为 shell 的单个进程开始您的第一个会话(登录会话)。大多数 Linux 命令,例如编辑文件,替换当前日期和时间,登录用户等,都可以使用各种 Linux 命令来完成。您在 shell 提示符下一一键入所有命令。这些程序始终控制着屏幕,完成后,您将返回 S Shell 提示符以键入一组新命令。但是,有时您需要在后台执行任务并将终端用于其他目的。例如,在编写 C 程序时,找到存储在磁盘上的所有 mp3 文件。

Bash shell 允许您使用称为作业控制的工具在后台运行任务(或命令)。作业控制是指选择性地停止,挂起进程的执行并在以后继续(恢复)其执行的能力。用户通常通过系统终端驱动程序和 bash 共同提供的交互式界面来使用此功能。

在作业控制设备的影响下的过程称为作业。每个作业都有一个唯一的 ID,称为作业编号。您可以使用以下命令来控制作业:

  • fg - Place job in the foreground.
  • bg - Place job in the background.
  • jobs - Lists the active jobs on screen.

非连续调度的命令称为后台进程。您无法在屏幕上看到后台进程。例如,Apache httpd 服务器在后台运行以提供网页。您可以将 Shell 脚本或任何命令放在后台。您可以在屏幕上看到该命令的命令称为前台进程。将作业置于后台的语法如下:

command &
command arg1 arg2 &
command1 | command2 arg1 &
command1 | command2 arg1 > output &

& 运算符将命令放在后台,并释放终端。在后台运行的命令称为作业。您可以在后台命令运行时键入其他命令。

$ find /nas -name "*.mp3" > /tmp/filelist.txt &

# [1] 1307

find 命令现在在后台运行。当 bash 在后台启动作业时,它将打印一行,显示作业号([1])和进程标识号(PID-1307)。作业在完成时向终端发送一条消息,如下所示,通过其编号标识该作业并显示其已完成:

[1]+  Done                    find /share/ -name "*.mp3" > /tmp/filelist

Subshell

每当您运行 shell 脚本时,它都会创建一个名为 subshel​​l 的新进程,并且您的脚本将使用 subshel​​l 执行。Subshel​​l 可用于执行并行处理。如果您在当前 shell 之上启动另一个 shell,则可以将其称为子 shell。键入以下命令以查看子 shell 值:

echo $BASH_SUBSHELL

echo "Current shell: $BASH_SUBSHELL"; ( echo "Running du in subshell: $BASH_SUBSHELL" ;cd /tmp; du 2>/tmp/error 1>/tmp/output)

子 Shell 程序不会继承变量的设置。使用 export 命令将变量和函数导出到 subshel​​l:

WWWJAIL=/apache.jail
export WWWJAIL
die() { echo "$@"; exit 2; }
export -f die
# now call script that will access die() and $WWWJAIL
/etc/nixcraft/setupjail -d cyberciti.com

但是,环境变量(例如 $HOME,$MAIL 等)将传递给子 Shell。您可以使用 exec 命令来避免使用 subshel​​l。exec 命令用指定程序替换该 Shell 程序,而无需交换新的子 Shell 程序或进程。例如,

exec command
# redirect the shells stderr to null
exec 2>/dev/null

The . (dot) Command and Subshell

. (dot) 该命令用于运行 shell 脚本,如下所示:

. script.sh

dot 命令允许您修改当前的 Shell 变量。例如,创建一个名为 /tmp/dottest.sh 的 shell 脚本,如下所示:

#!/bin/bash
echo "In script before : $WWWJAIL"
WWWJAIL=/apache.jail
echo "In script after : $WWWJAIL"

关闭并保存文件。如下运行:

chmod +x /tmp/dottest.sh

现在,在 Shell 提示符下定义一个名为 WWWJAIL 的变量:

WWWJAIL=/foobar
echo $WWWJAIL

样本输出:

/foobar

运行脚本:

/tmp/dottest.sh

检查 WWWJAIL 的值:

echo $WWWJAIL

您应该看到 $WWWJAIL(/ foobar)的原始值,因为该 Shell 脚本是在子 Shell 中执行的。现在,尝试点命令:

 . /tmp/dottest.sh
echo $WWWJAIL

Sample outputs:

/apache.jail

$WWWJAIL(/apache.jail)的值已更改,因为使用 dot 命令在当前 Shell 中运行了脚本。

上一页
下一页