Search code examples
bash

Splitting string with a while command


I can not find a way to correctly split a simple input string by a configurable separator

Script:

#!/bin/bash
while IFS='|' read -r name; do
  echo "$name";
done <<< "one|two|three"

Actual output :

one|two|three

Expected output:

one
two
three

I'm using `GNU bash, version 4.4.20(1)-release (x86_64-redhat-linux-gnu)`


Solution

  • IFS='|' read -r name will split the input using | as the delimiter but then you only give it one variable (name) in which to store the 3 components. Consider the following:

    $ IFS='|' read -r var1 <<< "one|two|three"
    $ typeset -p var1
    declare -- var1="one|two|three"                    # "one" plus leftover "two|three"
    
    $ IFS='|' read -r var1 var2 <<< "one|two|three"
    $ typeset -p var1 var2
    declare -- var1="one"
    declare -- var2="two|three"                        # "two" plus leftover "|three"
    
    $ IFS='|' read -r var1 var2 var3 <<< "one|two|three"
    $ typeset -p var1 var2 var3
    declare -- var1="one"
    declare -- var2="two"
    declare -- var3="three"                            # "three" plus *no leftover*
    
    $ IFS='|' read -r var1 var2 var3 var4 <<< "one|two|three"
    $ typeset -p var1 var2 var3 var4
    declare -- var1="one"
    declare -- var2="two"
    declare -- var3="three"
    declare -- var4=""                                 # not enough delimited items to populate "var4"
    

    In your case, assuming you know there will always be 3 items, is to provide 3 variables for read to populate, eg:

    while IFS='|' read -r name1 name2 name3; do        # modify to use 3 distinct variable names as you see fit
        echo "name1:$name1:"
        echo "name2:$name2:"
        echo "name2:$name3:"
    done <<< "one|two|three"
    

    This will generate:

    name1:one:
    name2:two:
    name2:three:
    

    Keep in mind this solution will only loop once, with all 3 items available in said single loop.


    If the intent is to loop 3 times, with each loop processing just 1 item, then we need to (in essence) break the input into 3 separate items - one per line.

    One option would be convert the | delimiters into linefeeds (\n).

    One idea using tr, eg:

    $ tr '|' '\n' <<< "one|two|three"
    one
    two
    three
    

    Modifying your current code to utilize the tr output:

    cnt=0
    
    while read -r name; do
        ((cnt++))
        echo "loop #${cnt} : name=$name"
    done < <(tr '|' '\n' <<< "one|two|three")
    

    This generates:

    loop #1 : name=one
    loop #2 : name=two
    loop #3 : name=three
    

    An alternative approach would use read to split the input into array elements (See Diego's answer) and then loop through the array elements, eg:

    IFS='|' read -ra names <<< "one|two|three"
    
    cnt=0
    
    for name in "${names[@]}"; do
        ((cnt++))
        echo "loop #${cnt} : name=$name"
    done
    

    This also generates:

    loop #1 : name=one
    loop #2 : name=two
    loop #3 : name=three