Recursively iterating through files in a directory can easily be done by:

find . -type f -exec bar {} \;

However, the above does not work for more complex things, where a lot of conditional branches, looping etc. needs to be done. I used to use this for the above:

while read line; do [...]; done < <(find . -type f)

However, it seems like this doesn't work for files containing obscure characters:

$ touch $'a\nb'
$ find . -type f

Is there an alternative that handles such obscure characters well?

  • 1
    find ... -exec bash -c 'echo filename is in \$0: "$0"' {} \; is a better way to do it.
    – jw013
    Jun 26, 2014 at 14:08
  • You can solve this, and keep your original design, by changing your read line to IFS= read -r line. The only character that will break it then is a newline.
    – phemmer
    Jun 26, 2014 at 14:43
  • 1
    @Patrick, but filenames can contain newlines. That's why -d $'\0' is preferable.
    – godlygeek
    Jun 26, 2014 at 15:14

4 Answers 4


Yet another use for safe find:

while IFS= read -r -d '' -u 9
    [Do something with "$REPLY"]
done 9< <( find . -type f -exec printf '%s\0' {} + )

(This works with any POSIX find, but the shell part requires bash. With *BSD and GNU find, you can use -print0 instead of -exec printf '%s\0' {} +, it will be slightly faster.)

This makes it possible to use standard input within the loop, and it works with any path.

  • 2
    Because I had to look it up: "read ... If no names are supplied, the line read is assigned to the variable REPLY." So do echo "Filename is '$REPLY'"
    – Andrew
    Oct 7, 2019 at 17:45

Doing this is as simple as:

find -exec sh -c 'inline script "$0"' {} \;


find -exec executable_script {} \;
  • The first example gave a whole bunch of inline: not found errors for me, but this does what I expect find -exec sh -c 'echo inline script "$0"' {} \;.
    – Liam
    May 24, 2021 at 16:05

The simplest (yet safe) approach is to use shell globbing:

$ for f in *; do printf ":%s:\n" "$f"; done 
:a b:
:e  f:

To make the above recurse into subdirectories (in bash), you can use the globstar option; also set dotglob to match files whose name begins with .:

$ shopt -s globstar dotglob
$ for f in **/*; do printf ":%s:\n" "$f"; done 
:a b:
:e  f:
:foo/file two:

Beware that up to bash 4.2, **/ recurses into symbolic links to directories. Since bash 4.3, **/ recurses only into directories, like find.

Another common solution is to use find -print0 with xargs -0:

$ touch -- 'a b' $'c\nd' $'e\tf' $'g\rh' '-e'
$ find . -type f -print0 | xargs -0 -I{} printf ":%s:\n" {}
:./e    f:
:./a b:

Note that the h:/g is actually correct since the file name contains a \r.


It's a bit difficult to do your read loop portably, but for bash in particular you can try something like this.

Relevant portion:

while IFS= read -d $'\0' -r file ; do
        printf 'File found: %s\n' "$file"
done < <(find . -iname 'foo*' -print0)

That instructs find to print its output delimited by NUL characters (0x00), and read to fetch NUL-delimited lines (-d $'\0') without handling backslashes as escapes for other characters (-r) and not do any word splitting on the lines (IFS=). Since 0x00 is a byte that can't occur in filenames or paths in Unix, this should handle all of your weird filename problems.

  • 1
    -d '' is equivalent to -d $'\0'.
    – l0b0
    Jun 26, 2014 at 15:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.