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
[/tmp] $ find /tmp -name "*.log" -type f -exec mv {} {%.*} \;
[/tmp] $ ls -l

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


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 '
    sub {
      if (/\.log\z/) {
        $old = $_;
        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.


¹ 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 /.

  • Would be nice make it without opening shell every file – Nir Nov 21 '17 at 13:53
  • 1
    @Nir, that's what the {} + form is for. It passes as many files as possible to sh so you run as few shs as possible. For the -execdir form, you'd still need at least one sh per different directory (and some find implementations like on FreeBSD would still run one sh per file even if there are several matching files in a given directory). – Stéphane Chazelas Nov 21 '17 at 14:05
  • I didn't know about the {} + option. It looks as the best option for this issue. Thanks. – Nir Nov 22 '17 at 9:54
  • Well, /tmp is just an example. Anyway I tried your solution with rename but it doesn't do anything: find . -name '*.log' -type f -execdir rename 's/\.log$/\.bluf/' {} +. The find command do return the correct files when I execute this command without the -execdir – Nir Nov 26 '17 at 9:02
  • @Nir, beware some Linux distributions come with a completely incompatible implementation of rename. The perl one might be called prename on your system. Also note that you don't need to escape the . in the replacement. – Stéphane Chazelas Nov 26 '17 at 9:55

Here's a one-liner:

find /tmp -name "*.log" -type f -exec sh -c 'f="{}"; mv "$f" "${f%.*}"' \;

It launches a shell, assigns the {} to a proper variable inside the shell and then applies the string manipulations using the shell syntax.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.