8

The file paths coming from this find command

find . -printf "%p \n"

do not escape unusual (whitespace, backslash, double quote...) characters.

The -ls option does print the escaped paths, but it just prepends the output of ls -dils to the output of printf.

I need a highly efficient command, so running an extra ls does not help, and neither does printing out all the extra characters.

Is there any other (elegant) way to ouput escaped paths with find?

Julen Larrucea
  • 183
  • 1
  • 1
  • 6
  • 6
    What are you actually trying to accomplish? A very common approach is to run the command in find -exec instead, which obviates the need to escape any shell metacharacters. – tripleee Apr 19 '17 at 10:33
  • 2
    What do you want to do with the (quoted) output of that find? Read it with the shell, save to a file, run some commands on the file names? – ilkkachu Apr 19 '17 at 10:34
  • If the next step of the pipeline can read nul-terminated paths, then just use -print0. – Kusalananda Apr 19 '17 at 10:34
  • @tripleee and @ilkkachu: I am trying to find an alternative to getfacl to backup ACLs, which executes terribly slow on large filesystems. With find I could easily print filenames and permissions in octal format into a file and easily restore them with chmod $1 $2. But this won't work if $2 has special characters. – Julen Larrucea Apr 19 '17 at 12:04
  • 2
    @JulenLarrucea, ACL:s are still quite different from just the permission bits, I don't think you can get them with find. – ilkkachu Apr 19 '17 at 12:45
  • This is becoming quite an annoyance for my dynamic m3u files in VLC! – Sridhar Sarnobat Sep 29 '22 at 21:38
  • BLUF: find can just be slow.I'll just comment that another use case for outputting escaped filenames is the situation where the find command takes a long time to run, and you don't know up front what you want to do with those files. I was just in that situation; I had to look at the content of the files, eliminate some irrelevant ones, and then interpret the JSON in the remaining files to find a path to a different file. There were a number of false steps in the latter, and having a plain text file containing the output avoided the need to run find each time. – Mike Maxwell Jun 01 '23 at 20:02

5 Answers5

6

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.

ilkkachu
  • 138,973
  • Thanks! but I am searching for something super fast. The -type option uses lstats which slows down the process too much. The -exec should be also slow. I want to quick backup the permissions of huge filesystems. – Julen Larrucea Apr 19 '17 at 12:10
  • @JulenLarrucea, oh, the -type f was an accident, but you do need to use stat() to get the file permissions anyway. – ilkkachu Apr 19 '17 at 12:40
  • 1
    The IFS=: read -rd '' mode file won't work for file paths that end in : (and contain no other :). Use -printf '%#m/%p\0 and IFS=/ read -rd/ instead. – Stéphane Chazelas Apr 19 '17 at 13:26
  • @ilkkachu: That's what I was doing. But well... If it is such a big deal, I'll keep the special characters and process them just in case of restoring. Or I'll probably just drop the whole idea, since the performance seems even worse than with the standard getfacl. – Julen Larrucea Apr 19 '17 at 13:32
  • @StéphaneChazelas, ... Apparently so, but what's the logic causing that? I though only whitespace in IFS was handled specially and trailing whitespace eaten? – ilkkachu Apr 19 '17 at 13:35
  • Because IFS is the Internal Field Delimiter in POSIX shells (not zsh nor pdksh), not separator. See https://unix.stackexchange.com/a/209184/22565 – Stéphane Chazelas Apr 19 '17 at 13:51
  • IMO that still doesn't make much sense regarding that multiple colons seem to work fine, in the middle or in the end. But, whatever, I already knew the shell's string processing is weird... – ilkkachu Apr 19 '17 at 14:04
  • 1
    That's the same reason as why a=a::b: is split (in printf '<%s>\n' $a for instance) into a, <empty>, b and not a, <empty>, b, <empty>. – Stéphane Chazelas Apr 19 '17 at 14:34
  • First one doesn't work. In bash, I get sh: ${@@Q}: bad substitution. Changing ${@@Q} to $@ is accepted, but it doesn't escape special characters. – Melab Jul 05 '17 at 16:54
  • @Melab, it works in Bash 4.4, though not in 4.3 or before. (described here) – ilkkachu Jul 06 '17 at 11:35
2

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
2

I found a far more simple solution:

find -exec echo \'{}\' \;

We may use this to pipe to any other program:

find -exec echo \'{}\' \; | xargs ls 

Or we can simply use any command instead of echo and add any number of arguments:

find -exec mv \'{}\' somewhereelse \;

Don't forget to prepend echo for testing first.

The last DOES NOT WORK because mv does not remove the quotes before accessing the file, use pipe instead:

find -exec echo mv \'{}\' somewhereelse \;| bash
JPT
  • 402
2

To print nicely quoted and escaped filenames (correctly handling apostrophes and quotes together) try the following:

find . -exec printf "%q\n" {} \;

which will produce (for example)

.
./DSC07051_.jpg
./Photos
"./Photos/TestDSC9910'Apos .jpg"
'./Photos/TestDSC9913 Space.jpg'
./Photos/DSC07053+.jpG
./Photos/DSC07048.jpg
./Photos/Photos.zip
'./Photos/TestDSC9912(x)Braces.jpg'
'./Photos/TestDSC9911"Quote.jpg'
'./Photos/TestDSC9919'\''The" (Lot).jpg'
./Photos/TestDSC9901Jpeg.jpEg
./DSC07053.jpg
brewmanz
  • 121
1

Why not escaping EVERY character? It then works for anything (ls, rm, echo etc.):

find -name *.txt | sed -E 's/(.)/\\\1/g' | xargs ls

(you'll probably need to adapt it to the -exec form so it becomes more efficient)

awvalenti
  • 191
  • Would probably still not work if the filename contained newlines. It would be better to use -print0 and then read the names as a nul-delimited list, or execute the utility directly from find with -exec. – Kusalananda Nov 17 '20 at 15:46