BASH: 子命令扩展中不可避免的分词?

BASH: Unavoidable wordsplitting in subcommand expansion?

所以我正在编写一个 BASH shell 脚本来为我正在处理的 Node 项目执行一些 CLI 测试(我没有在这个问题中标记 Node 因为这真的只是属于 BASH);我的 CLI 测试如下所示:

test_command=$'node source/main.js --input-regex-string \'pcre/(simple)? regex/replace/vim\' -o';
echo $test_command;
$test_command 1>temp_stdout.txt 2>temp_stderr.txt;
test_code=$?;
echo "test_code $test_code"
test_stdout=`cat temp_stdout.txt`;
test_stderr=`cat temp_stderr.txt`;

如您所见,我使用了 C 风格的引号 $'...',如 described here, which should make it so that $test_command expands literally to node source/main.js --input-regex-string 'pcre/(simple)? regex/replace/vim' -o which is what the echo on line 2 shows, however when I attempt to run the command on line 3, I'll get an error saying that regex/replace/vim' isn't a recognised command-line parametre in my script. Obviously, what's happening here is despite me seemingly quoting and escaping everything correctly, BASH is still splitting the regex/replace/vim' part into its own word。根据我阅读的关于 BASH 的引用和分词规则的所有内容,这不应该发生,但它确实发生了。我尝试更改第一行的引号以使用 strong/literal ' 引号('node source/main.js --input-regex-string "pcre/(simple)? regex/replace/vim" -o' 这只会导致第 3 行将整个内容视为一个词,因此不起作用)和weak/dynamic " quotes ("node source/main.js --input-regex-string 'pcre/(simple)? regex/replace/vim' -o" 与 strong-quote 示例完全相同,更不用说因为在这种情况下引用的字符串是正则表达式文字,它不适合魔术" 的扩展行为)代替 C 风格的引号,改变命令字符串本身的转义以适应正在使用的任何引号风格;我已经尝试向字符串添加额外的转义,例如 test_command=$'node source/main.js --input-regex-string \\'pcre/(simple)?\ regex/replace/vim\\' -o 只是为了见证完全相同的行为;并且我已经尝试更改我在第 3 行调用命令的方式:引用扩展,将其包含在 { ... }${ ... } 中,并结合前面提到的变体,所有这些仍然导致原始分词问题或者我只是被给了一个通用的 "bad substitution" 语法错误。

所以,简而言之,我的问题是 invoke/format 命令的正确方法是什么,作为字符串存储在 BASH 变量中,包含带引号的文字字符串,BASH 不会莫名其妙地对包含的带引号的字符串进行分词并破坏整个命令吗?

what is the correct way to invoke/format a command, stored as a string in a BASH variable, containing a quoted literal string, that BASH won't inexplicably word split the contained quoted string and break the whole command?

"correct" 方式(对我而言)是 而不是 将命令作为字符串存储在变量中。正确的方法是使用一个函数,该函数还允许在其中添加任何逻辑:

test_command() {
    node source/main.js --input-regex-string 'pcre/(simple)? regex/replace/vim' -o "$@"
}
test_command

正确的方法是将其存储为数组:

test_command=(node source/main.js --input-regex-string 'pcre/(simple)? regex/replace/vim' -o)
"${test_command[@]}"

现有 将命令作为字符串存储在变量中的运行 方法是使用eval which is evil。您可以 正确地 转义参数并将它们连接成一个字符串,然后使用 eval:

执行它
test_command=$(printf "%q " node source/main.js --input-regex-string 'pcre/(simple)? regex/replace/vim' -o)
eval "$test_command"

this shouldn't be happening but yet it is.

word splitting 执行于:

The shell scans the results of parameter expansion, command substitution, and arithmetic expansion that did not occur within double quotes for word splitting.

参数扩展产生的双引号或单引号并不特殊,它们是按字面意思来的。仅当参数扩展本身在双引号内时才重要。因为在您的代码片段中 $test_command 不在双引号内,所以结果是 word spitted,它确实:

The shell treats each character of $IFS as a delimiter, and splits the results of the other expansions into words using these characters as field terminators.

而且它不关心引号。它在确定哪个参数进行分词时关心它们 - 那些不在双引号内的参数。如果一个参数经过分词,结果只是在空格上粗略地分割,引号在那里并不特殊。

what is the correct way to invoke/format a command, stored as a string in a BASH variable, containing a quoted literal string

你假设两者之间没有区别

  1. 直接在 terminal/script
  2. 中输入命令
  3. 将完全相同的命令字符串存储到变量中,然后执行 $variable

但是还是有很多不同的!直接输入 bash 的命令比其他任何命令都要经过更多的处理步骤。这些步骤记录在 bash's manual:

  1. 标记化
    报价被解释。识别操作员。该命令在未加引号的部分之间的空白处拆分为单词。 IFS这里不用了。
  2. 几个 展开 从左到右的方式。也就是说,在将这些转换之一应用到标记后,bash 将继续使用 3 处理其结果。例如,您可以安全地使用主目录,其路径名中包含文字 $ 作为扩展 ~ 的结果不会进行变量扩展,因此 $ 仍未解释。
    • 大括号扩展{1..9}
    • 波浪线扩展~
    • 参数和变量扩展$var
    • 算术展开$((...))
    • 命令替换 $(...)`...`
    • 进程替换<()
  3. 分词
    使用 IFS.
  4. 拆分未引用扩展的结果
  5. 文件名扩展
    也称为 globbing:*?[...] 以及 shopt -s extglob.

诚然,这让大多数 bash 初学者感到困惑。在我看来,Whosebug 的大多数 bash 问题都与这些处理步骤有关。一些经典的例子是for i in {1..$n} does not work and

来自未加引号的变量的字符串只经过上面列出的一些处理步骤。如上所述,这些步骤是 "3. 分词""4. 文件名扩展".

如果要将所有处理步骤应用于字符串,可以使用eval 命令。但是,这是非常不受欢迎的,因为有更好的选择(如果您自己定义命令)或巨大的安全隐患(如果外部人员定义命令)。

在您的示例中,我根本看不出存储命令的理由。但是如果你真的想在其他地方以字符串的形式访问它,那么使用数组:

command=(node source/main.js --input-regex-string 'pcre/(simple)? regex/replace/vim' -o)
echo "${command[*]}" # print
"${command[@]}"      # execute