【问题标题】:Reading quoted/escaped arguments correctly from a string从字符串中正确读取引用/转义的参数
【发布时间】:2014-11-21 22:02:58
【问题描述】:

我在将参数传递给 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”周围没有引号是吗?

【问题讨论】:

标签: bash shell sh


【解决方案1】:

丑陋的想法警报:纯 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
}

我可能会也可能不会在以下位置保持更新:

这似乎是一件相当愚蠢的事情......但我很痒......哦,好吧。

【讨论】:

    【解决方案2】:

    一些介绍性的话

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

    • 很难进行一致的解析:不同的 shell 有不同的扩展,不同的非 shell 实现实现不同的子集(请参阅下面的 shlexxargs 之间的增量)。
    • 很难以编程方式生成。 ksh 和 bash 有 printf '%q',它将生成一个带有任意变量内容的 shell 引用字符串,但在 POSIX sh 标准中不存在与此等效的字符串。
    • 很容易解析糟糕。许多使用这种格式的人都使用eval,这有很大的安全问题。

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


    xargs,带有 bashisms

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

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

    ...将$ARGS解析后的内容放入数组array。如果您想改为从文件中读取,请将 &lt;filename 替换为 &lt;&lt;&lt;"$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 &lt;argfile 更安全,因为如果有更多或更长的参数超出了可以容纳的范围,它会抛出错误,而不是将多余的参数作为单独的命令运行。


    Python shlex -- 而不是 xargs -- 带有 bashisms

    如果您需要比 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;默认情况下,引号对 xargs 来说是特殊的,除非您使用 -0 选项
    • 公平点——xargs 在这种情况下确实不尊重 POSIX sh 行为。使用 Python 2 的 shlex.split() 函数替代更新。
    • 最好的办法是让 OP 回到正确的轨道上:如果 OP 需要这样的东西,那么他们的设计显然是错误的。除非 OP 正在编写 shell(或 cli),否则在这种情况下,语言的选择是错误的。无论哪种方式,OP 都做错了。
    • @gniourf_gniourf,...理论上,我同意。在实践中,人们往往会得到没有意义的要求(或者需要与长期存在的系统/实践兼容——有 ton 的 Java 应用程序遗留启动脚本依赖于 @ 987654339@ing 参数列表以 shell 引用的形式给出),并且能够充分利用糟糕的情况是有价值的......当然,“不要那样做”应该是给出的建议的一部分。
    • @gniourf_gniourf, ...我已在适当的介绍中进行了编辑。
    【解决方案3】:

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

    ## 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 将作为基于字符串的命令行处理的一部分执行。
    • 我不能确定我对exec 的断言。但我坚持我的说法,“Bash 字符串处理非常混乱”
    【解决方案4】:

    内嵌引号不保护空格;他们按字面意思对待。在bash中使用数组:

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

    在 POSIX shell 中,您无法使用 eval(这就是大多数 shell 支持数组的原因)。

    args='"hi there" test'
    eval "./swap $args"
    

    像往常一样,非常确保您知道$args 的内容,并了解在使用eval 之前如何解析结果字符串。

    【讨论】:

    • 我倾向于认为eval 这里有更多选项。即使在 POSIX 中,也可以从 xargs 读入"$@"
    猜你喜欢
    • 2014-08-02
    • 2019-07-22
    • 2017-01-07
    • 2023-03-25
    • 2022-01-12
    • 1970-01-01
    • 2011-01-05
    相关资源
    最近更新 更多