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