Quantcast
Channel: User Kamil Maciorowski - Super User
Viewing all articles
Browse latest Browse all 837

Answer by Kamil Maciorowski for How to delete only writable files?

$
0
0

Why is the approach not working as wanted?

From the POSIX standard [emphasis mine]:

If the -f option is not specified, and either the permissions of file do not permit writing and the standard input is a terminal or the -i option is specified, rm shall write a prompt to standard error and read a line from the standard input. If the response is not affirmative, rm shall do nothing more with the current file and go on to any remaining files.

The condition "the permissions of file do not permit writing" is in conjunction with "the standard input is a terminal". In yes n | rm … the standard input of rm is a pipe, not a terminal,


How to make it work

There are ways to pass input (like lines of n) to a program via its stdin that actually is a new terminal created deliberately to fool the program. E.g. we can write an expect script that runs rm and sends n (plus Enter) after any prompt containing ?.

A basic expect script:

#!/usr/bin/expect --set timeout -1spawn -noecho rm {*}$argvexpect {"\\?" {      send "n\r"      exp_continue   } eof {      lassign [wait] pid spawnid os_error_flag value      exit $value   }}

Save it as rmw and make it executable. Now instead of rm t/alpha t/beta t/gamma, run

/path/to/rmw t/alpha t/beta t/gamma

We put the script inside a regular file because we needed $argv. Embedding the script inside a shell script would be easy if expect -c supported $argv; but it does not. The following trick with /dev/stdin shall work and allow you to embed the code as a shell function:

rmw () {expect /dev/stdin "$@" <<'EOF'set timeout -1spawn -noecho rm {*}$argvexpect {"\\?" {      send "n\r"      exp_continue   } eof {      lassign [wait] pid spawnid os_error_flag value      exit $value   }}EOF}

Now you don't need a separate regular file and the command may be:

rmw t/alpha t/beta t/gamma

Notes:

  • rmw passes all its arguments to rm. It is possible to pass options (e.g. -r), not only operands.
  • The script exits with the exit value of rm.

Simpler approach

When there are just few non-directories to remove, for each file you can test the permissions and conditionally run a separate rm. For many non-directories such loop will probably be significantly slower than a single rm, still it should work. For directories (think rm -r) this approach will not work by itself.

A basic function that does it:

rmw () (   for f; do      test -w "$f" && rm -- "$f"   done)

Now instead of rm t/alpha t/beta t/gamma, run

rmw t/alpha t/beta t/gamma

Notes:

  • If permissions of some file change after the file undergoes test and before rm checks them (and if other conditions are met) then it may happen rm will still ask interactively. Decide if you want rm -f.
  • All arguments of rmw will be treated as pathnames.
  • There is no attempt to compute a meaningful exit value from exit values of multiple rms or whatever.

"Emulating" rm -r

The approach with test -w can be used even if you need rm -r. POSIX-ly:

find path/to/dir path/to/file path/to/whatever -depth \        -type d -exec rmdir -- {} \; \   -o ! -type d -exec test -w {} \; -exec rm -- {} \;

Notes:

  • rmdir will throw errors for non-empty directories; ignore them. Portable ways of checking if a directory is empty are rather inelegant, I decided not to obfuscate the code.
  • Some resources (including the manual of GNU find) call -exec"action", while e.g. -type is called "test". In fact "actions" return true or false just like "tests", they are tests (albeit sometimes trivial). This is an example of using -exec as a test: the first -exec runs an external command (test -w …) and only if it succeeds the next -exec will be evaluated.
  • If permissions of some file change after the file undergoes test and before rm checks them (and if other conditions are met) then it may happen rm will still ask interactively. Decide if you want rm -f.
  • The exit value from find will have nothing to do with exit value of any rm.

With GNU find and its non-portable tests we can do better:

find path/to/dir path/to/file path/to/whatever -depth \        -type d -empty    -delete \   -o ! -type d -writable -delete

Viewing all articles
Browse latest Browse all 837

Trending Articles