bash字符串引用多字参数到数组

Posted

技术标签:

【中文标题】bash字符串引用多字参数到数组【英文标题】:bash string quoted multi-word args to array 【发布时间】:2014-04-13 01:31:09 【问题描述】:

问题:

在 bash 脚本中,将包含围绕多个单词的文字引号的字符串转换为具有相同解析参数结果的数组的最佳方法是什么?

争议:

存在许多问题都采用规避策略来避免问题而不是找到解决方案,这个问题提出了以下论点,并希望鼓励读者专注于论点,如果您愿意,请参与挑战找到最优解。

提出的论点:

    尽管有很多场景应该避免这种模式,但由于存在更适合的替代解决方案,作者认为仍然存在有效的用例。这个问题将尝试产生一个这样的用例,但不声称它的可行性,只是说它是一个可以想象的场景,可能会出现在现实世界的情况下。 您必须找到满足要求的最佳解决方案。该用例是专门为其实际应用选择的。您可能不同意所做出的决定,但您并没有任务仅提供意见以提供解决方案。 无需修改输入或传输选择即可满足要求。两者都是根据真实世界场景专门选择的,以捍卫那些部分不受您控制的叙述。 没有针对特定问题的答案,本问题旨在解决该问题。如果您倾向于避免这种模式,那么只需避免该问题,但如果您认为自己已准备好迎接挑战,那么让我们看看您将如何解决问题。

有效用例:

将当前使用的现有脚本转换为通过命名管道或类似流接收参数。为了尽量减少对开发人员控制之外的无数脚本的影响,决定不更改界面。现有脚本必须能够像以前一样通过新的流实现传递相同的参数。

现有实现:

$ ./string2array arg1 arg2 arg3
args=(
    [0]="arg1"
    [1]="arg2"
    [2]="arg3"
)

需要更改:

$ echo "arg1 arg2 arg3" | ./string2array
args=(
    [0]="arg1"
    [1]="arg2"
    [2]="arg3"
)

问题:

正如Bash and Double-Quotes passing to argv 指出的那样,文字引号未按预期进行解析。

此工作台脚本可用于测试各种解决方案,它处理传输并制定可衡量的响应。建议您专注于使用字符串作为参数的解决方案脚本,并且应该将 $args 变量填充为数组。

string2array 工作台脚本:

#!/usr/bin/env bash
#string2arry

args=()

function inspect() 
  local inspct=$(declare -p args)
  inspct=$inspct//\[/\\n\\t[; inspct=$inspct//\'/; inspct="$inspct:0:-1\n)"
  echo -e $inspct#*-a 


while read -r; do
  # source the solution to turn $REPLY in $args array
  source $1 "$REPLY"
  inspect
done

标准解决方案 - 失败

将字符串转换为以空格分隔的单词数组的解决方案适用于我们上面的第一个示例:

#solution1

args=($@)

不良结果

不幸的是,标准解决方案对引用的多字参数产生了不希望的结果:

$ echo 'arg1 "multi arg 2" arg3' | ./string2array solution1
args=(
    [0]="arg1"
    [1]="\"multi"
    [2]="arg"
    [3]="2\""
    [4]="arg3"
)

挑战:

使用工作台脚本提供一个解决方案 sn-p,它将为收到的参数生成以下结果。

想要的结果:

$ echo 'arg1 "multi arg 2" arg3' | ./string2array solution-xyz
args=(
    [0]="arg1"
    [1]="multi arg 2"
    [2]="arg3"
)

解决方案应该在各个方面都与标准参数解析兼容。对于提供的解决方案,应通过以下单元测试。如果您能想到单元测试中当前缺少的任何内容,请发表评论,我们可以对其进行更新。

需求的单元测试

更新:测试简化并包括 Johnathan Leffer 测试

#!/usr/bin/env bash
#test_string2array
solution=$1
function test() 
  cmd="echo \"$1\" | ./string2array $solution"
  echo "$ $cmd"
  echo $1 | ./string2array $solution > /tmp/t
  cat /tmp/t
  echo -n "Result : "
  [[ $(cat /tmp/t|wc -l) -eq 7 ]] && echo "PASSED!" || echo "FAILED!"


echo 1. Testing single args
test 'arg1 arg2 arg3 arg4 arg5'
echo
echo 2. Testing multi args \" quoted
test 'arg1 "multi arg 2" arg3 "a r g 4" arg5'
echo
echo 3 Testing multi args \' quoted
test "arg1 'multi arg 2' arg3 'a r g 4' arg5"
echo
echo 4 Johnathan Leffer test
test "He said, \"Don't do that!\" but \"they didn't listen.\""

【问题讨论】:

好吧,你没有像 "He said, \"Don't do that!\", but they didn't listen."'$var''$(cmd)' 符号这样的参数。 必须稍微修改字符串才能成为有效的参数。看看你是否同意... 【参考方案1】:

declare 内置似乎可以做你想做的事;在我的测试中,您的 inspect 函数似乎无法正确测试所有输入:

# solution3
declare -a "args=($1)"

然后

$ echo "arg1 'arg2a arg2b' arg3" | while read -r; do
>  source solution3 "$REPLY"
>  for arg in "$args[@]"; do
>   echo "Arg $((++i)): $arg"
>  done
> done
Arg 1: arg1
Arg 2: arg2a arg2b
Arg 3: arg3

【讨论】:

您能否详细说明一下,inspect 功能与所需的解决方案无关,它只是简单地对 args 集合进行检查输出。如果你愿意,你可以用 `printf "%s\n" "$args[@]" 替换检查。它专门使用声明 -p 而不是声明 -a。 您应该实施自己的解决方案N 或改进任何现有的解决方案实施。 抱歉,您究竟要完成什么? declare 命令执行您所要求的操作:它采用给定的字符串并填充数组。据我所知,您只是在尝试枚举此命令的重新实现。 Stack Overflow 不是编程挑战网站。 好吧,我的错!我误解了你的实现。添加了单元测试结果和所有工作,除了 Johnathan Leffer 测试。反对票将被编辑,但由于某种原因,答案需要我进行编辑。 我看到了建议的测试。您没有引用嵌入的双引号,因此撇号不是在双引号字符串内,而是落在 字符串之外,因此引入了一个新的单引号字符串。我对答案进行了小幅修改,因此您可以根据需要撤销否决票。【参考方案2】:

您可以使用declare 而不是eval,例如:

代替:

string='"aString that may haveSpaces IN IT" bar foo "bamboo" "bam boo"'
echo "Initial string: $string"
eval 'for word in '$string'; do echo $word; done'

做:

declare -a "array=($string)"
for item in "$array[@]"; do echo "[$item]"; done

但请注意,如果输入来自用户并不会更安全!

所以,如果你尝试使用这样的字符串:

string='"aString that may haveSpaces IN IT" bar foo "bamboo" "bam boo" `hostname`'

你得到了hostname的评估(当然可能是rm -rf /之类的东西)!

非常简单的保护它的尝试只是替换像 backtrick ` 和 $: 之类的字符:

string='"aString that may haveSpaces IN IT" bar foo "bamboo" "bam boo" `hostname`'
declare -a "array=( $(echo $string | tr '`$<>' '????') )"
for item in "$array[@]"; do echo "[$item]"; done

现在你得到如下输出:

[aString that may haveSpaces IN IT]
[bar]
[foo]
[bamboo]
[bam boo]
[?hostname?]

有关使用不同方法的方法和优点的更多详细信息,您可能会在那个好的答案中找到:Why should eval be avoided in Bash, and what should I use instead?

另见https://superuser.com/questions/1066455/how-to-split-a-string-with-quotes-like-command-arguments-in-bash/1186997#1186997

但仍有攻击向量。 我非常希望在 bash 中使用双引号 (") 中的字符串引用方法,但不解释内容

【讨论】:

【参考方案3】:

所以我认为 xargs 实际上适用于您的所有测试用例,例如:

echo 'arg1 "multi arg 2" arg3' | xargs -0 ./string2array

【讨论】:

好主意!您如何将其放入解决方案文件中,以便可以针对单元测试运行它。 I.o.w.您有一个字符串变量,并使用 xargs 正确填充数组。 我的数组已经在$BASH_ARGV,我跑echo $#BASH_ARGV[@]; echo $BASH_ARGV[@]来测试它。在这里找到:***.com/a/2741116/3388817【参考方案4】:

第二次尝试

无需其他变量即可将元素附加到位。

#solution3
for i in $1; do
  [[ $i =~ ^[\"\'] ]] && args+=(' ')
  lst=$(( $#args[@]-1 ))
  [[ "$args[*]" =~ [[:space:]]$ ]] && args[$lst]+="$i/[\"\']/ " ||  args+=($i)
  [[ $i =~ [\"\']$ ]] && args[$lst]=$args[$lst]:1:-1
done

【讨论】:

【参考方案5】:

第一次尝试

一旦检测到开引号,就用组合词填充变量,只有在闭引号到达时才附加到数组中。

解决方案

#solution2
j=''
for a in $1; do
  if [ -n "$j" ]; then
    [[ $a =~ ^(.*)[\"\']$ ]] && 
      args+=("$j $BASH_REMATCH[1]")
      j=''
     || j+=" $a"
  elif [[ $a =~ ^[\"\'](.*)$ ]]; then
    j=$BASH_REMATCH[1]
  else
    args+=($a)
  fi
done

单元测试结果:

$ ./test_string2array solution2
1. Testing single args
$ echo "arg1 arg2 arg3 arg4 arg5" | ./string2array solution2
args=(
    [0]="arg1"
    [1]="arg2"
    [2]="arg3"
    [3]="arg4"
    [4]="arg5"
)
Result : PASSED!

2. Testing multi args " quoted
$ echo 'arg1 "multi arg 2" arg3 "a r g 4" arg5' | ./string2array solution2
args=(
    [0]="arg1"
    [1]="multi arg 2"
    [2]="arg3"
    [3]="a r g 4"
    [4]="arg5"
)
Result : PASSED!

3 Testing multi args ' quoted
$ echo "arg1 'multi arg 2' arg3 'a r g 4' arg5" | ./string2array solution2
args=(
    [0]="arg1"
    [1]="multi arg 2"
    [2]="arg3"
    [3]="a r g 4"
    [4]="arg5"
)
Result : PASSED!

【讨论】:

【参考方案6】:

就地修改

让 bash 将字符串转换为数组,然后循环修复它。

args=($@) cnt=$#args[@] idx=-1 chr=
for (( i=0; i<cnt; i++ )); do
  [[ $idx -lt 0 ]] && 
    [[ $args[$i]:0:1 =~ [\'\"] ]] && \
       idx=$i chr=$args[$idx]:0:1 args[$idx]="$args[$idx]:1"
    continue
  
  args[$idx]+=" $args[$i]"
  unset args[$i]
  [[ $args[$idx]: -1:1 == $chr ]] && args[$idx]=$args[$idx]:0:-1 idx=-1
done

【讨论】:

【参考方案7】:

修改分隔符

在此解决方案中,我们将空格转换为逗号,删除引号并重置多词参数的空格,以允许正确的参数解析。

#solution4
s=$*//[[:space:]]/\l
while [[ $s =~ [\"\']([^\"\']*)[\"\'] ]]; do
  s=$s/$BASH_REMATCH/$BASH_REMATCH[1]//\l/ 
done
IFS=\l
args=($s)

需要工作!!

【讨论】:

当原始参数中有逗号时,此解决方案会中断。 unmap 操作不是 map 操作的逆操作。 换行符根本不可能,所以也不需要检查它,Tx 用于发现 谁说参数中不允许换行?唯一不允许的字符是 ASCII null,我认为您不能将它们用作有用的分隔符。完整的血腥细节真的非常令人讨厌。我不确定是否有完整的解决方案,但我很佩服您尝试的尝试。我也没有想出解决方案——过去几十年我偶尔尝试过,但我最近没有认真尝试过使用 Bash 和数组。 (我最后一次认真的尝试主要是 Korn shell,而不是利用数组。) 换行不应该是可能的我没说不允许。这是不可能的,因为while read; 循环读取行,我们正在考虑解析行,因此不太可能包含换行符,同意吗?我很难接受你的“不要做那个测试”=) 在我意识到换行可能是我的解决方案(将包含在测试中)之前,我查看了一个固定的分隔符集合,甚至使用 printf '\x45' 的 ascii 十六进制范围,并替换了base 16 数字与参数字符串中当前使用的数字进行比较,确保 map ∝ unmap

以上是关于bash字符串引用多字参数到数组的主要内容,如果未能解决你的问题,请参考以下文章

使用正则表达式将字段拆分为数组的 Bash 脚本用于多字符分隔符

几个小编程题(数组去重,获取字符串中最多字符)

随机单词 Bash 脚本,如果提供一个数字作为第一个命令行参数,那么它将仅从具有那么多字符的单词中选择

数组去重,字符串查找最多字符方法总结

Bash编程之数组和字符串处理

bash复习