j脚本除错
如何考虑命令出错的情况。
比如:
#! /bin/bash
dir_name=/path/not/exist
cd $dir_name
rm *
上面脚本中,如果目录$dir_name
不存在,cd $dir_name
命令就会执行失败。如果$dir_name
为空,则会进入主目录删除所有文件。
以下写法才是正确:
[[ -d $dir_name ]] && cd $dir_name && rm *
如果不放心删除什么文件,可以先打印出来看一下:
[[ -d $dir_name ]] && cd $dir_name && echo rm *
上面命令中,echo rm *
不会删除文件,只会打印出来要删除的文件。
bash的-x
参数可以在执行每一行命令之前,打印该命令:
比如:
# script.sh
echo hello world
加上参数-x
之后的执行结果如下:
$ bash -x script.sh
+ echo hello world
hello world
也可以将-x
写在Shebang行中:
#! /bin/bash -x
# trouble: script to demonstrate common errors
number=1
if [ $number = 1 ]; then
echo "Number is equal to 1."
else
echo "Number is not equal to 1."
fi
执行结果如下:
$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.
输出的命令之前的+号,是由系统变量PS4决定,可以修改这个变量:
$ export PS4='$LINENO + '
$ trouble
5 + number=1
7 + '[' 1 = 1 ']'
8 + echo 'Number is equal to 1.'
Number is equal to 1.
环境变量:
变量$LINENO
返回它在脚本里面的行号:
#!/bin/bash
echo "This is line $LINENO"
运行结果如下:
$ ./test.sh
This is line 3
变量$FUNCNAME
返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数,以此类推:
function func1()
{
echo "func1: FUNCNAME0 is ${FUNCNAME[0]}"
echo "func1: FUNCNAME1 is ${FUNCNAME[1]}"
echo "func1: FUNCNAME2 is ${FUNCNAME[2]}"
func2
}
function func2()
{
echo "func2: FUNCNAME0 is ${FUNCNAME[0]}"
echo "func2: FUNCNAME1 is ${FUNCNAME[1]}"
echo "func2: FUNCNAME2 is ${FUNCNAME[2]}"
}
func1
运行结果如下:
$ ./test.sh
func1: FUNCNAME0 is func1
func1: FUNCNAME1 is main
func1: FUNCNAME2 is
func2: FUNCNAME0 is func2
func2: FUNCNAME1 is func1
func2: FUNCNAME2 is main
可以发现,有一个main函数。
变量$BASH_SOURCE
返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本,以此类推,跟变量$FUNCNAME
是一一对应关系。
下面有两个子脚本lib1.sh和lib2.sh:
# lib1.sh
function func1()
{
echo "func1: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
echo "func1: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
echo "func1: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
func2
}
# lib2.sh
function func2()
{
echo "func2: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
echo "func2: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
echo "func2: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
}
然后,主脚本main.sh调用上面两个子脚本:
#!/bin/bash
# main.sh
source lib1.sh
source lib2.sh
func1
执行主脚本main.sh,会得到下面的结果:
$ ./main.sh
func1: BASH_SOURCE0 is lib1.sh
func1: BASH_SOURCE1 is ./main.sh
func1: BASH_SOURCE2 is
func2: BASH_SOURCE0 is lib2.sh
func2: BASH_SOURCE1 is lib1.sh
func2: BASH_SOURCE2 is ./main.sh
变量$BASH_LINENO
返回一个数组,内容是每一轮调用对应的行号。${BASH_LINENO[$i]}
跟${FUNCNAME[$i]}
是一一对应关系,表示${FUNCNAME[$i]}
在调用它的脚本文件${BASH_SOURCE[$i+1]}
里面的行号。
下面有两个子脚本lib1.sh和lib2.sh:
# lib1.sh
function func1()
{
echo "func1: BASH_LINENO is ${BASH_LINENO[0]}"
echo "func1: FUNCNAME is ${FUNCNAME[0]}"
echo "func1: BASH_SOURCE is ${BASH_SOURCE[1]}"
func2
}
# lib2.sh
function func2()
{
echo "func2: BASH_LINENO is ${BASH_LINENO[0]}"
echo "func2: FUNCNAME is ${FUNCNAME[0]}"
echo "func2: BASH_SOURCE is ${BASH_SOURCE[1]}"
}
然后,主脚本main.sh调用上面两个子脚本:
#!/bin/bash
# main.sh
source lib1.sh
source lib2.sh
func1
执行主脚本main.sh,会得到下面的结果:
$ ./main.sh
func1: BASH_LINENO is 7
func1: FUNCNAME is func1
func1: BASH_SOURCE is main.sh
func2: BASH_LINENO is 8
func2: FUNCNAME is func2
func2: BASH_SOURCE is lib1.sh
上面例子中,函数func1
是在main.sh的第7行调用,函数func2
是在lib1.sh的第8行调用的。
mktemp 命令,trap 命令
Bash 脚本有时需要创建临时文件或临时目录。常见的做法是,在/tmp
目录里面创建文件或目录,这样做有很多弊端,使用mktemp
命令是最安全的做法。
/tmp目录是所有人可读写的,任何用户都可以往该目录里面写文件。创建的临时文件也是所有人可读的:
$ touch /tmp/info.txt
$ ls -l /tmp/info.txt
-rw-r--r-- 1 ruanyf ruanyf 0 12月 28 17:12 /tmp/info.txt
因此,临时文件最好使用不可预测、每次都不一样的文件名,防止被利用。临时文件使用完毕,应该删除。但是,脚本意外退出时,往往会忽略清理临时文件。
生成临时文件应该遵循下面的规则:
创建前检查文件是否已经存在。
确保临时文件已成功创建。
临时文件必须有权限的限制。
临时文件要使用不可预测的文件名。
脚本退出时,要删除临时文件(使用trap命令)。
mktemp
命令就是为安全创建临时文件而设计的。虽然在创建临时文件之前,它不会检查临时文件是否存在,但是它支持唯一文件名和清除机制,因此可以减轻安全攻击的风险。
直接运行mktemp命令,就能生成一个临时文件:
$ mktemp
/tmp/tmp.4GcsWSG4vj
$ ls -l /tmp/tmp.4GcsWSG4vj
-rw------- 1 ruanyf ruanyf 0 12月 28 12:49 /tmp/tmp.4GcsWSG4vj
Bash 脚本使用mktemp命令的用法如下:
#!/bin/bash
TMPFILE=$(mktemp)
echo "Our temp file is $TMPFILE"
为了确保临时文件创建成功,mktemp命令后面最好使用 OR 运算符||
,保证创建失败时退出脚本:
#!/bin/bash
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
为了保证脚本退出时临时文件被删除,可以使用trap命令指定退出时的清除操作:
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"
mktemp 命令的参数:
-d
:可以创建一个临时目录:
$ mktemp -d
/tmp/tmp.Wcau5UjmN6
-p
:可以指定临时文件所在的目录。默认是使用$TMPDIR
环境变量指定的目录,如果这个变量没设置,那么使用/tmp
目录:
$ mktemp -p /home/ruanyf/
/home/ruanyf/tmp.FOKEtvs2H3
-t
:可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X字符,表示随机字符,建议至少使用六个X。默认的文件名模板是tmp.后接十个随机字符:
$ mktemp -t mytemp.XXXXXXX
/tmp/mytemp.yZ1HgZV
trap命令:
trap命令用来在 Bash 脚本中响应系统信号。
最常见的系统信号就是 SIGINT(中断),即按 Ctrl + C 所产生的信号。trap命令的-l
参数,可以列出所有的系统信号:
$ trap -l
trap的命令格式如下:
$ trap [动作] [信号1] [信号2] ...
上面代码中,“动作”是一个 Bash 命令,“信号”常用的有以下几个:
HUP:编号1,脚本与所在的终端脱离联系。
INT:编号2,用户按下 Ctrl + C,意图让脚本终止运行。
QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
KILL:编号9,该信号用于杀死进程。
TERM:编号15,这是kill命令发出的默认信号。
EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。
trap命令响应EXIT信号的写法如下:
$ trap 'rm -f "$TMPFILE"' EXIT
上面命令中,脚本遇到EXIT信号时,就会执行rm -f "$TMPFILE"
。
trap 命令的常见使用场景,就是在 Bash 脚本中指定退出时执行的清理命令:
#!/bin/bash
trap 'rm -f "$TMPFILE"' EXIT
TMPFILE=$(mktemp) || exit 1
ls /etc > $TMPFILE
if grep -qi "kernel" $TMPFILE; then
echo 'find'
fi
上面代码中,不管是脚本正常执行结束,还是用户按 Ctrl + C 终止,都会产生EXIT信号,从而触发删除临时文件。
注意,trap命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。
如果trap需要触发多条命令,可以封装一个 Bash 函数:
function egress {
command1
command2
command3
}
trap egress EXIT
登录session&非登录session
登录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 的初始化脚本)。
下面是一个典型的.bash_profile
文件:
# .bash_profile
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
PATH=$PATH:$HOME/bin
SHELL=/bin/bash
MANPATH=/usr/man:/usr/X11/man
EDITOR=/usr/bin/vi
PS1='\h:\w\$ '
PS2='> '
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
export PATH
export EDITOR
非登录session:
非登录 Session 是用户进入系统以后,手动新建的 Session,这时不会进行环境初始化。比如,在命令行执行bash命令,就会新建一个非登录 Session。
非登录 Session 的初始化脚本依次如下:
/etc/bash.bashrc:对全体用户有效。
~/.bashrc:仅对当前用户有效。
注意,执行脚本相当于新建一个非互动的 Bash 环境,但是这种情况不会调用~/.bashrc
。
~/.bash_logout
脚本在每次退出 Session 时执行,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。
为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数:
-n:不运行脚本,只检查是否有语法错误。
-v:输出每一行语句运行结果前,会先输出该行语句。
-x:每一个命令处理之前,先输出该命令,再执行该命令。
$ bash -n scriptname
$ bash -v scriptname
$ bash -x scriptname
命令提示符
用户进入 Bash 以后,Bash 会显示一个命令提示符,用来提示用户在该位置后面输入命令。
命令提示符通常是美元符号$,对于根用户则是井号#。这个符号是环境变量PS1决定的,执行下面的命令,可以看到当前命令提示符的定义:
$ echo $PS1
Bash 允许用户自定义命令提示符,只要改写这个变量即可。改写后的PS1,可以放在用户的 Bash 配置文件.bashrc里面,以后新建 Bash 对话时,新的提示符就会生效。要在当前窗口看到修改后的提示符,可以执行下面的命令:
$ source ~/.bashrc
除了PS1,Bash 还提供了提示符相关的另外三个环境变量。
环境变量PS2是命令行折行输入时系统的提示符,默认为>
:
$ echo "hello
> world"
环境变量PS3是使用select命令时,系统输入菜单的提示符:
环境变量PS4默认为+
。它是使用 Bash 的-x
参数执行脚本时,每一行命令在执行前都会先打印出来,并且在行首出现的那个提示符。
比如下面是脚本test.sh:
#!/bin/bash
echo "hello world"
使用-x
参数执行这个脚本:
$ bash -x test.sh
+ echo 'hello world'
hello world