Escape unusual characters on filenames with `find . -printf "%p \n"`

Usually you'd want to use find -exec to run a command for all file names, or find -print0 to pipe the names to some command that can read entries separated by nul bytes (like xargs -0).

If you really want to have quoted strings, Bash has a couple of options to do that:

$ find -exec bash -c 'printf "%s\n" "${@@Q}"' sh {} +
'./single'\''quote'
'./space 1'
$'./new\nline'
'./double"quote'

$ find -exec bash -c 'printf "%q\n" "$@"' sh {} +
./single\'quote
./space\ 1
$'./new\nline'
./double\"quote

This does require an extra invocation of the shell, but handles multiple file names with one exec.


Regarding saving the permission bits (not ACL's though), you could do something like this (in GNU find):

find -printf "%#m:%p\0" > files-and-modes

That would output entries with the permissions, a colon, the filename, and a nul byte, like: 0644:name with spaces\0. It will not escape anything, but instead will print the file names as-is (unless the output goes to a terminal, in which case at least newlines will be mangled.)

You can read the result with a Perl script:

perl -0 -ne '($m, $f) = split/:/, $_, 2; chmod oct($m), $f; ' < files-and-modes 

Or barely in Bash, see comments:

while IFS=: read -r -d '' mode file ; do
    # do something useful
    printf "<%s> <%s>\n" "$mode" "$file"
    chmod "$mode" "$file"
done < files-and-modes

As far as I tested, that works with newlines, quotes, spaces, and colons. Note that we need to use something other than whitespace as the separator, as setting IFS=" " would remove trailing spaces if any names contain them.


With zsh, you could do:

print -r -- ./**/*(.D:q)

. being the equivalent of -type f, D being to include hidden files like find would, and :q for quoting (using zsh-style quoting, I can't tell if that's the kind of quoting you're expecting).

You can get different styles of quoting with:

$ print -r -- ./**/*(.D:q)
./$'\200' ./a\ b ./é ./\"foo\" ./It\'s\ bad ./$'\n'
$ files=(./**/*(.D))
$ print -r -- ${(q)files}
./$'\200' ./$'\n' ./a\ b ./é ./\"foo\" ./It\'s\ bad
$ print -r -- ${(qq)files}
'./�' './
' './a b' './é' './"foo"' './It'\''s bad'
$ print -r -- ${(qqq)files}
"./�" "./
" "./a b" "./é" "./\"foo\"" "./It's bad"
$ print -r -- ${(qqqq)files}
$'./\200' $'./\n' $'./a b' $'./é' $'./"foo"' $'./It\'s bad'

( being a placeholder displayed by my terminal emulator for that non-printable \200 byte).

Here, if you want to be able to store the permissions in such a way that can be restored, it's just a matter of:

find . -type f -printf '%m\0%p\0' > saved-permissions

To be restored (assuming GNU xargs) with:

xargs -r0n2 -a saved-permissions chmod

That would however run one chmod invocation per file, which would be terribly inneficient. You may want to use a shell where chmod is builtin like zsh again after zmodload zsh/files:

zmodload zsh/files
while IFS= read -rd '' mode && IFS= read -rd '' file; do
  chmod $mode $file
done < saved-permissions