Bash – Manipulate {} return string from find -exec

bashfindscriptingshell-script

I want to do it as efficient as possible in case there will be a lot of files.
What I want is rename all the files I found and remove their suffix.

For example:

[/tmp] $ ls -l
a.log
b.log
c.tmp
[/tmp] $ find /tmp -name "*.log" -type f -exec mv {} {%.*} \;
[/tmp] $ ls -l
a
b
c.tmp

This doesn't work. If it was a normal bash variable ${var%.*} would have returned var until the last ..

Best Answer

Start a shell to use shell parameter expansion operators:

find ~/tmp -name '*.log' -type f -exec sh -c '
  for file do
    mv -i -- "$file" "${file%.*}"
  done' sh {} +

Note that you don't want to do that on /tmp or any directory writable by others as that would allow malicious users to make you rename arbitrary .log files on the file system¹ (or move files into any directory²).

With some find and mv implementations, you can use find -execdir and mv -T to make it safer:

find /tmp -name '*.log' -type f -execdir sh -c '
  for file do
    mv -Ti -- "$file" "${file%.*}"
  done' sh {} +

Or use rename (the perl variant) that would just do a rename() system call so not attempt to move files to other filesystems or into directories...

find /tmp -name '*.log' -type f -execdir rename 's/\.log$//' {} +

Or do the whole thing in perl:

perl -MFile::Find -le '
  find(
    sub {
      if (/\.log\z/) {
        $old = $_;
        s/\.log\z//;
        rename($old, $_) or warn "rename $old->$_: $!\n"
      }
    }, @ARGV)' ~/tmp

But note that perl's Find::File (contrary to GNU find) doesn't do a safe directory traversal³, so that's not something you would like to do on /tmp either.


Notes.

¹ an attacker can create a /tmp/. /auth.log file, and in between find finding it and mv moving it (and that window can easily be made arbitrarily large) replace the "/tmp/. " directory with a symlink to /var/log resulting in /var/log/auth.log being renamed to /var/log/auth

² A lot worse, an attacker can create a /tmp/foo.log malicious crontab for example and /tmp/foo a symlink to /etc/cron.d and make you move that crontab into /etc/cron.d. That's the ambiguity with mv (also applies to cp and ln at least) that can be both move to and move into. GNU mv fixes it with its -t (into) and -T (to) options.

³ File::Find traverses the directory by doing chdir("/tmp"); read content; chdir("foo") ...; chdir("bar"); chdir("../..").... So someone can create a /tmp/foo/bar directory and at the right moment, rename it to /tmp/bar so the chdir("../..") would land you in /.

Related Question