shell历险之——引用的迷途
上篇:引用,奇怪的反斜线

我们知道在shell中有两类字符,一类是普通字符(literal),在shell中没有任何特殊意义;另一类是所谓“元字符”(meta),在shell中有特殊的含义或用法。

当我们需要去掉元字符的特殊含义而恢复其字面意义时就必须使用“引用”(quoting)。通常有三种引用方式,他们是转义(Escape,使用反斜杠字符\,即backslash),强引用(使用单引号',即single quote)和弱引用(使用双引号", 即double quote)。

转义:是用反斜杠放在需要转义的一个字符前,表示那个字符要看作一个普通字符。
强引用:是用单引号把要转义的字符串括起来,其中任何字符都看作普通字符,除了单引号自身。所以你无法在两个单引号之间包含单引号,用\转义也不行。
弱引用:是用双引号把要转义的字符串括起来,除了双引号"本身,其中的大部分字符都看作普通字符。例外的还有\,$,`三个特殊字符。因为\在""中是特殊字符,所以你可以在其中包含"本身,前提是必须转义。$是特殊字符,这表示你可以使用变量$var/${var}及其它,在ksh/bash中可以引用算术表达式的结果$((...)),还可以作命令替换$()。由于`是特殊字符在bsh中也可以作命令替换,但只能使用`...`的语法(这个在ksh/bash中也可以使用)。

关于三种引用,网中人的“shell十三问”之第四问已经讲得很清楚了,我这里就不重复了。

今天我们单独研究一下反斜线\。这是一个有魔力的字符,它可以用来对任何字符转义,也包括它自己。但是在不同的shell实现中它的表现似乎不尽相同,有时结果让你会大吃一惊。

先来看一个简单的例子,假定我们要输出单独一个\,先用bash:

  1. $ echo \\

  2. \

  3. $ echo "\\"

  4. \

  5. $ echo '\'

  6. \

复制代码


第一个echo,因为\是元字符,所以必须对它进行转义,所以我们必须用两个\。
第二个echo,因为""是弱引用,其中的\仍然是特殊字符,所以同样必须转义。
第三个echo,''是强引用,\在单引号之中是普通字符,这样就不用再转义了,所以只用一个\。

OK,假如我们要输出连续两个\,怎样呢?看一下:

  1. $ echo \\\\

  2. \\

  3. $ echo "\\\\"

  4. \\

  5. $ echo '\\'

  6. \\

 

(双引号内\是特殊字符,后面必须跟字符,否则则出现>,如果后面跟的是特殊字符,则将特殊字符做普通字符显示出来,如果后面跟的普通字符,这\和普通字符都显示出来)
嗯,好像没有什么奇怪的事发生,我们简单地使用双倍的\就搞定了。

那再让我们用ksh来试试来做同样的事:

  1. $ echo \\

  2. \

  3. $ echo "\\"

  4. \

  5. $ echo '\'

  6. \

复制代码


这个容易,简单的重复罢了。再来两个\:

  1. $ echo \\\\

  2. \

  3. $ echo "\\\\"

  4. \

  5. $ echo '\\'

  6. \

复制代码


等等,怎么搞的?为什么只输出了一个\?
再加一个\会如何?

  1. $ echo \\\\\

  2. >

  3. \

  4. $ echo \\\\\\

  5. \\

  6. $ echo "\\\\\"

  7. >

  8. \

  9. $ echo "\\\\\\"

  10. \\

  11. $ echo '\\'

  12. \\

复制代码


第一个echo没有立即执行,ksh给出了一个>(PS2提示符),等待我们继续输入,回车后echo仍然只输出了一个\。
第二个echo后面是六个\,呼~,这回终于输出两个\了。
双引号的情形也是类似,我们仍然需要六个\。
第五个echo,好在单引号的结果还算不错,只要两个\就行了。

你可以继续做这个试验,最后会发现在不用单引号时,我们需要2个或4个\输出1个\,需要6个或8个\输出2个\,10个或12个\来输出3个\,......真是又臭又长!

  1. 输出 命令行需要\的个数

  2. \ 2/4

  3. \\ 6/8

  4. \\\ 10/12

  5. \\\\ 14/16

  6. \\\\\ 18/20

  7. ... ...

  8. n个\ 4n-2/4n

复制代码


厌倦了吗?其实ksh下除了用',还有其它方法剪断这“懒婆娘的裹脚布”。窍门就在于echo命令的-E选项:

  1. $ echo -E \\

  2. \

  3. $ echo -E \\\\

  4. \\

  5. $ echo -E \\\\\\

  6. \\\

复制代码


很好!这样与bash下的情况就一样了,-E选项是不是很神奇呀!为什么呢?

原来bash和ksh的echo命令缺省的表现是不同的。我们知道echo命令可以接受一些转义字符序列来表示特殊的字符,如\n表示换行,\a表示蜂鸣,\t表示水平制表符等等。显然在解释转义序列时\是一个特殊字符。

在bash下echo缺省的设置是不解释这些转义序列,为了告诉它解释转义序列我们必须使用-e选项。

而在ksh下echo的缺省设置就会解释这些转义序列,我们可以用-E选项让它不解释转义序列。所以ksh下在不使用-E选项时实际上会发生两次转义的过程,第一次发生在ksh处理命令行时,第二次发生在echo命令处理它的参数时。让我们看下面这个简单的例子:

  1. $ set -x

  2. $ echo \\\\

  3. + echo \\

  4. \

复制代码


先用set -x让ksh显示命令行处理的结果,我们看到,第一个\转义第二个\,去掉它的特殊含义,同样第三个\转义第四个\去掉它的特殊含义,这样命令行处理完毕以后传给echo的参数是\\。echo然后将\\解释成了\并输出。于是我们只得到一个\。

bsh中echo的缺省表现与ksh中类似,解释转义序列,不过可惜无法关掉这一功能。我们还是可以使用外部命令来不解释转义序列原样输出,例如在linux下可以用
/bin/echo -E(可以省略)来做。

下来看一个从本论坛来的例子,提问者的意图是将dos格式的路径中的\变成\\,但他的shell好像工作得不好:

  1. $echo C:\\tmp | sed 's/\\/&\\/'

  2. C: mp


  3. $echo 'C:\abc'|sed 's/\\/\\\\/'

  4. C:bc

复制代码


在看过上面我们对引用和echo的讨论之后,您能为他解释一下其中的原因吗?
对了,这两条命令的语法都没有错,不过提问者使用的shell八成是ksh(也可能是bsh,但可能性较小),问题是出在echo上。在ksh下echo默认解释转义序列,所以命令行的
echo c:\\tmp部分先做命令行解释,\\变成\,于是执行:
echo 'c:\tmp'
而\t是一个转义序列,它代表水平制表符,所以echo最后输出
c:<水平制表符>mp
同样
echo 'c:\abc'
会输出
c:<蜂鸣字符>bc
你应该会听到你的终端发出“嘟”的一声。
这下清楚了吧?echo的输出就是错误的,后面sed的替换根本就没有匹配执行,当然不会有正确的结果。
上面两个命令在bash下是正确的,为什么?
那在ksh下怎样修改让它正确工作呢?如果是bsh呢?改法可能不止一种,不过这个还是留给亲爱的读者您作为一个练习好了。

 

 

下篇:命令替换 -- 另一种引用?

命令替换(command substitution),是指在命令行获取另一个命令的标准输出,换句话说,它是将一个命令的标准输出代换到另一个命令的命令行。在bsh/ksh/bash和csh中的语法都是`command`。注意这里的`是“反引号”(键盘上位于1键的左边)。例如:

  1. /root#echo we are now in `pwd`

复制代码


shell会将pwd命令的输出替换`pwd`,然后打印出we are now in /root

在ksh和bash中除了可以使用上面的语法外,又引入了一种新的语法,$(command)。于是上面的例子可以改写为下面等价的形式:

  1. \root#echo we are now in $(pwd)

复制代码


在ksh/bash环境下,使用$()显然比``优越,这是因为:
其一,前者更易读,不会产生歧义。而反引号`常常被初学者当成单引号';
其二,前者嵌套时更简单,直接使用就行。而后者嵌套时内部的反引号必须用\转义;
其三,它们对反斜杠\的处理不一样,在$()中可以减少转义的麻烦。而这一点与第二点是前因后果的关系。正是因为$()嵌套时不需转义,所以\在$()中就不需要作为一个特殊字符了。而``中的\必须是特殊字符,否则就无法嵌套使用了。

那么我们是否可以完全抛弃``的语法,只用$()就好了呢?这在大多数环境下是可行的,但如果过分强调这个的话,我认为还是太理想化了。事实上``还是有它存在的理由的,因为它的兼容性最好,在bsh/ksh/bash/csh统统可以执行。在大部分的传统unix上是没有安装bash的,ksh是不是都有安装我不能确定(100%正宗的ksh好像是专有软件),还有在一些嵌入式的linux或者高度裁剪的linux上会没有安装bash或者ksh。那么在这些环境下进行shell编程时我们就可能不得不使用最古老的bourne shell语法,这时优秀的$()语法就可能完全派不上用场。

扯远了,回到我们的主题。考察命令替换的执行机制,``或$()里面的内容原则上应该作为一个整体传递,交给子shell执行。所以其中的大部分字符在传递给子shell之前都是普通字符。那么实际上它们在本来命令替换的功能之外似乎也起到了一种引用的效果(也算是一种副产品吧),所以我们不妨把它看作上篇里面讲到的三种引用之外的另一种引用形式。于是我们自然会问一个问题,在这两种“引用”中有没有例外的特殊字符呢?当然会有,据我目前所知(不全面的地方欢迎大家指正):
``中的特殊字符有美元符$,反斜杠 \,双引号 ",单引号',还有自身`。
$()中的特殊字符有美元符$, 双引号"和单引号'。

既然命令替换可以看作是一种特殊的引用,我们不妨拿上篇里反斜杠的例子再来研究一下。假定我们要用两次echo,并且要做一次命令替换。先来看比较典型的ksh:

  1. $ echo `echo \\\\`

  2. \

复制代码


我们先来简单解释一下这个命令的处理过程。
第一步,shell处理整个命令行,由于``中的\是特殊字符,所以处理之后传给内部的echo的参数就变成了\\;
第二步,在子shell中执行echo \\,因为ksh的echo缺省处理转义序列,所以\在echo看来还是特殊字符,于是内部echo输出一个\;
第三步,将内部echo输出的\代入外部的echo的命令行,执行echo '\',最终输出一个\。

现在请大家猜一猜,ksh下为了要输出两个\,我们需要多少个\在命令行呢?大胆一点,使劲猜!
答案是20个(在bsh下也差不多,是18个)!

  1. $ echo `echo \\\\\\\\\\\\\\\\\\\\`

  2. \\

复制代码


吃惊吗?你会说:“这太离谱了!这条裹脚布比前面那条长了几倍!”是的,太离谱了,难怪有人把\(英文为backslash)的自身冗长又似乎没有规律可循的转义叫作:“backslashit!”;-D。不过在抱怨过后还是让我们来看看有什么方法来简化一下吧。
ksh下:
第一件武器:强引用。防止在第一次命令行处理时解释\。

  1. $ echo `echo '\\\\\\\\\\'`

  2. \\

复制代码


好刀!不错,10个\就够用了!

第二件武器:echo的-E选项。防止echo解释\。

  1. $ echo -E `echo -E '\\\\'`

  2. \\

复制代码


连环双刀!只剩4个\了。

终极武器:$()。防止执行内部的echo时在子shell的命令行解释\。

  1. $ echo -E $(echo -E '\\')

  2. \\

复制代码


枝枝杈杈砍精光,剩下两个对一双!这次终于达到最简了。

其他的shell情况如何呢?
bsh不支持$(...)的语法,`...`的情况与ksh类似,只是echo的处理与ksh稍有一点不同。
bash的情况类似,注意echo缺省不解释转移序列(等价于ksh的echo -E)。
tcsh不支持$(...)语法,`...`中情况也稍简单些:

  1. $ echo $0

  2. csh

  3. $ echo `echo \\`

  4. \

  5. $echo `echo \\\\`

  6. \\

  7. $ echo `echo '\\'`

  8. \\

复制代码


tcsh与这个例子有关的引用方面的特点是:
单引号和双引号中的\为普通字符。
echo中\默认为普通字符。
``中\为特殊字符。
详细内容请参看csh的相关文档。

最后,让我们继续讨论上面那个dos路径的实际问题作为一个练习,为了叙述方便,对原问题略加改动:
#!/bin/ksh
dospath='c:\tmp'
escaped=`echo $dospath|sed 's/\\/&&/'`
echo $escaped

在ksh下执行上面的脚本报错:
sed:-e 表达式 #1,字符 7:unterminated `s' command
你能找出所有的错误吗?
根据出错信息,首先容易知道sed的命令行有问题。对了,哪里有问题?还是经常捣乱的\吧?又对了,怎样修改呢?
兼容性最好的语法:

  1. escaped=`echo $dospath|sed 's/\\\\/&&/'`

复制代码


OK,执行通过!

再来,最优雅的的语法:

  1. escaped=$(echo $dospath|sed 's/\\/&&/')

复制代码


很好!也通过。

重新执行脚本,输出:

  1. c: mp

复制代码


还不对,那么哪里还有错误呢?嗯,对了,是echo的问题。如何修改?

  1. escaped=`echo -E $dospath|sed 's/\\\\/&&/'`

复制代码


不错。再次执行脚本,输出:

  1. c:\tmp

复制代码


仍然不是我们想要的结果。哪里还有问题呢?对了,别忘了最后一个echo!

  1. echo -E $escaped

复制代码



再次执行脚本,输出:

  1. c:\\tmp

复制代码


大功告成!

OK!通过这上下两篇关于引用和反斜杠\的讨论,相信您对shell的引用和转义有了更加深入的理解。让我们小结一下,帮助您强化记忆:

关于引用:
三种公认的引用方式:

  1. 强引用('...'),特殊字符:单引号'自身

  2. 弱引用("..."),特殊字符:双引号"自身,反斜杠\,美元符$,还有反引号`

  3. 转义(\.),没有例外,连\自身也可以被转义。

复制代码


woodie自己加入的两种“类引用”方式(一家之言,×××之,姑妄听之吧。欢迎拍砖!^_^):

  1. ``,bsh/ksh/bash/csh中可用,特殊字符:反引号自身`,美元符$,反斜杠\,还有单、双引号'和”。

  2. $(),ksh/bash中可用,特殊字符有美元符$,还有单、双引号'和”。

复制代码


关于echo:

  1. bsh:解释转义字符序列,且不能关掉

  2. ksh:缺省解释转义字符序列,可以用-E选项关掉

  3. bash:缺省不解释转义字符序列,可以用-e选项打开

复制代码



说明,我的测试环境:
Centos 4.2 x86-64
bash: 3.00.15(1)
ksh pdksh-5.2.14-30.3
bsh ash-0.3.8-20
csh tcsh-6.13-9