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:
- redirect the
ls
output in the file - redirect the file in the
while
input - 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)
Written by Thomas DE BONA
Related protips
6 Responses
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
Thanks to point out the pitfall, I'm going to edit the tip ;)
Way easier and safer with globs:
for file in *; do echo "[$file]"; done
In bash at least, IFS=$'\n' works !
My .001c
Bests
Keep in mind though that < <(ls -1)
is a bashism and would not work as expected with #!/bin/sh
set as shebang
Thanks @dipnlik. Definitely the easiest way to loop on directory files!