How to iterate over multiple variables and echo them using Shell Script?

Viewed 1319

Consider the below variables which are dynamic and might change each time. Sometimes there might even be 5 variables, But the length of all the variables will be the same every time.

var1='a b c d e... upto z'
var2='1 2 3 4 5... upto 26'
var3='I II III IV V... upto XXVI'

I am looking for a generalized approach to iterate the variables in a for loop & My desired output should be like below.

a,1,I
b,2,II
c,3,III
d,4,IV
e,5,V
.
.
goes on upto
z,26,XXVI

If I use nested loops, then I get all possible combinations which is not the expected outcome.

Also, I know how to make this work for 2 variables using for loop and shift using below link https://unix.stackexchange.com/questions/390283/how-to-iterate-two-variables-in-a-sh-script

4 Answers

With paste

paste -d , <(tr ' ' '\n' <<<"$var1") <(tr ' ' '\n' <<<"$var2") <(tr ' ' '\n' <<<"$var3")
a,1,I
b,2,II
c,3,III
d,4,IV
e...z,5...26,V...XXVI

But clearly having to add other parameter substitutions for more varN's is not scalable.

You need to "zip" two variables at a time.

var1='a b c d e...z'
var2='1 2 3 4 5...26'
var3='I II III IV V...XXVI'

zip_var1_var2 () {
    set $var1
    for v2 in $var2; do
        echo "$1,$v2"
        shift
    done
}

zip_var12_var3 () {
    set $(zip_var1_var2)
    for v3 in $var3; do
        echo "$1,$v3"
        shift
    done
}

for x in $(zip_var12_var3); do
  echo "$x"
done

If you are willing to use eval and are sure it is safe to do so, you can write a single function like

zip () {
    if [ $# -eq 1 ]; then
        eval echo \$$1
        return
    fi

    a1=$1
    shift
    x=$*

    set $(eval echo \$$a1)
    for v in $(zip $x); do
        printf '=== %s\n' "$1,$v" >&2
        echo "$1,$v"
        shift
    done
}

zip var1 var2 var3  # Note the arguments are the *names* of the variables to zip

If you can use arrays, then (for example, in bash)

var1=(a b c d e)
var2=(1 2 3 4 5)
var3=(I II III IV V)

for i in "${!var1[@]}"; do
    printf '%s,%s,%s\n' "${var1[i]}" "${var2[i]}" "${var3[i]}"
done

Use this Perl one-liner:

perl -le '@in = map { [split] } @ARGV; for $i ( 0..$#{ $in[0] } ) { print join ",", map { $in[$_][$i] } 0..$#in; }' "$var1" "$var2" "$var3"

Prints:

a,1,I
b,2,II
c,3,III
d,4,IV
e,5,V
z,26,XXVI

The Perl one-liner uses these command line flags:
-e : Tells Perl to look for code in-line, instead of in a file.
-l : Strip the input line separator ("\n" on *NIX by default) before executing the code in-line, and append it when printing.

The input variables must be quoted with double quotes "like so", to keep the blank-separated words from being treated as separate arguments.
@ARGV is an array of the command line arguments, here $var1, $var2, $var3.
@in is an array of 3 elements, each element being a reference to an array obtained as a result of splitting the corresponding element of @ARGV on whitespace. Note that split splits the string on whitespace by default, but you can specify a different delimiter, it accepts regexes.
The subsequent for loop prints @in elements separated by comma.

SEE ALSO:
perldoc perlrun: how to execute the Perl interpreter: command line switches
perldoc perlvar: Perl predefined variables

The following is (almost) a copy of this answer with a few tweaks that make it fit this question.

The Original Question

First let’s assign a few variables to play with, 26 tokens in each of them:

var1="$(echo {a..z})"
var2="$(echo {1..26})"
var3="$(echo I II III IV \
             V{,I,II,III} IX \
             X{,I,II,III} XIV \
             XV{,I,II,III} XIX \
             XX{,I,II,III} XXIV \
             XXV XXVI)"
var4="$(echo {A..Z})"
var5="$(echo {010101..262626..10101})"

Now we want a “magic” function that zips an arbitrary number of variables, ideally in pure Bash:

zip_vars var1       # a trivial test
zip_vars var{1..2}  # a slightly less trivial test
zip_vars var{1..3}  # the original question
zip_vars var{1..4}  # more vars, becasuse we can
zip_vars var{1..5}  # more vars, because why not

What could zip_vars look like? Here’s one in pure Bash, without any external commands:

zip_vars() {
  local var
  for var in "$@"; do
    local -a "array_${var}"
    local -n array_ref="array_${var}"
    array_ref=(${!var})
    local -ar "array_${var}"
  done
  local -n array_ref="array_${1}"
  local -ir size="${#array_ref[@]}"
  local -i i
  local output
  for ((i = 0; i < size; ++i)); do
    output=
    for var in "$@"; do
      local -n array_ref="array_${var}"
      output+=",${array_ref[i]}"
    done
    printf '%s\n' "${output:1}"
  done
}

How it works:

  1. It splits all variables (passed by reference (by variable name)) into arrays. For each variable varX it creates a local array array_varX.
    • It would be actually way easier if the input variables were already Bash arrays to start with (see below), but … we stick with the original question initially.
  2. It determines the size of the first array and then blindly expects all arrays to be of that size.
  3. For each index i from 0 to size - 1 it concatenates the ith elements of all arrays, separated by ,.

Arrays Make Things Easier

If you use Bash arrays from the very start, the script will be shorter and look simpler and there won’t be any string-to-array conversions.

zip_arrays() {
  local -n array_ref="$1"
  local -ir size="${#array_ref[@]}"
  local -i i
  local output
  for ((i = 0; i < size; ++i)); do
    output=
    for arr in "$@"; do
      local -n array_ref="$arr"
      output+=",${array_ref[i]}"
    done
    printf '%s\n' "${output:1}"
  done
}

arr1=({a..z})
arr2=({1..26})
arr3=(    I II III    IV
       V{,I,II,III}   IX
       X{,I,II,III}  XIV
      XV{,I,II,III}  XIX
      XX{,I,II,III} XXIV
                    XXV
                    XXVI)
arr4=({A..Z})
arr5=({010101..262626..10101})

zip_arrays arr1       # a trivial test
zip_arrays arr{1..2}  # a slightly less trivial test
zip_arrays arr{1..3}  # (almost) the original question
zip_arrays arr{1..4}  # more arrays, becasuse we can
zip_arrays arr{1..5}  # more arrays, because why not
Related