I have a main directory, let's say main. This main directory has some folders, among which are three folders named ccc ddd and lll, which are the only folders I want to target and work on. Each of these subfolders has some folders inside which are all named the same in the three subfolders, so that ccc ddd lll subfolders contain folders of the same name.

Then, each of these subsubfolders that are inside ccc ddd lll has a number of files named with a certain name like this c_000 d_000 l_000, c_001 d_001 l_001 and so on.

What I want to do first is rename these files so that the directory of their two parents will be attached in the beginning of the file name like this ccc_foo1_c_000 ddd_foo1_d_000 lll_foo1_l_000 and so on. I'm asking if anyone can advise me how this can be done in a .sh script?

This is a simple tree structure of my main folder:

.
`-- main
    |-- ccc
    |   `-- foO1
    |   |    |-- c_000
    |   |    `-- c_001
    |   |
         -- foo2
    |       |-- c_000
    |       `-- c_001
    |   
    |   
    |-- ddd
    |   `-- foO1
    |   |    |-- d_000
    |   |    `-- d_001
    |   |
         -- foo2
    |       |-- d_000
    |       `-- d_001
    |-- lll
    |      `-- foO1
    |  |    |-- l_000
    |  |    `-- l_001
    |  |
    |   -- foo2
    |      |-- l_000
    |      `-- l_001
    |
    |-- aaa
    |-- bbb
up vote 1 down vote accepted

You could do something like this:

for dir in ccc ddd lll; do 
    find "main/$dir" -type f -print0 | 
        while IFS= read -r -d '' f; do 
            dd=$(dirname "$f")
            new="${f/main\/}"
            new="${new//\//_}" 
            mv "$f" "$dd"/"$new"
        done
done

After the above script, your files will look like this:

.
`-- main
    |-- ccc
    |   |-- fo01
    |   |   |-- ccc_fo01_c_000
    |   |   `-- ccc_fo01_c_001
    |   `-- fo02
    |       |-- ccc_fo02_c_000
    |       `-- ccc_fo02_c_001
    |-- ddd
    |   |-- fo01
    |   |   |-- ddd_fo01_d_000
    |   |   `-- ddd_fo01_d_001
    |   `-- fo02
    |       |-- ddd_fo02_d_000
    |       `-- ddd_fo02_d_001
    `-- lll
        |-- fo01
        |   |-- lll_fo01_l_000
        |   `-- lll_fo01_l_001
        `-- fo02
            |-- lll_fo02_l_000
            `-- lll_fo02_l_001

Explanation

  • for dir in ccc ddd lll; do ...; done : do this only for these three directory names, saving each of them as $dir.
  • find "main/$dir" -type f -print0 | : find all files (type f) in main/$dir, and print them separated by the null string (-print0) to ensure that it works even if your file/directory names contain newlines.
  • while IFS= read -r -d '' f; do : this will iterate over the results of find, saving each as $f. The IFS= is needed to avoid breaking on whitespace, the -r is so that backslashes are not treated specially and the -d '' allows the reading of null-delimited input.
  • dd=$(dirname "$f") : $dd is now the directory where the current file resides. For example, main/ccc/fo01.
  • new="${f/main\/}" : this is using the shell's string manipulation abilities to remove the string main/ from the file's path.
  • new="${new//\//_}" : as above only now we are replacing all / with underscores. This results in a string like ccc_fo01_c000.
  • mv "$f" "$dd"/"$new" : finally, we rename the original file $f to $dd/$new. Remember that $dd is the directory this file was found in and $new is now the name you want to rename it to.

I suggest you add an echo before the mv to test before actually doing this.

  • Thank you very much for the answer! Did you have the chance to check the second part of the question? regarding copying depending on the names of the files manually copied. – Tak Mar 12 '15 at 16:11
  • 1
    @shepherd please post that as a separate question. – terdon Mar 12 '15 at 17:15
  • no worries. Done :) unix.stackexchange.com/questions/189833/… – Tak Mar 12 '15 at 23:03
  • find is overly complex here, given that the directory structure has fixed depth. Even keeping find involved, you could simplify by calling find ccc ddd lll … instead of first looping over the toplevel directories. – Gilles Mar 13 '15 at 0:35

Here's a simple POSIX shell solution that loops over the files, parses the directory parts of the names and renames them one by one.

for x in ccc/*/* ddd/*/* lll/*/*; do
  dir2=${x%/*}; dir1=${dir2%/*}; dir2=${dir2#"$dir1"}
  mv -- "$x" "${x%/*}/${dir1}_${dir2}_${x##*/}"
done

In bash, you can use the parameter expansion string replacement feature to avoid having to parse the file names.

for x in ccc/*/* ddd/*/* lll/*/*; do
  mv -- "$x" "${x%/*}/${x//\//_}"
done

In zsh, you can use zmv to rename files based on patterns.

autoload -U zmv
zmv '(ccc|ddd|lll)/(*)/(*)' '$1/$2/${1}_${2}_$3'

For a somewhat more soft-coded solution run from within your directory main:

#!/bin/bash

for f in $(find . -name '*_00*'); do
    mv "$f" "$(dirname "$f")/$(dirname "${f:2}" | tr "/" "_")_$(basename $f)"
done

This should iterate through all directories recursively looking for all files containing the pattern *_00*. Each of this files is renamed separately with a combination consisting of the path ($(dirname $"f")), the transformed path containing underscores and the first two characters removed ($(dirname "${f:2}" | tr "/" "_")) and the original filename itself ($(basename $f)).

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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