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. (Acase
statement might be simpler, albeit less similar to the OP's code.)read
ing$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