1

When iterating over some filenames double-quoting the parameter does not prevent the filename from being broken:

$ mkdir temp
$ cd temp/
$ touch a\ b
$ for f in $(find .) ; do echo "$f" ; done
.
./a
b
1

The directive for f in $(find .) splits the list of elements to iterate by space. In a modern shell, like zsh, expansion flags could be used.

for f in "${(@f)$(find .)}" ; do echo "$f" ; done

Staying with bash, we can work around this like mentioned in another question modifying the input separator, the character used to split elements, temporarily while assigning the results of find to a variable. Disabling gobbing for the time beeing prevents the expansion of wildcard characters, such as * in them.

set -f  # Disable globbing.
IFS=$(echo -e '\0') files=( $(find .  -print0) )  # Read newline separated files into an array.
for f in ${files[@]}; do echo $f; done
set +f  # Reenable globbing.

But this just shifts the problem, now it will not work when the newline is in any filename. There is just one thing forbidden in probably all filesystems which is the null character. But variables in bash may not hold this, so we cannot assign it to IFS.

Alternatively, xargs can be used to pass the paths from find to another program. Using the -print0 command to find and the -0 switch to xargs, they use the null character as a separator. Adding -n1 to the xargs call handles one filename at a time.

find . -print0 | xargs -0 -n1 echo
  • IFS='\n' in bash? Are you sure that does what you want? – manatwork Dec 5 '13 at 13:14
  • IFS should indeed not be tinkered with too much. Here, it is just used carefully for that one line only to convert the result to a clean array. – XZS Dec 5 '13 at 13:15
  • Tried it? There you set IFS to two characters: “\” and “n”. So whatever find outputs will be split on any of those two characters. pastebin.com/kj8PsrKw – manatwork Dec 5 '13 at 13:27
  • I forgot an expansion character. With $'\n' it indeed evaluates to the newline character. pastebin.com/3NGSB1n4 – XZS Dec 5 '13 at 14:34
  • for f in "$(find .)", in any shell, runs the loop body exactly once. In your bash solution, you missed disabling globbing, and you should note that this breaks with file names containing newlines. – Gilles Dec 5 '13 at 23:07
1

You double-quoted the variable substitution in the argument to echo, but not the command substitution in the iteration list of the for loop. That's where the split on whitespace and other nastiness is happening.

There is no fully reliable way to parse the output of find, because you can't tell whether a newline is part of a file name or a separator.

If you can assume that your file names don't contain newlines, then you can arrange to split the output of find by restricting the field separator characters to only the newline character (to protect spaces and tabs) and turning off globbing (to protect \[?*).

IFS='
'; set +f
for f in $(find .); do
  unset IFS; set +f
  echo "$f"
done
unset IFS; set +f

There are better ways to do this, though. The easiest way to use find reliably is to make it execute the command, instead of parsing its output. This is also potentially faster since the files have a good chance of being processed while their metadata is still in cache.

find . -exec echo {} \;

If you need a shell command, invoke a shell explicitly. Beware of quoting — pass the file name as an argument to the shell, don't try to interpolate it in the shell command.

find . -exec sh -c 'echo "$0"' {} \;

Instead of invoking one shell instance per file, you can group shell invocations and loop over a bunch of files. The find command does the bunching and ensures not to go over the command line length limit. Don't forget to pass a dummy argument as $0 so that the files are $1 and so on.

find . -exec sh -c 'for x; do echo "$x"; done' _ {} +

If you're using ksh93, bash or zsh, you can use their recursive globbing facility: **/PATTERN searches for a wildcard pattern in a directory and its subdirectories recursively. In ksh93, you need to run set -o globstar first. In bash, you need to run shopt -s globstar first, and beware that ** also recurses inside symbolic links to directories.

for f in **/*; do
  echo "$f"
done

Zsh also has glob qualifiers that subsume most uses of find. Other shells have no such thing.

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.