36

第二节:Bash编程易犯的错误

 5 years ago
source link: https://www.linuxprobe.com/bash-programming-2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇文章参见 第一节:Bash编程易犯的错误。

13. cat file | sed s/foo/bar/ > file

你不应该在一个管道中,从一个文件读的同时,再往相同的文件里面写,这样的后果是未知的。

你可以为此创建一个临时文件,这种做法比较安全可靠:

# sed 's/foo/bar/g' file > tmpfile && mv tmpfile file

或者,如果你用得是 GNU Sed 4.x 以上的版本,可以使用-i 选项即时修改文件的内容:

# sed -i 's/foo/bar/g' file

14. echo $foo

这种看似无害的命令往往会给初学者千万极大的困扰,他们会怀疑是不是因为 $foo 变量的值是错误的。事实却是因为,$foo 变量在这里没有使用双引号,所以在解析的时候会进行单词拆分和文件名展开,最终导致执行结果与预期大相径庭:

msg="Please enter a file name of the form *.zip"
echo $msg

这里整句话会被拆分成单词,然后其中的通配符会被展开,例如*.zip。当你的用户看到如下的结果时,他们会怎样想:


Please enter a file name of the form freenfss.zip lw35nfss.zip

再举一个例子(假设当前目录下有以 .zip 结尾的文件):

var="*.zip"   # var 包括一个星号,一个点号和 zip
echo "$var"   # 输出 *.zip
echo $var     # 输出所有以 .zip 结尾的文件

实际上,这里使用 echo 命令并不是绝对的安全。例如,当变量的值包含-n时,echo 会认为它是一个合法的选项而不是要输出的内容(当然如果你能够保证不会有-n 这种值,可以放心地使用 echo 命令)。

完全可靠的打印变量值的方法是使用 printf:

printf "%s\n" "$foo"

15. $foo=bar

略过

16. foo = bar

当赋值时,等号两边是不允许出现空格的,这同 C 语言不一样。当你写下 foo = bar 时,Shell 会将该命令解析成三个单词,然后第一个单词 foo 会被认为是一个命令,后面的内容会被当作命令参数。

同样地,下面的写法也是错误的:

foo= bar    # WRONG!
foo =bar    # WRONG!
$foo = bar; # COMPLETELY WRONG!

正确的写法应该是这样的:

foo=bar     # Right.
foo="bar"   # more Right.

17. echo This is wrong:

Hello world

How's it going?

正确的方法是,使用 cat 命令来完成:

Hello world

How's it going?

或者可以使用双引号,它也可以跨越多行,而且因为 echo 命令是内置命令,相同情况下它会更加高效:

echo "Hello world
How's it going?"

18. su -c 'some command'

这种写法“几乎”是正确的。问题是,在许多平台上,su 支持 -c 参数,但是它不一定是你认为的。比如,在 OpenBSD 平台上你这样执行会出错:

$ su -c 'echo hello'
su: only the superuser may specify a login class

在这里,-c是用于指定login-class。如果你想要传递 -c 'some command' 给 shell,最好在之前显示地指定 username:

$ su root -c 'some command' # Now it's right.

19. cd /foo; bar

如果你不检查 cd 命令执行是否成功,你可以会在错误的目录下执行 bar 命令,这有可能会带来灾难,比如 bar 命令是 rm -rf *。

你必须经常检查 cd 命令执行是否有错误,简单的做法是:

cd /foo && bar

如果在 cd 命令后有多个命令,你可以选择这样写:

cd /foo || exit 1
bar
baz
bat ... # Lots of commands.

出错时,cd 命令会报告无法改变当前目录,同时将错误消息输出到标准错误,例如"bash: cd: /foo: No such file or directory"。如果你想要在标准输出同时输出自定义的错误提示,可以使用复合命令(command grouping):

cd /net || { echo "Can't read /net. make sure you've logged in to the Samba network, and try again."; exit 1; }
do_stuff
more_stuff

注意,在{号和 echo 之间需要有一个空格,同时}之前要加上分号。

顺便提一下,如果你要在脚本里频繁改变当前目录,可以看看 pushd/popd/dirs 等命令,可能你在代码里面写的 cd/pwd 命令都是没有必要的。

说到这,比较下下面两种写法:

find ... -type d -print0 | while IFS= read -r -d '' subdir; do
   here=$PWD
   cd "$subdir" && whatever && ...
   cd "$here"
done
find ... -type d -print0 | while IFS= read -r -d '' subdir; do
   (cd "$subdir" || exit; whatever; ...)
done

下面的写法,在循环中 fork 了一个子 shell 进程,子 shell 进程中的 cd 命令仅会影响当前 shell的环境变量,所以父进程中的环境命令不会被改变;当执行到下一次循环时,无论之前的 cd 命令有没有执行成功,我们会回到相同的当前目录。这种写法相较前面的用法,代码更加干净。

20. [ bar == "$foo" ]

正确的用法:

[ bar = "$foo" ] && echo yes
[[ bar == $foo ]] && echo yes

21. for i in {1..10}; do ./something &; done

你不应该在&后面添加分号,删除它:

for i in {1..10}; do ./something & done

或者改成多行的形式:

for i in {1..10}; do
    ./something &
done

&和分号一样也可以用作命令终止符,所以你不要将两个混用到一起。一般情况下,分号可以被换行符替换,但是不是所有的换行符都可以用分号替换。

22. cmd1 && cmd2 || cmd3

有些人喜欢把&&和||作为if...then...else...fi 的简写语法,在多数情况下,这种写法没有问题。例如:

[[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."

但是,这种结构并不是在所有情况下都完全等价于 if...fi 语法。这是因为在&&后面的命令执行结束时也会生成一个返回码,如果该返回码不是真值(0代表 true),||后面的命令也会执行,例如:

i=0
true && ((i++)) || ((i--))
echo $i # 输出 0

看起来上面的结果应该是返回1,但是结果却是输出0,为什么呢?原因是这里 i++ 和 i-- 都执行了一遍。

其中,((i++))命令执行算术运算,表达式计算的结果为0。这里和 C 语言一样,表达式的结果为0被认为是 false。所以当 i=0 的时候,((i++))命令执行的返回码为1(false),从而会执行接下来的((i--))命令。

如果我们在这里使用前缀自增运算符的话,返回的结果恰恰为1,因为((++i))执行的返回码是0(true):

i=0
true && (( ++i )) || (( --i ))
echo $i # Prints 1

不过在你无法保证 y 的执行结果是,绝对不要依靠 x && y || z这种写法。上面这种巧合,在 i 初始化为-1时也会有问题。

如果你喜欢代码更加安全健壮,建议使用 if...fi 语法:

i=0
if true; then
   ((i++))
else
   ((i--))
fi

echo $i # 输出 1

23. echo "Hello World!"

在交互式的 Shell 环境下,你执行以上命令会遇到下面的错误:

bash: !": event not found

这是因为,在默认的交互式 Shell 环境下,Bash 发现感叹号时会执行历史命令展开。在 Shell 脚本中,这种行为是被禁止的,所以不会发生错误。

不幸地是,你认为明显正确地修复方法,也不能工作,你会发现反斜杠并没有转义感叹号:

# echo "hi\!"
hi\!

最简单地方法是禁用 histexpand 选项,你可以通过 set +H 或者 set +o histexpand 命令来完成。

下面四种写法都可以解决:

# 1. 使用单引号

echo 'Hello World!'

# 2. 禁用 histexpand 选项

set +H
echo "Hello World!"

# 3. 重置 histchars

histchars=

# 4. 控制 shell 展开的顺序,命令行历史展开是在单词拆分之前执行的

# 参见:Bash man 手册的History Expansion一节

exmark='!'

echo "Hello, world$exmark"

由于篇幅限制,本系列文章会分成多篇文章,下一篇参见第节:Bash编程易犯的错误。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK