ninja_ken  

之前讨论Bash时,其实刻意地略过了两个“不太起眼”的细节,一个是引号的使用,尤其是双引号,另一个是命令与参数以及参数与参数之间的分隔方式(术语叫做分词)。这一节我们就针对性地探讨下这两部分内容。

引号的作用

Bash中有三种引号,单引号、双引号和反引号(`)。反引号不必多说,它只用于命令替换语法;单引号也比较简单,就是表示纯粹的字符串;只有双引号比较特别,有一些特定的用途,它的特殊之处可以归纳为如下几点:

  1. 在双引号中,$`保留它们的特殊语义,因此Bash扩展(参数扩展${},算术扩展$(()))和命令替换($()`command`)仍然有效,比如

     [ken ~]$echo "$var"
     123
     [ken bash]$echo "`date`"
     2023年 05月 08日 星期一 21:15:52 CST
     [ken ~]$echo '$var'
     $var
    
  2. 条件表达式的字符串比较,如果[[ $var == str ]]中的str没有双引号包裹,Bash会按照模式匹配的方式去解释str?,*[]等字符具有模式匹配语义)。这一点我们在前面Bash基本表达式(二)简单命令、列表和条件表达式(模式匹配)一节介绍过,这里不再举例。

  3. 位置参数$@$*的展开
    我们在前面函数一节里曾提到过这个,现在探究下其中的细节。我们知道,$@$*代表所有的位置参数,所以我们可以直接用for语句对位置参数进行遍历并打印

     for para in $@
     do
         echo $para
     done
    

    可当我们用双引号把他们包起来时,"$@""$*"应该返回什么呢?一般而言,双引号包裹起来的内容会返回一个字符串,那么一个绕不开的问题是,这个字符串是什么样子的,是直接把所有的位置参数前后收尾相连呢,还是说在中间放一个空格或者Tab互相隔开?这个问题的答案,涉及到分词,下面来看看

分词

分词(Bash手册关键字Word Splitting),用于把命令行上的输入,切分为一个个的词(word)。当我们输入下面这个命令时

echo Hello world
Hello world

Bash会把它分为三个词,echoHelloworld,第一个词会被解释为命令(command),第二第三个解释为参数。Bash如何知道怎么去切分词的呢?“当然是用空格啊,傻瓜”,你可能心里嘀咕着。这话没错儿,但是不全对,因为我们偶尔用Tab键来分隔,比如在脚本hello.sh里写上

echo    Hello   world

执行脚本

[ken bash]$bash hello.sh
Hello world

除了Tab键,我们还有换行符,上面的hello.sh还可以改写为

echo \
Hello \
world

其中的\+回车表示换行符的意思(\起到了转义的效果,跟C语言一样)。所以,分词时究竟遵循什么规则呢?答案是IFS。IFS(Internal Field Separator,内部域分隔符)是Bash的内置变量,这个变量的值是切分词的依据。IFS默认值包括三个空白字符,,这就解释了前面的三个例子。如果查看变量IFS的内容以及长度会是这样

[ken@Ken-Laptop ~]$ echo $IFS

[ken@Ken-Laptop ~]$ echo ${#IFS}
3

因为IFS的值都是空白符,所以不好显示,我们可以用printf命令来写

[ken@Ken-Laptop ~]$ printf "%q\n" "$IFS"
$' \t\n'

这里有两个地方值得说明下,一个是printf的格式化字符串%q,它的作用是把一些不可打印字符(比如空白符、ASCII控制字符)按照POSIX $''的转义语法进行打印,另一个自然就是POSIX $''语法的使用,它用于将不可打印字符通过\转义序列的方式来表示(详细的转义序列可查询Bash手册关键字QUOTING),比如说

[ken bash]$a="\n"
[ken bash]$echo "$a"
\n
[ken bash]$b=$'\n'
[ken bash]$echo "$b"


[ken bash]$echo ${#a}
2
[ken bash]$echo ${#b}
1

可见a的值由两个字符\n组成,没有\转义;而b的值为一个字符,也就是换行符,发生了\转义。
接着回到分词的正题,如果说IFS的值被修改了,分词的效果会有什么变化呢?我们举个例子来说明下

function printParams()
{
    echo There are $# parameters
    echo Positional parameters using '$*' are: "$*"
    echo Positional parameters using '$@' are: "$@"
}

IFS="-$IFS"
echo IFS的长度为${#IFS}, 值为"$IFS"

echo "Round 1 start"
printParams a b c d-e-f-g

echo
echo "Round 2 start"
params="a b c d-e-f-g"
printParams $params

输出结果为

IFS的长度为4, 值为-

Round 1 start
There are 4 parameters
Positional parameters using $* are: a-b-c-d-e-f-g
Positional parameters using $@ are: a b c d-e-f-g

Round 2 start
There are 7 parameters
Positional parameters using $* are: a-b-c-d-e-f-g
Positional parameters using $@ are: a b c d e f g

OK,这个输出结果可能确实有点让人摸不着头脑,让我们来分析下。首先我们修改Bash内置变量IFS的值,给它在前面新增了个-,所以这时后IFS字符串的长度从3变为4。按照IFS的定义,此时-跟空格一样,也应该可以用于分词才对。所以我们接下来用函数printParams试验了两轮(Round),第一轮直接把参数字符串a b c d-e-f-g传给函数,第二轮我们设置了一个临时变量params并把参数字符串a b c d-e-f-g赋值给它,之后再通过变量值(参数展开)传递给函数printParams。打印结果表明,第一轮里面Bash认为有4个参数,分词符-并没有发生作用,第二轮里Bash认为有7个参数,分词符-发生作用。为什么会发生这种区别呢?答案是,分词只有在Bash扩展、命令替换等语法里才会生效。哇,真让人有点头大~
参数的个数讨论完,还有参数值"$*""$@"的打印结果没探讨,这里把答案告诉大家:
"$*"返回的字符串为,先把所有的位置参数展开,再将各参数用IFS值的第一个字符连接后返回,最终结果为$1c$2c$3...,其中c为IFS变量值的第一个字符
"$@"实际上返回了多个字符串,其结果为"$1" "$2" "$3"...,每个字符串之间用空格隔开
也即是说,在"$*"中IFS的值扮演了拼接的角色,最后只返回一个字符串,而在"$@"中IFS不参与,并且最后返回多个以空格分隔的字符串。现在你能解释上面Round 1和Round 2的参数值打印结果了吗?^_^

posted on 2023-05-10 09:56  ninja_ken  阅读(42)  评论(0)    收藏  举报