在GNU Parallel中访问关联数组

4

假设以下内容是Bash脚本:

declare -A ar='([one]="1" [two]="2" )'
declare -a ari='([0]="one" [1]="two")'
for i in ${!ari[@]}; do 
  echo $i ${ari[i]} ${ar[${ari[i]}]}
done
0 one 1
1 two 2

使用GNU Parallel是否可以做到同样的事情,确保使用关联数组的索引而不是序列?由于数组无法导出,这是否会使此操作变得困难或不可能?


可能是我没有理解你所寻找的内容,或者你点得太快了。请让我知道 :) - rici
Rici - 两个答案都帮助我并行处理数组,所以两个答案都是可以接受的。我没有意识到点击勾选标记会限制答案数量。感谢您详尽的回答! - Larry
3个回答

3

是的,这使得问题更加棘手。但并非不可能。

你无法直接导出一个数组。但是你可以使用declare -p将数组转换为该数组的描述,并将该描述存储在可导出的变量中。事实上,你可以将该描述存储在函数中并导出该函数,尽管这有点像一个hack,并且你必须处理在函数内执行declare命令会使声明的变量成为局部变量的事实,因此你需要在生成的declare函数中引入-g标志。

更新:自从Shellshock事件发生以来,上述hack方法已经无效了。这种主题的一种小变化确实有效。因此,如果你的bash已经更新,请跳到小标题“ShellShock版本”。

所以,这是一种可能的生成可导出函数的方式:

make_importer () {
  local func=$1; shift; 
  export $func='() {
    '"$(for arr in $@; do
          declare -p $arr|sed '1s/declare -./&g/'
        done)"'
  }'
}

现在我们可以创建数组并为它们构建一个导入器和导出器。
$ declare -A ar='([one]="1" [two]="2" )'
$ declare -a ari='([0]="one" [1]="two")'
$ make_importer ar_importer ar ari

看看我们所构建的东西

$ echo "$ar_importer"
() {
    declare -Ag ar='([one]="1" [two]="2" )'
declare -ag ari='([0]="one" [1]="two")'
  }

好的,这个格式有点丑,但这不是关于空格的问题。然而,这里有一个技巧。我们那里只有一个普通的(虽然是导出的)变量,但当它被导入到子shell中时,会发生一些魔法[注1]:

$ bash -c 'echo "$ar_importer"'

$ bash -c 'type ar_importer'
ar_importer is a function
ar_importer () 
{ 
    declare -Ag ar='([one]="1" [two]="2" )';
    declare -ag ari='([0]="one" [1]="two")'
}

现在看起来更漂亮了。 现在我们可以在给parallel的命令中运行它:

$ printf %s\\n ${!ari[@]} |
    parallel \
      'ar_importer; echo "{}" "${ari[{}]}" "${ar[${ari[{}]}]}"'
0 one 1
1 two 2

或者,在远程机器上执行:

$ printf %s\\n ${!ari[@]} |
    parallel -S localhost --env ar_importer \
      'ar_importer; echo "{}" "${ari[{}]}" "${ar[${ari[{}]}]}"'
0 one 1
1 two 2

ShellShock版本。

不幸的是,对shellshock的大量修复使得完成相同任务变得更加困难。特别地,现在需要将名为foo的函数导出为环境变量BASH_FUNC_foo%%,这是一个无效的名称(因为百分号)。但我们仍然可以定义该函数(使用eval)并导出它,如下所示:

make_importer () {
  local func=$1; shift; 
  # An alternative to eval is:
  #    . /dev/stdin <<< ...
  # but that is neither less nor more dangerous than eval.
  eval "$func"'() {
    '"$(for arr in $@; do
          declare -p $arr|sed '1s/declare -./&g/'
        done)"'
  }'
  export -f "$func"
}

如上所述,我们可以构建数组并制作导出器:

$ declare -A ar='([one]="1" [two]="2" )'
$ declare -a ari='([0]="one" [1]="two")'
$ make_importer ar_importer ar ari

但现在,在我们的环境中,这个函数实际上作为一个函数存在:
$ type ar_importer
ar_importer is a function
ar_importer () 
{ 
    declare -Ag ar='([one]="1" [two]="2" )';
    declare -ag ari='([0]="one" [1]="two")'
}

由于已经导出,我们可以在给parallel的命令中运行它:

$ printf %s\\n ${!ari[@]} |
    parallel \
      'ar_importer; echo "{}" "${ari[{}]}" "${ar[${ari[{}]}]}"'
0 one 1
1 two 2

不幸的是,它在远程机器上不再起作用(至少在我可以使用的 parallel 版本中),因为 parallel 不知道如何导出函数。如果这个问题得到解决,以下内容应该可以正常工作:

$ printf %s\\n ${!ari[@]} |
    parallel -S localhost --env ar_importer \
      'ar_importer; echo "{}" "${ari[{}]}" "${ar[${ari[{}]}]}"'

然而,有一个重要的警告:您不能将具有shellshock补丁的Bash中的函数导出到没有打补丁的Bash中,反之亦然。因此,即使`parallel`得到修复,远程机器必须运行与本地计算机相同的Bash版本。(或者至少,两者都必须有或都没有Shellshock补丁。)
注1:神奇的地方在于,`bash`将导出变量标记为函数的方式是导出值以`() {`开头。因此,如果您导出以这些字符开头并且是语法上正确的函数的变量,则`bash`子Shell将将其视为函数。(不过,请不要指望非`bash`子Shell会理解)

尝试使用“-vv -S localhost --env ar_importer”并行运行。你看到了多少个\呢 :-) - Ole Tange
@Larry:据我所知,Bash 没有提供任何机制来触发关联数组被修改时的操作。但如果您能解决这个问题,那么您可以按照您的建议分发修改。另一个可能性是将 declare 语句插入到文件中而不是创建函数,并使用类似 rsync 的机制分发该文件。一般来说,继承变量的 shell 方法是只读的,并且基于复制。我偶尔会(误)使用像 memcached 和 redis 这样的技术来使 shell 保持同步。 - rici
当然,在shellshock修复后,这个(make_importer)在bash中不再起作用。如果有新的方法,请告知。 - Larry
@Larry:好的,我添加了一个版本,可以在本地使用de-shellshocked bash。我需要看看是否有新的并行版本,或者我是否可以建议对并行进行补丁修复,以便它也可以在远程机器上工作。我正在努力避免表达关于shellshock修复的意见。 - rici
1
这个黑客的进一步开发版本现在已经成为env_parallel的一个重要组成部分(env_parallel是GNU Parallel的包装器,可以导出完整的环境)。 - Ole Tange
显示剩余7条评论

1
四年来发生了许多事情。GNU Parallel 20190222 带有 env_parallel。这是一个 shell 函数,它使得将大部分环境变量导出到 GNU Parallel 运行的命令中成为可能。
它支持 ashbashcshdashfishkshmkshpdkshshtcshzsh。不同的 shell 支持程度不同(详见 https://www.gnu.org/software/parallel/env_parallel.html)。对于 bash,您可以执行以下操作:
# Load the env_parallel function
. `which env_parallel.bash`
# Ignore variables currently defined
env_parallel --session
[... define your arrays, functions, aliases, and variables here ...]
env_parallel my_command ::: values
# The environment is also exported to remote systems if they use the same shell
(echo value1; echo value2) | env_parallel -Sserver1,server2 my_command
# Optional cleanup
env_parallel --end-session

所以在你的情况下,类似这样的东西:
env_parallel --session
declare -A ar='([one]="1" [two]="2" )'
declare -a ari='([0]="one" [1]="two")'
foo() {
  for i in ${!ari[@]}; do 
    echo $i ${ari[i]} ${ar[${ari[i]}]}
  done;
}
env_parallel foo ::: dummy
env_parallel --end-session

正如您所预期的那样,env_parallel 比纯粹的 parallel 稍微慢一些。

等等,什么?我想要并行执行循环,而不是让并行(或env_parallel)只运行一个执行循环的函数。这也可以工作:env_parallel 'echo {} ${ari[{}]} ${ar[${ari[{}]}]}' ::: ${!ari[@]} - Larry

1

GNU Parallel是一个Perl程序。如果Perl程序无法访问变量,那么我不认为变量可以通过Perl程序传递。

因此,如果您想并行化循环,我看到两个选项:

declare -A ar='([one]="1" [two]="2" )'
declare -a ari='([0]="one" [1]="two")'
for i in ${!ari[@]}; do 
  sem -j+0 echo $i ${ari[i]} ${ar[${ari[i]}]}
done
< p > sem 解决方案无法防止混合输出。

declare -A ar='([one]="1" [two]="2" )'
declare -a ari='([0]="one" [1]="two")'
for i in ${!ari[@]}; do 
  echo echo $i ${ari[i]} ${ar[${ari[i]}]}
done | parallel

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