bash 教程-4 shell 脚本 调试 环境 [MD]
我的GitHub | 我的博客 | 我的微信 | 我的邮箱 |
---|---|---|---|
baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目录
目录- 目录
- Bash 教程
- 脚本基础
- 如何运行一个脚本
- Shebang 行:#! env
- 权限和路径:chmod PATH
- 脚本参数:$n $# $@
- 读取命令执行结果:$?
- 配置项参数终止符:--
- 脚本基础命令
- 移除脚本参数:shift
- 解析脚本参数:getopts
- 终止当前脚本:exit
- 加载外部脚本:source
- 指定命令别名:alias
- 创建临时文件:mktemp
- 响应系统信号:trap
- 脚本调试
- 命令错误处理
- 几个调试参数
- 调试参数 -x
- 调试用的环境变量
- 脚本执行环境
- 定制环境参数:set
- 调整环境参数:shopt
- 对话 Session
- 登录 Session
- 非登录 Session
- 退出时执行的脚本
- 脚本基础
Bash 教程
- bash-tutorial
- Bash 教程
本文改编自 网道的 Bash 教程,主要为了精简大量本人不感兴趣的内容。
脚本基础
脚本 script 就是包含一系列命令的一个文本文件,所有能够在命令行完成的任务,都能够用脚本完成。
如何运行一个脚本
运行 shell 脚本一般有两种方法:
- 作为可执行程序运行:
./test.sh
- 此格式可让 shell 在
当前目录
寻找并执行test.sh
文件 - 注意一般不能写成
test.sh
,否则 shell 会去 PATH 里找文件 - 如果当前目录不在 PATH 里,写成
test.sh
会提示找不到文件
- 此格式可让 shell 在
- 作为解释器参数运行:
bash test.sh
- 直接运行解释器,其参数就是脚本文件路径
- 以这种方式运行脚本时,脚本中指定的解释器将不起作用,而会以运行时指定的解释器为准
Shebang 行:#!
env
脚本的第一行通常是指定解释器
,这一行以 #!
字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。注意:不能在 Shebang 行的行末添加注释。
#!/bin/bash # 【#!】后面是脚本解释器存放的位置,与脚本解释器之间可以有空格
#!/bin/sh # 必须确保 Bash 解释器存放在指定目录,否则脚本就无法执行
./script.sh # 脚本中有 Shebang 行时,可以直接调用执行脚本
/bin/sh ./script.sh # 如果没有 Shebang 行,需要将脚本传给解释器才行执行
一般情况下,我们并不需要区分
Bourne Shell
和Bourne Again Shell
,所以用#!/bin/sh
或#!/bin/bash
都可以
env
命令总是指向 /usr/bin/env
文件,如果不知道某个命令的具体路径,或者希望兼容其他用户的机器,可以使用 env
命令返回 Bash 可执行文件的位置。
#!/usr/bin/env bash # 查找【$PATH】环境变量里面第一个匹配的【bash】的路径
#!/usr/bin/env node # 其他脚本文件也可以使用,比如 Node.js 脚本的 Shebang 行
权限和路径:chmod PATH
脚本执行的前提条件是,脚本需要有执行权限。可以使用下面的命令,赋予脚本执行权限。
chmod +x script.sh # 给所有用户执行权限
chmod +rx script.sh # 给所有用户读权限和执行权限(755)
chmod u+rx script.sh # 只给脚本拥有者读权限和执行权限(700)
chmod 755 script.sh # 另一种语法,给所有用户读权限和执行权限
脚本调用时需要指定脚本的路径,如果脚本存放在环境变量 $PATH
中,就不需要指定路径。
# 可在主目录新建一个【~/bin】子目录,专门存放可执行脚本,然后把【~/bin】加入【$PATH】
export PATH=$PATH:~/bin # 将目录【~/bin】添加到环境变量【$PATH】的末尾
source ~/.bashrc # 将命令加到【~/.bashrc】中,然后重新加载一次,配置即可生效
脚本参数:$n
$#
$@
调用脚本的时候,脚本文件名后面可以带有参数。脚本文件内部,可以使用特殊变量,引用这些参数。
$0
:脚本文件名$n
:传递到脚本的第 n 个参数的值,建议使用${n}
来获取$#
:传递到脚本的参数数量$@
:全部的参数,参数之间使用空格分隔,可以利用 for 循环读取每一个参数$*
:全部的参数,参数之间使用$IFS
的第一个字符(默认为空格)分隔,也可以自定义
# 调用格式【./test.sh 29 男】
chmod 777 test.sh
echo $0 #【./test.sh】脚本文件名
echo $1 #【29】注意,对于 command -o xxx yyy,$1 是 -o,$2 是 xxx
echo $2 #【男】
echo $# #【2】传递到脚本的参数数量
echo $* #【29 男】以一个字符串显示所有向脚本或函数传递的参数
echo $@ #【29 男】
$*
与 $@
的区别:在双引号
中,$*
的作用是将所有参数当做一个参数
# 【./test.sh 29 男 白乾涛】
for i in $@; do echo -n $i-; done; # 【29-男-白乾涛】
for i in $*; do echo -n $i-; done; # 【29-男-白乾涛】
for i in "$@"; do echo -n $i-; done; # 【29-男-白乾涛】
for i in "$*"; do echo -n $i-; done; # 【29 男 白乾涛】注意,这里是将所有参数当做一个参数
for i in '$@'; do echo -n $i-; done; # 【$@】
for i in '$*'; do echo -n $i-; done; # 【$*】
读取命令执行结果:$?
环境变量 $?
可以读取前一个命令的返回值(即命令执行结果)。
cd $some_directory
if [ "$?" = "0" ]; then # 可以直接写成【if cd $some_directory; then】
rm * # 如果执行成功,就删除该目录里面的文件
else
echo "无法切换目录!" 1>&2
exit 1 # 否则退出脚本
fi
cd $some_directory && rm * # 第一步执行成功,才会执行第二步
cd $some_directory || exit 1 # 第一步执行失败,才会执行第二步
配置项参数终止符:--
-
和 --
开头的参数,会被 Bash 当作配置项
解释。如果它们不是配置项,而是实体参数的一部分,就需要使用 配置项参数终止符 --
告诉 Bash,在它后面以 -
和 --
开头的参数不是配置项,而是实体参数。
cat -- -f # 可以正确展示文件 -f 的内容
cat -- --file # 可以正确展示文件 --file 的内容
ls -- $myPath # 强制将变量 $myPath 当作实体参数(即路径名)解释
grep -- "--xxx" test.txt # 在文件里面搜索字符串【--xxx】
脚本基础命令
移除脚本参数:shift
使用 shift 命令可以移除脚本参数。
shift # 移除脚本当前的第一个参数,即原来的 $2 变成 $1,原来的 $3 变成 $2
shift 3 # 移除脚本当前的前三个参数,即原来的 $4 变成 $1,原来的 $5 变成 $2
解析脚本参数:getopts
在脚本内部,可以使用 getopts
命令解析复杂的脚本参数,比如取出所有的带有前置连词线 -
的参数。
它带有两个参数。
- 第一个参数
optstring
是由脚本所有的连词线参数组成的字符串,顺序不重要- 带有参数值的配置项参数,后面必须带有一个冒号
:
- 比如,某个脚本可以有三个配置项参数
-l
、-h
、-a
,其中只有-a
可以带有参数值,另外两个都是开关参数,那么第一个参数就可以写成lha:
- 带有参数值的配置项参数,后面必须带有一个冒号
- 第二个参数
name
是一个变量名,用来保存当前取到的配置项参数
while getopts 'lha:' OPTION; do # 每次循环就会读取一个连词线参数以及对应的参数值
count=$(($OPTIND - 1)) # 代表已经处理的参数个数,变量 OPTIND 在开始执行前是 1
case "$OPTION" in # 变量 OPTION 保存的是当前处理的那一个连词线参数
l)
echo "参数 l,count=$count" # 不带参数值时,每次执行 OPTIND 就会加 1
;;
h)
echo "参数 h,count=$count"
;;
a) # 如果连词线参数带有参数值,处理参数的时候可以获取参数值
echo "参数 a,count=$count" # 带参数值时,每次执行 OPTIND 就会加 2
echo "参数值是 $OPTARG" # 环境变量 $OPTARG 会保存当前连词线参数的参数值
;;
?) # 如果输入了 optstring 中未指定的参数,比如 -x,那么 OPTION 等于【?】
echo "存在非法参数,脚本使用格式: $(basename $0) [-l] [-h] [-a xxx]" >&2
exit 1 # 终止当前脚本的执行,并向 Shell 返回一个退出值,1 代表执行失败
;;
esac
done # 正常退出 while 循环,就意味着连词线参数全部处理完毕
echo "已处理的参数个数:$count" # 不仅指连词线参数,还包括连词线参数携带的参数值
shift $count # 将这些连词线参数移除后,就可以使用 $1 $2 等处理命令的主参数
echo "主参数:$@"
- 只要遇到不带连词线的参数,
getopts
就会执行失败,从而退出while
循环- 可以解析
command -l -a xxx -h yyy
中的连词线参数h
- 不可以解析
command -l zzz -a xxx -h yyy
中的连词线参数ah
- 可以解析
getopts
也可以正确处理多个连词线参数写在一起的形式,比如command -lh
- 变量
$OPTIND
- 在
getopts
开始执行前是1
,然后每次执行就会加1
或2
(带参数值) - 连词线参数全部处理完毕后,
$OPTIND - 1
就是已经处理的参数个数(包含参数值) - 使用
shift
命令将这些参数移除后,就可以使用$1
$@
等处理命令的主参数
- 在
终止当前脚本:exit
exit
命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。
exit # 终止当前脚本,并将最后一条命令的退出状态,作为整个脚本的退出状态
exit 0 # 退出值 0 代表执行成功(正常结束),只要退出值非 0,就可认为执行失败
exit 1 # 退出值 1 代表执行失败(发生错误),2 用法不对,126 不可执行,127 命令未发现
exit
与 return
命令的区别:
return
命令是函数的退出,并返回一个值给调用者,脚本依然执行exit
是整个脚本的退出,如果在函数之中调用exit
,则退出函数,并终止脚本执行
加载外部脚本:source
source
命令可以在当前 Shell 执行脚本,而不会新建一个子 Shellsource
命令执行脚本时,不需要export
变量就可以在脚本中读取当前 Shell 的变量source
命令通常用于重新加载一个配置文件,或在脚本内部加载外部库source xxx
可简写为. xxx
#!/bin/bash
source ./lib.sh # 在脚本内部加载外部库
function_from_lib # 在脚本里面使用外部库定义的函数
echo $var_from_shell # 在脚本里面读取当前 Shell 的变量
指定命令别名:alias
alias
命令用来为一个命令指定别名。一般来说,都会把常用的别名写在 ~/.bashrc
的末尾。
alias NAME=DEFINITION # 等号两侧不能有空格
alias search=grep # 为 grep 命令起一个 search 的别名
alias today='date +"%A, %B %-d, %Y"' # 为长命令指定一个更短的别名
today # 【星期六, 一月 22, 2022】
alias rm='rm -i' # 通过指定别名,可以修改已有命名的默认行为
alias echo='echo →' # 别名也可以接受参数,参数会直接传入原始命令
alias # 显示所有别名
unalias lt # 解除别名
注意,只能为命令定义别名,为其他部分(比如很长的路径)定义别名是无效的。
创建临时文件:mktemp
mktemp
命令是为安全创建临时文件而设计的:
- 生成的临时文件名是随机的,权限是只有用户本人可读写
- 支持唯一文件名和清除机制
- 可以使用
trap
命令指定退出时的清除操作 - 注意:此命令创建临时文件前,不会 检查临时文件是否存在,需要自行判断是否创建成功
- 注意:
/tmp
目录是所有人可读写
的,在此目录下创建的文件默认是所有人可读
的
参数:
-d
:使用默认的文件名模板创建一个临时目录,默认的文件名模板是tmp.
后接十个随机字符-p
:指定临时文件所在的目录,默认是使用环境变量$TMPDIR
,未设置值时使用/tmp
目录-t
:指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X
字符,表示随机字符
mktemp # 【/tmp/tmp.4GcsWSG4vj】使用默认的文件名模板创建一个临时文件
mktemp -d # 【/tmp/tmp.Wcau5UjmN6】使用默认的文件名模板创建一个临时目录
mktemp -p /home/bqt/ # 【/home/bqt/tmp.FOKEtvs2H3】指定临时文件所在的目录
mktemp -t tmp.XXXXXXX # 【/tmp/tmp.yZ1HgZV】指定临时文件的文件名模板
echo "创建了临时文件 $(mktemp)" # 获取创建的临时文件
TMPFILE=$(mktemp) || exit 1 # 创建失败时退出脚本
trap 'rm -f "$TMPFILE"' EXIT # 使用 trap 命令指定,脚本退出时清除临时文件
响应系统信号:trap
trap
命令用来在 Bash 脚本中响应系统信号,例如按 Ctrl + C
所产生的中断信号 SIGINT
。
trap -l # 列出所有 64 个系统信号
trap [动作] [信号1 信号2 ...] # 命令格式,动作指的是一个 Bash 命令
trap 'rm -f "$TMPFILE"' EXIT # 遇到 EXIT 信号时(脚本退出时),执行指定的清理命令
常用的信号有以下几个:
SIGHUP
:编号 1,脚本与所在的终端脱离联系SIGINT
:编号 2,用户按下Ctrl + C
,意图让脚本终止运行SIGQUIT
:编号 3,用户按下Ctrl + 斜杠
,意图退出脚本SIGKILL
:编号 9,该信号用于杀死进程SIGTERM
:编号 15,这是kill
命令发出的默认信号EXIT
:编号 0,这不是系统信号,而是 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
命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。
脚本调试
编写 Shell 脚本的时候,一定要考虑到命令失败的情况,否则很容易出错。
#! /bin/bash
dir=/path/not/exist
cd $dir # 如果目录不存在,cd 命令就会执行失败,就不会改变当前目录
rm * # 脚本会继续执行,从而删光当前目录的文件
cd $dir && rm * # 如果变量为空,cd 会进入用户主目录,从而删光用户主目录的文件
[[-d $dir]] && cd $dir && rm * # 比较安全的写法,先判断目录是否存在再执行
[[-d $dir]] && cd $dir && echo rm * # 不删除文件,而是先打印要删除的文件看一下
命令错误处理
如果脚本里面有运行失败的命令(返回值非 0
),Bash 只是显示有错误,并且继续执行后面的命令,而不会终止执行。
实际开发中,如果某个命令失败,往往需要脚本停止执行,防止错误累积。
command || exit 1 # 如果命令 command 执行失败了,脚本停止执行
command1 && command2 # 如果 command1 执行成功了,才继续执行 command2
command || {echo "失败"; exit 1;} # 停止执行之前完成多个操作
if ! command; then echo "失败"; exit 1; fi # 写法二
if ["$?" -ne 0]; then echo "失败"; exit 1; fi # 写法三
几个调试参数
为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数。
bash -n scriptname # 不运行脚本,只检查是否有语法错误
bash -v scriptname # 每一行语句输出运行结果前,先输出该行语句
bash -x scriptname # 每一个命令处理之前,先输出该命令,再执行该命令
调试参数 -x
bash
的-x
参数可以在执行每一行命令之前,打印该命令- 输出的命令之前的
+
号,是由环境变量PS4
决定的,可以修改这个变量的值 set
命令也可以设置 Shell 的行为参数,有利于脚本除错
bash -x # 加上 -x 参数启动一个 bash,后续执行每条命令前,都会先显示该命令
bash -x test.sh # 加上 -x 参数执行脚本,执行脚本中的每条命令前,都会先显示该命令
#! /bin/bash -x # 参数 -x 也可以写在脚本的 Shebang 行(演示代码,这一行不能有注释)
export PS4='- ' # 可以修改环境变量 PS4 的值,默认为 + 号
$ echo `pwd` # 执行此命令前会先执行 pwd 命令,有 n 层调用就会有 n 个连续的 +
++ pwd # 执行每条命令(pwd)前,都会先显示该命令,注意这里打印的是 ++
+ echo /home/bqt # 执行每条命令(echo)前,都会先显示该命令
/home/bqt # 这一行是 echo 命令自身的打印
调试用的环境变量
LINENO
:返回变量在脚本文件中的行号FUNCNAME
:返回当前的函数调用堆栈
。数组的 0 号成员是当前调用的函数,1 号成员是调用当前函数的函数,记作m(i)
BASH_SOURCE
:返回当前的脚本调用堆栈
。数组的 0 号成员是当前执行的脚本,1 号成员是调用当前脚本的脚本,记作n(i)
BASH_LINENO
:返回每一轮调用对应的行号。f(i)
跟m(i)
是一一对应关系,表示${FUNCNAME[$i]}
在调用它的脚本文件${BASH_SOURCE[$i+1]}
里面的行号
#!/bin/bash
echo "当前行号 $LINENO" # 当前行号 3
echo "${FUNCNAME[0]} - ${FUNCNAME[1]}" # main -
echo "${BASH_SOURCE[0]} - ${BASH_SOURCE[1]}" # ./test.sh -
echo "${BASH_LINENO[0]} - ${BASH_LINENO[1]}" # 0 -
脚本执行环境
Bash 执行脚本时,会创建一个子 Shell。这个子 Shell 就是脚本的执行环境,Bash 默认给定了这个环境的各种参数。
定制环境参数:set
set
命令用来修改子 Shell 环境的运行参数,即定制环境。
set -u
:遇到不存在的变量时,报错并停止执行,而不是继续向下执行set -n
:不运行命令,只检查语法是否正确set -x
:在运行结果之前,先输出执行的那一行命令。可使用set +x
关闭set -e
:只要返回值非0
就终止执行。可使用set +e
关闭,等价于command || true
set -f
:不对通配符进行文件名扩展。可使用set +f
关闭set -v
:打印 Shell 接收到的每一行输入。可使用set +v
关闭set -E
:函数内
的命令在执行错误时,即使设置了set -e
,仍然可以被trap
命令捕获set -o pipefail
:管道命令中只要一个子命令失败,整个管道命令就失败set -o noclobber
:防止使用重定向运算符>
覆盖已经存在的文件
set # 显示所有的环境变量和 Shell 函数
set -u # 等同于【set -o nounset】遇到不存在的变量时,报错并停止执行
set -n # 等同于【set -o noexec】不运行命令,只检查语法是否正确
set -x # 等同于【set -o xtrace】在运行结果之前,先输出执行的那一行命令
set -e # 等同于【set -o errexit】只要返回值非 0 就终止执行
set -f # 等同于【set -o noglob】不对通配符进行文件名扩展
set -v # 等同于【set -o verbose】打印 Shell 接收到的每一行输入
set -E # 使函数内的命令在执行错误时,始终可以被 trap 命令捕获
set -o pipefail # 管道命令中只要一个子命令失败,整个管道命令就失败
set -o noclobber # 防止使用重定向运算符 > 覆盖已经存在的文件
set -Eeuxo pipefail # 参数可以采用混合写法,建议放在 Bash 脚本的头部
bash -euxo pipefail test.sh # 也可以在执行 Bash 脚本的时候,从命令行传入参数
关于 set -o pipefail
的解释:
- 参数
set -e
不适用于管道命令(即多个子命令通过管道运算符|
组合成的命令) - Bash 默认会把管道命令中,最后一个子命令的返回值,作为整个命令的返回值
- 所以,只要最后一个子命令返回值非
0
,管道命令就代表执行成功 - 通过
set -eo pipefail
可实现,管道命令中只要一个子命令失败,整个管道命令就失败
调整环境参数:shopt
shopt
命令跟 set
命令的作用很类似,也可以用来调整 Shell 的参数。区别是:set
是 POSIX 规范的一部分,而 shopt
是 Bash 特有的命令。
shopt # 查看所有参数,以及它们的开关状态
shopt xxx # 查询某个参数的开关状态
shopt -s xxx # 打开某个参数
shopt -u xxx # 关闭某个参数
shopt -q xxx # 通过命令的执行结果 $? 查询某个参数的开关状态,0 表示打开,1 表示关闭
对话 Session
用户每次使用 Shell,都会开启一个与 Shell 的 Session。
Session 有两种类型:登录 Session 和非登录 Session,也可以叫做 login shell 和 non-login shell。
登录 Session
登录 Session 是用户登录系统以后,系统为用户开启的原始 Session,通常需要用户输入用户名和密码进行登录。
登录 Session 一般会进行 整个系统环境 的初始化,启动的初始化脚本依次如下:
- 针对
所有用户
的初始化脚本/etc/profile
:全局配置脚本。Linux 更新时候会更新此文件,因此建议不要直接修改这个文件/etc/profile.d/
:此目录所有.sh
文件。如果想修改所有用户的登陆环境,建议在此目录中新建.sh
脚本
- 针对
当前用户
的初始化脚本~/.bash_profile
:个人配置脚本。该脚本若存在则不再往下执行。一般建议在此文件中修改个人的登录环境~/.bash_login
:C shell 的初始化脚本。该脚本若存在则不再往下执行~/.profile
:Bourne shell 和 Korn shell 的初始化脚本
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 testrc # 指定用另一个脚本代替 ~/.bashrc 脚本
退出时执行的脚本
脚本在每次退出 Session 时都会执行脚本 ~/.bash_logout
,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。
如果没有退出时要执行的命令,这个文件也可以不存在。
2022-01-15