我正在尝试弄清楚如何将${parameter%word}
扩展与$@
和 一起使用$*
。它首先尝试制作一个使用 Ghostscript 组合 pdf 的脚本,但我遇到了一些参数扩展的奇怪行为,现在我只是好奇为什么会发生这种行为。
基本上我试图从每个参数的末尾删除“.pdf”,然后用任意字符串将它们连接起来(我正在使用“-”进行测试),然后在结果的末尾添加“.pdf” 。例如,预期行为是test.sh a.pdf b.pdf c.pdf
-> a-b-c.pdf
。这是我正在运行的测试脚本:
IFS='-'
echo ${*%.pdf}.pdf
echo "${*%.pdf}.pdf"
a=${*%.pdf}.pdf
b="${*%.pdf}.pdf"
echo $a
echo $b
如果我bash test.sh a.pdf b.pdf c.pdf
,我得到:
a b c.pdf
a-b-c.pdf
a b c.pdf
a b c.pdf
如果我zsh test.sh a.pdf b.pdf c.pdf
,我得到:
a b c.pdf
a.pdf-b.pdf-c.pdf
a-b-c.pdf
a.pdf-b.pdf-c.pdf
我知道 zsh 和 bash 是不同的,所以我不担心为什么它们给出不同的结果。然而,在每种情况下,构建字符串的 4 种方法中只有一种能够按预期工作(第二种用于 bash,第三种用于 zsh)。
为什么这些看似相似的构造字符串的尝试会产生如此不同的结果?任何见解都值得赞赏。谢谢!
答案1
如何${*%word}
工作等取决于外壳。POSIX未指定结果。有两种主要的合理行为:转换(前缀或后缀删除)可以应用于每个单词,或者应用于连接单词的结果。在支持数组的 shell 中,很自然地将转换应用于每个单词:这就是 bash 和 ksh93 所做的。在不支持数组的 shell 中,很自然地首先连接单词(这就是 ash/dash 所做的)。例如:
# No arrays: $* joined = 'abc abc'; strip off b* → 'a'
$ dash -c 'echo ${*%%b*}' _ abc abc
a
# Arrays: $* = ('abc' 'abc'); strip off b* from each element → ('a' 'a'); then join
$ bash -c 'echo ${*%%b*}' _ abc abc
a a
第一个字符IFS
用于连接组成的单词$*
。仅当模式可以与该字符匹配时,它才会对删除的内容产生影响。例如:
# No arrays: $* joined = 'abc-def-ghi'; strip off -* → 'abc'
$ dash -c 'IFS=-; echo "${*%%-*}"' _ abc-def ghi
abc
# Arrays: $* = ('abc-def' 'ghi'); strip off -* from each element → ('abc' 'ghi'); then join
$ bash -c 'IFS=-; echo "${*%%-*}"' _ abc-def ghi
abc-ghi
当替换是在单词上下文中时,扩展到此结束。单词上下文包括双引号和赋值;看什么时候需要双引号?和shell 变量的扩展以及 glob 和 split 对它的影响更多细节。这解释了echo "${*%.pdf}.pdf"
: 的第一个字符IFS
用于连接,并且没有后续的分割,因此 bash 中的输出是a-b-c.pdf
。两者的值a
也是b
如此a-b-c.pdf
。
当替换位于列表上下文中(即未加引号)时,如第一个示例所示,结果会进行分词(和通配)。这是基于IFS
,因此a-b-c.pdf
分为a
,b
和c.pdf
。该echo
命令打印三个单词,中间有一个空格。在您的示例中echo $a
, and发生完全相同的事情: orecho $b
的值在字符处分割。a
b
IFS
Zsh 对待@
和*
有所不同。作为*
参数名称,它在双引号内应用字符串样式的行为(先连接然后转换),否则应用数组样式的行为(转换每个元素)。另一方面,参数@
始终被视为数组。因此:
$ zsh -c 'echo "${*%.pdf}"' _ a.pdf b.pdf c.pdf
a.pdf b.pdf c
$ zsh -c 'echo ${*%.pdf}' _ a.pdf b.pdf c.pdf
a b c
$ zsh -c 'echo "${@%.pdf}"' _ a.pdf b.pdf c.pdf
a b c
$ zsh -c 'echo ${@%.pdf}' _ a.pdf b.pdf c.pdf
a b c
与其他 shell 中发生的情况不同,字符串赋值不会导致$*
以字符串方式进行处理:双引号才是重要的。这解释了为什么a=${*%.pdf}; echo $a
像 一样工作echo ${*%.pdf}
而不像a="${*%.pdf}"; echo $a
.
对于IFS=-
,连接时会使用破折号,*
无论是由于双引号还是字符串赋值,只要它处于单词上下文中,就会发生这种情况。
# ('a.pdf' 'b.pdf' 'c.pdf); strip each element → ('a' 'b' 'c'); print list
$ zsh -c 'IFS=-; echo ${*%.pdf}' _ a.pdf b.pdf c.pdf
a b c
# join → 'a.pdf-b.pdf-c.pdf'; strip the single word and print it
$ zsh -c 'IFS=-; echo "${*%.pdf}"' _ a.pdf b.pdf c.pdf
a.pdf-b.pdf-c
# ('a.pdf' 'b.pdf' 'c.pdf); strip each element → ('a' 'b' 'c'); `$*` in word context so join → 'a-b-c'; print word
$ zsh -c 'IFS=-; a=${*%.pdf}; echo "$a"' _ a.pdf b.pdf c.pdf
a-b-c
# join → 'a.pdf-b.pdf-c.pdf'; strip the single word; print the word
$ zsh -c 'IFS=-; a="${*%.pdf}"; echo "$a"' _ a.pdf b.pdf c.pdf
a.pdf-b.pdf-c
请注意,您几乎不应该使用$*
.仅使用 来连接位置参数才有用IFS
,并且它使得无法区分IFS
连接创建的字符与IFS
参数中已有的字符。"$@"
几乎总是正确的形式。请注意,您确实需要双引号来避免单词扩展(即使在 zsh 中也是如此,尽管在 zsh 中省略引号的影响要小得多)。
为了使您的脚本易于理解,请一次执行一步:去掉每个部分的后缀,然后连接这些部分。使用数组变量来存储中间结果。
parts=("${@%.pdf}") # using @ because we want to have array behavior
IFS=-
joined="${parts[*]}" # using * and not @ for joining
echo "$joined.pdf"
此代码片段在 bash 和 zsh 中的工作方式相同。
答案2
这解释了会发生什么:
#!/bin/bash
IFS='-'
var='a-b-c.pdf'
echo $var
echo "$var"
您echo ${*%.pdf}.pdf
确实创建了您想要的字符串,但由于缺少引号,单词分割会作用于-
.
或这个:
[[ ${*%.pdf}.pdf =~ ' ' ]]
+ [[ a-b-c.pdf =~ ]]
echo $?
+ echo 1
1
其中[[ ]]
没有分词,并且set -vx
/bash -vx
表明扩展不包含空格。