I have written a sample script to split the string but it is not working as expected
#!/bin/bash
IN="One-XX-X-17.0.0"
IFS='-' read -r -a ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
echo "Element:$i"
done
#split 17.0.0 into NUM
IFS='.' read -a array <<<${ADDR[3]};
for element in "${array[@]}"
do
echo "Num:$element"
done
output
One XX X 17.0.0 17 0 0
but I expected the output to be:
One
XX
X
17.0.0
17
0
0
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
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 -
fishset array (string split . -- $var) # not for multiline $var
Method 2
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.
-
bashpattern substitution ("${i//.}") checks if there’s a.in an element. (Acasestatement might be simpler, albeit less similar to the OP‘s code.) -
reading$arrayby 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
Method 3
With awk it would cost you one line:
IN="One-XX-X-17.0.0"
awk -F'[-.]' '{ for(i=1;i<=NF;i++) printf "%s : %sn",($i~/^[0-9]+$/?"Num":"Element"),$i }' <<<"$IN"
-F'[-.]'– field separator based on multiple characters, in our case-and.
The output:
Element : One Element : XX Element : X Num : 17 Num : 0 Num : 0
Method 4
Here my way:
OIFS=$IFS
IFS='-'
IN="One-XX-X-17.0.0"
ADDR=($IN)
for i in "${ADDR[@]}"; do
echo "Element:$i"
done
IFS='.'
array=(${ADDR[3]})
for element in "${array[@]}"
do
echo "Num:$element"
done
result as expected:
Num:17 Num:0 Num:0
Method 5
Often it is possible to put the string splitting into a subshell. If so, that solves the trouble of properly restoring noglob or IFS settings.
a="world-thing-hello" t="$(set -o noglob; IFS=-; set -- $a; echo $2)" echo "This is the $t."
This is the thing.
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0