请解释一下使用 IFS 进行这些参数扩展的行为?

请解释一下使用 IFS 进行这些参数扩展的行为?

我正在尝试弄清楚如何将${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分为abc.pdf。该echo命令打印三个单词,中间有一个空格。在您的示例中echo $a, and发生完全相同的事情: orecho $b的值在字符处分割。abIFS

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表明扩展不包含空格。

相关内容