Bash: Iterating over lines in a variable

247

71

How does one properly iterate over lines in bash either in a variable, or from the output of a command? Simply setting the IFS variable to a new line works for the output of a command but not when processing a variable that contains new lines.

For example

#!/bin/bash

list="One\ntwo\nthree\nfour"

#Print the list with echo
echo -e "echo: \n$list"

#Set the field separator to new line
IFS=$'\n'

#Try to iterate over each line
echo "For loop:"
for item in $list
do
        echo "Item: $item"
done

#Output the variable to a file
echo -e $list > list.txt

#Try to iterate over each line from the cat command
echo "For loop over command output:"
for item in `cat list.txt`
do
        echo "Item: $item"
done

This gives the output:

echo: 
One
two
three
four
For loop:
Item: One\ntwo\nthree\nfour
For loop over command output:
Item: One
Item: two
Item: three
Item: four

As you can see, echoing the variable or iterating over the cat command prints each of the lines one by one correctly. However, the first for loop prints all the items on a single line. Any ideas?

Alex Spurling

Posted 2011-05-16T12:14:48.887

Reputation: 2 632

Just a comment for all answers: I had to do $(echo "$line" | sed -e 's/^[[:space:]]*//') in order to trim the newline character. – servermanfail – 2019-09-04T10:48:40.340

Answers

312

With bash, if you want to embed newlines in a string, enclose the string with $'':

$ list="One\ntwo\nthree\nfour"
$ echo "$list"
One\ntwo\nthree\nfour
$ list=$'One\ntwo\nthree\nfour'
$ echo "$list"
One
two
three
four

And if you have such a string already in a variable, you can read it line-by-line with:

while IFS= read -r line; do
    echo "... $line ..."
done <<< "$list"

glenn jackman

Posted 2011-05-16T12:14:48.887

Reputation: 18 546

21The reason done <<< "$list" is crucial is because that will pass "$list" as the input to read – wisbucky – 2015-03-11T21:00:01.877

23I need to stress this: DOUBLE-QUOTING $list is very crucial. – André Chalella – 2015-06-10T07:44:13.270

3How would I do that in ash? Because I get a syntax error: unexpected redirection. – atripes – 2015-06-18T16:30:46.683

1I suggest you ask a new question. – glenn jackman – 2015-06-18T17:01:01.573

8echo "$list" | while ... could appear more clear than ... done <<< "$line" – kyb – 2018-05-14T10:07:17.170

6There are downsides to that approach as the while loop will run in a subshell: any variables assigned in the loop will not persist. – glenn jackman – 2018-05-14T14:13:03.293

Whether here-docs (the part following <<<) are evaluated in their own subshells may differ across bash versions. Fore more, see: https://stackoverflow.com/a/51181884/1054322

– MatrixManAtYrService – 2019-03-13T23:23:03.377

Let me leave a note and stress this: READING THE COMMENTS OF THE MOST VOTED ANSWER IS CRUCIAL. – BBerastegui – 2019-10-23T16:33:37.893

30Just a note that the done <<< "$list" is crucial – Jason Axelson – 2012-09-26T07:53:47.943

72

You can use while + read:

some_command | while read line ; do
   echo === $line ===
done

Btw. the -e option to echo is non-standard. Use printf instead, if you want portability.

maxelost

Posted 2011-05-16T12:14:48.887

Reputation: 2 191

14Note that if you use this syntax, variables assigned inside the loop won't stick after the loop. Oddly enough, the <<< version suggested by glenn jackman does work with variable assignment. – Sparhawk – 2013-10-03T04:51:56.413

8@Sparhawk Yes, that's because the pipe starts a subshell executing the while part. The <<< version does not (in new bash versions, at least). – maxelost – 2013-10-21T13:53:12.120

32

#!/bin/sh

items="
one two three four
hello world
this should work just fine
"

IFS='
'
count=0
for item in $items
do
  count=$((count+1))
  echo $count $item
done

nobody important

Posted 2011-05-16T12:14:48.887

Reputation: 329

2This outputs all items, not lines. – Tomasz Posłuszny – 2015-08-13T15:05:26.260

4@TomaszPosłuszny No, this outputs 3 (count is 3) lines on my machine. Note the setting of the IFS. You could also use IFS=$'\n' for the same effect. – jmiserez – 2016-05-10T12:33:16.803

15

Here's a funny way of doing your for loop:

for item in ${list//\\n/
}
do
   echo "Item: $item"
done

A little more sensible/readable would be:

cr='
'
for item in ${list//\\n/$cr}
do
   echo "Item: $item"
done

But that's all too complex, you only need a space in there:

for item in ${list//\\n/ }
do
   echo "Item: $item"
done

You $line variable doesn't contain newlines. It contains instances of \ followed by n. You can see that clearly with:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo $list | hexdump -C

$ ./t.sh
00000000  4f 6e 65 5c 6e 74 77 6f  5c 6e 74 68 72 65 65 5c  |One\ntwo\nthree\|
00000010  6e 66 6f 75 72 0a                                 |nfour.|
00000016

The substitution is replacing those with spaces, which is enough for it to work in for loops:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C

$ ./t.sh 
00000000  4f 6e 65 20 74 77 6f 20  74 68 72 65 65 20 66 6f  |One two three fo|
00000010  75 72 0a                                          |ur.|
00000013

Demo:

$ cat t.sh
#! /bin/bash
list="One\ntwo\nthree\nfour"
echo ${list//\\n/ } | hexdump -C
for item in ${list//\\n/ } ; do
    echo $item
done

$ ./t.sh 
00000000  4f 6e 65 20 74 77 6f 20  74 68 72 65 65 20 66 6f  |One two three fo|
00000010  75 72 0a                                          |ur.|
00000013
One
two
three
four

Mat

Posted 2011-05-16T12:14:48.887

Reputation: 6 193

Interesting, can you explain what is going on here? It looks like you are replacing \n with a new line... What is the difference between the original string and the new one? – Alex Spurling – 2011-05-16T13:15:41.633

@Alex: updated my answer - with a simpler version too :-) – Mat – 2011-05-16T13:24:58.500

I tried this out and it doesn't appear to work if you have spaces in your input. In the example above we have "one\ntwo\nthree" and this works but it fails if we have "entry one\nentry two\nentry three" as it also adds a new line for the space too. – John Rocha – 2012-11-08T18:15:32.623

2Just a note that instead of defining cr you can use $'\n'. – devios1 – 2013-12-21T19:12:12.937

3

You can also first convert the variable into an array, then iterate over this.

lines="abc
def
ghi"

declare -a theArray

while read -r line
do
    theArray+=($line)            
done <<< "$lines"

for line in "${theArray[@]}"
do
    echo "$line"
    #Do something complex here that would break your read loop
done

This is only usefull if you do not want to mess with the IFS and also have issues with the read command, as it can happen, if you call another script inside the loop that that script can empty your read buffer before returning, as it happened to me.

Torge

Posted 2011-05-16T12:14:48.887

Reputation: 123