我有一个命令输出一些我想传递给另一个命令的参数。但是,当我在子 shell 中运行该命令时,输出受单词拆分除非我引用整个内容,在这种情况下它只是一个单词。
我希望子 shell 输出有单词拆分,但我希望单词拆分时尊重引号,但事实并非如此。除了 之外,还有其他方法eval
可以将子 shell 输出拆分为单词但尊重引号吗?
细节
鉴于args
命令定义为
#!/bin/sh -
printf "%d args:" "$#"
printf " <%s>" "$@"
echo
我可以跑
$ args 'foo bar' 'one two'
2 args: <foo bar> <one two>
但是我找不到让子 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'>
当然,我可以使用eval
$ eval args $(echo "'foo bar' 'one two'")
2 args: <foo bar> <one two>
但eval
这样做很危险,而且会引入各种其他可怕的错误可能性。我不希望参数扩展或通配符再次发生,等等。我只希望分词能够尊重引号。真的没有办法做到这一点吗bash
?
答案1
Bash 确实没有很好的方法将字符串解析为子字符串,同时又不影响引号。无论它是来自命令扩展(即$( )
-- 我认为您称之为子 shell)还是普通变量扩展 ( $varname
)。
- 如果您用双引号括住扩展名,则根本不会进行拆分。
- 如果您没有对扩展名使用双引号,它会在空格处进行拆分,但不会注意引号或转义符。它还会尝试扩展任何看起来像文件名通配符的内容,这可能会导致喜剧和/或悲剧。
- 如果你使用
eval
双引号扩展,它会解析全部;
shell 语法,包括其他命令和变量扩展、重定向、带有或 的多个命令&
等。这里有很多导致不良结果的机会。 - 如果你
eval
在非引号扩展中使用,则会得到 split-on-whitespace-and-expand-wildcards 效果,关注通过常规的完整解析。这里几乎所有事情都可能出错。 read -a
是众多糟糕产品中最好的一款。它完全无法理解引号的含义,但至少它不会扩展文件名通配符。
因此 bash 本身无法做到这一点。但xargs
可以——其默认的拆分成单词解析尊重引号和转义符,因此根据具体情况,您可能能够直接使用它:
$ echo "\"'foo bar'\" 'one two'" | xargs args
2 args: <'foo bar'> <one two>
这样做有几个潜在的问题:首先,根据输出量,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>
请注意,这使用 进行进程替换<( )
。这是 bash 独有的功能,即使 bash 处于 sh 模拟模式,也不会起作用。因此,您必须使用显式 bash shebang(#!/bin/bash
或#!/usr/bin/env bash
)启动脚本(并且不要通过使用 运行脚本来覆盖 shebang sh scriptname
)。