执行环境

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.shshell脚本,如下所示:

#!/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中运行了脚本。

上一页
下一页