This is a quick tip rather than an involved tutorial. Here’s the YouTube version.

I had a situation today where I wanted to migrate a large-ish codebase from standard CSS files to Sass files with a *.scss file extension. There were around a hundred files nested in many different sub-folders. I didn’t fancy renaming them ‘by hand’, and the Finder batch rename is only really useful when all the files are in one folder and/or you can easily select them all. So I happened to ask in the Sublime Text Discord how people did this. It was suggested I look at zmv which works in the ZSH shell.

Turns out this is a great little tool I’d not used before, and as it met my needs beautifully, I thought I’d give a brief overview for my future self and any passing travellers.

The task at hand

So, to reiterate my my use case, I had a project where I wanted to change all files ending in .css to .scss. So, as zmv requires ZSH shell, at the risk of stating the obvious, you’ll need a ZSH shell. It’s the default shell in macOS these days so if you are on Catalina onwards – you already have it.

So assuming you have ZSH, let’s take a look at what we can do. First you will need to load the zmv function by running autoload zmv from the command prompt.

Now, I’ll show you the command that did my bidding, and then hopefully explain what’s going on. First, get into a parent folder of all the files you want to rename. Then I was able to run this. But don’t go doing that yet!!

zmv '(**/)(*).css' '$1_$2.scss'

The basics of how zmv works

Zmv lets us swap one thing with another, so everything in the first set of quotes with everything in the second set of quotes. Within the first one, zmv is letting us do two sets of replacements/groups. Each is within a set of parenthesis, the first is the file path, (**/) and the second the file name, (*).

Then we can manipulate what we have ‘captured’ in the second set of quotes. Each thing we ‘captured’ is represented by a $ in the second set of quotes, so you can see in this case that not only did I change the file extension, I added a proceeding _ before it. In my case this was because I wanted them as Sass partials, which are designated with the underscore.

Now, if you went with that prior command, typed it in and pressed enter, zmv will do its thing, and you’d better keep everything crossed you got everything right! Thankfully, zmv lets you perform a ‘dry run’.

Test your command first with the -n flag

You can run zmv commands first with the -n flag. So we would first run that prior command like this:

zmv -n '(**/)(*).css' '$1_$2.scss'

And that will give you a display of what is going to happen to each file. Lovely!

Then, once you are happy everything is as you need, run it again without the flag.

Zmv is obviously capable of much more but if it’s something you only need to do once every so often this will give you enough to go on!. If you want a few more examples, Filipe has some more examples, including how to ignore folders and more. So if you wanted to do our prior command but dodge files in node_modules it could be amended thus for a test run, then again without the -n when you are happy:

zmv -n '(^node_modules)/(**/)(*).css' '$1_$2.scss'

Notice that first group with the ‘hat’ symbol to negate the node_modules folder and it’s content? What a lovely, simple and powerful ZSH function.