李守中

Bash 6 脚本技巧

Table of Contents

1 一定要检查文件夹是否存在

给一个例子:

#! /bin/bash

dir_name=/path/not/exist

cd $dir_name
rm *

目录 $dir_name 不存在, cd $dir_name 命令就会执行失败。脚本就会在当前目录下继续执行,然后 rm * 命令就会删除当前目录的文件。

cd $dir_name && rm * 也不行,如果 $dir_name 为空,命令就变成了 cd && rm * ,这次是删除用户主目录的文件。

[[ -d $dir_name ]] && cd $dir_name && rm * 这样才对。

[[ -d $dir_name ]] && cd $dir_name && echo rm * 如果不放心删除什么文件,可以先打印出来看一下。 echo rm * 不会删除文件,只会打印出来要删除的文件。

2 用 bash -x 查 bug

bash 的 -x 参数可以在执行每一行命令之前,打印该命令。一旦出错,这样就比较容易追查。

$ cat>test.sh<<'EOF'
#!/bin/bash
echo "hello world1"
echo "hello world2"
echo "hello world3"
EOF

$ bash -x test.sh 
+ echo 'hello world1'
hello world1
+ echo 'hello world2'
hello world2
+ echo 'hello world3'
hello world3

3 用环境变量查 bug

3.1 $LINENO 保存行号

环境变量 $LINENO 保存了这个变量出现的行号。

$ cat>test.sh<<'EOF'
#!/bin/bash

echo "This is line $LINENO"
echo "This is line $LINENO"
EOF

$ bash test.sh
This is line 3
This is line 4

3.2 $FUNCNAME 保存函数调用栈

环境变量 $FUNCNAME 返回一个数组,内容是当前的函数调用堆栈。数组的 0 号成员是当前被调用的函数,1 号成员是调用了当前函数的函数,以此类推。

$ cat>test.sh<<'EOF'
#!/bin/bash

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
EOF

$ bash 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

3.3 $BASH_SOURCE 保存脚本调用栈

环境变量 $BASH_SOURCE 返回一个数组,内容是当前的脚本调用堆栈。数组的 0 号成员是当前执行的脚本,1 号成员是调用了当前脚本的脚本,以此类推。

$ cat>lib1.sh<<'EOF'
# 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
}
EOF

$ cat>lib2.sh<<'EOF'
# 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]}"
}
EOF

$ cat>main.sh<<'EOF'
#!/bin/bash
# main.sh
source lib1.sh
source lib2.sh
func1
EOF

$ bash 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

3.4 $BASH_LINENO 保存调用命令所在的行号

环境变量 $BASH_LINENO 返回一个数组,内容是每一轮调用对应的行号。 ${BASH_LINENO[$i]}${FUNCNAME[$i]} 是对应关系,表示 ${FUNCNAME[$i]} 在调用它的脚本文件 ${BASH_SOURCE[$i+1]} 里面的行号。

$ cat>lib1.sh<<'EOF'
# 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
}
EOF

$ cat>lib2.sh<<'EOF'
# 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]}"
}
EOF

$ cat>main.sh<<'EOF'
#!/bin/bash
# main.sh
source lib1.sh
source lib2.sh
func1
EOF

$ bash 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

4 修改 shell 运行参数查 bug

4.1 POSIX 规范中的 set 命令

set -u (等同于 set -o nounset) 脚本执行过程中遇到不存在的变量,报错。默认情况下,不存在的变量按空处理。

set -e 只要执行过程中发生错误,终止执行。但它不适用于管道命令。由于 Bash 总会把通过管道运算符 | 组合起来的,多个子命令的,最后一个子命令的返回值作为整个命令的返回值,这导致了只要最后一个子命令不失败,管道命令总是会执行成功。那么管道命令后面的命令依然会被执行, set -e 就失效了。

$ cat>test.sh<<'EOF'
#!/bin/bash
set -e
foo | echo a
echo bar
EOF

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

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

set -E 纠正设置了 set -e 之后 trap 命令无法捕捉信号的问题。

set -xbash -x 效果相同。但 set -x 可以做到只对某段代码打开执行过程输出:

#!/bin/bash

number=1

set -x
if [ $number = "1" ]; then
  echo "Number equals 1"
else
  echo "Number does not equal 1"
fi
set +x

echo "set -x closed"

set 命令还有一些其他参数。

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

4.2 Bash 特有的 shopt 命令

shoptset 的区别在于,set 是从 Ksh 继承的,属于 POSIX 规范的一部分,而 shopt是 Bash 特有的。

直接输入 shopt 可以查看所有参数,以及它们各自打开和关闭的状态。

shopt 命令后面跟着参数名,可以查询该参数是否打开。

$ shopt globstar
globstar  off

shopt -s 用来打开某个参数。

shopt -u 用来关闭某个参数。

shopt -q 也可以查询某个参数是否打开,但它不直接输出查询结果,而是通过命令的执行状态 $? 表示查询结果。如果状态为 0,表示该参数打开;如果为 1,表示该参数关闭。

$ shopt -q globstar
$ echo $?
1

这个用法主要用于脚本,供 if 条件结构使用:

# 如果打开了这个参数,就执行 if 结构内部的语句
if !(shopt -q globstar); then
    ...
fi


Last Update: 2023-05-25 Thu 11:56

Contact: [email protected]     Generated by: Emacs 27.1 (Org mode 9.3)

若正文中无特殊说明,本站内容遵循: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议