Is quoting filenames enough security for running `xargs sudo rm -rf`?

Solution 1:

In Linux, any character is a valid filename constituting character except:

  • \0 (ASCII NUL): as used for string termination in C
  • / (forward slash): as used for path separation

So, your approach will definitely not work in many cases as you can imagine e.g. does it handle a newline (\n) in filename? (Hint: No).

Few notes:

  • Don't parse ls; use dedicated tools (there is at least one for most use cases)
  • When dealing with filenames, try to leverage the NUL separated output provided by almost all GNU tools that work with such data
  • Take care when piping, make sure both programs can understand NUL separations
  • Whenever you're invoking xargs, see if you can get away with find ... -exec; in most cases, you will be fine with just find alone

I think these will get you going for now. steeldriver already provided the NUL separated idea in the comment (printf -- '%s\0' /path/to/some/folder/* | head -zn -2 | xargs -0 rm), use this as a starting point.

Solution 2:

xargs does support some quoting: with single quotes, double quotes or backslash which allows it to accept arbitrary arguments¹, but with a syntax that is different from the Bourne-like shells' quoting syntax.

The GNU implementation of ls as found on Ubuntu doesn't have any quoting mode that is compatible with the xargs input format.

Its ls --quoting-style=shell-always is compatible with ksh93, bash and zsh shells quoting syntax, but only when the output of ls is interpreted by the shell in the same locale as ls was when it output it. Also, some locales, like those using BIG5, BIG5-HKSCS, GBK or GB18030 should be avoided.

So with those shells, you can actually do:

typeset -a files
eval "files=($(ls --quoting-style=shell-always))"
xargs -r0a <(printf '%s\0' "${files[@]:0:3}") ...

But that has little advantage over:

files=(*(N))                 # zsh
files=(~(N)*)                # ksh93
shopt -s nullglob; files=(*) # bash

The only case where it becomes useful is when you want to use the -t option of ls to sort the files by mtime/atime/ctime or -S/-V. But even then, you might as well use zsh's:

files=(*(Nom))

for instance to sort the files by mtime (use oL for -S, and n for -V).

To remove all but the two most recently modified regular files:

rm -f -- *(D.om[3,-1])

¹ there are still some length limitations (by execve() and in some non-GNU xargs implementations much lower arbitrary ones), and some non-GNU xargs implementations will choke of input that contains sequences of bytes not forming valid characters.