我06年开始接触shell编程, 一开始照着别人的例子写些简单的脚本, 后来在网上找些shell语法的教程来看看(我想大多数同学学习shell也是这么个过程), 觉得shell挺简单的, 比其他语言简单多了. 但是随着写shell脚本次数的增多, 发现根本不是那么回事, 觉得shell太灵异了, 经常出现一些奇怪的错误, 比如:
#!/usr/bin/env bash action = "$1" echo "You want $action"
把上面的代码保存为test.sh并加上可执行权限, 执行./test.sh exit, 得到这样的错误提示:
./test.sh: line 3: action: command not found
再改下:
#!/usr/bin/env bash action= "$1" echo "You want $action"
不错, 好像没错误了, 不过怎么啥都不打印啦?
有过其他语言编程经验的同学可能也会像我犯那样的错误, 同时会产生这样的疑问: 怎么shell中赋个值还这么多麻烦啊?
if [ $user = "admin" ]; then echo "You are admin!" fi
上述代码判断一个用户是否为管理员, 但是有时候上面的代码运行时会出现这样的错误:
-bash: [: =: unary operator expected
这意思好像是说"期待一元运算符"? 啥意思?
有的教程里指出这样就可以了:
if [ x$user = "xadmin" ]; then echo "You are admin!" fi
我试了下, 好像还真行, 为啥这样就可以呢? 这个”x”这么神奇? 其他的字母也可以这么神奇吗?
我看有的人这样写:
if [ "x$user" = "xadmin" ]; then
还有人这样写:
if [ x$user = xadmin ]; then
甚至有人这样:
if [ x$user = x"admin" ]; then
后来我无意中发现这样就可以了:
if [ "$user" = "admin" ]; then
这加不加双引号和加在哪到底有什么不同?
read str < a echo $str
怎么输出是”str1 str2″, 而不是”str1 TAB str2″呢, 我的TAB哪去了?
: << COMMENT
COMMENT
比如:
echo xxx : << COMMENT echo "mm said, you could not touch me!" COMMENT echo yyy
好像还真行, 但是下面这个怎么不行呢?
echo xxx : << COMMENT file=a result=$(grep str $file) COMMENT echo "mm said, you can touch me!"
还有其他的神奇注释吗?
其实类似上面这些灵异的例子还有很多, 但是纵观那些shell教程, 很少有能把shell的这些灵异的地方给读者讲明白的. 下面, 我结合我自己的一些经验, 力图把shell的一些本质语法给大家讲明白, 让大家在遇到一些灵异的问题时, 能迅速的定位和解决问题.
| & ; ( ) < > space tab
这些字符没有被引号引起来时, 可以用来分割单词
command | command2 command |& command2
把command的输出通过管道连接到command2的输入, |&连标准错误也一起做为command2的输入.
这里要注意的时, command2是在子shell里面执行的, command2对环境所做的改变不会影响到command所在的shell环境. 这就解释了本文开头的问题3.1
引用用来去掉某些字符的特殊意义. 比如想使用元字符的字面意义必须对其进行引用.
引用有3类: 反斜线引用(\)、单引号引用、双引号引用.
单引号引用屏蔽单引号内的任何字符所具有的特殊意义, 包括反斜线(\), 所以单引号引用不能再包含单引号(比较杯具…)
双引号引用中除了 $ 、 ` 、 \ 、 ! , 其他特殊字符的意义都被屏蔽.
小技巧:
参数是用来存储值的实体, 它可以是数字(0, 1, 2 …)、name、某些特殊字符(@, *, …). 当参数是一个name时, 也叫变量(variable), 变量赋值:
name=[value]
等号2边不能有空格, 如果有空格的话, shell解释程序怎么知道你到底是想要运行name命令还是给name赋值呢? 所以的shell的变量赋值才不得不这样”讲究”
小技巧:
LANG= sort file
上面的命令表示在运行sort file的时候LANG为空, 不会影响其他的后续命令. 你是否还记得这样的代码:
tmp_LANG=$LANG LANG=zh_CN codes ... LANG=$tmp_LANG
小技巧:
$* == $1 $2 $3 ... "$*" == "$1c$2c$3...", c为IFS的第一个字符
IFS 参见这里
$2 == $* "$@" == "$1" "$2" "$3" ...
$* 和 $@ 啥区别? 见后文
命令行被分割成单词后, 开始执行扩展. 扩展有大括号扩展(brace expansion), 波浪号扩展(tilde expansion), 参数和变量扩展(parameter and variable expansion), 算术扩展(arithmetic expansion), 命令替换(command substitution), 单词分割(word splitting), 路径扩展(pathname expansion). 扩展的优先级也如上所示. 有的系统还支持进程替换(process substitution)
echo a{b,c} ab ac echo {1..10} 1 2 3 4 5 6 7 8 9 10 echo {10..1} 10 9 8 7 6 5 4 3 2 1 echo {1..10..3} 1 4 7 10 echo {a..f} a b c d e f echo {a..f..2} a c e
echo ~/sdfa
/home/taoshanwen/sdfa
~+ => PWD
~- => OLDPWD
还有很多, 详见bash man
echo $((9 + 8 * 9)) 81 echo $((9 + 8 ** 9)) 134217737
ls dir1 > 1 ls dir2 > 2 diff 1 2
但你试试这样:
diff <(ls dir1) <(ls dir2)
是不是也可以? 很神奇吧. 上面的这个语法<(command)就是进程替换. <(command)表示把command的输出生成一个临时文件, 并把这个文件名作为另外一个命令的参数. 对于上面的命令, 就是把”ls dir1″命令的输出生成一个临时文件, 并把临时文件名做为diff命令的第一个参数. 再举一个例子:
wget -q -O >(cat) http://baidu.com
wget命令会把下载后的文件保存到文件中去, 但是我们可以用上面的命令不让它保存到文件中去, 而是显示出来. wget的”-O”选项后本来应该是一个文件名的参数, 但是我们现在用>(cat)代替, 表示wget下载下来的内容放到一个临时文件中, 然后把这个临时文件名再传给>()里面的cat命令.
灵活运用进程替换, 将会非常的方便, 严重推荐
上述扩展如果没有双引号扩起来, 扩展完后, shell将会对结果用IFS进行单词分割. 例如:
str="a b c" echo $str a b c echo "$str" a b c
为什么加不加双引号结果会迥然不同? 因为没加双引号时, shell会对扩展结果进行单词分割, $str的扩展结果为”a b c”, 分割后变成3个单词a、b、c, 这3个单词做为echo命令的三个参数, 最终输出结果自然是”a b c”了.
想起来本文开头的3.2问题了吗? 知道怎么回事了吧?
另外, 扩展结果为空的话, 如果没有被双引号或者单引号扩起来的话, 会被删掉. 例如:
#!/usr/bin/env bash user="$1" mysql -u $user db -e "$sql"
上面这个脚本如果第一个参数为空的话, $user将会被删掉, 从而mysql的用户名会变成db, 正确的代码应该是:
mysql -u "$user" db -e "$sql"
那你知道下面这些代码的错误之处了吗?
str=$(cat file) for line in "$str"; do echo "$line" done
说到这里, 我们来说说$*和$@的差别. 它们在不加双引号时完全一样, 但是不加双引号时, 他们都有一个问题, 就是扩展会进行单词分割, 如果输入的参数中含有空格, 可能有时候结果就不是我们想要的了, 比如:
#!/usr/bin/env bash for i in $*; do echo $i done
保存上述的程序为test.sh, 该程序想打印每个输入参数,
taoshanwen@taoshanwen-laptop ~$ ./test.sh ab cd ef ab cd ef taoshanwen@taoshanwen-laptop ~$ ./test.sh "ab xx" "cd yy" "ef zz" ab xx cd yy ef zz
上述结果并不是我们想要的, 那怎么取得准确的输入参数呢? “$@”可以解决, 你可以试试,
echo a*
ab ac ad
echo "xx" => xx echo a"xx" => axx
经过上面这么多的了解, 我们大致知道了shell解释器的解释过程:
<<[-]word here-document delimiter
把here-document作为某个命令的标准输入. 例子:
grep a << EOF
asdf
qweszd
asdf
EOF
如果word用双引号括住, delimiter就是word删除引用后的结果, here-document里面不进行任何扩展. 如果word没有用双引号括住, 那么here-document里面会进行参数替换、命令替换、算术扩展.
我们再来看看本文开头说的那个神奇的注释,
: << COMMENT
COMMENT
*”:”* 是一个shell内置命令, 它不干任何事情, 它的返回值为0. 这样就好理解了, 被注释的内容实际上是作为: 的标准输入, 而这个命令啥事情都没干, 起到注释的作用了. 但是你现在知道为啥下面这个没起到注释作用了吗? 咋解决呢?
echo xxx : << COMMENT file=a result=$(grep str $file) COMMENT echo "mm said, you can touch me!"
<<< here-strings
把word作为命令的标准输入, 例子:
grep a <<< abc
taoshanwen@taoshanwen-laptop ~$ type ls ls 是 `ls --color -N --show-control-chars' 的别名 ls 是 /bin/ls taoshanwen@taoshanwen-laptop ~$ type [ [ 是 shell 内嵌 [ 是 /usr/bin/[
# less color configure # blue export LESS_TERMCAP_mb=$'\E[01;34m' # red export LESS_TERMCAP_md=$'\E[01;31m' # magenta export LESS_TERMCAP_me=$'\E[01;35m' # write export LESS_TERMCAP_se=$'\E[0m' # yellow export LESS_TERMCAP_so=$'\E[01;44;33m' # cyan export LESS_TERMCAP_ue=$'\E[01;36m' # green export LESS_TERMCAP_us=$'\E[01;32m'
保证你的man会色彩缤纷, 重点突出, 非常方便
http://sourceforge.net/projects/log4sh/, shell里的日志工具, 和log4系列的其他日志库配置基本差不多
http://shunit.sourceforge.net/, shell的单元测试工具
http://bashdb.sourceforge.net/, shell的调试工具
高效操作Bash
) { :&};:
上面的命令能迅速的灭了你的系统, 慎用! ulimit -u进行限制
http://www.datsi.fi.upm.es/~frosal/, 简单的加密工具, 会把shell转换成一个二进制文件
http://wzce.tripod.com/wzsh.html, 更加强大的加密工具