bashloopsiterationrangesequence

How to iterate over multiple ranges / sequences / numbers specified in var?


I can iterate over multiple ranges / sequences / numbers with constructions like:

for i in $(seq 1 3) 5 $(seq 7 9) 11; do echo $i; done
for i in {1..3} 5 {7..9} 11; do echo $i; done

But how to achieve the same result if ranges / numbers specified in var:

var="{1..3} 5 {7..9} 11"

I found that this can be done with eval and seq:

var="seq 1 3; echo 5; seq 7 9; echo 11"; for i in $(eval "$var"); do echo $i; done

Here shown that eval echo allows to use ranges in vars like (much nicer than the previous one):

var="{1..3} 5 {7..9} 11"; for i in $(eval echo "$var"); do echo $i; done

Is it possible to achieve the same result somehow without using eval? Or using eval in this particular case is OK?


Update

Since there are additional overhead with parsing ranges / numbers defined in plain string variable and since I have control over forming such parameter it will be also worth considering solutions for input array var defined like the following:

ranges=( '1 3' 5 '7 9' 11 )

Or even more straightforward and simpler for implementation (but a bit redundant in expression) with using also ranges instead of numbers:

ranges=( '1 3' '5 5' '7 9' '11 11' )

In case of using array with ranges as input string this question becomes pretty simple and boils down to double loop over array with ranges and over each range of this array.

With specifying ranges as array there will be no need of using eval or parsing string with ranges to array (that have the same but hidden danger of eval).


Solution

  • 1. First answer...

    You could use:

    var="{1..3} 5 {7..9} 11"
    
    declare -a "array=($var)"
    for i in "${array[@]}"; do
        echo $i
    done
    
    1
    2
    3
    5
    7
    8
    9
    11
    

    But care!! This work like an eval:

    var='{1..3} $(uptime) 6 7'
    echo "$var"
    
    {1..3} $(uptime) 6 7
    
    declare -a "array=($var)"
    for i in "${array[@]}"; do
        echo $i
    done
    
    1
    2
    3
    16:36:29
    up
    22
    days,
    4:54,
    23
    users,
    load
    average:
    2.48,
    1.89,
    1.83
    6
    7
    

    Note that using double quotes:

    var='{1..3} "$(uptime)" 6 7'
    

    Will produce more readable:

    1
    2
    3
     16:41:03 up 22 days, 4:59, 23 users, load average: 1.47, 2.09, 1.98
    6
    7
    

    About:

    Or using eval in this particular case is OK?

    This is your own responsibility! If you are knowing what you do, you are confident on where come $var content, then it could be ok.

    2. Without eval, you have do loops.

    If you want to avoid eval, you could use a function like:

    readRange() { # Usage: readRange "$var" <arrayName>
        local -a arin
        local elem
        local -i iter start end
        local -n arout=$2
        arout=()
        read -ra arin <<< "$1"
        for elem in "${arin[@]}"; do
            case $elem in
                '{'[0-9]*'..'*[0-9]'}' )
                    IFS='.{}' read -r _ start _ end <<< "$elem"
                    for ((iter=start; iter<=end; iter++)); do
                        arout+=($iter)
                    done
                ;;
                *[^0-9]* )
                    echo ERROR 1>&2
                    return -1
                ;;
                *)
                    arout+=($elem)
                ;;
            esac
        done
    }
    
    var="{1..3} 5 {7..9} 11"
    readRange "$var" resultArray
    for i in "${resultArray[@]}"; do
        echo "$i"
    done
    
    1
    2
    3
    5
    7
    8
    9
    11
    

    3. Or to use seq:

    readRange() { # Usage: readRange "$var" <arrayName>
        local -a arin
        local elem start end
        local -n arout=$2
        arout=()
        read -ra arin <<< "$1"
        for elem in "${arin[@]}"; do
            read -r start end <<< ${elem//[^0-9]/ }
            if [[ -n $end ]]; then
                arout+=($(seq $start $end))
            else
                arout+=($start)
            fi
        done
    }
    
    var="{1..3} 5 {7..9} 11"
    readRange "$var" resultArray
    for i in "${resultArray[@]}"; do
        echo "$i"
    done
    
    1
    2
    3
    5
    7
    8
    9
    11