Bash脚本解决一种问题的多种方法

2019年4月17日

字符串转义

字符串里有引号怎么办

echo 'Today'"'"'s "program"'
Today's "program"
echo Today"'"s '"program"'
Today's "program"
echo Today\'s '"program"'
Today's "program"
echo Today\'s \"program\"
Today's "program"

单引号里没有特殊字符。

字符串里带有特殊字符,如换行符

x=$'\x0d\x0a'
echo ${#x}
2
echo "$x"

这里把变量x设为回车和换行这两个字符,${#x}计算x的长度,打印出2,符合预期。echo打印出空行,符合预期。

常常见到网友建议使用echo -e,-e是echo自身再次对传入的字符串解析,不是Bash自带的解析。

x="\x0d\x0a"
echo ${#x}
8
echo "$x"
\x0d\x0a
echo -e "$x"

可见”\x0d\x0a”并不自带解析特殊字符的功能。

但是$''因为是单引号,所以不能开展变量或运行代码。

使用变量

var=apple
echo $var
echo ${var}
echo ${var}s

当变量后不紧跟字符时,${}结构认为是不必要的。一般情况下应使用$var保持简洁性。

对一个命令修改环境变量,并还原

设置变量并还原的一般方法是把变量的原始值用另一个变量存起来,如《Linux命令行与shell脚本编程大全》13.1.5 更改字段分隔符所做的。但如果只对一条命令应用新的变量值,我们有另外的办法。

IFS=- read -ra parts <<< foo-bar-baz
echo ${parts[1]}
bar

[1]

这段代码先把bash分隔符设置为-,然后read把here字符串作为标准输入读取,读来的参数用IFS参数分割,最后存到变了parts里。

我们知道如果两条命令放在同一行执行,要在第一个命令的结尾加分号(;),但是这里不需要。这为什么是合法的语法呢?POSIX 2018规范2.9.1 Simple Commands说道,“A “simple command” is a sequence of optional variable assignments and redirections, … The words that are not variable assignments or redirections shall be expanded. If any fields remain following their expansion, the first field shall be considered the command name and remaining fields are the arguments for the command.”

所以,simple command是一个或多个变量赋值,后面可以跟着一个命令。既然这样你可能会尝试a=b echo $a,发现没有输出,似乎错了。原因是$a变量扩展发生在执行该行之前,那个时候变量a还没有值。可以改为a=b bash - c 'echo $a'或者a=b eval 'echo $a',这样就正确输出b了。

bash -c方法创建新进程,速度慢。eval比较快。

time a=b eval 'echo $a'
b

real	0m0.000s
user	0m0.000s
sys	0m0.000s
time a=b bash -c 'echo $a'
b

real	0m0.012s
user	0m0.001s
sys	0m0.005s

由此可见eval比bash -c方法快。time是bash内部命令。

命令替换

命令替换意思是把命令的输出赋值给变量。

d=`date`
d=$(date)

命令替换可以嵌套。[2]

$()符合POSIX标准,``只是为了兼容Bourne shell。[3]

管道与输入重定向

管道运算符(|)的右边必须是命令,把左边命令的标准输出作为右边命令的标准输入。

echo "hello" | wc
      1       1       6

重定向运算符的右算符可以由命令算出,该语法叫做进程替换(Process subsitution)。Bash有两种语法:<(command), >(command);Zsh还支持=(command)[4]

paste <(cut -d: -f1 /etc/passwd) <(cut -d: -f5 /etc/passwd)

如果标准输入的内容是由表达式算出的,还可以用here文档重定向

$ wc << HEREDOC
this is a test
May the force be with you
HEREDOC
 2 10 41

$ wc << helloworld
this is a test
May the force be with you
helloworld
 2 10 41

按照Bash Reference Manual和POSIX 2018,<<的右边指定一个单词,here文档以下一行开始,以下一个仅包含该单词的行结尾。heredoc里面可以运行命令。<<后面的单词叫做heredoc关键字

$ cat <<helloworld
$(echo May the force be with you)
helloworld
May the force be with you

但是如果heredoc关键字被引号引起来了,就不能执行命令了。

$ cat <<'heredoc'
$(echo May the force be with you)
heredoc
$(echo May the force be with you)

根据定义,heredoc关键字后面可以有其他文字。

$ cat <<heredoc                      | wc
> $(echo May the force be with you)
> heredoc
      1       6      26

可以看成如下图所示,左边绿色的一包整体重定向到了wc文件。

cat <<heredoc
$(echo May the force be with you)
heredoc
| wc

加上管道符号本来就可以在行尾,以下的结构也是正确的。

cat << heredoc |
$(echo May the force be with you)
heredoc
wc
注:

cat << heredoc >output
$(echo May the force be with you)
heredoc

的语法是正确的,但我们不能改为

cat << heredoc >
$(echo May the force be with you)
heredoc
output

因为>本来就不能在行尾。

如果想要把here文档一行里面写完,<<运算符就会出错。

$ wc << HEREDOC this is a test HEREDOC
> bash: warning: here-document at line 9 delimited by end-of-file (wanted `HEREDOC')
wc: this: No such file or directory
wc: is: No such file or directory
2 2 8 a
wc: test: No such file or directory
wc: HEREDOC: No such file or directory
2 2 8 total

如果here文档需要一行写完,可以用用<<<here字符串运算符。here字符串运算符的右边与管道运算符一样,接受的是命令。

$ wc << gqqnbig
> $(echo "this is a test")
> gqqnbig
 1  4 15
$ wc <<< $(echo this is a test)
 1  4 15
$ echo "this is a test" |wc
      1       4      15

以上是为一个命令提供标准输入的三个方法。

算术运算

echo $((2+3))
5
echo $((2*3))
6
echo $(expr 2+3)
2+3
echo $(expr 2\*3)
2*3
echo $(expr 2 \* 3)
6
echo `expr 2\*3`
2*3
whereis expr
expr: /usr/bin/expr /usr/share/man/man1/expr.1.gz

算术运算推荐使用语法$((...))。expr是外部命令,它的存在仅仅是为了兼容Bourne shell,并且有一些坑,如需要对*转义、运算符两边要有空格等。

注:bash不支持浮点运算。可以用外部命令bc,如echo "2*1.24"|bc

$[1+2]是老的语法,不应该使用。

条件判断(if)

条件判断的基本形式如下

$ if pwd; then
>  echo true
>else
>  echo false
>fi
/home/gqqnbig
true

$ if no-such-command; then echo true; else echo false; fi
no-such-command: command not found
false

if只能测试命令的返回值!那怎么进行逻辑判断呢?Bash提供了内部命令test和[,它们是同一个命令。此外,在/usr/bin往往还有test和[文件,它们可被bash作为外部命令使用。外部命令test和[是由同一份源代码通过条件编译符号LBRACKET编出的。内部命令test不支持浮点值。

内部命令test的用法可参见Bash Conditional Expressions

复合条件测试

if [ .. ] && [ .. ]; then
if [ .. ] || [ .. ]; then
if [ ... -a ...]; then # and
if [ ... -o ...]; then # or

https://www.gnu.org/software/bash/manual/bash.html#Bourne-Shell-Builtins

双小括号语法

双小括号语法类似$((..)),支持高级数学表达式,包括算术运算、比较、逻辑运算。双小括号语法是Bash的独家语法。

双方括号语法

双方括号语法提供字符串比较的高级特性。双方括号语法是Bash的独家语法。

运行脚本

运行脚本可以用source内部命令,也可以用.

source是Bourne Shell的内部命令,.是Bourne Again Shell(Bash)新增的内部命令。推荐用.,因为简洁好打。source把脚本在当前shell执行。脚本无法忽略信号,因为信号由当前Shell设置了。

#!/bin/bash
# Testing signal trapping
trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT
echo This is a test script
count=1
while [ $count -le 10 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
echo "This is the end of the test script"
test1.sh

[5]

$ . test1.sh "hello"
This is a test script
Loop #1
Loop #2
Loop #3
Loop #4
^C Sorry! I have trapped Ctrl-C

发现如果用. test1.sh 运行,按下ctrl-c后脚本就停止了。

$ ./test.sh
This is a test script
Loop #1
Loop #2
^C Sorry! I have trapped Ctrl-C
Loop #3
Loop #4
Loop #5
Loop #6
^C Sorry! I have trapped Ctrl-C
Loop #7
Loop #8
Loop #9
Loop #10
This is the end of the test script

发现如果用./test1.sh 运行,按下ctrl-c后脚本继续运行。

如果脚本第一行没有#!/bin/bash,就不行自己运行,要用bash test1.sh。中文圈比较少提到这一行的正式名称,它叫做shebang,

参考资料

  1. Peteris Krumins. bash最佳实践2:使用字符串. . 2014-09-10 [2019-04-18].
  2. . Command Substitution. . [2019-04-16].
  3. . Appendix B Major Differences From The Bourne Shell. . [2019-04-16].
  4. Oliver Kiddle, Jerry Peek, and Peter Stephenson; . From Bash to Z Shell: Conquering the Command Line. . 2004-11, (): 270 [].
  5. Richard Blum, Christine Bresnahan; . 第16章控制脚本. Linux命令行与shell脚本编程大全 第三版. 2016, (): 334 [].