李守中

Bash 4 脚本

Table of Contents

1 执行脚本

1.1 脚本的第一行

通常,脚本的第一行以 #! 开始,它为有执行权限的脚本指定了解释器。

~#! 这个 2 字符统称为 shebang,所以这一行就叫做 shebang 行。与解释器路径之间有无空格都可以。

#!/bin/sh
#!/bin/bash

shebang 行不是必需的,但建议加上。如果缺少 shebang 行,就需要手动为脚本调用解释器。

# 有 shebang 行,且用户具有文件的执行权限
$ ./script.sh
# 或者
$ /bin/sh ./script.sh
# 或者
$ bash ./script.sh

1.2 source 命令

source 命令用于执行一个脚本。( 通常用于重新加载一个配置文件 )

source .bashrc

source 命令最大的特点是在当前 Shell 执行脚本。而使用 ./script.sh 格式直接执行脚本时,会新建一个子 Shell。所以,以 source 命令执行脚本时,不需要关注环境变量的问题。

给一个例子:

#!/bin/bash
# 脚本 test.sh
echo $foo
# 当前 Shell 新建一个变量 foo
$ foo=1

# 打印输出 1
$ source test.sh
1

# 打印输出空字符串
$ ./test.sh

source 命令的另一个用途,是在脚本内部加载外部库。

#!/bin/bash

# 加载一个外部库后,就可以在脚本里面使用这个外部库定义的函数。
source ./lib.sh

function_from_lib

source 有一个简写形式,可以使用一个点 . 来表示。

$ . .bashrc

2 脚本结束

2.1 执行结果

命令或者脚本执行结束后,会有一个返回值。0 表示执行成功,非 0 ( 通常是 1 ) 表示执行失败。

环境变量 $? 可以读取前一个命令的返回值。可以借此方式在脚本中对命令执行结果进行判断。

cd $some_directory
if [ "$?" = "0" ]; then
    rm *
else
    echo "Could not change directory! Aborting." 1>&2
    exit 1
fi

由于 if 可以直接判断命令的执行结果,所以上面的脚本可以改写成下面的样子。

if cd $some_directory; then
    rm *
else
    echo "Could not change directory! Aborting." 1>&2
    exit 1
fi

更简洁的写法是利用两个逻辑运算符 &&||

cd $some_directory && { rm *; echo -n;}  || exit 1

2.2 exit 命令

exit 命令用于终止当前脚本的执行,并向 Shell 返回一个退出值。

# 中止当前脚本,将最后一条命令的退出值,作为整个脚本的退出值
exit

# 退出值为 0 执行成功
exit 0

# 退出值为 1 执行失败
exit 1

脚本的退出值 非 0 则为执行失败:

  • 0 正常。
  • 1 发生错误。
  • 2 用法不对。
  • 126 不是可执行脚本。
  • 127 命令没有找到。如果
  • 脚本被信号 N 终止则退出值为 128 + NN 可自定义。

下面是一个例子。

if [ $(id -u) != "0" ]; then
    echo "require root permission"
    exit 1
fi

exit 与 return 命令的差别是:

  • return 命令是函数的退出,并返回一个值给调用者,脚本依然执行。
  • exit 是整个脚本的退出。

3 脚本参数

3.1 基本使用

执行脚本时,脚本名后面可以带参数。

$ script.sh arg1 arg2 arg3

脚本文件内部,可以使用特殊变量,引用这些参数。

  • $0: 脚本文件名,即 script.sh
  • $1~$9: 对应脚本的第 1-9 个参数。
  • $#: 参数的数量。
  • $@: 全部的参数。参数间使用空格分隔。
  • $*: 全部的参数。参数间使用变量 $IFS 值的第一个字符分隔,默认为空格,但是可以自定义。
  • 脚本的参数多于 9 个时,第 10 个参数可以用 ${10} 的形式引用,以此类推。

如果命令是 command -o foo bar ,那么 -o$1foo$2bar$3

多个参数放在双引号里面,视为一个参数。

$ ./script.sh "a b"

用户可以输入任意数量的参数,利用 for 循环,可以读取每一个参数。

#!/bin/bash

for i in "$@"; do
  echo $i
done

3.2 参数处理

3.2.1 shift 命令

shift 每次执行都会移除脚本当前的第一个参数,后面的参数向前移一位,即 $2 变成 $1$3 变成 $2 以此类推。

while 循环结合 shift 可以读取每一个参数。

#!/bin/bash

echo "一共输入了 $# 个参数"

while [ "$1" != "" ]; do
  echo "剩下 $# 个参数"
  echo "参数:$1"
  shift
done

shift 命令可以接受一个整数作为参数,指定移除的操作的步长,默认为 1。

# 移除前 3 个参数,原来的 $4 变成 $1
shift 3

3.2.2 配置项终止符 --

配置项终止符 -- 后面的,以 --- 开头的字符不是配置项,而是实体参数。

# 输出文件 -f 和 --file 的内容的正确写法
cat -- -f
cat -- --file

在变量前面加上 -- 可以让变量正常被解释为对应的值,而不是被当作配置项。

$ myPath="-l"
$ ls -- $myPath
ls: cannot access '-l': No such file or directory

另一个例子,如果想在文件里面搜索 --hello ,这时也要使用 --

# --hello 以 -- 开头,如果不用参数终止符,grep 会把 --hello 当作配置项,从而报错。
$ grep -- "--hello" example.txt

3.2.3 getopts 命令处理短参数

getopts 是 Bash 的内建方法。要注意和用 GNU C 库实现的 getopt 区分。

getopts 可以取出所有的带有前置单连词线 - 的参数。它通常被用在脚本内部,可以解析复杂的脚本命令行参数。

getopts 带有两个参数。第一个参数 optstring 是字符串,给出脚本所有的单连词线参数 ( 即形如 -a -c 的参数 )。第二个参数 name 是一个变量名,用来保存当前取到的参数。

getopts optstring name

getopts 执行后会产生变量:

  • $OPTIND: 表示当前配置项在参数列表中的位移。初始值是 1,getopts 每执行一次,该变量加 1。
  • $OPTARG: 保存配置项的值。
3.2.3.1 所有参数都有定义
#!/bin/bash
# test.sh

while getopts 'lha:' OPTION; do
    case "$OPTION" in
        l)
            echo "arg l"
        ;;

        h)
            echo "arg h"
        ;;

        a)
            avalue="$OPTARG"
            echo "value of arg a is $OPTARG"
        ;;

        ?)
            echo "script usage: $0 [-l] [-h] [-a somevalue]" >&2
            exit 1
        ;;
    esac
done
shift "$(($OPTIND - 1))"
$ ./test.sh -lh -a "a_args"
arg l
arg h
value of arg a is a_args

某脚本有 -l -h -a 三个参数,其中只有 -a 可以带有参数值,而 -l-h 是开关参数。

getopts 的第一个参数为 a:lh 。getopts 规定,带有值的配置项,后面必须跟一个冒号 : 。所以 a:lha 后面有一个冒号。配置项顺序不重要 ( la:h lha: 都行 )。

getopts 的第二个参数 name 是一个变量名,用来保存取到的配置项,即 l ha

getopts 'lha:' OPTION 命令每次执行就会读取一个 配置项 以及它的 配置项值 。变量 $OPTION 保存了当前被处理的配置项 ( 即 l ha )。如果用户输入了没有指定的配置项 ( 比如 -x ),那么 $OPTION 等于 ?

如果某个配置项带有值,比如 -a foo 。那么处理 -a 配置项的时候,变量 $OPTARG 保存了配置项的值 foo

只要遇到不带连词线 - 的参数,getopts 就会执行失败 。比如,getopts 不可以解析 <command> foo -l

多个连词线配置项写在一起的形式,比如 <command> -lh 可以被正确处理。

变量 $OPTIND 在 getopts 开始执行前是 1,然后每次执行就会加 1。在 getopts 执行完毕后 $OPTIND - 1 就是已处理的连词线配置项个数。使用 shift 命令将这些配置项移除,保证后面的代码可以用 $1 $2 等内建变量来处理命令的其他参数。

3.2.3.2 传入未定义参数

如果传入的配置项没有在 optstring 中定义,并且 optstring 的第一个字符是冒号 : ,变量 $OPTARG 的值为第一个被找到的,没有定义的配置项字符,且不会有任何输出写入到标准错误。

其他情况下,变量 $OPTARG 被 unset 后,Bash 将一条诊断信息写入到标准错误。这种情况被视为将参数提供给调用应用程序时检测到的错误,而不是 getopts 处理中的错误。

#!/bin/bash
# test2.sh

while getopts ':lha:' OPTION; do
    case "$OPTION" in
        l)
            echo "arg l"
        ;;

        h)
            echo "arg h"
        ;;

        a)
            avalue="$OPTARG"
            echo "value of arg a is $OPTARG"
        ;;

        ?)
            echo "undefined arg $OPTARG"
            echo "script usage: $0 [-l] [-h] [-a somevalue]" >&2
            exit 1
        ;;
    esac
done
$ ./test2.sh -lh -d
arg l
arg h
undefined arg d
script usage: ./test2.sh [-l] [-h] [-a somevalue]

3.2.4 getopt 命令处理长参数

getopt 是外部程序 ( 使用 GNU C 库实现 )。要注意和 Bash 内置的 getopts 区分。

它有三种格式:

getopt optstring parameters
getopt [options] [--] optstring parameters
getopt [options] -o|--options optstring [options] [--] parameters

getopt 命令的常用选项有:

  • -a 意为 alternative 可以使 -option 被识别为 --option 。必须与 -l 同时使用。
  • -l 后接长选项列表。
  • -n 后接一个字符串。如果 getopt 执行时返回错误,输出此字符串。在调用多个脚本时很有用。
  • -o 后接短参数选项,用法与 getopts 类似。
  • -u 使引号失效。例如在引号不生效时 --longopt "select * from db1.table1"$2 只取到 select ,而不是完整的 SQL 语句。而在引号生效时可以取到引号内完整的内容。

选项的使用定义规则类似 getopts。比如 ab:c:: 被理解为:

  • a 后没有冒号,表示 a 是一个开关配置项,不带值。
  • b 后跟 1 个冒号,表示配置项必须有值。
  • c 后跟 2 个冒号,表示配置项有一个可选值 ( 可以没有值 )。但值和配置项之间不能有空格,比如配置项 c 的值是 456 ,不能写成 -c 456-c=456 ,必须写成 -c456
  • 长选项的定义相同,但用逗号分割。

上述的三种格式经常有人搞混,下面给三个例子。

第一种格式:

$ getopt ab:c:: -a -b 22 33 -c
 -a -b 22 -c  -- 33

$ getopt ab:c:: -a -b "22 33" -c
 -a -b 22 33 -c  --

注意这种格式的第二种写法有个坑。被处理后获得的参数值并不会被加上引号,如果参数值带空格的话会给后续的参数解析带来额外的工作。

第二种格式:

$ getopt -n "Error in command" -- ab:c:: -a -b 22 -c
 -a -b '22' -c '' --

$ getopt -n "Error in command" -- ab:c:: -a -b "22 33" -c
 -a -b '22 33' -c '' --

第三种格式:

$ getopt -n "Error in command" -o ab:c:: -- -a -b 22 -c
 -a -b '22' -c '' --

给一个在脚本中使用的例子:

#!/bin/bash

#echo $@
ARGS=`getopt -o ab:c:: --long along,blong:,clong:: -n 'example.sh' -- "$@"`
if [ $? != 0 ]; then
    echo "Terminating..."
    exit 1
fi

#echo $ARGS
#将规范化后的命令行参数分配至位置参数($1,$2,...)
eval set -- "${ARGS}"

while true; do
    case "$1" in
        -a|--along)
            echo "Option a";
            shift
            ;;
        -b|--blong)
            echo "Option b, argument $2";
            shift 2
            ;;
        -c|--clong)
            case "$2" in
                "")
                    echo "Option c, no argument";
                    shift 2  
                    ;;
                *)
                    echo "Option c, argument $2";
                    shift 2;
                    ;;
            esac
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Internal error!"
            exit 1
            ;;
    esac
done

#处理剩余的参数
for arg in $@ do
    echo "processing $arg"
done
$ ./getopt.sh -b 123 -a -c456 file1 file2 
Option b, argument 123
Option a
Option c, argument 456
processing file1
processing file2

4 特殊字符

4.1 反斜杠

4.1.1 转义

某些字符在 Bash 里有特殊含义,比如 $ & *

如果要输出这些字符,就要在它们之前加 \ 使其变为普通字符,这样的行为叫转义:

$ echo $date

$ echo \$date
$date

\ 本身也是特殊字符,如果想要原样输出 \ ,就需要对它自身转义:

$ echo \\
\

4.1.2 不可打印的字符

\ 除了用于转义,还可以表示一些不可打印的字符:

  • \a: 响铃。
  • \b: 退格。
  • \n: 换行。
  • \r: 回车。
  • \t: 制表符。

要在命令行使用这些不可打印的字符,可以把它们放在 ""'' 内,然后使用 echo -e:

$ echo a\tb
atb

$ echo -e "a\tb"
a        b

换行符是一个特殊字符,表示命令结束。Bash 收到这个字符以后,就会对输入的命令进行解释执行。换行符前面加上 \ 就使得换行符变成一个普通字符,Bash 会将其当作空格处理,从而可以将一行命令写成多行:

$ mv \
/path/to/foo \
/path/to/bar

# 等同于
$ mv /path/to/foo /path/to/bar

4.2 引号

双引号比单引号宽松。

4.2.1 单引号

单引号用于保留字符的字面含义,所有特殊字符在单引号里面,都会变为普通字符。比如 * $ \ 等。

$ echo '*'
*

$ echo '$USER'
$USER

$ echo '$((2+2))'
$((2+2))

$ echo '$(echo foo)'
$(echo foo)

由于 \ 在单引号里面变成了普通字符,所以,如果一对单引号之中还有单引号,则要在外层的单引号前面加 $'...' ,然后再对里层的单引号转义。

# wrong
$ echo it's

# wrong
$ echo 'it\'s'

# right
$ echo $'it\'s'

更合理的方法是改在双引号之中使用单引号:

$ echo "it's"
it's

4.2.2 双引号

大部分特殊字符在双引号里面,都会失去特殊含义,变成普通字符。

但美元符号 $ ,反引号 ` 和反斜杠 \ 这三个字符在双引号之中,依然有特殊含义。

换行符在双引号之中,会失去特殊含义,Bash 不再将其解释为命令的结束,只是作为普通的换行符。所以可以利用双引号,在命令行输入多行文本:

$ echo "hello
world"
hello
world

双引号可以保存命令输出的原始格式:

# 单行输出
$ echo $(cal)
January 2022 Su Mo Tu We Th Fr Sa 1 2 3 ... 30 31

# 原始格式输出
$ echo "$(cal)"
    January 2022    
Su Mo Tu We Th Fr Sa
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31              

5 容易被坑的地方

5.1 $() 的含义只有执行后替换

反引号 ` 或者 $() 可以用命令执行的结果替换原始字符串。

命令替换是指: 先执行 $() 中的命令,将命令的输出结果替换到 $() 位置处。

命令替换和变量替换差不多,都是在命令开始执行前执行,并将结果替换到命令行。

5.2 () 和 {} 的区别

如果用于一串命令的执行,那么小括号和大括号主要区别在于:

  • () 执行一串命令时,需要重新开启一个子 Shell 来执行。
  • {} 执行一串命令时,在当前 Shell 中执行。
  • (){} 都是把一串命令放在括号里面,并且命令之间用 ; 隔开。
  • () 最后一条命令可以不用分号。命令不必和括号用空格隔开。
  • {} 最后一条命令要用分号。第一条命令和左括号之间必须有一个空格。
  • (){} 中括号里面的某条命令的重定向只影响该命令,但括号外的重定向则会影响到括号里的所有命令。

给一个例子:

$ name=sc
$ echo $name
sc
$ (name=liming;echo $name)
liming
$ echo $name
sc
$ { name=liming;echo $name;}
liming
$ echo $name
liming


Last Update: 2023-05-25 Thu 14:50

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

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