Shell环境和除错
2023-1-19
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

set、shopt

Bash 执行脚本时,会创建一个子 Shell:
上面代码中,script.sh是在一个子Shell 里面执行。这个子 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。
 
set命令用来修改子 Shell 环境的运行参数,即定制环境,一共有十几个参数可以定制。
如果命令行下不带任何参数,直接运行set,会显示所有的环境变量和 Shell 函数。

set -u

执行脚本时,如果遇到不存在的变量,Bash 默认忽略它:
$a是一个不存在的变量,执行结果如下:
echo $a输出了一个空行,Bash 忽略了不存在的$a,然后继续执行echo bar。大多数情况下,这不是开发者想要的行为,遇到变量不存在,脚本应该报错,而不是一声不响地往下执行。
 
set -u就用来改变这种行为。脚本在头部加上它,遇到不存在的变量就会报错,并停止执行。
运行结果如下:
可以看到,脚本报错了,并且不再执行后面的语句。
-u还有另一种写法o nounset,两者是等价的:
 

set -x

默认情况下,脚本执行后,只输出运行结果,没有其他内容。如果多个命令连续执行,它们的运行结果就会连续输出。有时会分不清,某一段内容是什么命令产生的。
set -x用来在运行结果之前,先输出执行的那一行命令。
执行上面的脚本,结果如下:
执行echo bar之前,该命令会先打印出来,行首以+表示。这对于调试复杂的脚本是很有用的。
 
-x还有另一种写法o xtrace
脚本当中如果要关闭命令输出,可以使用set +x
上面的例子中,只对特定的代码段打开命令输出。
 

Bash 的错误处理

如果脚本里面有运行失败的命令(返回值非0),Bash 默认会继续执行后面的命令。
上面脚本中,foo是一个不存在的命令,执行时会报错。但是,Bash 会忽略这个错误,继续往下执行。
 
这种行为很不利于脚本安全和除错。实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。这时,一般采用下面的写法。
 
如果停止执行之前需要完成多个操作,就要采用下面三种写法。
另外,除了停止执行,还有一种情况。如果两个命令有继承关系,只有第一个命令成功了,才能继续执行第二个命令,那么就要采用下面的写法。

set -e

上面这些写法多少有些麻烦,容易疏忽。set -e从根本上解决了这个问题,它使得脚本只要发生错误,就终止执行。
执行结果如下:
 
set -e根据返回值来判断,一个命令是否运行失败。但是,某些命令的非零返回值可能不表示失败,或者开发者希望在命令失败的情况下,脚本继续执行下去。这时可以暂时关闭set -e,该命令执行结束后,再重新打开set -e
set +e表示关闭-e选项,set -e表示重新打开-e选项
 
还有一种方法是使用command || true,使得该命令即使执行失败,脚本也不会终止执行。
上面代码中,true使得这一行语句总是会执行成功,后面的echo bar会执行。
-e还有另一种写法o errexit
 

set -o pipefail

set -e有一个例外情况,就是不适用于管道命令。
所谓管道命令,就是多个子命令通过管道运算符(|)组合成为一个大的命令。Bash 会把最后一个子命令的返回值,作为整个命令的返回值。也就是说,只要最后一个子命令不失败,管道命令总是会执行成功,因此它后面命令依然会执行,set -e就失效了。
执行结果如下:
foo是一个不存在的命令,但是foo | echo a这个管道命令会执行成功,导致后面的echo bar会继续执行。
 
set -o pipefail用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。
运行后,结果如下:
 

set -E

一旦设置了-e参数,会导致函数内的错误不会被trap命令捕获。-E参数可以纠正这个行为,使得函数也能继承trap命令。
上面示例中,myfunc函数内部调用了一个不存在的命令foo,导致执行这个函数会报错。
但是,由于设置了set -e,函数内部的报错并没有被trap命令捕获,需要加上-E参数才可以。
执行上面这个脚本,就可以看到trap命令生效了。
 

其他参数

set命令还有一些其他参数。
  • set -n:等同于set -o noexec,不运行命令,只检查语法是否正确。
  • set -f:等同于set -o noglob,表示不对通配符进行文件名扩展。
  • set -v:等同于set -o verbose,表示打印 Shell 接收到的每一行输入。
  • set -o noclobber:防止使用重定向运算符>覆盖已经存在的文件。
上面的-f-v参数,可以分别使用set +fset +v关闭。
 

set 命令总结

重点介绍的set命令的几个参数,一般都放在一起使用。
这两种写法建议放在所有 Bash 脚本的头部。
 
另一种办法是在执行 Bash 脚本的时候,从命令行传入这些参数。
 

shopt 命令

shopt命令用来调整 Shell 的参数,跟set命令的作用很类似。之所以会有这两个类似命令的主要原因是,set是从 Ksh 继承的,属于 POSIX 规范的一部分,而shopt是 Bash 特有的。
直接输入shopt可以查看所有参数,以及它们各自打开和关闭的状态。
shopt命令后面跟着参数名,可以查询该参数是否打开。
  • -s用来打开某个参数
    • -u用来关闭某个参数
      • 举例来说,histappend这个参数表示退出当前 Shell 时,将操作历史追加到历史文件中。这个参数默认是打开的,如果使用下面的命令将其关闭,那么当前 Shell 的操作历史将替换掉整个历史文件。
    • -q的作用也是查询某个参数是否打开,但不是直接输出查询结果,而是通过命令的执行状态($?)表示查询结果。如果状态为0,表示该参数打开;如果为1,表示该参数关闭。
      • 上面命令查询globstar参数是否打开。返回状态为1,表示该参数是关闭的。这个用法主要用于脚本,供if条件结构使用。
     

    脚本除错

    编写 Shell 脚本的时候,一定要考虑到命令失败的情况,否则很容易出错。
    如果目录$dir_name不存在,cd $dir_name命令就会执行失败。这时,就不会改变当前目录,脚本会继续执行下去,导致rm *命令删光当前目录的文件。
     
    如果改成下面的样子,也会有问题。
    只有cd $dir_name执行成功,才会执行rm *。但是,如果变量$dir_name为空,cd就会进入用户主目录,从而删光用户主目录的文件。
     
    下面的写法才是正确的。
     
    如果不放心删除什么文件,可以先打印出来看一下。
     

    bash-x参数

    bash-x参数可以在执行每一行命令之前,打印该命令。一旦出错,这样就比较容易追查。
    加上-x参数,执行每条命令之前,都会显示该命令。
    行首为+的行,显示该行是所要执行的命令,下一行才是该命令的执行结果。
     
    下面再看一个-x写在脚本内部的例子。
    上面的脚本执行之后,会输出每一行命令。
    输出的命令之前的+号,是由系统变量PS4决定,可以修改这个变量。
    另外,set命令也可以设置 Shell 的行为参数,有利于脚本除错。
     
     

    环境变量

    有一些环境变量常用于除错。
    LINENO
    变量LINENO返回它在脚本里面的行号。
    执行上面的脚本test.sh$LINENO会返回3
     
    FUNCNAME
    变量FUNCNAME返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数,以此类推。
    执行上面的脚本test.sh,结果如下。
    执行func1时,变量FUNCNAME的0号成员是func1,1号成员是调用func1的主脚本main。执行func2时,变量FUNCNAME的0号成员是func2,1号成员是调用func2func1
     
    BASH_SOURCE
    变量BASH_SOURCE返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本,以此类推,跟变量FUNCNAME是一一对应关系。
    下面有两个子脚本lib1.shlib2.sh
    然后,主脚本main.sh调用上面两个子脚本。
    执行主脚本main.sh,会得到下面的结果。
    执行函数func1时,变量BASH_SOURCE的0号成员是func1所在的脚本lib1.sh,1号成员是主脚本main.sh;执行函数func2时,变量BASH_SOURCE的0号成员是func2所在的脚本lib2.sh,1号成员是调用func2的脚本lib1.sh
     
    BASH_LINENO
    变量BASH_LINENO返回一个数组,内容是每一轮调用对应的行号。${BASH_LINENO[$i]}${FUNCNAME[$i]}是一一对应关系,表示${FUNCNAME[$i]}在调用它的脚本文件${BASH_SOURCE[$i+1]}里面的行号。
    下面有两个子脚本lib1.shlib2.sh
    然后,主脚本main.sh调用上面两个子脚本。
    执行主脚本main.sh,会得到下面的结果。
    函数func1是在main.sh的第7行调用,函数func2是在lib1.sh的第8行调用的。
     

    mktemp、trap

    直接创建临时文件,尤其在/tmp目录里面,往往会导致安全问题。首先,/tmp目录是所有人可读写的,任何用户都可以往该目录里面写文件。创建的临时文件也是所有人可读的。
    其次,如果攻击者知道临时文件的文件名,他可以创建符号链接,链接到临时文件,可能导致系统运行异常。攻击者也可能向脚本提供一些恶意数据。因此,临时文件最好使用不可预测、每次都不一样的文件名,防止被利用。
    最后,临时文件使用完毕,应该删除。但是,脚本意外退出时,往往会忽略清理临时文件。
    生成临时文件应该遵循下面的规则。
    • 创建前检查文件是否已经存在
    • 确保临时文件已成功创建
    • 临时文件必须有权限的限制
    • 临时文件要使用不可预测的文件名
    • 脚本退出时,要删除临时文件(使用trap命令)
     

    mktemp 命令

    mktemp命令就是为安全创建临时文件而设计的。虽然在创建临时文件之前,它不会检查临时文件是否存在,但是它支持唯一文件名和清除机制,因此可以减轻安全攻击的风险。
    直接运行mktemp命令,就能生成一个临时文件。
    上面命令中,mktemp命令生成的临时文件名是随机的,而且权限是只有用户本人可读写。
     
    Bash 脚本使用mktemp命令的用法如下。
    为了确保临时文件创建成功,mktemp命令后面最好使用 OR 运算符(||),保证创建失败时退出脚本。
    为了保证脚本退出时临时文件被删除,可以使用trap命令指定退出时的清除操作。
     
    mktemp 命令的参数
    -d参数可以创建一个临时目录。
    -p参数可以指定临时文件所在的目录。默认是使用$TMPDIR环境变量指定的目录,如果这个变量没设置,那么使用/tmp目录。
    -t参数可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X字符,表示随机字符,建议至少使用六个X。默认的文件名模板是tmp.后接十个随机字符。
     

    trap 命令

    trap命令用来在Bash脚本中响应系统信号。最常见的系统信号就是 SIGINT(中断),即按Ctrl + C所产生的信号。trap命令的-l参数,可以列出所有的系统信号:
    trap的命令格式如下:
    上面代码中,“动作”是一个 Bash 命令,“信号”常用的有以下几个:
    • HUP:编号1,脚本与所在的终端脱离联系
    • INT:编号2,用户按下 Ctrl + C,意图让脚本终止运行
    • QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本
    • KILL:编号9,该信号用于杀死进程
    • TERM:编号15,这是kill命令发出的默认信号
    • EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。
    trap命令响应EXIT信号的写法如下:
    trap命令的常见使用场景,就是在Bash脚本中指定退出时执行的清理命令:
    不管是脚本正常执行结束,还是用户按Ctrl + C终止,都会产生EXIT信号,从而触发删除临时文件。
    注:trap命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。
     
    如果trap需要触发多条命令,可以封装一个Bash函数:
     
     
     

    Bash 启动环境

    用户每次使用 Shell,都会开启一个与 Shell 的 Session(对话)。
    Session有两种类型:登录Session 和非登录 Session,也可以叫做 login shell 和 non-login shell。

    登录 Session

    登录Session是用户登录系统以后,系统为用户开启的原始Session,通常需要用户输入用户名和密码进行登录。
    登录Session 一般进行整个系统环境的初始化,启动的初始化脚本依次如下:
    • /etc/profile:所有用户的全局配置脚本
    • /etc/profile.d目录里面所有.sh文件
    • ~/.bash_profile:用户的个人配置脚本。如果该脚本存在,则执行完就不再往下执行。
    • ~/.bash_login:如果~/.bash_profile没找到,则尝试执行这个脚本(C shell 的初始化脚本)。如果该脚本存在,则执行完就不再往下执行
    • ~/.profile:如果~/.bash_profile~/.bash_login都没找到,则尝试读取这个脚本(Bourne shell 和 Korn shell 的初始化脚本)
    Linux发行版更新的时候,会更新/etc里面的文件,比如/etc/profile,因此不要直接修改这个文件。如果想修改所有用户的登陆环境,就在/etc/profile.d目录里面新建.sh脚本。
     
    如果想修改个人的登录环境,一般是写在~/.bash_profile里面。下面是一个典型的.bash_profile文件。
    可以看到,这个脚本定义了一些最基本的环境变量,然后执行了~/.bashrc
    bash命令的--login参数,会强制执行登录 Session 会执行的脚本。
    bash命令的--noprofile参数,会跳过上面这些 Profile 脚本。
     

    非登录 Session

    非登录 Session 是用户进入系统以后,手动新建的 Session,这时不会进行环境初始化。比如,在命令行执行bash命令,就会新建一个非登录 Session。
    非登录 Session 的初始化脚本依次如下:
    • /etc/bash.bashrc:对全体用户有效
    • ~/.bashrc:仅对当前用户有效
    对用户来说,~/.bashrc通常是最重要的脚本。非登录 Session 默认会执行它,而登录 Session一般也会通过调用执行它。每次新建一个 Bash 窗口,就相当于新建一个非登录 Session,所以~/.bashrc每次都会执行。注意,执行脚本相当于新建一个非互动的 Bash 环境,但是这种情况不会调用~/.bashrc
    bash命令的--norc参数,可以禁止在非登录 Session 执行~/.bashrc脚本。
    bash命令的--rcfile参数,指定另一个脚本代替.bashrc

    .bash_logout

    ~/.bash_logout脚本在每次退出 Session 时执行,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。如果没有退出时要执行的命令,这个文件也可以不存在。
     

    启动选项

    为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数。
    • -n:不运行脚本,只检查是否有语法错误
    • -v:输出每一行语句运行结果前,会先输出该行语句
    • -x:每一个命令处理之前,先输出该命令,再执行该命令
     

    键盘绑定

    Bash允许用户定义自己的快捷键。全局的键盘绑定文件默认为/etc/inputrc,可以在主目录创建自己的键盘绑定文件.inputrc文件。如果定义了这个文件,需要在其中加入下面这行,保证全局绑定不会被遗漏。
    .inputrc文件里面的快捷键,可以像这样定义,"\C-t":"pwd\n"表示将Ctrl + t绑定为运行pwd命令。
     

    命令提示符

    环境变量 PS1

    命令提示符通常是美元符号$,对于根用户则是井号#。这个符号是环境变量PS1决定的,查看当前命令提示符的定义:
    Bash 允许用户自定义命令提示符,只要改写这个变量即可。改写后的PS1,可以放在用户的 Bash 配置文件.bashrc里面,以后新建 Bash 对话时,新的提示符就会生效。要在当前窗口看到修改后的提示符,可以执行下面的命令。
    命令提示符的定义,可以包含特殊的转义字符,表示特定内容。
    • \a:响铃,计算机发出一记声音。
    • \d:以星期、月、日格式表示当前日期,例如“Mon May 26”。
    • \h:本机的主机名。
    • \H:完整的主机名。
    • \j:运行在当前 Shell 会话的工作数。
    • \l:当前终端设备名。
    • \n:一个换行符。
    • \r:一个回车符。
    • \s:Shell 的名称。
    • \t:24小时制的hours:minutes:seconds格式表示当前时间。
    • \T:12小时制的当前时间。
    • \@:12小时制的AM/PM格式表示当前时间。
    • \A:24小时制的hours:minutes表示当前时间。
    • \u:当前用户名。
    • \v:Shell 的版本号。
    • \V:Shell 的版本号和发布号。
    • \w:当前的工作路径。
    • \W:当前目录名。
    • \!:当前命令在命令历史中的编号。
    • \#:当前 shell 会话中的命令数。
    • \$:普通用户显示为$字符,根用户显示为#字符。
    • \[:非打印字符序列的开始标志。
    • \]:非打印字符序列的结束标志。
    举例来说,[\u@\h \W]\$这个提示符定义,显示出来就是[user@host ~]$(具体的显示内容取决于你的系统)。
    改写PS1变量,就可以改变这个命令提示符。
    注意,$后面最好跟一个空格,这样的话,用户的输入与提示符就不会连在一起。
     

    颜色

    默认情况下,命令提示符是显示终端预定义的颜色。Bash允许自定义提示符颜色。使用下面的代码,可以设定其后文本的颜色。
    • \033[0;30m:黑色
    • \033[1;30m:深灰色
    • \033[0;31m:红色
    • \033[1;31m:浅红色
    • \033[0;32m:绿色
    • \033[1;32m:浅绿色
    • \033[0;33m:棕色
    • \033[1;33m:黄色
    • \033[0;34m:蓝色
    • \033[1;34m:浅蓝色
    • \033[0;35m:粉红
    • \033[1;35m:浅粉色
    • \033[0;36m:青色
    • \033[1;36m:浅青色
    • \033[0;37m:浅灰色
    • \033[1;37m:白色
    如果要将提示符设为红色,可以将PS1设成下面的代码:
    但是,上面这样设置以后,用户在提示符后面输入的文本也是红色的。为了解决这个问题, 可以在结尾添加另一个特殊代码\[\033[00m\],表示将其后的文本恢复到默认颜色。
    除了设置前景颜色,Bash 还允许设置背景颜色:
    • \033[0;40m:蓝色
    • \033[1;44m:黑色
    • \033[0;41m:红色
    • \033[1;45m:粉红
    • \033[0;42m:绿色
    • \033[1;46m:青色
    • \033[0;43m:棕色
    • \033[1;47m:浅灰色
    下面是一个带有红色背景的提示符。
     
     

    环境变量 PS2,PS3,PS4

    除了PS1,Bash 还提供了提示符相关的另外三个环境变量。环境变量PS2是命令行折行输入时系统的提示符,默认为>
    上面命令中,输入hello以后按下回车键,系统会提示继续输入。这时,第二行显示的提示符就是PS2定义的>
    环境变量PS3是使用select命令时,系统输入菜单的提示符。
    环境变量PS4默认为+。它是使用 Bash 的-x参数执行脚本时,每一行命令在执行前都会先打印出来,并且在行首出现的那个提示符。
    比如脚本test.sh
    使用-x参数执行这个脚本:
    输出的第一行前面有一个+,这就是变量PS4定义的。
     
  • 计算机基础
  • Linux
  • Shell脚本
  • 数组Linux目录配置
    目录