在分词子shell输出时让bash尊重引号

Old*_*Pro 2 bash bash-scripting

我有一个命令,它输出一些我想传递给另一个命令的参数。但是,当我在子 shell 中运行命令时,输出会受到分词,除非我引用整个内容,在这种情况下,它是单个词。

我希望 subshel​​l 输出拆分单词,但我希望单词拆分尊重引号,但事实并非如此。有什么方法(除了eval)可以将子shell输出拆分为单词但尊重引号?

细节

鉴于args命令定义为

#!/bin/sh -
printf "%d args:" "$#"
printf " <%s>" "$@"
echo
Run Code Online (Sandbox Code Playgroud)

我可以跑

$ args 'foo bar' 'one two'
2 args: <foo bar> <one two>
Run Code Online (Sandbox Code Playgroud)

但我找不到一种方法让子shell 传递 2 个参数。

$ echo "\"'foo bar'\" 'one two'"
"'foo bar'" 'one two'
$ args $(echo "\"'foo bar'\" 'one two'")
4 args: <"'foo> <bar'"> <'one> <two'>
$ args "$(echo "\"'foo bar'\" 'one two'")"
1 args: <"'foo bar'" 'one two'>
Run Code Online (Sandbox Code Playgroud)

当然,我可以使用 eval

$  eval args $(echo "'foo bar' 'one two'")
2 args: <foo bar> <one two>
Run Code Online (Sandbox Code Playgroud)

但它eval是危险的,并引入了各种其他可怕的事情出错的可能性。我不希望再次发生参数扩展或通配符等。我只希望分词以尊重引号。真的没有办法做到这一点bash吗?

Gor*_*son 6

Bash 确实没有一个很好的方法来将字符串解析为子字符串,同时尊重引号。无论它是来自命令扩展(也就是说,$( )我认为你称之为子shell)还是普通的变量扩展($varname)。

  • 如果双引号扩展,则根本不进行拆分。
  • 如果您不双引号扩展,它会在空格处拆分,但不会注意引号或转义符。它还尝试扩展任何看起来像文件名通配符的内容,这可能会导致喜剧和/或悲剧。
  • 如果您eval在双引号扩展上使用,它会解析所有shell 语法,包括其他命令和变量扩展、重定向、多个带有;or 的命令&等。这里有很多机会导致错误的结果。
  • 如果您eval在非引用扩展上使用,则会获得拆分空白和扩展通配符的效果,然后是常规的完整解析。在这里,几乎一切都可能出错。
  • read -a是最好的一个坏很多。它在尊重引号方面完全失败,但至少它没有扩展文件名通配符。

所以bash本身无法做到这一点。但是xargs可以 - 它的默认分词解析尊重引号和转义符,因此根据情况您可以直接使用它:

$ echo "\"'foo bar'\" 'one two'" | xargs args
2 args: <'foo bar'> <one two>
Run Code Online (Sandbox Code Playgroud)

这有几个潜在的问题:一方面,根据有多少输出,xargs可能决定在命令的多次运行之间拆分它们。您可以调整这与它在一定程度上-n-s-x选项,但它并不完全令人满意。

另一个可能的问题是,如果命令实际上是要在当前 shell 中执行的 shell 函数、复杂命令或内置命令,这将不起作用。你可以适应它,但它很乱;您需要使用xargs printf '%s\0'转换为以空分隔的字符串序列,然后使用while IFS= read -r -d ''循环将其转换为 bash 数组,最后您可以对数组执行某些操作:

$ argarray=()
$ while IFS= read -r -d '' arg; do argarray+=("$arg"); done < <(echo "\"'foo bar'\" 'one two'" | xargs printf '%s\0')
$ args "${argarray[@]}"
2 args: <'foo bar'> <one two>
Run Code Online (Sandbox Code Playgroud)

请注意,这使用了一个进程替换<( )。这是一个仅限 bash 的功能,当 bash 处于 sh-emulation 模式时甚至不起作用。因此,您必须使用显式 bash shebang (#!/bin/bash#!/usr/bin/env bash)启动脚本(并且不要通过使用 运行脚本来覆盖 shebang sh scriptname)。

  • @Noein如果您遇到这种情况不起作用的情况,那么可能值得提出一个新问题,其中包含使您的情况复杂化的具体细节。 (3认同)