0%

高级 Bash 脚本编程指南(04):引用、转义、退出和退出状态

这篇文章将学习 Bash 脚本中的引用、转义、退出及退出状态相关知识。

引用的作用

引用就是将字符串用引号括起来,引用的主要作用就是保护字符串中的特殊字符不被 shell 或者是 shell 脚本重新解释或扩展。因此在 Bash 中,当我们引用一个字符串,我们的目标是保留它的字面含义。

1
2
3
4
# ls t*
t.sh
# ls "t*"
ls: cannot access 't*': No such file or directory

特定的程序和工具能够展开引用字符串中的特殊字符。引用的一个重要的作用就是保护 Shell 中的命令行参数,但还是允许调用的程序来扩展它:

1
grep '[Ff]irst' *.txt

引用也可以抑制 echo 命令 吞掉 换行符:

1
2
3
4
5
6
7
# echo $(ls -l)
total 0 -rw-r--r-- 1 root root 0 May 28 14:26 t.sh -rw-r--r-- 1 root root 0 May 28 14:31 u.sh

# echo "$(ls -l)"
total 0
-rw-r--r-- 1 root root 0 May 28 14:26 t.sh
-rw-r--r-- 1 root root 0 May 28 14:31 u.sh

引用变量

应用变量时,通常将变量放在双引号内,此时除了 $、` 反引号、\ 转义符 之外的其他特殊字符都将失去它们的特殊含义。在双引号中,仍然可以通过 $variable 获取变量的值。

使用双引号可以防止字符串分割,此时即使参数中拥有很多空白字符,被包在双引号中依旧算单一字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash

List="one two three"

# 空白符将变量分隔成多个成多个部分
for a in $List
do
echo "str $a"
done


echo "---"

# 被抱在双引号中,在单一变量中,保留所有空格
for a in "$List"
do
echo "str $a"
done
1
2
3
4
5
6
# ./quoto_string.sh
str one
str two
str three
---
str one two three

如下是一个更复杂的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
variable1="a variable containing five words"

# 使用如下 7 个参数执行 COMMAND
# "This" "is" "a" "variable" "containing" "five" "words"
COMMAND This is $variable1

# 使用如下 1 个参数执行 COMMAND
# "This is a variable containing five words"
COMMAND "This is $variable1"

variable2=""

# 不带参数执行 COMMAND 命令。
COMMAND $variable2 $variable2 $variable2
# 使用 3 个空字符串参数执行 COMMAND
COMMAND "$variable2" "$variable2" "$variable2"

# 使用 1 个字符串(包含 2 个空格)参数执行 COMMAND
COMMAND "$variable2 $variable2 $variable2"

单引号总体上与双引号类似,但是在单引号内,除了 ' 字符之外,其他所有特殊字符都没有特殊的含义。所以可以认为单引号比双引号更严格。这也是为什么在单引号内无法进行变量替换。尤其注意,单引号内中的转义符 \ 也失去转义作用,所以想在单引号引用的字符串内转义 单引号,是无法实现的:

1
2
3
4
# echo "It's me"
It's me
# echo 'It\'s me'
>

转义

转义是一种引用单个字符的方法,通过在特殊字符前面添加 \ 来告诉 shell 按照字面意思去解释这个字符,而不是使用它的特殊含义。需要注意的是,在某些特定的命令中,例如 echosed 等,转义符往往起到相反的效果,它反倒可能引发出这个字符特殊的含义。

如下展示了在 echosed 中使用的具有特殊含义的转义字符:

  • \n:换行
  • \r:回车
  • \t:水平 tab
  • \v:垂直 tab
  • \b:退格
  • \a:警报
  • \nnn:ASCII 码的八进制形式
  • \xhh:十六进制数表示
  • ":转义引号
  • $:转义 $
  • \:转义 \

在 $’…’ 字符串扩展结构中,可以通过转义八进制或十六进制的 ASCII 码形式给变量赋值,例如 quote=$’\042’。

如下脚本展示了在 echo 中使用转义字符的例子:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/bash

echo ""

echo "this will print
as two lines"

echo "this will print \
as one lines"

echo
echo
echo "========"

echo "\v\v\v"
echo "========"
echo "VERTICAL TABS"
echo -e "\v\v\v"

echo "QUOTATION MARK"
echo -e "\042"
echo "========"


echo; echo "NEWLINE and (maybe) BEEP"
echo $'\n'
echo $'\a'

echo "Introducing the \$'...' string-expansion construct ..."
echo "... featuring more quotation marks"
echo $'\t \042 \t'
echo $'\t \x22 \t'
echo

quote=$'\042'
echo "$quote Quoted String $quote and this lies outside the quotes"
echo

triple_underline=$'\137\137\137'
echo "$triple_underline UNDERLINE $triple_underline"
echo

ABC=$'\101\102\103\010'
echo $ABC
echo

escape=$'\033'
echo "\"escape\" echoes an $escape"
echo

exit 0
  • 在 echo 命令中,通过 -e 选项来打印转义字符
  • 如果使用 $'\nnn'$'\xhh' 字符串扩展时,就不需要使用 -e 选项
  • 在字符串扩展中,可以使用连续多个 ASCII 码字符

根据转义符所在的上下文(强引用、弱引用、命令替换或者在 here document)的不同,它的行为也会有所不同。

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
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash

# z
echo \z
# \z
echo \\z
# \z
echo '\z'
# \\z
echo '\\z'
# \z
echo "\z"
# \z
echo "\\z"

echo "**********"

# z
echo `echo \z`
# z
echo `echo \\z`
# \z
echo `echo \\\z`
# \z
echo `echo \\\\z`
# \\z
echo `echo \\\\\\z`
# \z
echo `echo "\z"`
# \z
echo `echo "\\z"`

echo "**********"
# \z
cat <<EOF
\z
EOF

# \z
catg << EOF
\z
EOF

含有转义字符的字符串可以赋值给变量,但是仅仅将单一的转义字符赋值给变量是不行的,例如如下代码:

1
2
variable=\
echo "$variable"

此时 \ 其实转义了换行符,最终效果是:

1
variable=echo "$variable"

转义空格能够避免在命令参数列表中的字符分隔问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# touch file{1,2,3}
# file_list="file1 file2 file3"
# ls -l $file_list
-rw-r--r-- 1 root root 0 May 30 14:11 file1
-rw-r--r-- 1 root root 0 May 30 14:11 file2
-rw-r--r-- 1 root root 0 May 30 14:11 file3
# ls -l "$file_list"
ls: cannot access 'file1 file2 file3': No such file or directory

# ls -l file1 file2 file3
-rw-r--r-- 1 root root 0 May 30 14:11 file1
-rw-r--r-- 1 root root 0 May 30 14:11 file2
-rw-r--r-- 1 root root 0 May 30 14:11 file3

# ls -l file1\ file2\ file3
ls: cannot access 'file1 file2 file3': No such file or directory

转义符也提供了一种可以撰写多行命令的方式,因为它将之后的换行符进行了转义,使得跨越多行的命令最终都在一行上。

1
2
(cd /source/directory && tar cf - . ) | \
(cd /dest/directory && tar xpvf -)

当然由于 bash 脚本中,如果以 | 作为一行的结束字符,那么不需要加转义符 \ 也可以写多行命令。但是好的编写习惯还是在编写多行命令时,始终使用行尾转义符 \

1
2
3
# ok, but not recommended
tar cf - -C /source/directory . |
tar xpvf - -C /dest/directory

退出与退出状态

exit 用来退出脚本,它可以返回一个值给父进程。每个命令都有退出状态,命令执行成功返回 0,如果返回非 0,通常被认为是一个错误代码。好的 UNIX 命令、程序在正常退出时都会返回一个 0 退出码(当然也有例外)。

脚本中的函数也会返回一个退出状态,在脚本或者脚本函数中执行的最后命令会决定它们的退出状态。当脚本以不带参数的 exit 来结束时,脚本的退出状态也是由最后执行的命令决定(exit 命令之前)

因此如下三种形态,其效果都是一样的,都是以最后执行的命令(exit 命令之前)来决定退出状态:

1
2
3
command_1
...
command_last
1
2
3
4
command_1
...
command_last
exit $?
1
2
3
4
command_1
...
command_last
exit

$? 用于返回上一个命令的退出状态,在函数返回后,$? 也可以给出函数的退出状态,这也是函数的返回值。在管道执行后,$? 给出的是最后执行的那条命令的退出状态。

使用 ! comand 可以反转 command 的退出状态。注意 !command 之间有一个空格,否则 !command 将会触发 Bash 的历史机制,显示这个命令的调用历史。

1
2
3
4
5
6
7
8
# echo hello
hello
# echo $?
0
# lsddkba
lsddkba: command not found
# echo $?
127
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# true
# echo $?
0
# ! true
# echo $?
1
# ls | lsdabl
lsdabl: command not found
# echo $?
127

# ! 不会改变管道的执行,只改变退出状态
# ! ls | lsdabl
lsdabl: command not found
# echo $?
0

某些退出码具有保留含义,不应该在脚本中重新定义这些退出码。