Reputation: 295403
This question is inspired by bash nested variable in for loop.
If I have an array in bash, and I want to be able to run an arbitrary command for every element of that array, is there a way to do that via a generic function, as opposed to with a loop? That is:
dest_array=( host1:/foo host2:/bar host3:/baz )
copy ./file dest_array
and have each expansion called:
copy ./file host1:/foo
copy ./file host2:/bar
copy ./file host3:/baz
Even better, is there a way to do this for multiple arrays? For instance:
sources=( first second )
dests=( host1:/foo host2:/bar )
copy sources dests
invoking (in no particular order):
copy first host1:/foo
copy first host2:/bar
copy second host1:/foo
copy second host2:/bar
Upvotes: 1
Views: 96
Reputation: 7499
What do you think about this solution:
run_for() {
local -n _sources=$1; shift
local src_label=$1; shift
local -n _dests=$1; shift
local dst_label=$1; shift
local -a cmd=( "$@" ) execute
local retval=0
for src_item in "${_sources[@]}"; do
for dst_item in "${_dests[@]}"; do
execute=()
for cmd_item in "${cmd[@]}"; do
case $cmd_item in
$src_label) execute+=("$src_item") ;;
$dst_label) execute+=("$dst_item") ;;
*) execute+=("$cmd_item") ;;
esac
done
"${execute[@]}" || (( retval |= $? ))
done
done
return "$retval"
}
This solution also works with bash
4.3 or later and uses for
loop 3 times (not really pretty). However, it has a more straightforward usage and handles the following case correctly:
sources=( first second DEST )
dests=( host1 host2 host3 )
run_for sources SOURCE dests DEST echo "<<" SOURCE - DEST ">>"
This solution cannot handle the following:
run_for sources SOURCE dests DEST echo aaSOURCE - DESTaa
Although, I would not really consider aaSOURCE
to be a valid label. If it is, the following could, in my opinion, copy the behavior of your solution while still preserving the more straightforward usage. It will also have the same drawback when one of the sources is equal to $dst_label
:
for src_item in "${_sources[@]}"; do
tmp=("${cmd[@]//$src_label/$src_item}")
for dst_item in "${_dests[@]}"; do
execute=("${tmp[@]//$dst_label/$dst_item}")
"${execute[@]}" || (( retval |= $? ))
done
done
Upvotes: 1
Reputation: 295403
Consider the following function, written for bash 4.3 or later:
run_for_each() {
local -n _items=$1; shift
local sigil=$1; shift
local -a args=( "$@" )
local -a call
local retval=0
for item in "${_items[@]}"; do
call=( "${args[@]//$sigil/$item}" )
"${call[@]}" || (( retval |= $? ))
done
return "$retval"
}
As an example of usage:
sources=( first second )
dests=( host1:/foo host2:/bar )
run_for_each sources SOURCE \
run_for_each dests DEST \
rsync -Pv SOURCE DEST
If you wanted to make it concurrent, that might look like:
run_for_each_concurrent() {
local -n _items=$1; shift
local sigil=$1; shift
local -a args=( "$@" )
local -a pids=( )
local -a call
local retval=0
for item in "${_items[@]}"; do
call=( "${args[@]//$sigil/$item}" )
"${call[@]}" & pids+=( "$!" )
done
for pid in "${pids[@]}"; do
wait "$pid" || (( retval |= $? ))
done
return "$retval"
}
...which will run one process per array entry, all at the same time; wait for them all to exit; and return the ORed-together exit status of all those subprocesses.
By the way -- if you don't have bash 4.3, the above can be made to work with older releases by replacing this line:
local -n _items=$1; shift
with the following instead:
printf -v cmd 'local -a _items=( "${%q[@]}" )' "$1" && eval "$cmd"; shift
Upvotes: 2