Search code examples
arraysbashprocess-substitution

Process Substitution For Each Array Entry, Without Eval


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).


Solution

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