每个数组条目的进程替换,无需使用Eval

4

我有一个任意字符串的数组,例如 a=(1st "2nd string" $'3rd\nstring\n' ...)


我想将这些字符串传递给一个解释其参数为文件的命令,例如 paste

对于固定数量的变量,我们可以使用进程替换。

paste <(printf %s "$var1") <(printf %s "$var2") <(printf %s "$var3")

但是只有在变量数量预先知道的情况下,才能使用这种方法。
对于数组a,我们可以编写一些相对安全的代码,例如:

eval paste $(printf '<(printf %%s %q) ' "${a[@]}")

感兴趣的是:有没有一种方法可以处理替换a的每个条目,而不使用eval?请记住,a的条目可以包含任何字符(除了\0,因为bash不支持它)。


printf %s "$var1""$var1"在实际上是等价的,这些只是更复杂命令的占位符吗? - chepner
也许我错了,但对我来说 printf似乎是必要的。这个问题中唯一的占位符是 pastea内部的值。如果不用 <(printf %s "$var1") ,你会如何模拟一个与 $ var1 内容相同的文件? - Socowi
哦,没错,你想要$var1作为(伪)匿名文件的内容 - chepner
2个回答

5
这是使用递归一次设置一个参数列表的示例,该技术偶尔非常有用。
使用进程替换将文本转换为管道可能不是解决问题的最佳方案,但它具有重复使用现有工具的优点。
我试图使代码相对通用,但可能需要进行一些调整。
需要Bash 4.3才能使用nameref(如果您尚未达到该版本,则可以使用固定数组名称)。 Namerefs需要谨慎使用,因为它们不卫生;局部变量可以被名称捕获。因此,使用下划线开头的变量名。
# A wrapper which sets up for the recursive call
from_array() {
  local -n _array=$1
  local -a _cmd=("${@:2}")
  local -i _count=${#_array[@]}
  from_array_helper
}

# A recursive function to create the process substitutions.
# Each invocation adds one process substitution to the argument
# list, working from the end.
from_array_helper() {
  if (($_count)); then
    ((--_count))
    from_array_helper <(printf %s "${_array[_count]}") "$@"
  else
    "${_cmd[@]}" "$@"
  fi
}

例子

$ a=($'first\nsecond\n' $'x\ny\n' $'27\n35\n')
$ from_array a paste -d :
first:x:27
second:y:35

好主意,谢谢!我使用了你的方法创建了一个版本,避免了可能的名称冲突。但是,两个版本都有点长。我保持这个问题开放,以鼓励人们找到更好的解决方案。 - Socowi

1
这个解决方案受到 rici的答案 的启发。它解决了由namerefs可能引起的名称冲突,但需要用户指定一个在要执行的命令中不出现的分隔符。尽管如此,该分隔符可以在数组中出现而没有问题。
# Search a string in an array
# and print the 0-based index of the first identical element.
# Usage: indexOf STRING "${ARRAY[@]}"
# Exits with status 1 if the array does not contain such an element.
indexOf() {
    search="$1"
    i=0
    while shift; do
        [[ "$1" = "$search" ]] && echo "$i" && return
        ((++i))
    done
    return 1
}

# Execute a command and replace its last arguments by anonymous files.
# Usage: emulateFiles DELIMITER COMMAND [OPTION]... DELIMITER [ARGUMENT]...
# DELIMITER must differ from COMMAND and its OPTIONS.
# Arguments after the 2nd occurrence of DELIMITER are replaced by anonymous files.
emulateFiles() {
    delim="$1"
    shift
    i="$(indexOf "$delim" "$@")" || return 2
    cmd=("${@:1:i}")
    strings=("${@:i+2}")
    if [[ "${#strings[@]}" = 0 ]]; then
        "${cmd[@]}"
    else
        emulateFiles "$delim" "${cmd[@]}" <(printf %s "${strings[0]}") \
                     "$delim" "${strings[@]:1}"
    fi
}

使用示例
a=($'a b\n c ' $'x\ny\nz\n' : '*')
$ emulateFiles : paste : "${a[@]}"
a b x   :   *
 c  y       
    z       
$ emulateFiles : paste -d: : "${a[@]}"   # works because -d: != :
a b:x:::*
 c :y::
:z::
$ emulateFiles delim paste -d : delim "${a[@]}"
a b:x:::*
 c :y::
:z::

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