`

Bash的陷阱

 
阅读更多
原帖:
感谢fcicq,他的new 30 days系列为我们带来了不少好文章。
今天想分析的是这篇Bash Pitfalls, 介绍了一些bash编程中的经典错误。fcicq说可能不适合初学者,而我认为,正是bash编程的初学者才应该好好阅读一下这篇文章。
下面就逐个分析一下这篇文章中提到的错误。不是完全的翻译,有些没用的话就略过了,有些地方则加了些注释。
 

1. for i in `ls *.mp3`

常见的错误写法:
for i in `ls *.mp3`; do     # Wrong!
为什么错误呢?因为for...in语句是按照空白来分词的,包含空格的文件名会被拆成多个词。如遇到 01 - Don't Eat the Yellow Snow.mp3 时,i的值会依次取 01,-,Don't,等等。
用双引号也不行,它会将ls *.mp3的全部结果当成一个词来处理。
for i in "`ls *.mp3`"; do   # Wrong!
正确的写法是
for i in *.mp3; do

2. cp $file $target

这句话基本上正确,但同样有空格分词的问题。所以应当用双引号:
cp "$file" "$target"
但是如果凑巧文件名以 - 开头,这个文件名会被 cp 当作命令行选项来处理,依旧很头疼。可以试试下面这个。
cp -- "$file" "$target"
运气差点的再碰上一个不支持 -- 选项的系统,那只能用下面的方法了:使每个变量都以目录开头。
for i in ./*.mp3; do
  cp "$i" /target
  ...

3. [ $foo = "bar" ]

当$foo为空时,上面的命令就变成了
[ = "bar" ]
类似地,当$foo包含空格时:
[ multiple words here = "bar" ]
两者都会出错。所以应当用双引号将变量括起来:
[ "$foo" = bar ]      # 几乎完美了。
但是!当$foo以 - 开头时依然会有问题。在较新的bash中你可以用下面的方法来代替,[[ 关键字能正确处理空白、空格、带横线等问题。
[[ $foo = bar ]]      # 正确
旧版本bash中可以用这个技巧(虽然不好理解):
[ x"$foo" = xbar ]    # 正确
或者干脆把变量放在右边,因为 [ 命令的等号右边即使是空白或是横线开头,依然能正常工作。(Java编程风格中也有类似的做法,虽然目的不一样。)
[ bar = "$foo" ]      # 正确

4. cd `dirname "$f"`

同样也存在空格问题。那么加上引号吧。
cd "`dirname "$f"`"
问题来了,是不是写错了?由于双引号的嵌套,你会认为`dirname 是第一个字符串,`是第二个字符串。错了,那是C语言。在bash中,命令替换(反引号``中的内容)里面的双引号会被正确地匹配到一起,不用特意去转义。
$()语法也相同,如下面的写法是正确的。
cd "$(dirname "$f")"

5. [ "$foo" = bar && "$bar" = foo ]

[ 中不能使用 && 符号!因为 [ 的实质是 test 命令,&& 会把这一行分成两个命令的。应该用以下的写法。
[ bar = "$foo" -a foo = "$bar" ]       # Right!
[ bar = "$foo" ] && [ foo = "$bar" ]   # Also right!
[[ $foo = bar && $bar = foo ]]         # Also right!

6. [[ $foo > 7 ]]

很可惜 [[ 只适用于字符串,不能做数字比较。数字比较应当这样写:
(( $foo > 7 ))
或者用经典的写法:
[ $foo -gt 7 ]
但上述使用 -gt 的写法有个问题,那就是当 $foo 不是数字时就会出错。你必须做好类型检验。
这样写也行。
[[ $foo -gt 7 ]]

7. grep foo bar | while read line; do ((count++) ); done

由于格式问题,标题中我多加了一个空格。实际的代码应该是这样的:
grep foo bar | while read line; do ((count++)); done         # 错误!
这行代码数出bar文件中包含foo的行数,虽然很麻烦(等同于grep -c foo bar或者 grep foo bar | wc -l)。乍一看没有问题,但执行之后count变量却没有值。因为管道中的每个命令都放到一个新的子shell中执行,所以子shell中定义的count变量无法传递出来。

8. if [grep foo myfile]

初学者常犯的错误,就是将 if 语句后面的 [ 当作if语法的一部分。实际上它是一个命令,相当于 test 命令,而不是 if 语法。这一点C程序员特别应当注意。
if 会将 if 到 then 之间的所有命令的返回值当作判断条件。因此上面的语句应当写成
if grep foo myfile > /dev/null; then

9. if [bar="$foo"]

同样,[ 是个命令,不是 if 语句的一部分,所以要注意空格。
if [ bar = "$foo" ]

10. if [ [ a = b ] && [ c = d ] ]

同样的问题,[ 不是 if 语句的一部分,当然也不是改变逻辑判断的括号。它是一个命令。可能C程序员比较容易犯这个错误?
if [ a = b ] && [ c = d ]        # 正确

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

不能在同一条管道操作中同时读写一个文件。根据管道的实现方式,file要么被截断成0字节,要么会无限增长直到填满整个硬盘。如果想改变原文件的内容,只能先将输出写到临时文件中再用mv命令。
sed 's/foo/bar/g' file > tmpfile && mv tmpfile file

12. echo $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
所以即使是echo也别忘记给变量加引号。

13. $foo=bar

变量赋值时无需加 $ 符号——这不是Perl或PHP。

14. foo = bar

变量赋值时等号两侧不能加空格——这不是C语言。

15. echo <<EOF

here document是个好东西,它可以输出成段的文字而不用加引号也不用考虑换行符的处理问题。不过here document输出时应当使用cat而不是echo。
# This is wrong:
echo <<EOF
Hello world
EOF
# This is right:
cat <<EOF
Hello world
EOF

16. su -c 'some command'

原文的意思是,这条基本上正确,但使用者的目的是要将 -c 'some command' 传给shell。而恰好 su 有个 -c 参数,所以su 只会将 'some command' 传给shell。所以应该这么写:
su root -c 'some command'
但是在我的平台上,man su 的结果中关于 -c 的解释为
-c, --commmand=COMMAND
            pass a single COMMAND to the shell with -c
也就是说,-c 'some command' 同样会将 -c 'some command' 这样一个字符串传递给shell,和这条就不符合了。不管怎样,先将这一条写在这里吧。

17. cd /foo; bar

cd有可能会出错,出错后 bar 命令就会在你预想不到的目录里执行了。所以一定要记得判断cd的返回值。
cd /foo && bar
如果你要根据cd的返回值执行多条命令,可以用 ||。
cd /foo || exit 1;
bar
baz
关于目录的一点题外话,假设你要在shell程序中频繁变换工作目录,如下面的代码:
find ... -type d | while read subdir; do
  cd "$subdir" && whatever && ... && cd -
done
不如这样写:
find ... -type d | while read subdir; do
  (cd "$subdir" && whatever && ...)
done
括号会强制启动一个子shell,这样在这个子shell中改变工作目录不会影响父shell(执行这个脚本的shell),就可以省掉cd - 的麻烦。
你也可以灵活运用 pushd、popd、dirs 等命令来控制工作目录。

18. [ bar == "$foo" ]

[ 命令中不能用 ==,应当写成
[ bar = "$foo" ] && echo yes
[[ bar == $foo ]] && echo yes

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

& 后面不应该再放 ; ,因为 & 已经起到了语句分隔符的作用,无需再用;。
for i in {1..10}; do ./something & done

20. cmd1 && cmd2 || cmd3

有人喜欢用这种格式来代替 if...then...else 结构,但其实并不完全一样。如果cmd2返回一个非真值,那么cmd3则会被执行。所以还是老老实实地用 if cmd1; then cmd2; else cmd3 为好。

21. UTF-8的BOM(Byte-Order Marks)问题

UTF-8编码可以在文件开头用几个字节来表示编码的字节顺序,这几个字节称为BOM。但Unix格式的UTF-8编码不需要BOM。多余的BOM会影响shell解析,特别是开头的 #!/bin/sh 之类的指令将会无法识别。
MS-DOS格式的换行符(CRLF)也存在同样的问题。如果你将shell程序保存成DOS格式,脚本就无法执行了。
$ ./dos
-bash: ./dos: /bin/sh^M: bad interpreter: No such file or directory

22. echo "Hello World!"

交互执行这条命令会产生以下的错误:
-bash: !": event not found
因为 !" 会被当作命令行历史替换的符号来处理。不过在shell脚本中没有这样的问题。
不幸的是,你无法使用转义符来转义!:
$ echo "hi\!"
hi\!
解决方案之一,使用单引号,即
$ echo 'Hello, world!'
如果你必须使用双引号,可以试试通过 set +H 来取消命令行历史替换。
set +H
echo "Hello, world!"

23. for arg in $*

$*表示所有命令行参数,所以你可能想这样写来逐个处理参数,但参数中包含空格时就会失败。如:
#!/bin/bash
# Incorrect version
for x in $*; do
  echo "parameter: '$x'"
done
$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg'
parameter: '1'
parameter: 'arg2'
parameter: 'arg3'
正确的方法是使用 "$@"。
#!/bin/bash
# Correct version
for x in "$@"; do
  echo "parameter: '$x'"
done
$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg 1'
parameter: 'arg2'
parameter: 'arg3'
在 bash 的手册中对 $* 和 $@ 的说明如下:
*    Expands to the positional parameters, starting from one.  
     When the expansion occurs within double quotes, it 
     expands to a single word with the value of each parameter 
     separated by the first character of the IFS special variable.  
     That is, "$*" is equivalent to "$1c$2c...",
@    Expands to the positional parameters, starting from one. 
     When the expansion occurs within double quotes, each 
     parameter expands to a separate word.  That  is,  "$@"  
     is equivalent to "$1" "$2" ...  
可见,不加引号时 $* 和 $@ 是相同的,但"$*" 会被扩展成一个字符串,而 "$@" 会被扩展成每一个参数。

24. function foo()

在bash中没有问题,但其他shell中有可能出错。不要把 function 和括号一起使用。最为保险的做法是使用括号,即

 

foo() {
  ...
}
分享到:
评论

相关推荐

    bash高级编程 advanced bash script

    7. **陷阱(Trap)与信号处理**:理解如何捕获和处理系统信号,以优雅地结束脚本或在特定事件上执行清理工作。 8. **子shell与进程管理**:学习如何启动子shell,以及如何使用wait命令同步进程。 9. **命令别名与...

    高级bash shell手册

    编写高效、可维护的Bash脚本需要遵循一定的最佳实践,如注释、错误处理、适当使用变量和函数,以及避免命令替换和引用陷阱。 通过深入阅读《高级Bash Shell手册》并实践其中的示例,用户可以掌握Bash的高级特性和...

    Pure Bash Bible 中文版 PDF

    - **三元运算符**,**陷阱(trap)**,**忽略终端中断**,**窗口大小变化响应**,**命令前缀和后缀**:Bash提供了控制脚本执行流程的多种方式,如`if`、`case`、`trap`和函数。 10. **性能优化**: - **禁用...

    Bash脚本编写中遇到的问题

    在Bash脚本编写过程中,开发者常常会遇到各种挑战,这些问题涵盖了语法错误、逻辑陷阱、环境依赖、变量处理等多个方面。下面将详细讨论这些常见问题及其解决方案。 首先,语法错误是初学者最常见的问题之一。Bash...

    BASH 中文文档

    - **注意事项**:提醒读者注意常见的错误和陷阱。 ##### 4. 特别说明 - **制作环境**:文档是在特定的环境下编辑完成的,包括使用的软件和工具。 - **出版情况**:文档未完全翻译完成前已有许多读者请求印刷版,...

    高级Bash 脚本编程指南 PDF

    综上所述,**高级Bash 脚本编程指南**涵盖了这些核心概念,并深入讨论了更多高级话题,如数组、函数、信号处理、进程控制、陷阱、作业控制等。通过学习这本书,你可以提升自己的Bash脚本技能,更好地利用Linux或Unix...

    高级Bash脚本编程指南(书中代码)

    在实际编程中,我们还会遇到错误处理和陷阱处理。`trap`命令可以设置在特定信号发生时执行的命令,例如在脚本退出时清理资源: ```bash trap 'rm -f temp_file' EXIT # 在脚本退出时删除temp_file ``` 文件测试...

    Linux Bash脚本编程大全_超清版

    通过阅读《Linux Bash脚本编程大全》这本书,你不仅可以掌握这些基本概念,还能了解到更多高级主题,如进程控制、信号处理、陷阱(traps)和子shell,以及如何编写更复杂的脚本来管理系统和服务。这本书可能还包含了...

    BASH官方文档BASH官方文档

    **BASH 官方文档概述** BASH(Bourne-Again SHell)是GNU项目下的一个命令解释器,它是Linux及类Unix系统中的默认shell。它兼容传统的Bourne shell语法,并扩展了许多功能,使得它更加灵活和强大。这份文档包含了...

    高级Bash脚本编程指南.pdf

    错误处理可以通过陷阱(trap)指令来实现,当特定信号发生时执行预定的清理工作。 **5. 函数库和模块化** 通过source命令,我们可以导入其他脚本作为函数库,实现代码的模块化。这有助于保持脚本整洁,提高代码...

    Bash Quick Reference bash命令快速指南

    Signals and Traps(信号与陷阱) - **定义**:用于响应系统信号。 - **示例**:`trap 'echo "Received SIGTERM"' SIGTERM`。 ##### 27. Special Characters(特殊字符) - **定义**:bash中的特殊字符及其含义。 ...

    Advanced Bash−Scripting Guide

    10. **陷阱和信号处理**:学习如何捕捉和处理脚本运行过程中的信号事件。 11. **子shell**:理解如何创建和使用子shell,以及它们在脚本中的应用。 12. **别名和壳内函数**:创建别名以简化常用命令,以及创建壳内...

    高级bash脚本指导

    - Shell编程中可能遇到的常见问题和陷阱。 9. **参考资料和更新情况** - 原书的链接地址。 - 早期翻译版本和最新翻译版本的信息。 - 翻译进度和翻译社区的联系地址。 以上列出的内容和知识点都是对高级Bash...

    高级Bash 脚本编程指南.pdf

    Bash变量是未类型的,这意味着它们可以存储任何类型的数据,但同时也需要程序员注意类型相关的陷阱。 #### 3. 测试与条件判断 - **Test结构**:展示了如何使用test命令进行条件判断,包括文件属性测试、数值比较和...

    Advanced Bash-Scripting Guide(中译版)

    10. **脚本调试和错误处理**:了解如何设置陷阱(trap)、调试脚本和捕获运行时错误。 11. **函数库和脚本模块化**:创建可重用的脚本片段,以及如何组织和导入这些库。 12. **进程控制**:启动、停止和管理后台...

    Advanced Bash-Scripting Guide 中/英版本

    12. **陷阱(Trap)**:允许在接收到特定信号时执行命令,实现优雅的退出或清理工作。 13. **函数和子Shell**:通过子Shell可以在脚本内部创建一个独立的环境,不影响主Shell的状态。 14. **位置参数**:在脚本中...

Global site tag (gtag.js) - Google Analytics