1. Overview

Generally, the conversion of file and directory names in Linux can efficiently work with various text-control commands as tr, sed, and rename. For files and directories, the conversion of the names to lowercase can occur by first scanning the target location of interest using the find command, and beginning the rename process as soon as uppercase letters show up in the resulting tree.

However, the renaming process might fail when uppercase elements exist in multiple places in the directory tree. In this tutorial, we present an alternative approach that tries to overcome this issue, along with the case of symbolically linked content.

2. Example Directory Structure

We’ll show how a renaming script can act on filenames and directory names. Let’s consider an example with a directory structure containing four directories. Each of these contains a different number of sub-directories and files at various levels of depth.

As a matter of example, all but level-3 contain sub-elements with some uppercase characters. Let’s have a look at our directory structure for this case:

$ tree
.
├── Level-0
│   └── File_L0.txt
├── Level-1
│   └── Sub_Level-1
│       └── file_L1-S1.txt
├── Level-2
│   └── Sub_Level-1
│       ├── File_L2-S1.txt
│       └── Sub_Level-2
│           └── File_L2-S2.txt
└── level-3
    ├── file_l3.txt
    └── sub_level-1
        ├── file_l3-s1.txt
        └── sub_level-2
            └── file_l3-s1-s2.txt

 

3. Building the File and Directory Renaming Script

In the current section, we analyze the specific details of a script code, reverseLowerCase.sh. To evaluate the effect of the script in advance on the screen before any filename change, the user can declare what is his intention.

The process initiates by invoking the following command, providing as an argument the target directory of interest (full path):

$ ./reverseLowerCase.sh /home/dimtyp/Base

To perform any effective renaming, we use the previous command adding at the end the keyword do_rename.

$./reverseLowerCase.sh /home/dimtyp/Base do_rename

3.1. The Renaming Action

Initially, we define two variables for the files that hold the output. Also, we provide the directory where we apply the script and a flag to parse our intention to effectively apply or just show on the screen the required changes:

#!/bin/bash
LEVELSFILE=$PWD/fileDirs 
OUTPUT=$PWD/myOutput
myPath="$1"
myAction="$2"

At next, two functions are defined:

  • A friendly header message appears on the screen through the following function based on the proper choice of the second argument
  • The proper renaming action to lowercase when the second argument is do_rename
[ "$myAction" = "do_rename" ] && MSG==" ****  RENAMING  **** " || MSG=" **** PRINT-ONLY ****" 
message() {
    echo
    echo "====================="
    echo "$MSG"
    echo "====================="
    echo
}
	
renaming_action() {
    lowerCaseFileExistFlag=$([ -e "${myDirName}/${myLowercaseBasename}" ] && echo $?) 
    dontOverwriteFlag=${lowerCaseFileExistFlag:+n}
    case "$myAction" in
        do_rename) mv -v$dontOverwriteFlag "${myDirName}/${myBaseName}" "${myDirName}/${myLowercaseBasename}" >> $OUTPUT  ;;
                *) echo "${myDirName}/${myBaseName} --> ${myDirName}/${myLowercaseBasename}" >> $OUTPUT ;;
    esac
}

The renaming_action function finally covers an additional special case. When two or more files exist in the same subdirectory with names differing only in the type case (for example Readme.txt and README.txt), all filenames are going to share the same lowercase name. So, each renaming attempt from the mv command is going to overwrite the already existing identical name.

For the command mv, the -n option prevents this from happening, and this is set with the variable dontOverwriteFlag when the target filename exists, ignoring the overwriting action.

3.2. Determination of the Deepest Directories’ Level

A while-loop follows determining the maximum depth-level of the directory under examination. We print the number of elements per level excluding hidden files (-name ‘[!.]*’):

cd "$myPath"
echo "myPath: $myPath"
echo "myAction: ${myAction:-NONE}"
message
level=0
elementsNumber=100
while [ $f -ne 0 ]; do 
    elementsNumber=`find "$myPath" -mindepth $level -name '[!.]*' | wc -l | awk '{print $1}'`
    ((level++ ))
    echo "files-dirs: $elementsNumber -- depth: $level" >> $LEVELSFILE
done
cat $LEVELSFILE | column -t
echo
echo "Max-Depth: $((level - 1))"
echo

3.3. The Effective Renaming Mechanism

Starting ascending from the previously evaluated deepest level, the second while loop renames filenames with uppercase characters. The basename and dirname commands split the full filename path to allow this update:

while [ $level -ge 0 ]; do 
    for aFile in `find . -mindepth "$level" -maxdepth "$level" -name '[!.]*' -print | tr '\n' ' '`; do 
        myDirname=$(dirname $aFile)
        myBasename=$(basename $aFile)
        myLowercaseBasename=${myBasename,,} 
        symlinkFlag=0
        [ ${myBasename} != ${myLowercaseBasename} ] && renaming_action && symlinkFlag=1

Before the end, we examine whether a file is a symbolic link because a target file renaming will break the link relationship. When this is the case, we update the target file the symbolic links points to. We need to know though whether the target file the symbolic link points to is already renamed or not. The symlinkFlag variable is informing us with the value 1:

        if [ -L "$aFile" ]; then
            case $symlinkFlag in
                0) aSymlinkFile="${myDirname}/${myBasename}";;
                1) aSymlinkFile="${myDirname}/${myLowercaseBasename}";;
            esac
            targetFile=$(readlink -f "$aSymlinkFile")
            if [[ "$targetFile" =~ ^"$myPath".* ]]; then
                ln -sf "${targetFile,,}" "${aSymlinkFile}"
            fi
        fi
    done 
    (( level-- ))
done

Finally, we read the effective or hypothetical renaming actions on the screen, and we delete the files holding the results:

cat -n $OUTPUT | column -t
rm $LEVELSFILE
rm $OUTPUT

3.5. Executing the Renaming Script

When we execute our script, the screen shows the executed name modifications:

$ ./reverseLowerCase.sh /home/dimtyp/Base
myPath: /home/dimtyp/Base
myAction: NONE

=====================
 **** PRINT-ONLY ****
=====================

files-dirs:  17  --  depth:  1
files-dirs:  16  --  depth:  2
files-dirs:  12  --  depth:  3
files-dirs:  7   --  depth:  4
files-dirs:  2   --  depth:  5
files-dirs:  0   --  depth:  6

Max-Depth: 5

1   ./Level-2/Sub_Level-1/Sub_Level-2/File_L2-S2.txt  -->  ./Level-2/Sub_Level-1/Sub_Level-2/file_l2-s2.txt
2   ./Level-2/Sub_Level-1/Sub_Level-2                 -->  ./Level-2/Sub_Level-1/sub_level-2
3   ./Level-2/Sub_Level-1/File_L2-S1.txt              -->  ./Level-2/Sub_Level-1/file_l2-s1.txt
4   ./Level-1/Sub_Level-1/file_L1-S1.txt              -->  ./Level-1/Sub_Level-1/file_l1-s1.txt
5   ./Level-0/File_L0.txt                             -->  ./Level-0/file_l0.txt
6   ./Level-2/Sub_Level-1                             -->  ./Level-2/sub_level-1
7   ./Level-1/Sub_Level-1                             -->  ./Level-1/sub_level-1
8   ./Level-0                                         -->  ./level-0
9   ./Level-2                                         -->  ./level-2
10  ./Level-1                                         -->  ./level-1

By means of the tree command, we can verify the effective name changes (when do_rename is used):

/home/dimtyp/Base
├── level-0
│   └── file_l0.txt
├── level-1
│   └── sub_level-1
│       └── file_l1-s1.txt
├── level-2
│   └── sub_level-1
│       ├── file_l2-s1.txt
│       └── sub_level-2
│           └── file_l2-s2.txt
└── level-3
    ├── file_l3.txt
    └── sub_level-1
        ├── file_l3-s1.txt
        └── sub_level-2
            └── file_l3-s1-s2.txt

4. Conclusion

In this tutorial, we presented a custom script aiming to achieve the wanted renaming task. It determines the deepest directories’ level and the tree scanning occurs upwards performing all required updates. Finally, the script examines the symbolic linking issue case by identifying a symbolic link with the target file inside the directory tree and updating the relationship.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.