1. Use zsh
The simplest solution is to use zsh , which is a non- bash that supports reading values separated by read -d "" using read -d "" (since version 4.2, released in 2004), and the only main shell that can store zeros in variables. Moreover, the last component of the pipeline does not start in a subshell in zsh , so the variables set there are not lost. We can simply write:
#!/usr/bin/env zsh find . -print0 |while IFS="" read -r -d "" file; do echo "$file" done
With zsh we can also easily avoid the null-delimiter problem (at least in the case of find. -print ) by using setopt globdots , which makes globes match hidden files, and ** , which returns to subdirectories. This works in almost all versions of zsh , even those older than 4.2:
#!/usr/bin/env zsh setopt globdots for file in **/*; do echo "$file" done
2. Use the POSIX shell and od
2.1 Use pipes
A generic POSIX-compliant solution to iterate over values separated by zeros should convert the input data so that information is not lost, and zero values are converted to something else that is easier to handle. We can use od to print the octal values of all input bytes and easily convert the data back using printf :
#!/usr/bin/env sh find . -print0 |od -An -vto1 |xargs printf ' %s' \ |sed 's/ 000/@/g' |tr @ '\n' \ |while IFS="" read -r file; do file='printf '\134%s' $file' file='printf " $file@ "' file="${file%@}" echo "$file" done
2.2 Use a variable to store intermediate results
Note that in while loop will work in a sub-shell (at least different from the zsh shells and the original, non-public domain of the Corn shell), which means that the variables set in this loop will not be visible in the rest of the code. If this is unacceptable, then in while loop can be started from the main building, and its input can be stored in a variable:
#!/usr/bin/env sh VAR='find . -print0 |od -An -vto1 |xargs printf ' %s' \ |sed 's/ 000/@/g' |tr @ '\n'' while IFS="" read -r file; do file='printf '\134%s' $file' file='printf " $file@ "' file="${file%@}" echo "$file" done <<EOF $VAR EOF
2.3 Use a temporary file to store intermediate results
If the output from the find very long, the script will not be able to save the output to a variable and may crash. Moreover, most shells use temporary files to implement heredoc , so instead of using a variable, we could explicitly write to a temporary file and avoid the problems of using variables to store intermediate results.
#!/usr/bin/env sh TMPFILE="/tmp/$$_'awk 'BEGIN{srand(); print rand()}''" find . -print0 |od -An -vto1 |xargs printf ' %s' \ |sed 's/ 000/@/g' |tr @ '\n' >"$TMPFILE" while IFS="" read -r file; do file='printf '\134%s' $file' file='printf " $file@ "' file="${file%@}" echo "$file" done <"$TMPFILE" rm -f "$TMPFILE"
2.4 Use named pipes
We can use named pipes to solve the two problems mentioned above: now reading and writing can be performed in parallel, and we do not need to store intermediate results in variables. Please note, however, that this may not work in Cygwin.
#!/usr/bin/env sh TMPFILE="/tmp/$$_'awk 'BEGIN{srand(); print rand()}''" mknod "$TMPFILE" p { exec 3>"$TMPFILE" find . -print0 |od -An -vto1 |xargs printf ' %s' \ |sed 's/ 000/@/g' |tr @ '\n' >&3 } & while IFS="" read -r file; do file='printf '\134%s' $file' file='printf " $file@ "' file="${file%@}" echo "$file" done <"$TMPFILE" rm -f "$TMPFILE"
3. Modify the above solutions to work with the original Bourne shell.
The above solutions should work in any POSIX shell, but fail in the original Bourne shell, which defaults to /bin/sh in Solaris 10 and earlier. This shell does not support % -substitution, and trailing newlines in file names must be stored in a different way, for example:
#!/usr/bin/env sh TMPFILE="/tmp/$$_'awk 'BEGIN{srand(); print rand()}''" mknod "$TMPFILE" p { exec 3>"$TMPFILE" find . -print0 |od -An -vto1 |xargs printf ' %s' \ |sed 's/ 000/@/g' |tr @ '\n' >&3 } & while read -r file; do trailing_nl="" for char in $file; do if [ X"$char" = X"012" ]; then trailing_nl="${trailing_nl} " else trailing_nl="" fi done file='printf '\134%s' $file' file='printf "$file"' file="$file$trailing_nl" echo "$file" done <"$TMPFILE" rm -f "$TMPFILE"
4. Use a non-zero separator
As stated in the comments, Haravikka's answer is not entirely correct. Here is a modified version of his code that handles all kinds of strange situations, such as paths starting with ~:/\/: and ending with line feeds in file names. Note that this only works for relative path names; a similar trick can be done with absolute paths, preceding them with /./ , but read_path() needs to be changed to handle this. This method is based on the tricks of Richs sh (POSIX shell) .
#!/usr/bin/env sh read_path() { path= IFS= read -r path || return $? read -r path_next || return 0 if [ X"$path" = X"././" ]; then path="./" read -r path_next || return 0 return fi path="./$path" while [ X"$path_next" != X"././" ]; do path='printf '%s\n%s' "$path" "$path_next"' read -r path_next || return 0 done } find ././ |sed 's,^\./\./,&\n,' |while read_path; do echo "$path" done