从字符串中正确读取带引号/转义的参数

12

我遇到了一个问题,无法在Bash脚本中将参数传递给命令。

poc.sh:

#!/bin/bash

ARGS='"hi there" test'
./swap ${ARGS}

交换:

#!/bin/sh
echo "${2}" "${1}"

当前的输出为:

there" "hi

只更改poc.sh文件(因为我相信swap已经正确地实现了我想要的功能),如何让poc.sh将"hi there"作为两个参数进行传递,并且"hi there"没有引号包围?


这是BashFAQ#50的主题:http://mywiki.wooledge.org/BashFAQ/050 - Charles Duffy
4个回答

24

简短介绍

如果可能的话,不要使用 shell-quoted 字符串作为输入格式。

  • 解析一致性很难:不同的 shell 有不同的扩展,不同的非 shell 实现实现了不同的子集(请参阅下面 shlexxargs 之间的差异)。
  • 编程生成很困难。ksh 和 bash 有 printf '%q',它将生成一个带有任意变量内容的 shell-quoted 字符串,但在 POSIX sh 标准中不存在等效物。
  • 容易受到错误解析。许多人使用 eval 来处理这种格式,这存在重大安全问题。

NUL-delimited 流是更好的做法,因为它们可以精确地表示 任何 可能的 shell 数组或参数列表,没有任何歧义。


xargs,使用 bashisms

如果您从使用 shell 引用的人类生成的输入源获取参数列表,则可以考虑使用 xargs 解析它。例如:

array=( )
while IFS= read -r -d ''; do
  array+=( "$REPLY" )
done < <(xargs printf '%s\0' <<<"$ARGS")

swap "${array[@]}"

这将解析$ARGS的内容放入数组array中。如果你想从文件中读取,可以将<filename替换为<<<"$ARGS"。(下面假设使用文件输入以减少复杂性)。


xargs, POSIX兼容

如果您正在尝试编写符合POSIX sh标准的代码,则会变得更加棘手。(这里假设使用文件输入以降低复杂度):

# This does not work with entries containing literal newlines; you need bash for that.
run_with_args() {
  while IFS= read -r entry; do
    set -- "$@" "$entry"
  done
  "$@"
}
xargs printf '%s\n' <argfile | run_with_args ./swap

相较于运行xargs ./swap <argfile,这些方法更加安全,因为如果参数过多或过长而无法容纳,则会抛出错误,而不是将超出的参数作为单独的命令运行。


使用 Python 的 shlex 模块 -- 而非带有 bashisms 的 xargs

如果您需要比 xargs 实现更精准的 POSIX sh 解析,请考虑使用 Python 的 shlex 模块:

shlex_split() {
  python -c '
import shlex, sys
for item in shlex.split(sys.stdin.read()):
    sys.stdout.write(item + "\0")
'
}
while IFS= read -r -d ''; do
  array+=( "$REPLY" )
done < <(shlex_split <<<"$ARGS")

这似乎无法与 ARGS="\"a\\\"b\" c" 一起使用?错误报告 xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option - Cyker
最好的做法是让 OP 回到正确的轨道上:如果 OP 需要这样的东西,那么他们的设计显然是错误的。除非 OP 正在编写一个 shell(或 cli),否则选择语言是错误的。无论哪种方式,OP 都做错了。 - gniourf_gniourf
1
@gniourf_gniourf,我已经编辑了一个适当的介绍。 - Charles Duffy
如果你打算将参数列表折叠成一个字符串,最好使用args="$*"来更明确地表达这个意图。 - Charles Duffy
同样地,OPTS='-name "*.txt"' 的行为会因为本地目录内有哪些文件和本地的shell设置而不同(如果存在等效于nullglobfailglob的选项,则*.txt将被从选项列表中完全删除)。 - Charles Duffy
显示剩余6条评论

4

嵌套引号不能保护空格,它们会被视为字面量。在中使用数组:

args=( "hi there" test)
./swap "${args[@]}"

在POSIX shell中,你只能使用eval(这也是大多数shell支持数组的原因)。
args='"hi there" test'
eval "./swap $args"

通常情况下,在使用eval之前,一定要非常确信你已经了解了$args的内容,并且理解了生成的字符串将如何被解析。


1
我倾向于认为在这里有更多的选项而不是使用 eval。即使在 POSIX 中,也可以从 xargs 读取到 "$@" - Charles Duffy

2

丑陋的想法警告:纯Bash函数

这是一个纯Bash编写的引号字符串解析器(多么可怕的乐趣)!

注意:就像上面的xargs示例一样,在转义引号的情况下会出错。这可以修复...但在实际编程语言中做得更好。

示例用法

MY_ARGS="foo 'bar baz' qux * "'$(dangerous)'" sudo ls -lah"

# Create array from multi-line string
IFS=$'\r\n' GLOBIGNORE='*' args=($(parseargs "$MY_ARGS"))

# Show each of the arguments array
for arg in "${args[@]}"; do
    echo "$arg"
done

示例输出

foo
bar baz
qux
*

解析参数函数

该函数逐个字符地进行处理,将其添加到当前字符串或当前数组中。

set -u
set -e

# ParseArgs will parse a string that contains quoted strings the same as bash does
# (same as most other *nix shells do). This is secure in the sense that it doesn't do any
# executing or interpreting. However, it also doesn't do any escaping, so you shouldn't pass
# these strings to shells without escaping them.
parseargs() {
    notquote="-"
    str=$1
    declare -a args=()
    s=""

    # Strip leading space, then trailing space, then end with space.
    str="${str## }"
    str="${str%% }"
    str+=" "

    last_quote="${notquote}"
    is_space=""
    n=$(( ${#str} - 1 ))

    for ((i=0;i<=$n;i+=1)); do
        c="${str:$i:1}"

        # If we're ending a quote, break out and skip this character
        if [ "$c" == "$last_quote" ]; then
            last_quote=$notquote
            continue
        fi

        # If we're in a quote, count this character
        if [ "$last_quote" != "$notquote" ]; then
            s+=$c
            continue
        fi

        # If we encounter a quote, enter it and skip this character
        if [ "$c" == "'" ] || [ "$c" == '"' ]; then
            is_space=""
            last_quote=$c
            continue
        fi

        # If it's a space, store the string
        re="[[:space:]]+" # must be used as a var, not a literal
        if [[ $c =~ $re ]]; then
            if [ "0" == "$i" ] || [ -n "$is_space" ]; then
                echo continue $i $is_space
                continue
            fi
            is_space="true"
            args+=("$s")
            s=""
            continue
        fi

        is_space=""
        s+="$c"
    done

    if [ "$last_quote" != "$notquote" ]; then
        >&2 echo "error: quote not terminated"
        return 1
    fi

    for arg in "${args[@]}"; do
        echo "$arg"
    done
    return 0
}

我可能会或者不会在以下链接更新此内容:

似乎是一件相当愚蠢的事情...但我有这种冲动...哦,算了。


0

这可能不是最健壮的方法,但它很简单,并且似乎适用于您的情况:

## demonstration matching the question
$ ( ARGS='"hi there" test' ; ./swap ${ARGS} )
there" "hi

## simple solution, using 'xargs'
$ ( ARGS='"hi there" test' ; echo ${ARGS} |xargs ./swap )
test hi there

注释:Bash字符串处理非常令人困惑。xargs通过直接通过exec传递参数来绕过Bash在基于字符串的命令行处理中执行的正常、反复无常的字符串处理,从而消除了猜测的工作。 - Brent Bradburn
2
我不能确定关于exec的断言。但是我坚持认为“Bash字符串处理非常令人困惑” - Brent Bradburn
1
xargs 的处理(当没有使用 -0 或 GNU 扩展 -d 时)也是相当反复无常的,虽然它大多数情况下与 shell 兼容,但并非完全兼容。此外,echo ${XARGS} 有其自身的 bug,这些 bug 取决于您使用的 echo 版本;printf '%s\n' "$XARGS" 更加可靠 - Charles Duffy
“不是100%确定”,例如,在xargs中在换行符之前加上反斜杠会导致错误,而在shell中,这要么忽略换行符,要么将其作为字面数据处理,具体取决于上下文。 - Charles Duffy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接