0%

高级 Bash 脚本编程指南(02):特殊字符

在 bash 中,如果一个字符不仅具有其字面含义,而且还具有元语义(meta-meaning),那么就可以称该字符为 特殊字符。特殊字符如同命令、关键字一样,是 bash 脚本的重要组成部分。这篇文章会对 bash 脚本编程中的特殊字符进行总结。

注释符 #

# 是 bash 里的注释符,如果脚本行以 # 开头,那么该行就被认为是注释行,不会被执行。bash 本身没有多行注释的语法,相反是通过 多个单行注释 来编写一段注释内容。

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

# this is comment
echo "comment 1"

# this is multi comment
# line1
# line2
# end
echo "comment 2"

注释也可以在命令行的结尾,但此时要注意命令结尾和 # 字符之间需要存在空格:

1
2
3
4
5
# echo "this follow comment" # comment
this follow comment

# echo "this not follow comment"# comment
this not follow comment# comment

脚本中并不是所有的 # 都会被认为是注释的开始,例如 echo 语句中被引用或转移的 # 字符就没有特殊含义,一些模式匹配操作也使用 # 字符:

1
2
3
4
5
6
7
8
# echo "this # not start comment"
this # not start comment

# echo this \# not start comment
this # not start comment

# echo this # start comment
this
1
2
3
# t="test one"
# echo ${t#test}
one

命令分隔符 ;

可以在一行编写多条命令,这些命令之间使用 ; 进行分隔。

1
2
3
4
5
6
7
8
9
# echo "1"; echo "2"
1
2
# echo "1";echo "2"
1
2
# echo "1" ; echo "2"
1
2

在同一行的 if then 之间也要使用使用 ; 分隔。在一行中编写复杂 if-elif-else 语句的语法为:

1
if list; then list; [ elif list; then list; ] ... [ else list; ] fi
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
#!/bin/bash

if [ true ]
then
echo "true"
else
echo "false"
fi

if [ true ]; then
echo "true"
else
echo "false"
fi

if [ true ]; then echo "true"; else echo "false"; fi


t=2
if [ $t -eq 0 ]
then
echo "0"
elif [ $t -eq 1 ]
then
echo "1"
else
echo "2"
fi

if [ $t -eq 0 ]; then echo "0"; elif [ $t -eq 1 ]; then echo "1"; else echo "2"; fi

case 语句终止符 ;;

1
2
3
4
5
6
7
8
#!/bin/bash

t=5
case "$t" in
1) echo "1" ;;
2) echo "2" ;;
*) echo "any" ;;
esac

另外,bash4 中引入了 ;&;;& 语法,用于增加 case 匹配的灵活性。

  • ;; 匹配某个分支并执行对应语句后,不会继续匹配
  • ;& 匹配某个分支并执行对应语句后,会继续执行下一个匹配分支的语句
  • ;;&匹配某个分支并执行对应语句后,会继续尝试匹配下一个分支

点字符 .

bash 中,点字符 . 的含义较多,下面将逐一介绍。

  • 等同于 source 命令,. filename 或者 source filename 都可以在当前 shell 环境中执行指定的脚本文件
1
2
3
4
5
6
7
8
# cat var_set.sh
t="tmp"

# echo $t

# . var_set.sh
# echo $t
tmp
  • 如果文件名 . 开头,则代表该文件是隐藏文件
1
2
3
4
# touch .hidden
# ls
# ls -a
. .. .hidden
  • 当作为目录路径时,. 表示当前目录,.. 表示上层目录
1
# cp ../var_set.sh .
  • 在正则表达式中,. 匹配任意的单个字符

双引号 " 与单引号 '

  • 用双引号来进行部分引用(大部分特殊字符都失去其特殊含义)
  • 用单引号来进行全引用(全部特殊字符都失去其特殊含义)

部分引用全引用 的详细区别将在后续文章介绍。

逗号符 ,

逗号符 , 可以串联一系列算术表达式,此时算术表达式依次执行,但是只返回最后一个算术表达式的结果。此时这个逗号符的功能类似于 C 语言中的逗号运算符。

1
2
3
# let "t = ((1 + 1, 2 + 2, 3 + 3))"
# echo "$t"
6

花括号扩展 中,逗号符也可以作为可选字符串的分隔符:

1
2
# echo /{,usr/,usr/local/}bin
/bin /usr/bin /usr/local/bin

在 Bash4 中,引入了通过 ,,, 来在参数替换中实现将 大写字符转换为小写形式;; 实现的最长匹配,, 则是最短匹配。

1
2
3
4
# v="VeryMixedVariable"
# skip pattern, default use ?
echo "${v,}"
echo "${v,,}"

转义字符 \

\ 转义字符可以对特殊字符进行转义,使其失去其特殊含义,而表示其字面含义。

1
2
3
4
# echo "$var"

# echo "\$var"
$var

/ 字符

/ 字符主要有两个含义:

  • / 作为路径分隔符使用
  • 在算术运算中表示 除法运算

反引号 `` 字符

反引号 command 字符被称为命令替换符,它可以获取反引号内 command 命令的执行结果,之后可以将该结果赋值给变量。

1
2
3
t=`echo 1`
# echo $t
1

冒号字符 :

: 字符在 shell 中表示 NOP 空操作,它与 shell 内建命令 true 有相同的效果。它本身也是 bash 的内建命令之一,返回值是 true(0)。

1
2
# :
# echo $?
  • 可以作为 while 循环的条件,实现无限循环:
1
while :; do echo "loop"; done
  • 可以在 if/then 中充当占位符:
1
if true; then :; else echo "false"; fi
  • 可以在二元操作中作为占位符
1
2
3
4
5
6
7
# ${username=`whoami`}
Command 'root' not found, did you mean:
......

# : ${username=`whoami`}
# echo $username
root
  • > 一起使用,在不改变文件权限的情况下清空文件,如果文件不存在将创建一个文件
1
# : > 1.txt
  • : 也可以作为域分隔符,例如在 $PATH/etc/password 中使用

取反操作符 !

取反操作符 ! 可以对命令返回的状态码进行取反。

1
2
3
4
5
6
# ! true
# echo $?
1

# if [ ! true ]; then echo "true"; else echo "false"; fi
false

* 字符

  • 当作为文件通配符使用时,* 可以匹配任意字符串
  • 当在正则表达式中使用时,* 可以匹配任意多个(包含 0 个)前一个字符
  • 在算术运算中,* 作为乘法使用,** 作为乘方使用
1
2
# ls *
11.sh 1.sh
1
2
3
4
# echo $(( 10*2 ))
20
# echo $(( 10**2 ))
100

? 字符

  • 当作为文件通配符使用时,? 可以匹配任意单个字符
  • 在参数替换表达式中,? 用来测试一个变量是否已经赋值
  • 在正则表达式中使用时,? 可以匹配 0-1 个前一个字符
  • (()) 中,可以使用类似于 C 语言的三元操作符 expr?expr:expr(注意中间没有空格)
1
2
3
4
5
# ls ?.sh
1.sh

# ls ??.sh
11.sh
1
2
3
# (( var = 10 > 9?10:9 ))
# echo $var
10
1
2
3
4
# t=1
# : ${t?}
# : ${p?}
-bash: p: parameter not set

$ 字符

$ 字符也有多个作用:

  • $ 最主要的作用是进行变量替换,即取出变量的内容
  • ${} 进行参数替换
  • $'...' 引用字符串扩展,这种方式可以将转义八进制或十六进制的值转换成 ASCII 或者 Unicode 字符
  • $*$@ 都是位置参数。
  • $? 退出状态变量,或者称为退出码。保存命令、函数或脚本自身的退出状态
  • $$ 进程ID变量,保存运行脚本的当前进程 ID
  • 在正则表达式中,$ 匹配行结尾
1
2
3
4
5
6
7
# t=1
# echo $t
1

# quote=$'\042'
# echo -e $quote
"

() 符号

(comamnd1; command2; ...) 可以认为起到命令组的作用,() 会产生一个子 shell 来执行括号中的一些列命令。由于父进程脚本不能访问子进程中所创建的变量,因此在子 shell 中定义的变量,在脚本的其他部分是不可见的。

1
2
3
# a=123; (a=321; echo $a); echo $a
321
123

() 也用于 bash 数组初始化:

1
Array=(element1 element2 element3)

{} 符号

bash 中,花括号的作用也非常多,下面逐一介绍。

首先是花括号扩展结构,此时用花括号包裹一系列选项,并在选项之间用逗号分隔开,bash 会将花括号中的选项依次展开,然后执行命令。注意此时花括号内不能有空格(除非进行转义):

1
2
3
4
# echo \"{These,words,are,quoted}\"
"These" "words" "are" "quoted"
# cat file{1,2,3} > combined
# cp file.{txt,backup}

bash 的第三版引入了 {a..z}{0..100} 等可扩展 花括号扩展语法

1
2
3
4
5
6
# echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z
# # echo {0..50}
0 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

# base64_charset=( {A..Z} {a..z} {0..9} + / = )

{} 可以创建代码块,又被称为内联组,它实际上创建了一个匿名函数。不同于标准的函数,代码块内的变量在脚本的其他部分依然是可见的。

1
2
3
4
# { local a; a=123; }
-bash: local: can only be used in a function
# a=123; { a=132; }; echo $a
132

代码块可以经由 I/O 重定向进行输入或者输出:

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

File=/etc/fstab

{
read line1
read line2
} < $File

echo "First line in $File is:"
echo $line1

echo "Second line in $File is:"
echo $line2

exit 0
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
#!/bin/bash

E_SUCCESS=0
E_NOARGS=65


if [ -z "$1" ]; then
echo "Usage: `basename $0` rpm-file"
exit $E_NOARGS
fi

{
echo
echo "Archive Description"
rpm -qpi $1
echo

echo
echo "Archive Listring"
rpm -qpl $1
echo

rpm -i --test $1
if [ $? -eq $E_SUCCESS ]; then
echo "$1 can be installed"
else
echo "$1 cannot be installed"
fi
echo
} > "$1.test"

echo "Result of RPM test in file $1.test"

exit $E_SUCCESS

注意,与 () 中的命令组不同的是,{} 中的代码块不会在一个新的 shell 子进程中运行。

{} 其他的作用包括:

  • 用于 for 循环语句
  • 文本占位符,在 xargs -i-i 选项等效于 -I {},即将 {} 作为本文占位符
1
ls . | xargs -i -t cp ./{} $1
  • 在 find 命令中作为路径名

[] 符号

[] 符号也有多种多种用途。首先是作为测试符号,[ 是 shell 内建测试命令。

1
[ : ] && echo "true"

在数组上下文中,[xxx] 表示用特定的偏移量 xxx 访问数组中元素:

1
2
3
4
5
# array=(1 2 3 4 5)
# echo ${array[0]}
1
# echo ${array[2]}
3

[] 也可以在正则表达式中表示字符范围

1
# cat sep.sh | grep -E "[0-9]"

$[...] 表示整数扩展符,可以在 $[] 中计算整数的算术表达式。

1
2
3
4
5
6
# a=1
# b=2
# echo $[$a + $b]
3
# echo $[$a * $b]
2

另外,[[]] 也key已进行测试,相比于 [],它更加灵活。[[]] 是 shell 的关键字。

1
2
# [[ : ]] && echo "true"
true

(()) 符号

可以在 (()) 中实现整数的运算:

1
2
3
4
5
6
7
# t=$(( 1+ 2 ))
# echo $t
3

# (( a= 10 ))
# echo $a
10

重定向

  • scriptname >filename 将脚本 scriptname 的标准输出重定向到文件 filename
  • scriptname &>filename 将脚本 scriptname 的标准输出和标准错误重定向到文件 filename 中。该方式与以下两种方式效果等价
    • scriptname >&filename
    • scriptname >filename 2>&1
  • scriptname >&2 将脚本 scriptname 的标准输出重定向到标准错误
  • scriptname >>filename 将脚本 scriptname 的标准输出以追加的方式添加到 filename 文件末尾
  • scriptname &>>filename 将脚本 scriptname 的标准输出和标准错误以追加的方式添加到 filename 文件末尾
  • << 可以在 here document 中实现重定向
  • >> 可以在 here string 中实现重定向

重定向在用于清除测试条件的输出时特别有用,例如测试一个命令是否存在:

1
2
3
# type command &>/dev/null
# echo $?
0
1
2
3
4
5
6
7
8
9
# command_test() { type $1 &>/dev/null; }

# command_test type
# echo $?
0

# command_test nosuch
# echo $?
1

在某些上下文中,< > 可以实现字符串或数字的比较。

1
2
# if [[ ab < ac ]]; then echo true; else echo false; fi
true

另外,<> 可以在正则表达式中表示单词的边界:

1
2
3
4
5
6
7
# echo "string str stringstr" | grep -o -E "str"
str
str
str
str
# echo "string str stringstr" | grep -o -E "\<str\>"
str

管道符 |

管道可以将上一个命令的输出作为下一个命令的输入,管道是一种可以将一系列命令连接起来的绝佳方式。

1
# ls *sh | sort | uniq

命令的输出同样可以通过管道输入到脚本中:

1
2
3
4
#!/bin/bash
# upper.sh

tr a-z A-Z
1
2
3
4
ls -l | bash upper.sh
TOTAL 40
-RW-R--R-- 1 ROOT ROOT 169 MAY 19 16:20 BRACE_IN.SH
-RW-R--R-- 1 ROOT ROOT 484 MAY 19 16:27 BRACE_OUT.SH

在管道中,每个进程的输出必须作为下一个进程的输入被正确读入,否则数据流会被阻塞,管道就无法按照预期工作。而且需要注意,**管道是在一个子进程中运行的,因此它并不能修改父进程脚本中的变量。

1
2
3
4
# variable="initial_value"
# echo "new_value" | read variable
# echo "$variable"
initial_value

如果管道中的任意一个命令意外中止了,管道将会提前中断,称之为 管道破裂。出现这种情况,系统将发送 SIGPIPE 信号。

||

|| 实现逻辑或操作,即测试结构中,任意一个测试条件为真,整个表达式为真,返回 0。

&&

&& 实现逻辑与操作,即测试结构中,所有测试条件为真,整个表达式才为真,返回 0。

& 符号

& 表示后台操作符,如果命令后带有 &,那么该命令将会在后台运行。

1
2
# sleep 10 &
[1] 2094420

在脚本中,命令甚至循环都可以在后台运行。

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

for i in `seq 1 10`
do
echo -n "$i "
done &

echo

for i in `seq 11 20`
do
echo -n "$i "
done

echo

- 符号

- 符号在 bash 中也有多种用途:

  • 选项前缀,在使用命令时,如果需要使用选项参数,选项前面要加 -。另外,-- 则通常表示命令的长选项前缀
1
#  ls -al
  • 可以作为某些操作符的前缀
1
2
# if [ 10 -gt 20 ]; then echo "true"; else echo "false"; fi
false
  • 在参数替换中,作为默认参数的前缀
1
2
3
4
5
6
7
# t=${INIT_VALUE:-10}
# echo $t
10
# INIT_VALUE=20
# t=${INIT_VALUE:-10}
# echo $t
20
  • - 也可以指代标准输入或者标准输出

例如下面的 cat - 表示从标准输入中进行读取

1
2
3
# cat -
one
one

如下命令将整个文件树从一个目录移动到另一个目录中:这里 tar cf - . 表示输出到标准输出中,这是一种在管道中使用面向文件工具作为过滤器的方法:

1
(cd /source/directory && tar cf - . ) | (cd /dest/directory && tar xpvf -)
  • 在 cd 命令,cd - 表示返回先前的工作目录
  • - 也可以用于减号运算符

= 符号

  • = 可以作为赋值操作符
  • 在某些情况下,也可以作为字符串比较操作符
1
# if [ "1" == "2" ]; then echo "true"; else echo "false"; fi

+ 符号

  • + 可以作为加号运算符
  • 在正则表达式中,+ 表示匹配一个或多个 前一个字符
  • 一些特定的指令和内建命令使用 + 启用特定的选项,使用 - 禁用特定的选项
  • 在参数替换中,${parameter:+word} 用来使用可选值。即如果 parameter 为空或者未设置,则不进行替换,否则会使用 word 的扩展
1
2
3
4
5
6
7
8
# INIT_VALUE=
# t=${INIT_VALUE:+123}
# echo $t

# INIT_VALUE=10
# t=${INIT_VALUE:+123}
# echo $t
123

~ 符号

  • ~ 可以表示当前用户的主目录,相当于 $HOME 变量,~xxx 则表示用户 xxx 的主目录
  • ~+ 表示当前工作目录,相当于 $PWD 变量
  • ~- 表示之前的工作目录,相当于 $OLDPWD 变量
  • ~ 也可以作为正则表达式中的行首匹配符

^ 符号

  • ^ 在正则表达式中匹配行首
  • ^ 以及 ^^ 在参数替换中,可以实现大写转换。^^ 实现了最长匹配,^ 则是最短匹配

控制字符

控制字符可以改变终端或文件显示的一些行为。这篇文章详细总结了 bash 的一些控制字符。

空白符

空白符包含空格、制表符、换行符或它们的任意组合。在某些场景,比如变量赋值时,空白符不应该出现,否则会造成语法错误。特殊变量 $IFS 可以作为一些特定命令的输入域(field)分隔符,默认值为空白符。如果想要在字符串或者变量中保留空白符,需要引用。

UNIX 可以使用 POSIX 字符类 [:space:] 来寻找和操作空白符。

Reference