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

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

我有一个命令输出一些我想传递给另一个命令的参数。但是,当我在子 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)。

相关内容