提问人:deadbeef 提问时间:11/24/2017 最后编辑:codeforesterdeadbeef 更新时间:11/9/2023 访问量:3646
循环访问带引号的字符串列表
Iterate over a list of quoted strings
问:
我正在尝试对字符串列表运行一个 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"
答:
使用数组而不是普通变量。
arr=(foo "bar_no_space" "baz with space")
要打印值,请执行以下操作:
print '%s\n' "${arr[@]}"
并调用您的命令:
my-command --arg "${arr[0]}" --arg "${arr[1]}" --arg "{$arr[2]}"
评论
STRING
你能试试这样的事情吗:
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
评论
string='foo "bar_with_\"" "baz with space"'
已解决: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
无论哪种情况,它都不支持任意执行代码 - 参数仍被转义。
纯 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
}
我可能会也可能不会在以下位置更新:
这似乎是一件相当愚蠢的事情......但我有痒......那好吧。
评论
这是一种没有字符串数组或其他困难的方法(但有 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
评论
eval
;
eval
我知道你的问题是关于 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 脚本的精神的(:
评论
STRING
不是带引号的字符串列表;它是一个字符串。在该字符串中,引号的含义不比任何其他字符多。bash -c '...'
eval
bash -c '...'