Split string using IFS

In old versions of bash you had to quote variables after <<<. That was fixed in 4.4. In older versions, the variable would be split on IFS and the resulting words joined on space before being stored in the temporary file that makes up that <<< redirection.

In 4.2 and before, when redirecting builtins like read or command, that splitting would even take the IFS for that builtin (4.3 fixed that):

$ bash-4.2 -c 'a=a.b.c.d; IFS=. read x <<< $a; echo  "$x"'
a b c d
$ bash-4.2 -c 'a=a.b.c.d; IFS=. cat <<< $a'
a.b.c.d
$ bash-4.2 -c 'a=a.b.c.d; IFS=. command cat <<< $a'
a b c d

That one fixed in 4.3:

$ bash-4.3 -c 'a=a.b.c.d; IFS=. read x <<< $a; echo  "$x"'
a.b.c.d

But $a is still subject to word splitting there:

$ bash-4.3 -c 'a=a.b.c.d; IFS=.; read x <<< $a; echo  "$x"'
a b c d

In 4.4:

$ bash-4.4 -c 'a=a.b.c.d; IFS=.; read x <<< $a; echo  "$x"'
a.b.c.d

For portability to older versions, quote your variable (or use zsh where that <<< comes from in the first place and that doesn't have that issue)

$ bash-any-version -c 'a=a.b.c.d; IFS=.; read x <<< "$a"; echo "$x"'
a.b.c.d

Note that that approach to split a string only works for strings that don't contain newline characters. Also note that a..b.c. would be split into "a", "", "b", "c" (no empty last element).

To split arbitrary strings you can use the split+glob operator instead (which would make it standard and avoid storing the content of a variable in a temp file as <<< does):

var='a.new
line..b.c.'
set -o noglob # disable glob
IFS=.
set -- $var'' # split+glob
for i do
  printf 'item: <%s>\n' "$i"
done

or:

array=($var'') # in shells with array support

The '' is to preserve a trailing empty element if any. That would also split an empty $var into one empty element.

Or use a shell with a proper splitting operator:

  • zsh:

    array=(${(s:.:)var} # removes empty elements
    array=("${(@s:.:)var}") # preserves empty elements
    
  • rc:

    array = ``(.){printf %s $var} # removes empty elements
    
  • fish

    set array (string split . -- $var) # not for multiline $var
    

Fix, (see also S. Chazelas' answer for background), with sensible output:

#!/bin/bash
IN="One-XX-X-17.0.0"
IFS='-' read -r -a ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    if [ "$i" = "${i//.}" ] ; then 
        echo "Element:$i" 
        continue
    fi
    # split 17.0.0 into NUM
    IFS='.' read -a array <<< "$i"
    for element in "${array[@]}" ; do
        echo "Num:$element"
    done
done

Output:

Element:One
Element:XX
Element:X
Num:17
Num:0
Num:0

Notes:

  • It's better to put the conditional 2nd loop in the 1st loop.

  • bash pattern substitution ("${i//.}") checks if there's a . in an element. (A case statement might be simpler, albeit less similar to the OP's code.)

  • reading $array by inputting <<< "${ADDR[3]}" is less general than <<< "$i". It avoids needing to know which element has the .s.

  • The code assumes that printing "Element:17.0.0" is unintentional. If That behavior is intended, replace the main loop with:

    for i in "${ADDR[@]}"; do
       echo "Element:$i" 
       if [ "$i" != "${i//.}" ] ; then 
       # split 17.0.0 into NUM
           IFS='.' read -a array <<< "$i"
           for element in "${array[@]}" ; do
               echo "Num:$element"
           done
       fi
    done