循环访问带引号的字符串列表

Iterate over a list of quoted strings

提问人:deadbeef 提问时间:11/24/2017 最后编辑:codeforesterdeadbeef 更新时间:11/9/2023 访问量:3646

问:

我正在尝试对字符串列表运行一个 for 循环,其中一些字符串被引用,而另一些则不是这样:

STRING='foo "bar_no_space" "baz with space"'
for item in $STRING; do
    echo "$item"
done

预期结果:

foo
bar_no_space
baz with space

实际结果:

foo
"bar_no_space"
"baz
with
space"

我可以通过运行以下命令来实现预期的结果:

bash -c 'for item in '"$STRING"'; do echo "$item"; done;'

我想在不生成新的 bash 进程或使用 eval 的情况下做到这一点,因为我不想冒执行随机命令的风险。

请注意,我不控制 STRING 变量的定义,我通过环境变量接收它。所以我不能写这样的东西:

array=(foo "bar_no_space" "baz with space")
for item in "${array[@]}"; do
    echo "$item"
done

如果有帮助,我实际上要做的是将字符串拆分为可以传递给另一个命令的参数列表。

我有:

STRING='foo "bar_no_space" "baz with space"'

我想跑:

my-command --arg foo --arg "bar_no_space" --arg "baz with space"
Bash 行情

评论

2赞 chepner 11/24/2017
STRING不是带引号的字符串列表;它是一个字符串。在该字符串中,引号的含义不比任何其他字符多。
0赞 chepner 11/24/2017
请注意,这并不比 ;您仍在执行任意代码。bash -c '...'eval
0赞 deadbeef 11/24/2017
@chepner我知道会执行代码。基本上,我希望 bash 能够公开一种以内部方式解析字符串的方法(因为它必须以某种方式这样做),但没有所有其他解释功能。我想没有这样的事情,我必须在 bash 之外使用解析方法。不过,我真的不明白所有的反对票。人们是因为没有答案而投票否决的吗?bash -c '...'

答:

3赞 codeforester 11/24/2017 #1

使用数组而不是普通变量。

arr=(foo "bar_no_space" "baz with space")

要打印值,请执行以下操作:

print '%s\n' "${arr[@]}"

并调用您的命令:

my-command --arg "${arr[0]}" --arg "${arr[1]}" --arg "{$arr[2]}"

评论

1赞 deadbeef 11/24/2017
我不控制 的定义,我通过环境变量接收它。STRING
2赞 chepner 11/24/2017
@deadbeef 那么,无论谁决定在单个字符串中传递任意字符串列表,谁就犯了一个错误。设计被打破了。
0赞 zaTricky 4/19/2021
将路径作为 bash 脚本的参数恰好描述了这种情况。设计没有被破坏
0赞 abhishek phukan 11/24/2017 #2

你能试试这样的事情吗:

sh-4.4$ echo $string                                                                                                                                                                
foo "bar_no_space" "baz with space"                                                                                                                                                 
sh-4.4$ echo $string|awk 'BEGIN{FS="\""}{for(i=1;i<NF;i++)print $i}'|sed '/^ $/d'                                                                                                   
foo                                                                                                                                                                                 
bar_no_space                                                                                                                                                                        
baz with space                                                                                                                                                                      

评论

0赞 chepner 11/24/2017
这在一般情况下是行不通的:string='foo "bar_with_\"" "baz with space"'
0赞 abhishek phukan 11/25/2017
@chepner没有尝试过一般的字符串.i将他提供的字符串作为示例字符串。谢谢你指出来
2赞 coolaj86 6/2/2019 #3

已解决:xargs + subshell

晚了几年参加聚会,但是......

恶意输入:

SSH_ORIGINAL_COMMAND='echo "hello world" foo '"'"'bar'"'"'; sudo ls -lah /; say -v Ting-Ting "evil cackle"'

注意:我最初有一个,但后来我意识到在测试脚本的变体时,这将是一个灾难的秘诀。rm -rf

完美地转换为安全参数:

# DO NOT put IFS= on its own line
IFS=$'\r\n' GLOBIGNORE='*' args=($(echo "$SSH_ORIGINAL_COMMAND" \
  | xargs bash -c 'for arg in "$@"; do echo "$arg"; done'))
echo "${args[@]}"

看到你确实可以像这样传递这些参数:$@

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

输出:

hello world
foo
bar;
sudo
rm
-rf
/;
say
-v
Ting-Ting
evil cackle

我不好意思说我花了多少时间研究这个问题来弄清楚,但是一旦你发痒了......你知道吗?

击败 xargs

可以通过提供转义引号来欺骗 xargs:

SSH_ORIGINAL_COMMAND='\"hello world\"'

这可以使文字引用成为输出的一部分:

"hello
world"

否则,它可能会导致错误:

SSH_ORIGINAL_COMMAND='\"hello world"'
xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option

无论哪种情况,它都不支持任意执行代码 - 参数仍被转义。

1赞 coolaj86 6/2/2019 #4

纯 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 *
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赞 Sz. 11/4/2023
天哪...... :) +1 为的乐趣......我能感觉到/分享你的动机,但我甚至不愿意看那些代码。当然,不是因为你,而是因为 bash。(其实我刚刚做了......无法抗拒。甚至没有那么可怕......
1赞 aralex 10/7/2021 #5

这是一种没有字符串数组或其他困难的方法(但有 bash 调用和):eval

STRING='foo "bar_no_space" "baz with space"'
eval "bash -c 'while [ -n \"\$1\" ]; do echo \$1; shift; done' -- $STRING"

输出:

foo
bar_no_space
baz with space

如果你想用字符串做一些更困难的事情,那么你可以拆分你的脚本:echo

split_qstrings.sh

#!/bin/bash
while [ -n "$1" ]
do
    echo "$1"
    shift
done

另一部分处理难度更大(例如字符大写):a

STRING='foo "bar_no_space" "baz with space"'

eval "split_qstrings.sh $STRING" | while read line 
do
   echo "$line" | sed 's/a/A/g'
done

输出:

foo
bAr_no_spAce
bAz with spAce

评论

0赞 jwd 11/9/2023
如果其他人不清楚:这种方法(使用 )的缺点是,如果输入字符串是恶意的,它可以在您的系统上运行任意命令。例如:字符串后面可能跟着额外的命令,这些命令将运行。eval;eval
0赞 jwd 11/9/2023 #6

我知道你的问题是关于 Bash 的,但由于它经常在相同的地方可用,你可以看看 Perl 的内置 Text::P arseWords 模块来完成繁重的工作,将结果发送回 Bash。

例如:

#!/usr/bin/env bash

STRING='foo "bar_no_space" "baz with space"'
readarray -t arr < <( perl -MText::ParseWords -e '$,="\n"; print shellwords(@ARGV),"";' "$STRING" )

for item in "${arr[@]}"; do
    echo "$item"
done

# prints:
#   foo
#   bar_no_space
#   baz with space

正如所写的那样,它使用换行符作为分隔符,但你可以做任何你想做的事。 特别是,如果你的输入字符串本身包含原始换行符(例如,在引号内),你可以很容易地使用分隔符或其他东西。NUL

事实上,我现在要这样做:

#!/usr/bin/env bash

STRING='foo "bar with space" "baz\
with\
newline"'
readarray -t -d '' arr < <( perl -MText::ParseWords -e '$,="\0"; print shellwords(@ARGV),"";' "$STRING" )

for item in "${arr[@]}"; do
    echo "item: $item"
done

# prints:
#   item: foo
#   item: bar with space
#   item: baz
#   with
#   newline

当然,如果你的输入字符串有原始字符,那么它仍然是一个潜在的问题(尽管不是安全问题)。NUL

另请注意:如果您的字符串包含不匹配的引号字符,则可能会得到意外结果。

尽管这不是纯粹的 Bash 代码,但我认为将“艰苦的工作”卸载给更适合任务的外部程序是符合 shell 脚本的精神的(: