13

How can I assign a newline-separated string with e.g. three lines to three variables ?

# test string
s='line 01
line 02
line 03'

# this doesn't seem to make any difference at all
IFS=$'\n'

# first naive attempt
read a b c <<< "${s}"

# this prints 'line 01||':
# everything after the first newline is dropped
echo "${a}|${b}|${c}"

# second attempt, remove quotes
read a b c <<< ${s}

# this prints 'line 01 line 02 line 03||':
# everything is assigned to the first variable
echo "${a}|${b}|${c}"

# third attempt, add -r
read -r a b c <<< ${s}

# this prints 'line 01 line 02 line 03||':
# -r switch doesn't seem to make a difference
echo "${a}|${b}|${c}"

# fourth attempt, re-add quotes
read -r a b c <<< "${s}"

# this prints 'line 01||':
# -r switch doesn't seem to make a difference
echo "${a}|${b}|${c}"

I also tried using echo ${s} | read a b c instead of <<<, but couldn't get that to work either.

Can this be done in bash at all ?

1
  • 3
    You might want to consider mapfile or readarray and use arrays instead of differently named variables for something like this. Commented May 22, 2017 at 11:48

2 Answers 2

12

read default input delimiter is \n

{ read a; read b; read c;} <<< "${s}"

-d char : allows to specify another input delimiter

For example is there is no character SOH (1 ASCII) in input string

IFS=$'\n' read -r -d$'\1' a b c <<< "${s}"

We set IFS to $'\n' because IFS default value is :

$ printf "$IFS" | hd -c
00000000  20 09 0a                                          | ..|
0000000      \t  \n                                                    
0000003

EDIT: -d can take a null argument the space is mandatory between -d and null argument:

IFS=$'\n' read -r -d '' a b c <<< "${s}"

The read builtin documentation is available by typing help read at the bash prompt.

EDIT: after comment about a solution for any number of lines

function read_n {
    local i s n line
    n=$1
    s=$2
    arr=()
    for ((i=0;i<n;i+=1)); do
        IFS= read -r line
        arr[i]=$line
    done <<< "${s}"
}

nl=$'\n'
read_n 10 "a${nl}b${nl}c${nl}d${nl}e${nl}f${nl}g${nl}h${nl}i${nl}j${nl}k${nl}l"

printf "'%s'\n" "${arr[@]}"

Another way without using read

IFS=$'\n'   # avoid space/tab splitting
set -f      # avoid file globbing
arr=($s)
# possibly restore defaults
IFS=$' \t\n'
set +f
Sign up to request clarification or add additional context in comments.

3 Comments

This seems to be the only way to get this going. Not very elegant and does not scale nicely, but at least it works.
I don't understand what you mean by not elegant and does not scale, isn't that the answer to your question
You are correct that this answers my question; however, the three lines in my question only served as an example, my actual use case has more lines and called read e.g. 10 times for 10 lines does not seem very elegant to me - a solution where the same code works for any number of lines would scale better.
9

You are looking for the readarray command, not read.

readarray -t lines <<< "$s"

(Theoretically, $s does not need to be quoted here. Unless you are using bash 4.4 or later, I would quote it anyway, due to some bugs in previous versions of bash.)

Once the lines are in an array, you can assign the separate variables if you need to

a=${lines[0]}
b=${lines[1]}
c=${lines[2]}

4 Comments

This would be the preferable approach, but it unfortunately only works on Linux, not on macOS as readarray (aka mapfile) is only available in Bash 4 on the former, but not the latter.
One can easily install a recent version of bash on OS X. The system bash should basically be considered an implementation of POSIX shell.
I am using GNU bash, version 4.4.12(1)-release (x86_64-apple-darwin15.6.0) installed on OS X 10.11.6 using homebrew. Recent enough.
This is my preferred solution.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.