Bash 脚本 set 命令教程

作者: 阮一峰

日期: 2017年11月15日

服务器的开发和管理离不开 Bash 脚本,掌握它需要学习大量的细节。

set命令是 Bash 脚本的重要环节,却常常被忽视,导致脚本的安全性和可维护性出问题。本文介绍它的基本用法,让你可以更安心地使用 Bash 脚本。

一、简介

我们知道,Bash 执行脚本的时候,会创建一个新的 Shell。


$ bash script.sh

上面代码中,script.sh是在一个新的 Shell 里面执行。这个 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。

set命令用来修改 Shell 环境的运行参数,也就是可以定制环境。一共有十几个参数可以定制,官方手册有完整清单,本文介绍其中最常用的四个。

顺便提一下,如果命令行下不带任何参数,直接运行set,会显示所有的环境变量和 Shell 函数。


$ set

二、set -u

执行脚本的时候,如果遇到不存在的变量,Bash 默认忽略它。


#!/usr/bin/env bash

echo $a
echo bar

上面代码中,$a是一个不存在的变量。执行结果如下。


$ bash script.sh

bar

可以看到,echo $a输出了一个空行,Bash 忽略了不存在的$a,然后继续执行echo bar。大多数情况下,这不是开发者想要的行为,遇到变量不存在,脚本应该报错,而不是一声不响地往下执行。

set -u就用来改变这种行为。脚本在头部加上它,遇到不存在的变量就会报错,并停止执行。


#!/usr/bin/env bash
set -u

echo $a
echo bar

运行结果如下。


$ bash script.sh
bash: script.sh:行4: a: 未绑定的变量

可以看到,脚本报错了,并且不再执行后面的语句。

-u还有另一种写法-o nounset,两者是等价的。


set -o nounset

三、set -x

默认情况下,脚本执行后,屏幕只显示运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。

set -x用来在运行结果之前,先输出执行的那一行命令。


#!/usr/bin/env bash
set -x

echo bar

执行上面的脚本,结果如下。


$ bash script.sh
+ echo bar
bar

可以看到,执行echo bar之前,该命令会先打印出来,行首以+表示。这对于调试复杂的脚本是很有用的。

-x还有另一种写法-o xtrace


set -o xtrace

四、Bash 的错误处理

如果脚本里面有运行失败的命令(返回值非0),Bash 默认会继续执行后面的命令。


#!/usr/bin/env bash

foo
echo bar

上面脚本中,foo是一个不存在的命令,执行时会报错。但是,Bash 会忽略这个错误,继续往下执行。


$ bash script.sh
script.sh:行3: foo: 未找到命令
bar

可以看到,Bash 只是显示有错误,并没有终止执行。

这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法。


command || exit 1

上面的写法表示只要command有非零返回值,脚本就会停止执行。

如果停止执行之前需要完成多个操作,就要采用下面三种写法。


# 写法一
command || { echo "command failed"; exit 1; }

# 写法二
if ! command; then echo "command failed"; exit 1; fi

# 写法三
command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi

另外,除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法。


command1 && command2

五、 set -e

上面这些写法多少有些麻烦,容易疏忽。set -e从根本上解决了这个问题,它使得脚本只要发生错误,就终止执行。


#!/usr/bin/env bash
set -e

foo
echo bar

执行结果如下。


$ bash script.sh
script.sh:行4: foo: 未找到命令

可以看到,第4行执行失败以后,脚本就终止执行了。

set -e根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e,该命令执行结束后,再重新打开set -e


set +e
command1
command2
set -e

上面代码中,set +e表示关闭-e选项,set -e表示重新打开-e选项。

还有一种方法是使用command || true,使得该命令即使执行失败,脚本也不会终止执行。


#!/bin/bash
set -e

foo || true
echo bar

上面代码中,true使得这一行语句总是会执行成功,后面的echo bar会执行。

-e还有另一种写法-o errexit


set -o errexit

六、set -o pipefail

set -e有一个例外情况,就是不适用于管道命令。

所谓管道命令,就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e就失效了。

请看下面这个例子。


#!/usr/bin/env bash
set -e

foo | echo a
echo bar

执行结果如下。


$ bash script.sh
a
script.sh:行4: foo: 未找到命令
bar

上面代码中,foo是一个不存在的命令,但是foo | echo a这个管道命令会执行成功,导致后面的echo bar会继续执行。

set -o pipefail用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。


#!/usr/bin/env bash
set -eo pipefail

foo | echo a
echo bar

运行后,结果如下。


$ bash script.sh
a
script.sh:行4: foo: 未找到命令

可以看到,echo bar没有执行。

七、总结

set命令的上面这四个参数,一般都放在一起使用。


# 写法一
set -euxo pipefail

# 写法二
set -eux
set -o pipefail

这两种写法建议放在所有 Bash 脚本的头部。

另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数。


$ bash -euxo pipefail script.sh

八、参考链接

(完)

留言(27条)

请问是不是因为要发广告而写了?

支持阮老师,多多出文!

阮老师这篇文章的广告较少~~~O(∩_∩)O哈哈~

挺好的 这些小细节如果正儿八经要从什么书里面专门去学的话 一个是找起来效率太低 有时候你甚至根本不知道应该搜索什么

满心期待出新,但怎么感觉广告味渐渐变浓了呢?只有我这么觉得吗?

阮老师,经常看你的文章,惊叹于你对技术和未来的深刻思考与见解。在此想请教你对一个问题的看法,希望你能对此话题感兴趣,如果能写成博文分享你的看法那就更好了。由于近些年关于量子通信和量子计算机各项技术的突破经常见诸科技新闻,我产生了自己的一些思考:在未来,量子计算机是否会颠覆经典计算机,量子通信的保密性是否会使密码学大厦轰然倒塌......。量子技术的发展必然会对人类社会产生影响,但到底会影响哪些方面,影响的程度又如何,是否会完全改写某一领域,对此想听听阮老师的见解,还望不吝赐教。

登个广告,就有人不乐意了?!
做人别太自私,各取所需就行。
峰哥的东西还是不错的。

1. 个人不建议用-e/-u这样的写法,不容易读,写成

set -o errexit
set -o nounset
set -o pipefail

会好些

2. 很多编辑器支持对于特定后缀的文件在创建时添加指定字符串作为模版,对于vim, 示例如下:

" file headers
" empty line in the list is converted to a newline
let g:Headers = {
  \"py": ["#!/usr/bin/env python", "# encoding=utf8", "", ""],
  \"sh": ["#!/bin/bash", "", "set -o errexit", "set -o nounset", "set -o pipefail", "", ""],
\}
au BufNewFile * call AppendHeader()

为了发而发~

@夏恺:

请问第二个能具体说说在哪么,我怎么找到的网上都是templates里添加

能够数十年如一日的进行这些科普,阮老师的这种精神值得学习。

You should try Python, it's more powerful than bash.

-x 结合 PS4 环境变量设置,能够比较清晰地调试

感谢!!!

建议可以用alias命令改写bash,这样就不用每次都要添加选项了

很实用的小知识,收藏了,虽然写脚本的时候不多

shell的三种执行方式,./script.sh和sh script.sh方式下遇到不存在的变量,脚本不会继续往下执行了;但是第三种source script.sh方式下还会继续执行后面的脚本操作。

脚本就没有系统的学过,每次用都是查stackoverflow,东凑西拼的,请问谁有比较系统的书推荐吗?

一直都只会设置classpath或者PATH,没有想到还有这招啊....

最近刚刚开始学习linux, 谢谢老师!

很久不用忘记了,一搜就看到这篇文章。阮老师的文章一如既往的简明清晰,文章排版,内容不错!

阮老师的文章几乎偏偏通俗易懂,言简意赅,赞!!!

可以用 'failedCmd || :' 的写法在任何情况下都返回 true, 也就是返回 0

set -m这么重要居然没讲。。

个人认为写博客有两个境界,一种是写给自己看的,另一种是写给别人看的。

我还是要继续向阮老师学习。

最后补下四种参数的全称:

-u: Treat unset variables as an error when substituting.
-x: Print commands and their arguments as they are executed.
-e: Exit immediately if a command exits with a non-zero status.
-o: option-name.

当我在脚本中加入 set -uxeo pipefail
yzstate=$(tail -800 /root/logs/2021-04-13.1.log |grep -c 'ai ret')
这个变量的值为零时下面的命令就不会运行, 这个想不通诶,求大佬指点

引用linuxops的发言:

当我在脚本中加入 set -uxeo pipefail
yzstate=$(tail -800 /root/logs/2021-04-13.1.log |grep -c 'ai ret')
这个变量的值为零时下面的命令就不会运行, 这个想不通诶,求大佬指点

因为grep未匹配到内容时,返回的是1,非0,因为有e的原因就停下来了。

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接