shell教程
OutOfMemory.CN技术专栏-> shell-> shell教程-> 第31章. Gotchas

第31章. Gotchas

Turandot:Glienigmisonotre,lamorteuna!Caleph:No,no!Glienigmisonotre,unalavita!Puccini将保留字和字符声明为变量名.case=value0#引发错误.23skidoo=value1#也会有错误.#以数字开头的
 

Turandot: Gli enigmi sono tre, la morte una!

Caleph: No, no! Gli enigmi sono tre, una la vita!

 Puccini

将保留字和字符声明为变量名.

case=value0       # 引发错误.
23skidoo=value1   # 也会有错误.
# 以数字开头的变量名是由shell保留使用的.
# 试试 _23skidoo=value1. 用下划线开头的变量名是允许的.

# 但是 . . .   仅使用下划线来用做变量名也是不行的.
_=25
echo $_           # $_ 是一个特殊的变量,被设置为最后命令的最后一个参数.

xyz((!*=value2    # 引起严重的错误.
# 在第三版的Bash, 标点不能在变量名中出现.

用连字符或其他保留字符当做变量名(或函数名).

var-1=23
# 用 'var_1' 代替.

function-whatever ()   # 错误
# 用 'function_whatever ()' 代替.


# 在第三版的 Bash, 标点不能在函数名中使用.
function.whatever ()   # 错误
# 用 'functionWhatever ()' 代替.

给变量和函数使用相同的名字. 这会使脚本不能分辨两者.

do_something ()
{
  echo "This function does something with \"$1\"."
}

do_something=do_something

do_something do_something

# 这些都是合法的,但让人混淆.

不适当地使用宽白符(whitespace). 和其它的编程语言相比,Bash非常讲究空白字符的使用.

var1 = 23   # 'var1=23' 正确.
# 上面一行,Bash试图执行命令"var1"
# 并且它的参数是"="和"23".

let c = $a - $b   # 'let c=$a-$b' 或 'let "c = $a - $b"'是正确的.

if [ $a -le 5]    # if [ $a -le 5 ]   是正确的.
# if [ "$a" -le 5 ]   会更好.
# [[ $a -le 5 ]] 也可以.

未初始化的变量(指赋值前的变量)被认为是NULL值的,而不是有零值.

#!/bin/bash

echo "uninitialized_var = $uninitialized_var"
# uninitialized_var =

混淆测试里的=-eq 操作符. 请记住, = 是比较字符变量而 -eq 比较整数.

if [ "$a" = 273 ]      # $a 是一个整数还是一个字符串?
if [ "$a" -eq 273 ]    # 如果$a 是一个整数,用这个表达式.

# 有时你能混用 -eq 和 = 而没有不利的结果.
# 然而 . . .


a=273.0   # 不是一个整数.

if [ "$a" = 273 ]
then
  echo "Comparison works."
else
  echo "Comparison does not work."
fi    # Comparison does not work.

# 与   a=" 273"  和 a="0273" 一样.


# 同样, 问题仍然是试图对非整数值使用 "-eq" 测试.

if [ "$a" -eq 273.0 ]
then
  echo "a = $a"
fi  # 因错误信息而中断.
# test.sh: [: 273.0: integer expression expected

误用字符串比较操作符.


例子 31-1. 数字和字符串比较是不相等同的

#!/bin/bash
# bad-op.sh: 在整数比较中使用字符串比较.

echo
number=1

# 下面的 "while" 循环有两个错误:
#+ 一个很明显,另一个比较隐蔽.

while [ "$number" < 5 ]    # 错误! 应该是:  while [ "$number" -lt 5 ]
do
  echo -n "$number "
  let "number += 1"
done
#  尝试运行时会收到错误信息而退出:
#+ bad-op.sh: line 10: 5: No such file or directory
#  在单括号里, "<" 需要转义,
#+ 而即使是如此, 对此整数比较它仍然是错的.


echo "---------------------"


while [ "$number" \< 5 ]    #  1 2 3 4
do                          #
  echo -n "$number "        #  看起来好像是能工作的, 但 . . .
  let "number += 1"         #+ 它其实是在对 ASCII 码的比较,
done                        #+ 而非是对数值的比较.

echo; echo "---------------------"

# 下面这样便会引起问题了. 例如:

lesser=5
greater=105

if [ "$greater" \< "$lesser" ]
then
  echo "$greater is less than $lesser"
fi                          # 105 is less than 5
#  事实上, "105" 小于 "5"
#+ 是因为使用了字符串比较 (以ASCII码的排序顺序比较).

echo

exit 0

有时在测试时的方括号([ ])里的变量需要引用起来(双引号). 如果没有这么做可能会引起不可预料的结果. 参考例子 7-6, 例子 16-5, 和 例子 9-6.

在脚本里的命令可能会因为脚本没有运行权限而导致运行失败. 如果用户不能在命令行里调用一个命令,即使把这个命令加到一个脚本中也一样会失败. 这时可以尝试更改访命令的属性,甚至可能给它设置suid位(当然是以root来设置).

试图用 - 来做重定向操作(事实上它不是操作符)会导致令人讨厌的意外.

command1 2> - | command2  # 试图把command1的错误重定向到一个管道里...
#    ...不会工作.

command1 2>& - | command2  # 也没有效果.

Thanks, S.C.

用 Bash 版本 2+ 的功能可以当有错误信息时引发修复动作. 老一些的 Linux机器可能默认的安装是 1.XX 版本的Bash.

#!/bin/bash

minimum_version=2
# 因为 Chet Ramey 经常给Bash增加新的特性,
# 你把 $minimum_version 设为 2.XX比较合适,或者是其他合适的值.
E_BAD_VERSION=80

if [ "$BASH_VERSION" \< "$minimum_version" ]
then
  echo "This script works only with Bash, version $minimum or greater."
  echo "Upgrade strongly recommended."
  exit $E_BAD_VERSION
fi

...

在非Linux的机器上使用Bourne shell脚本(#!/bin/sh)的Bash专有功能可能会引起不可预料的行为. Linux系统通常都把sh 取别名为 bash, 但在其他的常见的UNIX系统却不一定是这样.

使用Bash中没有文档化的属性是危险的尝试. 在这本书的前几版中有几个脚本依赖于exit或return的值没有限制不能用负整数(虽然限制了exitreturn 的最大值是255). 不幸地是, 在版本 2.05b 以上这种情况就消失了. 参考See 例子 23-9.

一个带有DOS风格新行符 (\r\n) 的脚本会执行失败, 因为#!/bin/bash\r\n 不是合法的,不同于合法的#!/bin/bash\n. 解决办法就是把脚本转换成UNIX风格的新行符.

#!/bin/bash

echo "Here"

unix2dos $0    # 脚本先把自己改成DOS格式.
chmod 755 $0   # 更改回执行权限.
               # 'unix2dos'命令会删除执行权限.

./$0           # 脚本尝试再次运行自己本身.
               # 但它是一个DOS文件而不会正常工作了.

echo "There"

exit 0

shell脚本以 #!/bin/sh 行开头将不会在Bash兼容的模式下运行. 一些Bash专有的功能可能会被禁用掉. 那些需要完全使用Bash专有扩展特性的脚本应该用#!/bin/bash开头.

脚本里在 here document终结输入的字符串前加入空白字符会引起不可预料的结果.

脚本不能export(导出)变量到它的父进程(parent process),或父进程的环境里. 就像我们学的生物一样,一个子进程可以从父进程里继承但不能去影响父进程.

WHATEVER=/home/bozo
export WHATEVER
exit 0
 bash$ echo $WHATEVER
 
 bash$ 
可以确定, 回到命令提示符, $WHATEVER 变量仍然没有设置.

子SHELL(subshell)设置和操作变量 , 然后尝试在子SHELL的作用范围外使用相同名的变量将会导致非期望的结果.


例子 31-2. 子SHELL缺陷

#!/bin/bash
# 在子SHELL中的变量缺陷.

outer_variable=outer
echo
echo "outer_variable = $outer_variable"
echo

(
# 子SHELL开始

echo "outer_variable inside subshell = $outer_variable"
inner_variable=inner  # Set
echo "inner_variable inside subshell = $inner_variable"
outer_variable=inner  # Will value change globally?
echo "outer_variable inside subshell = $outer_variable"

# 导出变量会有什么不同吗?
#    export inner_variable
#    export outer_variable
# 试试看.

# 子SHELL结束
)

echo
echo "inner_variable outside subshell = $inner_variable"  # Unset.
echo "outer_variable outside subshell = $outer_variable"  # Unchanged.
echo

exit 0

# 如果你没有注释第 19 和 20行会怎么样?
# 会有什么不同吗?

echo 的输出用管道(Piping)输送给read命令可能会产生不可预料的结果. 在这个情况下, read 表现地好像它是在一个子SHELL里一样. 可用set 命令代替 (就像在例子 11-16里的一样).


例子 31-3. 把echo的输出用管道输送给read命令

#!/bin/bash
#  badread.sh:
#  尝试用 'echo 和 'read'
#+ 来达到不用交互地给变量赋值的目的.

a=aaa
b=bbb
c=ccc

echo "one two three" | read a b c
# 试图重新给 a, b, 和 c赋值.

echo
echo "a = $a"  # a = aaa
echo "b = $b"  # b = bbb
echo "c = $c"  # c = ccc
# 重新赋值失败.

# ------------------------------

# 用下面的另一种方法.

var=`echo "one two three"`
set -- $var
a=$1; b=$2; c=$3

echo "-------"
echo "a = $a"  # a = one
echo "b = $b"  # b = two
echo "c = $c"  # c = three
# 重新赋值成功.

# ------------------------------

#  也请注意echo值到'read'命令里是在一个子SHELL里起作用的.
#  所以,变量的值只在子SHELL里被改变了.

a=aaa          # 从头开始.
b=bbb
c=ccc

echo; echo
echo "one two three" | ( read a b c;
echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
# a = one
# b = two
# c = three
echo "-----------------"
echo "Outside subshell: "
echo "a = $a"  # a = aaa
echo "b = $b"  # b = bbb
echo "c = $c"  # c = ccc
echo

exit 0

事实上, 也正如 Anthony Richardson 指出的那样, 管道任何的数据到循环里都会引起相似的问题.

# 循环管道问题.
#  Anthony Richardson编写此例,
#+ Wilbert Berendsen补遗此例.


foundone=false
find $HOME -type f -atime +30 -size 100k |
while true
do
   read f
   echo "$f is over 100KB and has not been accessed in over 30 days"
   echo "Consider moving the file to archives."
   foundone=true
   # ------------------------------------
   echo "Subshell level = $BASH_SUBSHELL"
   # Subshell level = 1
   # 没错, 现在是在子shell里头运行.
   # ------------------------------------
done

#  foundone 变量在此总是有false值
#+ 因此它是在子SHELL里被设为true值的
if [ $foundone = false ]
then
   echo "No files need archiving."
fi

# =====================现在, 使用正确的方法:=================

foundone=false
for f in $(find $HOME -type f -atime +30 -size 100k)  # 没有使用管道.
do
   echo "$f is over 100KB and has not been accessed in over 30 days"
   echo "Consider moving the file to archives."
   foundone=true
done

if [ $foundone = false ]
then
   echo "No files need archiving."
fi

# ==================另一种方法==================

#  脚本中读变量值的相应部分替换在代码块里头读变量,
#+ 这使变量能在相同的子SHELL里共享了.
#  Thank you, W.B.

find $HOME -type f -atime +30 -size 100k | {
     foundone=false
     while read f
     do
       echo "$f is over 100KB and has not been accessed in over 30 days"
       echo "Consider moving the file to archives."
       foundone=true
     done

     if ! $foundone
     then
       echo "No files need archiving."
     fi
}

相关的问题是:当尝试写 tail -f 的输出给管道并传递给grep时会发生问题.

tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
# "error.log"文件里将不会写入任何东西.

--

在脚本中使用"suid" 的命令是危险的, 因为这会危及系统安全. [1]

用shell编写CGI程序是值得商榷的. Shell脚本的变量不是"类型安全的", 这样它用于CGI连接使用时会引发不希望的结果. 其次, 它很难防范骇客的攻击.

Bash 不能正确处理双斜线 (//) 字符串.

在Linux 或 BSD上写的Bash脚本可能需要修正以使它们也能在商业的UNIX (或 Apple OSX)上运行. 这些脚本常使用比一般的UNIX系统上的同类工具更强大功能的GNU 命令和过滤工具. 这方面一个明显的例子是文本处理工具tr.

 

Danger is near thee --

Beware, beware, beware, beware.

Many brave hearts are asleep in the deep.

So beware --

Beware.

 A.J. Lamb and H.W. Petrie

Notes

[1]

给脚本设置suid 权限是没有用的.

© 内存溢出 OutOfMemory.CN