Need To Rename Files in Bulk? Here's a Python CLI Called file-renamer
Tags: showdev, python, cli
Published: 2019-10-07 06:21:50

---
title: "Need To Rename Files in Bulk? Here's a Python CLI Called file-renamer"
published: true
description: sharing a python CLI for mass-renaming files
tags: showdev, python, cli
---

### Github repo: [https://github.com/isalevine/file-renamer](https://github.com/isalevine/file-renamer)

One of my hobbies is making electronic music, and thanks to resources like [samplesfrommars.com](https://samplesfrommars.com/), I have tons and tons of audio files to manage. One drawback of the my main drum machine is that it limits readable filenames--anything after a certain number of characters is cut off, and any name-collisions will point to the first file with that name.

A lot of my music sample library has folders full of long, repetitive filenames, like this:

![screenshot of folder showing long, repetitive audio file names](https://thepracticaldev.s3.amazonaws.com/i/kuv4owm09ut9hy6lkru1.png)

I got really tired of renaming them one-by-one, so I wrote my first Python script, file-renamer, as a CLI to do mass-renaming!

# file-renamer Overview

Basically, I wanted a way to change or remove specific strings from multiple filenames at once. As a CLI, file-renamer also provides a way to navigate a filesystem to modify files in different folders quickly and easily from the command line.

![gif of file-renamer in use on the command line](https://thepracticaldev.s3.amazonaws.com/i/qdhvicb7u45h32dvvk0y.gif)

## Setup

After cloning the repo to your computer, you can run file-renamer with `$ python3 script.py` in the repo's directory.

The only configuration you will need to do is setting a default path. See `script.py` line 37 (as of this writing) for the code to change:

```python
# script.py

path = os.path.realpath('./drum-samples') # set default path here
```

## Use

You can navigate your filesystem using the standard Unix commands `pwd`, `ls`, and `cd`. Renaming is done inside a particular folder with the `rn` or `rn all` commands. To quit, enter `q` or `quit`.

**Change Directory:**
```
cd
```

**List Files:**
```
ls
```

**Print Directory:**
```
pwd
```

**Rename (single file, change entire filename):**
```
rn <current_filename.ext> <new_filename.ext>
```
Ex.
```
$ Enter command: rn Airhorn.wav AIRHOOOORN.wav
```

**Rename All (multiple files, replace LAST string match found):**
```
rn all <string_input> <string_output>
```
Ex.
```
$ Enter command: rn all Hihat HH
```

Note: Currently, `rn all` will replace the LAST matching instance of `<string_input>` encountered in the filename *(i.e. first match encountered when reading right-to-left).* See [str.rpartition() in the Python docs](https://docs.python.org/2/library/stdtypes.html#str.rpartition) for more information.

**Exit Program:**
```
q / quit
```


# Python's `pathlib`, `os`, and `os.path` modules

The functionality of this script is powered by Python's  [`pathlib`](https://docs.python.org/3/library/pathlib.html), [`os`](https://docs.python.org/3/library/os.html), and [`os.path`](https://docs.python.org/3/library/os.path.html#module-os.path) modules. Here's how they're used:

### Navigating filesystem:

* [os.path.realpath(path)](https://docs.python.org/3/library/os.path.html#os.path.realpath) - returns a string with the full filepath for a supplied file or directory
* [os.path.join(path1, path2, ...)](https://docs.python.org/3/library/os.path.html#os.path.join) - returns a concatenated string from supplied paths (with included separators)
* [Path.parent](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent) - returns a string of the path's parent directory

All of these methods return strings which can be passed to `pathlib.Path(str)` to create a `Path` object, which is similar to a string but has different functionality:

* [pathlib.Path(str)](https://docs.python.org/3/library/pathlib.html#pathlib.Path) - creates a `Path` object out of a supplied string

In the file-renamer code, here's how they are implemented:

```python
# script.py
import pathlib
import os

path = os.path.realpath('./drum-samples')   # set default path here
current_directory = pathlib.Path(path)

...

# inside main loop, after getting user_input
        if user_input[0:2] == "cd":
            temp_directory = current_directory
        
            if user_input[3:5] == "..":
                new_path = current_directory.parent
                current_directory = pathlib.Path(new_path)
            else:
                new_path = os.path.join(current_directory, user_input[3:])
                current_directory = pathlib.Path(new_path)
...

        if not os.path.isdir(current_directory):
            print("Not a valid directory!")
            current_directory = temp_directory
```
Here, when `user_input` is `cd ..` , a `new_path` string is created from `current_directory.parent`, and is turned back into a Path object with `current_directory = pathlib.Path(new_path)`.

Otherwise, when user input is `cd` followed by something else, `new_path` is a string created by using `os.path.join` to add the contents of `user_input` onto the end of `current_directory`.


### Validate file and directory names:

In the snippet above, we also see that `os.path.isdir()` is used to throw an error if no directory is found. 

The `os.path` module provides a number of useful validation methods, including two which are used in this script:

* [os.path.isdir(path)](https://docs.python.org/3/library/os.path.html#os.path.isdir) - returns `true` if the supplied path is an existing directory, or `false` if not
* [os.path.isfile(path)](https://docs.python.org/3/library/os.path.html#os.path.isfile) - returns `true` if the supplied path is an existing file, or `false` if not

In addition to the above snippet, we see `os.path.isfile()` in action here:

```python
# script.py

# Here, array comes from using .split() on user_input
	if array[1] and array[2] and os.path.isfile(array[1]):
	    rename_whole_filename(array[1], array[2])
	    
	else:
	    print("Rename aborted! (Probably because the original filename wasn't found.)")
```

After validating that the `user_input` variable, now split into `array`, contains an existing filename at `array[1]` and a new filename at `array[2]`, `os.path.isfile(array[1])` allows us to confirm that the existing filename *actually exists*. Otherwise, it will return `false`, which we handle with an `else:` error message.

Same thing happens with `os.path.isdir(current_directory)` above--it confirms that `current_directory` is an existing directory.

### Rename files:

Hilariously, the core functionality of this whole CLI boils down to one simple `os` method:

* [os.rename(src, dst)](https://docs.python.org/3/library/os.html#os.rename)

Yep, that's it. Supply a string for `src` and `dst`, and `os.rename()` will take care of changing the filename.

Here's how it's used in the code:

```python
# script.py
def rename_partial_filename(filename, input, output):
    src = str(filename)
    head, sep, tail = src.rpartition(input)
    dst = head + output + tail
    os.rename(src, dst)
    return dst
```

Here, `src.rpartition(input)` allows us to search the filename-string `src` from right-to-left, searching for a match for the string `input`. If found, that one instance is replaced with the given string `output`, and reconcatenated as the string `dst`. Calling `os.rename(src, dst)` will change the filename in your system.

Note: This method currently encounters errors when a file does NOT contain the given input! Instead, it will continue searching with `.rpartition()` until it finds a match in its parent directories, **potentially causing directory errors!** You've been warned...

### That's it! Now you're ready to mass-rename your files! :)

# Interested in contributing?

Once again, here's the link to the GitHub repo: [https://github.com/isalevine/file-renamer](https://github.com/isalevine/file-renamer)

Please feel free to fork/clone/hack apart as you wish! If you do, I'd love to hear how you're using the script, whether it's for music sample libraries or something else entirely.

### If you'd like to tackle some specific issues with the current code, here are a few on my list:

1. In `rn` and `rn all`, build in handling for spaces--currently, **there is no working way to remove a substring without adding any characters!**
2. In `rn all`, build in an option to specify how many substring matches are replaced. (Originally, **all** matches were replaced--hence the switch to using `.rpartition()`...)
3. Add output logs for filename changes--currently, text is output to the console, but could easily be captured in a log file.

Thanks for reading! Please feel free to share any tips or advice for working with Python's `pathlib`, `os`, and `os.path` modules below!