4

I have a directory with a bunch of subdirectories with a bunch of files with the extension .no_sub.

I want to rename every file with extension .no_sub to the same name with .no_sub removed.

So foo.no_sub -> foo.

Can I do this in Bash? (I am on Ubuntu 20.04)

ilkkachu
  • 138,973
Scorb
  • 614

7 Answers7

15

Using standard find, sh, and mv:

find . -type f -name '*.no_sub' -exec sh -c '
    for pathname do
        mv -- "$pathname" "${pathname%.no_sub}"
    done' sh {} +

This finds any regular file with a name that ends in the string .no_sub, in or below the current directory. For batches of these pathnames, a short in-line shell script is called. This sh -c script iterates over the given batch of pathnames and renames each of them by removing the .no_sub filename suffix. The removal of the filename suffix is done using the standard ${variable%suffix} parameter expansion.

No check is made for filename collisions.

This is similar to the solution provided by Marcus Müller in that the renaming of an individual file happens in an identical way, but uses find to generate the list of pathnames for the loop in a way that will pick up hidden names and includes an explicit file type filter to iterate over regular files only.

See also: Understanding the -exec option of `find`


Since we know that each filename given to the inline script ends with .no_sub, we may avoid repeating .no_sub in there if we want to:

find . -type f -name '*.no_sub' -exec sh -c '
    for pathname do
        mv -- "$pathname" "${pathname%.*}"
    done' sh {} +
Kusalananda
  • 333,661
13

Use the power of your shell to get all files, then use the usual tools to rename them

shopt -s nullglob ## as recommended and explained in the comments
shopt -s globstar
shopt -s dotglob
for fname in **/*.no_sub ; do
  mv -- "${fname}" "${fname%.no_sub}"
done

Here,

  • shopt -s globstar enables ** as a recursive glob
  • shopt -s dotglob enables finding .*.no_sub
  • The for loop is a special-character-safe way to go through all files (don't ever parse ls for that)
  • The mv syntax is mv source target;
  • I'm sometimes overly careful, but I also like ${fname} better than just $fname, because there can't be a variable name confusion. It just expands to the content of the variable fname, i.e. to the current file
  • The variable expansion ${variable%pattern} expands to the variable content, but reduced by the suffix pattern pattern
  • 1
    may want to use ./**/*.no_sub as the glob, and/or use -- with mv to stop any filenames starting with dashes from looking like options to mv. Also, maybe mv -n to not overwrite the target file if it exists (or not, depends on what you want). – ilkkachu Jul 18 '21 at 23:25
  • thanks for the safety edit, @ilkkachu !! yeah, files called -rf can be .. tricky. – Marcus Müller Jul 18 '21 at 23:30
  • Use shopt -s nullglob as well, just in case there are no .no_sub files. That will prevent you from calling mv -- "**/*.no_sub" "**/*", which could be disastrous if **/* expands to something that ends in a valid directory name. – chepner Jul 19 '21 at 16:18
  • @chepner, nullglob is definitely a good idea when using for over a glob, but as long as the expansions are in quotes, they globs shouldn't expand again, you'd only get problems for a file literally called **/*.no_sub. – ilkkachu Jul 19 '21 at 16:31
  • Oh, right; I overlooked the quoting. So nullglob would just prevent a call to mv that's doomed to return an error (unless there really is a file named **/*.no_sub, which would then be renamed **/* :) ). – chepner Jul 19 '21 at 16:34
  • @chepner, which could then trash a file called **/*, yes :) But in any case, nullglob should be used even if only for correctness. Also dotglob, if you want to catch files like dir/.dotfile.no_sub, or even .dotdir/file.no_sub. – ilkkachu Jul 19 '21 at 16:45
  • there can't be **/*.no_sub because otherwise match wouldn't be empty. So it seems it's actually safe always – RiaD Jul 20 '21 at 15:39
11

You can do this with find and xargs:

find . -name '*.nosub' -print0 | xargs -0 rename .nosub ""

The -print0 option to find and the -0 option of xargs are there to correctly handle filenames with spaces. The rename command is the one provided by the util-linux package.

larsks
  • 34,737
  • This would remove the first .nosub substring from a filename, renaming somefile.nosub.abc.nosub into somefile.abc.nosub. – Kusalananda Jul 20 '21 at 01:27
  • That's true, but at least in my experience it's practically unlikely. If you're not confident about spurious matches, I guess use one of the other solutions. As @cas demonstrates, it's easier to protect against this using the Perl rename utility. – larsks Jul 20 '21 at 02:07
3

Using the perl rename utility (aka prename, file-rename, perl-rename on various different distros):

find . -type f -name '*.no_sub' -print0 |
  rename -0 's/\.no_sub$//'
cas
  • 78,579
2

Using POSIX find:

find . -type f -name '*.no_sub' \
-exec perl -le '
  rename $_, s/\.no_sub$//r
    for @ARGV;
' {} +;

We look for regular files whose Names end with .no_sub and place the names in a bunch on the commandline of perl.

Perl then invokes it's built-in rename command and removes the trailing extension .no_sub, in effect, moving the file.

guest_7
  • 5,728
  • 1
  • 7
  • 13
2

No need for loops, just use the rename utility from the util-linux package.

$ /usr/bin/rename.ul '.no_sub' '' *.no_sub

See the rename man page, especially the last, "shortening" example in the EXAMPLES section.

1

Similar to Kusalananda's answer, but this will process the file one by one and without using a loop.

find . -type f -name '*.no_sub' -exec sh -c 'mv -- "$1" "${1%.no_sub}"' sh {} \;
annahri
  • 2,075