An interactive file mover inspired by git

Time for the annual blog post!

I recently had to change the names of a bunch of files at work. I thought about how annoying it was going into Finder and renaming individual files, but how easy it was in my code editor to change the references to those files. So I had the idea to write a tool that could open a list of files in an editor, one on each line, and whatever file path was on each line when the file was saved and closed, that would be the new file path.

This is the approach git uses when doing tasks like writing a commit message or performing a rebase. I thought it would be ideal for renaming files as you could use all the features you’re used to in your editor like multiple cursors and regex, without having to learn any complicated CLI syntax.

Git Rebase Git Rebase

Now some weeks later, I’ve made the repo public on GitHub and published the tool as imv (interactive move) on NPM.

imv in action

Usage

Here’s how it works:

  1. You give imv a glob pattern of files you want to move or rename.
imv --cleanup "./home/*.png"
  1. It opens the list in your favourite code editor, determined either by the --editor parameter or automatically via your git config.
./home/customer.png
./home/puppy.png
./home/cat.png
  1. You make the edits you want, with the tools you already know.
./home-page/user.png
./home-page/puppy.png
./home-page/cat.png
  1. Save the file and close it. imv will make the changes, the file on each line becomes the new file location.

  2. imv deletes the old /home directory because we specified --cleanup and all the files have moved.

  3. You’re done!

Development

imv was written in TypeScript as a Node CLI application. It was quite enjoyable, but sometimes frustrating coming up against some of the problems I encountered. For example, it hadn’t really crossed my mind that some file systems are case sensitive and some are not until I tried to rename a file with a different case and fs.move() didn’t work.

There was no correct/official way I could find to determine whether the file system is case sensitive. In the end, I opted to require the user to specify the --overwrite parameter just in case they are on a case sensitive file system that might overwrite an existing file if your rename matched it. It’s an edge case, so really not worth overthinking it for version one. Right, I think I’ve said the word ‘case’ too many times now.

Working with users’ files is risky, so I used Jest to test imv. It saved a lot of boring manual labour running the tool each time in the terminal. I also added the option to send existing files that would overwrite to the computer’s recycle bin so you wouldn’t lose them by mistake.

GitHub CI

GitHub recently released a new version of their Actions continuous integration feature, and it’s really picking up speed. I’ve never used it, since the first version was a confusing drag and drop editor, but now it’s configured more traditionally in a YAML workflow file.

It seems to be very powerful; you can specify the events when a job should be run, you can make any step conditional and give dependencies before they start. You can also give a job a ‘matrix’ of variables that will run the job once with each possible combination you provided. I found this last one very useful for testing imv on multiple operating systems.

Here’s an example similar to what I’ve set up for imv:

name: build
on: [push]
jobs:
  build:
    name: ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macOS-latest]
    steps:
    - name: Checkout branch
      uses: actions/checkout@v1
    - name: Use Node.js 10.x
      uses: actions/setup-node@v1
      with:
        node-version: 10.x
    - name: npm install, lint and test
      run: |
        npm ci
        npm run lint
        npm run test-once

You can see under matrix an array of the big three platforms; Ubuntu, Windows, and MacOS. GitHub will run this build job three times, one for each OS, and replace ${{ matrix.os }} with one from the array. This way, I can easily run my tests on every platform without having to install them locally.

To conclude then, go check it out! And hopefully there aren’t too many bugs… 🐜