izuna izuna - 1 month ago 8x
Linux Question

Rename recursively hundreds files using dir name as pattern

Current structure:

Cat and wolf
----- Volume 1.zip
----- Volume 2.zip
Cat and fox
---- Volume 01.rar
Rat and eagle
---- Rat and eagle 01.7x

As you can see, the structure doesn't follow an easy pattern. I want it to become the folder name + number. e.g.:

Cat and wolf 01.zip
Cat and wolf 02.zip
Cat and fox 01.rar
Rat and eagle 01.7z

There's any way to archieve this result?


This was quite a challenging problem. The trickiest aspect was the requirement for recursion. But also, as you noted, the naming pattern is not straightforward. In order to construct the destination file name, we have to parse out the trailing number from the original file name stem and concatenate that onto the recursively-constructed prefix of directory names.

Below is my solution, implemented in bash. It iterates over all objects in the given base directory. For subfiles, it moves them into the destination directory under the required file name, while for subdirectories, it recurses, building up a prefix of directory names concatenated on a separator string. The initial base directory, destination directory, and separator string are all parameterized as function arguments. I also, for convenience, remove the base directory after it has been processed, but only if it is found to be empty at that time.

function renameSubFilesToDest {

    local base;
    local dest;
    local sep;
    local prefix;

    local file;
    local fileBase;
    local -i num;
    local ext;
    local new;
    local -i rc;

    ## parse arguments
    if [[ $# -lt 1 ]]; then echo 'too few arguments.' >&2; return 1; fi;
    if [[ $# -gt 4 ]]; then echo 'too many arguments.' >&2; return 1; fi;
    base="$1"; ## unconditionally take base dir as 1st arg
    ## take destination dir as optional 2nd arg, default to base if not given
    if [[ $# -ge 2 ]]; then dest="$2"; else dest="$base"; fi;
    ## take name separator as optional 3rd arg, default to space if not given
    if [[ $# -ge 3 ]]; then sep="$3"; else sep=' '; fi;
    ## take new name prefix as optional 4th arg, default to empty string if not given
    if [[ $# -ge 4 ]]; then prefix="$4"; else prefix=''; fi;

    ## iterate over all objects in the base dir
    for file in "$base"/*; do
        fileBase="$(basename -- "$file";)";
        if [[ -d "$file" ]]; then
            ## recurse on dir, appending file basename and trailing sep to prefix
            renameSubFilesToDest "$file" "$dest" "$sep" "$prefix$fileBase$sep";
            rc=$?; if [[ $rc -ne 0 ]]; then return 1; fi;
            ## don't process files at the first depth level
            if [[ -z "$prefix" ]]; then continue; fi;
            ## parse num and ext from file name using bash extended regular expressions
            if [[ ! "$fileBase" =~ ([1-9][0-9]*)\.([^.]+)$ ]]; then
                echo "warning: file \"$fileBase\" does not match expected pattern." >&2;
            ## derive the final file name in dest
            new="$dest/$prefix$(printf %02d $num).$ext";
            printf '%s -> %s\n' "$file" "$new"; ## print status messages at run-time
            mv -i -- "$file" "$new";
            rc=$?; if [[ $rc -ne 0 ]]; then echo "error: mv [$rc]." >&2; return 1; fi;

    ## for convenience, remove the dir if it is now empty
    find "$base" -maxdepth 0 -empty -delete;

    return 0;

} ## end renameSubFilesToDest()

Here's a little helper function for the upcoming demo, which simply sets up your input file structure in the current directory:

function setupDemo {
    mkdir Cat\ and\ wolf Cat\ and\ fox Rat\ and\ eagle;
    touch Cat\ and\ wolf/Volume\ {1,2}.zip;
    touch Cat\ and\ fox/Volume\ 01.rar
    touch Rat\ and\ eagle/Rat\ and\ eagle\ 01.7z
} ## end setupDemo()

Here's a demo of the function on the input file structure:

setupDemo; ## set up the file structure

find *; ## show it
## Cat and fox
## Cat and fox/Volume 01.rar
## Cat and wolf
## Cat and wolf/Volume 1.zip
## Cat and wolf/Volume 2.zip
## Rat and eagle
## Rat and eagle/Rat and eagle 01.7z

renameSubFilesToDest .; ## run the solution
## ./Cat and fox/Volume 01.rar -> ./Cat and fox 01.rar
## ./Cat and wolf/Volume 1.zip -> ./Cat and wolf 01.zip
## ./Cat and wolf/Volume 2.zip -> ./Cat and wolf 02.zip
## ./Rat and eagle/Rat and eagle 01.7z -> ./Rat and eagle 01.7z

find *; ## show the result
## Cat and fox 01.rar
## Cat and wolf 01.zip
## Cat and wolf 02.zip
## Rat and eagle 01.7z

As GhostCat pointed out in his comment, this test case does not technically cover a truly recursive file structure, which would require at least two levels of subdirectories. I wrote my solution to be fully recursive because you implied in your title that recursion was the goal, and, in any case, a fully recursive solution would be more generic and potentially more useful. Here's a demonstration with a second level, and how about we change the separator for this demonstration as well:

mkdir -p level\ one/level\ two;
touch level\ one/level\ two/some\ file\ 7.txt;

find *;
## level one
## level one/level two
## level one/level two/some file 7.txt

renameSubFilesToDest . . _;
./level one/level two/some file 7.txt -> ./level one_level two_07.txt

find *;
## level one_level two_07.txt