Process Substitution For Each Array Entry, Without Eval

心已入冬 提交于 2019-12-12 14:09:24

问题


I have an array of arbitrary strings, for instance a=(1st "2nd string" $'3rd\nstring\n' ...).
I want to pass these strings to a command that interprets its arguments as files, for instance paste.

For a fixed number of variables, we could use process substitution

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

but that does only work if the number of variables is known beforehand.
For the array a, we could write something fairly safe like

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

Out of interest: Is there a way to process-substitute each of a's entries without using eval? Remember that a's entries can contain any character (except for \0 because bash doesn't support it).


回答1:


This is an example of how you can use recursion to set up an argument list one argument at a time. The technique is occasionally useful.

Using process substitution to turn text into a pipe is possibly not the optimal solution to the problem at hand, but it does have the virtue of reusing existing tools.

I tried to make the code reasonably general, but it's possible that some more adjustments would be need to made.

Bash 4.3 is needed for the nameref (although you could do it with a fixed array name if you haven't yet reached that version). Namerefs require caution because they are not hygienic; a local variable can be captured by name. Hence the use of variable names starting with underscores.

# 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
}

Example

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



回答2:


This solution is inspired by rici's answer. It resolves the possible name collision caused by namerefs, but requires the user to specify a delimiter that does not appear in the command to be executed. Nevertheless, the delimiter can appear in the array without problems.

# 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
}

Usage examples

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::


来源:https://stackoverflow.com/questions/51040310/process-substitution-for-each-array-entry-without-eval

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!