Trying to rename certain file types within recursive directories

96 views Asked by At

I have a bunch of files within a directory structure as such:

Dir
    SubDir
            File
            File
    Subdir
            SubDir
                      File
            File
    File

Sorry for the messy formatting, but as you can see there are files at all different directory levels. All of these file names have a string of 7 numbers appended to them as such: 1234567_filename.ext. I am trying to remove the number and underscore at the start of the filename.

Right now I am using bash and using this oneliner to rename the files using mv and cut:

for i in *; do mv "$i" "$(echo $i | cut -d_ -f2-10)"; done

This is being run while I am CD'd into the directory. I would love to find a way to do this recursively, so that it only renamed files, not folders. I have also used a foreach loop in the shell, outside of bash for directories that have a bunch of folders with files in them and no other subdirectories as such:

foreach$ set p=`echo $f | cut -d/ -f1`
foreach$ set n=`echo $f | cut -d/ -f2 | cut -d_ -f2-10`
foreach$ mv $f $p/$n
foreach$ end

But that only works when there are no other subdirectories within the folders.

Is there a loop or oneliner I can use to rename all files within the directories? I even tried using find but couldn't figure out how to incorporate cut into the code.

Any help is much appreciated.

4

There are 4 answers

3
John Bollinger On BEST ANSWER

bash does provide functions, and these can be recursive, but you don't need a recursive function for this job. You just need to enumerate all the files in the tree. The find command can do that, but turning on bash's globstar option and using a shell glob to do it is safer:

#!/bin/bash

shopt -s globstar

# enumerate all the files in the tree rooted at the current working directory
for f in **; do
    # ignore directories
    test -d "$f" && continue

    # separate the base file name from the path
    name=$(basename "$f")
    dir=$(dirname "$f")

    # perform the rename, using a pattern substitution on the name part
    mv "$f" "${dir}/${name/#???????_/}"
done

Note that that does not verify that file names actually match the pattern you specified before performing the rename; I'm taking you at your word that they do. If such a check were wanted then it could certainly be added.

3
Cyrus On

With Perl‘s rename (standalone command):

shopt -s globstar
rename -n 's|/[0-9]{7}_([^/]+$)|/$1|' **/*

If everything looks fine remove -n.

globstar: If set, the pattern ** used in a pathname expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a /, only directories and subdirectories match.

0
flu On

How about this small tweak to what you have already:

for i in `find . -type f`; do mv "$i" "$(echo $i | cut -d_ -f2-10)"; done

Basically just swapping the * with `find . -type f`

0
ghoti On

Should be possible to do this using find...

find -E . -type f \
  -regex '.*/[0-9]{7}_.*\.txt' \
  -exec sh -c 'f="${0#*/}"; mv -v "$0" "${0%/*}/${f#*_}"' {} \;

Your find options may be different -- I'm doing this in FreeBSD. The idea here is:

  • -E instructs find to use extended regular expressions.
  • -type f causes only normal files (not directories or symlinks) to be found.
  • -regex ... matches the files you're looking for. You can make this more specific if you need to.
  • exec ... \; runs a command, using {} (the file we've found) as an argument.

The command we're running uses parameter expansion first to grab the target directory and second to strip the filename. Note the temporary variable $f, which is used to address the possibility of extra underscores being part of the filename.

Note that this is NOT a bash command, though you can of course run it from the bash shell. If you want a bash solution that does not require use of an external tool like find, you may be able to do the following:

$ shopt -s extglob   # use extended glob format
$ shopt -s globstar  # recurse using "**"
$ for f in **/+([0-9])_*.txt; do f="./$f"; echo mv "$f" "${f%/*}/${f##*_}"; done

This uses the same logic as the find solution, but uses bash v4 extglob to provide better filename matching and globstar to recurse through subdirectories.

Hope these help.