Charles Duffy
Charles Duffy

Reputation: 295403

How can I dynamically substitute array entries for an arbitrary command parameter?

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

Answers (2)

PesaThe
PesaThe

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

Charles Duffy
Charles Duffy

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.


Portability Modifications (Adapting for Bash 3.2 Compatibility)

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

Related Questions