3

Following solution found here I've tried to implement loop over a tuple in bash:

NUMBERS='1,2,3 4,5,6 7,8,9'

for TOUPLE in $NUMBERS;
do IFS=',';
    set -- $TOUPLE
    echo \($1, $2, $3\)
done

echo ''

for TRIPLE in $NUMBERS;
do IFS=',';
    set -- $TRIPLE
    echo \($1, $2, $3\)
done

These should basically be the same loops, and they should print the same output. However, when I execute the script I get the following output:

(1, 2, 3)
(4, 5, 6)
(7, 8, 9)

(1, , )
(2, , )
(3 4, , )
(5, , )
(6 7, , )
(8, , )
(9, , )

Why does the second loop acts differently from the first one?

Gitnik
  • 564
  • 7
  • 26
  • There is no tuple support in `bash` by default and using a variable to store your tuple got lucky because you had a `,` separated 3 characters as entries, don't assume it will work for spaces though – Inian May 30 '18 at 13:40
  • @Inian I know, but I actually read kind of a CSV file from command line, and I need to use one record in it two perform some things. So I always have comma separated values grouped together in my use case. – Gitnik May 30 '18 at 13:42

2 Answers2

6

The first loop has for TOUPLE in $NUMBERS with a default $IFS -- the numbers are split at whitespace. Inside the loop you change $IFS and this change is permanent. It affects not just the following set -- $TOUPLE but also the rest of the script.

When the second for TRIPLE in $NUMBERS runs the change to $IFS keeps $NUMBERS from being split properly. It's split on commas instead of whitespace. Oops.

If you want to isolate the loops from each other the simplest way to ensure it is to wrap them in parentheses so they're executed in subshells. Variable assignments in subshells don't affect the parent shell.

(
    for TOUPLE in $NUMBERS; do
        IFS=','
        set -- $TOUPLE
        echo "($1, $2, $3)"
    done
)

echo

(
    for TRIPLE in $NUMBERS; do
        IFS=','
        set -- $TRIPLE
        echo "($1, $2, $3)"
    done
)

If you switch set to a read you can change $IFS temporarily for the duration of the reads and avoid this mess entirely.

for TUPLE in $NUMBERS; do
    IFS=',' read a b c <<< "$TUPLE"
    echo "($a, $b, $c)"
done

I'd go a step further and avoid looping over a string variable at all. An array is better suited and doesn't involve any string splitting. It's a good habit to stay away from unquoted variable expansions.

tuples=(1,2,3 4,5,6 7,8,9)
for tuple in "${tuples[@]}"; do
    IFS=',' read a b c <<< "$tuple"
    echo "($a, $b, $c)"
done
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
2

You should "cache" the IFS variable and restore it between the two loops

NUMBERS='1,2,3 4,5,6 7,8,9'
OLDIFS=$IFS

for TOUPLE in $NUMBERS;
do IFS=',';
    set -- $TOUPLE
    echo \($1, $2, $3\)
done
IFS=$OLDIFS

echo ''

for TRIPLE in $NUMBERS;
do IFS=',';
    set -- $TRIPLE
    echo \($1, $2, $3\)
done
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Paolo Crosetto
  • 1,038
  • 7
  • 17