Shell 脚本输出在作为参数传递给脚本时错误地分割

Shell 脚本输出在作为参数传递给脚本时错误地分割

假设我有以下两个 shell 脚本:

#!/bin/sh
#This script is named: args.sh

echo 1 "\"Two words\"" 3

, 和:

#!/bin/sh
#This script is named: test.sh

echo "Argument 1: "$1
echo "Argument 2: "$2
echo "Argument 3: "$3

当我将脚本称为:

sh test.sh $(sh args.sh)

,我收到:

Argument 1: 1
Argument 2: "Two
Argument 3: words"

当我期望得到:

Argument 1: 1
Argument 2: Two words
Argument 3: 3

复制 的输出sh args.sh并将其粘贴为 的输入sh test.sh就可以了;所以我认为这实际上并不是 shell 正在做的事情。我可以通过调用来实现所需/预期的输出sh args.sh | xargs sh test.sh

但是,我想知道是否有一种等效的方法可以在不将第一个脚本(args.sh)的输出通过管道传输到 xargs 的情况下执行此操作?我希望脚本按原始顺序调用;参数脚本将参数输出到第二个。我也在寻找关于为什么此调用不能按预期工作的解释?

答案1

部分问题是 args.sh 返回的字符串没有像直接命令一样进行解析,而是仅通过 $IFS ( $' \t\n') 的值进行解析。尝试使用以下命令打开命令跟踪set -x

$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh 1 '"Two' 'words"' 3
Argument 1: 1
Argument 2: "Two
Argument 3: words"
$

请注意以单个 开头的行+:有四个参数,其中'"Two''words"'两个被解析为单独的参数。您想要做的是更改 $IFS。

$ set -x
$ IFS='"'
+ IFS='"'
$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh '1 ' 'Two words' ' 3'
Argument 1: 1
Argument 2: Two words
Argument 3:  3
$

这不适用于每个输出。最好的办法是更改 args.sh 的输出,以使用不同于空格的内容分隔输出,例如逗号或冒号:

$ cat /tmp/args.sh
#!/bin/sh
#This script is named: args.sh

echo "1,Two words,3"
$ IFS=,
$ sh /tmp/test.sh $(sh /tmp/args.sh)
+ sh /tmp/args.sh
+ sh /tmp/test.sh 1 Two words 3
Argument 1: 1
Argument 2: Two words
Argument 3: 3
$

答案2

当变量替换$var或命令替换$(cmd)不加引号时,结果将发生以下转换:

  1. 将生成的字符串拆分为单词。分割发生在空白处(空格、制表符和换行符序列);这可以通过设置来配置IFS(参见1,2)。
  2. 每个生成的单词都被视为全局模式,如果它与某些文件匹配,则该单词将被文件名列表替换。

请注意,结果不是字符串,而是字符串列表。此外,请注意,"此处不涉及引号字符;它们是 shell 源语法的一部分,而不是字符串扩展的一部分。

shell 编程的一般规则是始终在变量和命令替换两边加上双引号,除非您知道为什么必须将它们去掉。所以在 中test.sh,写echo "Argument 1: $1".要将参数传递给test.sh,您遇到了麻烦:您需要将单词列表从 传递args.shtest.sh,但您选择的方法涉及命令替换,并且仅提供简单字符串的方式。

如果你能保证传递的参数不包含任何换行符,并且调用过程稍作改变也是可以接受的,那么你可以设置IFS为只包含换行符。确保args.sh每行输出一个文件名,并且没有杂乱的引号。

IFS='
'
test.sh $(args.sh)
unset IFS

如果参数可能包含任意字符(大概除了空字节,不能将其作为参数传递),则需要执行一些编码。任何编码都可以;当然,这与直接传递参数不同:那是不可能的。例如,在(如果您的 shell 不支持,则args.sh替换为实际的制表符):\t

for x; do
  printf '%s_\n' "$x" |
  sed -e 's/q/qq/g' -e 's/ /qs/g' -e 's/\t/qt/g' -e 's/$/qn/'
done

并在test.sh

for arg; do
  decoded=$(printf '%s\n' "$arg" |
            sed -e 's/qs/ /g' -e 's/qt/\t/g' -e 's/qn/\n/g' -e 's/qq/q/g')
  decoded=${decoded%_qn}
  # process "$decoded"
done

您可能更愿意更改test.sh为接受字符串列表作为输入。如果字符串不包含换行符,args.sh | test.sh则在args.sh(解释):

while IFS= read -r line; do
  # process "$line"
done

另一种完全不需要引用的方法是从第一个脚本调用第二个脚本。


args.sh "$@"

相关内容