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 + N
。N
可自定义。
下面是一个例子。
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
是 $1
, foo
是 $2
, bar
是 $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:lh
的 a
后面有一个冒号。配置项顺序不重要 (la:h
lha:
都行)。
getopts 的第二个参数 name
是一个变量名,用来保存取到的配置项,即 l
h
或 a
。
getopts 'lha:' OPTION
命令每次执行就会读取一个 配置项 以及它的 配置项值 。变量 $OPTION
保存了当前被处理的配置项 (即 l
h
或 a
)。如果用户输入了没有指定的配置项 (比如 -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