Last Updated: February 25, 2016
·
2.825K
· debona

SHELL: Easily loop on line break separated items

tl;dr

Loop on files without worrying about the spaces in the file names:

while read file
do
    echo "[$file]"
done < <(ls -1)

Full answer

There are two way to loop on line break separated items, e.g. on a list of files.

Assume the following commands run in a directory that contains these files:

$ ls -l
total 0
-rw-r--r--  1 debona  staff  0 17 oct 19:58 a file with spaces
-rw-r--r--  1 debona  staff  0 17 oct 22:51 common_file

The "for loop", the painful way

for file in `ls -1`
do
    echo "[$file]"
done

As the "for loop" breaks down the list on each IFS (which is not the line break by default) it prints the following output:

[a]
[file]
[with]
[spaces]
[common_file]

OK, so let set the IFS on the line break:

IFS='
' # Do you really think that IFS="\n" could work?
for file in `ls -1`
do
    echo "[$file]"
done

Yay, it loop on each files:

[a file with spaces]
[common_file]

Why is that painful? Typically, changing the IFS is becoming painful when there are nested loops. If you need to loop on a file list, you have to change the IFS again to nest a loop on space separated items inside.

The "while loop", the easy way

ls -1 | while read file
do
    echo "[$file]"
done

The read shell builtin read one line from the standard input. Be careful, it can read empty line whereas the for loop ignore the empty items.
As @gkalabin mention in comments, in this case, the while loop body is executed in a subshell.

The two main pitfalls with subshell:

  • All the stuff inside a subshell won't be available after it runs.
  • If you exit inside a subshell, you exit the subshell itself, not the "main script".

while is a shell keyword which does not subshell by itself. In our case, the while loop body is a subshell because of the pipe.

To avoid piping the ls output in the while input, we could use a temporary file:

  1. redirect the ls output in the file
  2. redirect the file in the while input
  3. after looping on it, remove the file

Hopefully, your beloved shell provides a straightforward way to do that: the process substitution

<(ls -1) # this is the process substitution of the `ls - 1` command

echo <(ls -1) # it prints a path to a "special file" which contains the output of the command
# => /dev/fd/63
cat <(ls -1) # it prints the `ls -1` output
# => a file with spaces
# => common_file

So the while loop can be rewrite like this:

while read file
do
    echo "[$file]"
done < <(ls -1)

6 Responses
Add your response

Be aware of the fact that while loop will be executed in a subshell.
For example:

CNT=0
ls -1 | while read file
do
  CNT=$((CNT+1))
done
echo $CNT

Output will be zero

over 1 year ago ·

Thanks to point out the pitfall, I'm going to edit the tip ;)

over 1 year ago ·

Way easier and safer with globs:

for file in *; do echo "[$file]"; done

over 1 year ago ·

In bash at least, IFS=$'\n' works !

My .001c
Bests

over 1 year ago ·

Keep in mind though that < <(ls -1) is a bashism and would not work as expected with #!/bin/sh set as shebang

over 1 year ago ·

Thanks @dipnlik. Definitely the easiest way to loop on directory files!

over 1 year ago ·