How to use zmv — Z Shell’s super-smart file renamer

I’d like to introduce you — if you’re not already acquainted — to the Z Shell’s incredibly handy function zmv. If you ever need to change at the command line the names of a batch of files consistently, it’s the tool you’ll want to turn to first. It’s not well known, and having been given the nod by a colleague, I thought I’d explore and pass on some notes about taking advantage of it.

As I say, zmv is a function provided by the Z Shell, but it’s not made available by default. To ensure that it is, you need to add autoload zmv to your .zshrc file. Likely you have some other autoloads in there already. Restart your Terminal to activate it. Now you can call zmv like any other command.

The basic structure of a zmv call is zmv 'input_pattern' 'output_pattern'. The patterns are included in single quotes so that they’re not interpreted first by the shell and then passed to the command, though there is another way to avoid this. Here’s an example. Recently I had a batch of output files that I’d mis-spelled:

Fightners 01 The Test.mp4
Fightners 02 Goodbye My Lovely.mp4
Fightners 03 What Larks.mp4
Fightners 04 The Classroom.mp4

zmv provides an easy way to change the wrong part of each filename while retaining the correct part — and do so across all files that match the input pattern. So I used:

zmv 'Fightners (*)' 'The Frighteners $1'

As you might expect, the first pattern finds all files that begin with Fightners followed by a space; the rest of the pattern matches against anything. The brackets are standard Z Shell syntax: match the enclosed pattern. zmv passes the result of the match into the output pattern as the variable $1. If you had multiple pairs of brackets, they’d be accessed in the output as $1, $2, $3 etc., in order.

Using the first file above as an example, it’s found by the input pattern and $1 becomes 01 The Test.mp4. This is then passed into the output pattern to make The Frighteners 01 The Test.mp4. Then it repeats for all the other found files.

Here’s an example of multiple output variables, changing the 6 in the middle of the filename to a 1:

zmv '(*)6(???.HEIC)' '${1}1${2}'

I’ve used the ${var} form for referencing variables here to avoid confusion between $11 (variable 1 followed by 1) and $11 (variable 11).

Before committing yourself, you should run the command with the -n switch. This performs a dry run so you can see exactly what the results are going to be. This gets particularly important once you start using complex patterns.

Counting Files

zmv can also make use of shell variables, which allows you to do neat things like renumbering a bunch of files:

count=1 ; zmv '(*)_*.jpg' '$1-$((count++)).jpg'

This sets the variable count, which is then accessed (and incremented) in the output pattern. As usual the pair double-brackets in the output treat the variable as integer, which is then incremented.

So if I have a stack of IMG_4xxx.jpg files, they’ll be renumbered neatly to IMG-x.jpg starting at whatever count is initialized to, 1 in this case.

Don’t like that uppercase IMG? Then use Z Shell’s standard substitution ${VAR_NAME:l} to make them all lower case:

count=1 ; zmv '(*)_*.jpg' '${1:l}-$((count++)).jpg'

Don’t like the single digit in the output filename? Then use the following syntax to pad the value with up to four zeros:

count=1 ; zmv '(*)_*.jpg' '${1:l}-${(l:4::0:)$((count++))}.jpg'

The l (for left) and two sets of colons handle the padding: inside the first pair of colons is an expression the gives the number of digits to pad to, the second pair contains the pad character.

The l doesn’t only with with output, you can use it on the input side too. This example changes files with a filename of 50 characters or more:

c=1 ; zmv "${(l:50::?:)}*.log" '$((c++)).log'

The eagle-eyed will have spotted that the input term uses double-quotes. Here we’re using the shell to do the expansion to get all the files to be passed into zmv.

You can use r:expression::string: to pad on the right side. Both l and r can take an optional third argument, ie. r:expression::string1:string2: which adds a string that will be inserted before padding takes place. If string2 is too long, it is truncated.

Other Options

For basic operations, you can add the -W switch, which maps the match from the input wildcard to an equivalent wildcard in the output:

zmv -W '*.JPG' '*.jpg'

This saves the use of brackets and output variables, but is less flexible.

You can ditch the single quotes too, if you preface the call with noglob:

noglob zmv -W *.JPG *.jpg

Out of the box, zmv moves files — hence the name. But it can copy them instead, if you use the -C switch. Here’s how you could copy one file to multiple locations. Say you have a directory, GitHub, for your software projects. You can copy a top-level README.MD file to every sub-directory that contains a LICENSE file with the following:

zmv -C '(**/)LICENCE' '${1}README.MD'

Making Substitutions

So far we’ve taken the output variable $1 and made it lower case. We can also tweak it further, within the output pattern, using this syntax:

${1//pattern/replacement}

So if $1 contains a sequence of words separated by dots, you can convert those dots to dashes like this:

${1//./-}

The pattern can be any Z Shell pattern, just like the zmv input pattern, and you can use brackets to embed patterns within patterns.

A variant on this can be seen in the following example, to change spaces (in any file with a space in its name) to underscores:

zmv '* *' '${f:gs/ /_}'

The $f is another zmv-generated variable — it’s the filename that matches the entire input pattern. The s/ / _ substitutes a space for a _

. The g prefix tells zmv to perform the substitution globally, throughout the value of $f.

This line removes the first four characters from the filenames:

zmv '*' '$f[5,-1]'

$f, as we know, is the matched filename. The angle brackets contain a range of characters — a standard Z Shell subscript — for the fifth element (characters in this case) through to the last element (ie. the element at the length of the whole minus 1).

Other Z Shell Functions

Finally, if you want to see what other functions Z Shell has to offer, just check out where they’re stored by viewing $FPATH:

echo $FPATH

It’ll show something like this:

/usr/local/share/zsh/site-functions:/usr/share/zsh/site-functions:/usr/share/zsh/5.8/functions

So just enter the following command for the function list:

ls /usr/local/share/zsh/site-functions

Repeat it for the other directories in the $FPATH list.

Get the Details

You can find all the details of Z Shell’s pattern matching, substitutions and the rules that guide them here: http://zsh.sourceforge.net/Doc/Release/Expansion.html. There’s also some useful info in the comments to the zmv source code.

More Posts on the Z Shell

Enjoy some old school 3D arcade action — courtesy of the Raspberry Pi Pico

In the mid-1980s, I loved Phantom Slayer. Written for the Tandy Color Computer and made available for the Dragon 32, Phantom Slayer was a 3D maze shooter. Think a very basic version of Doom with colours but no textures. It wasn’t sophisticated, but it was quick and, more to the point, incredibly atmospheric.

The Pi Pico version of Phantom Slayer
Do you have what it takes to face down the Phantoms?
Continue reading

Hail to the Acorn Atom, the Pi Pico predecessor from 1980

If the Raspberry Pi is the BBC Micro de nos jours then the Pi Pico is perhaps the spiritual successor to that earlier Acorn micro: the Atom. So in homage to that ground-breaking pre-Beeb cased computer, here’s the latest offering from Smittytone’s Retro T-Shirt Store.

Atomic apparel
Continue reading

Play Hunt the Wumpus, Raspberry Pi Pico style

Here’s something a little different: a basic C project that you can follow to build a fun handheld game with a Raspberry Pi Pico. Your mission: to enter a dark cave, and then locate and destroy the monstrous Wumpus.

Locate and eliminate the monstrous Wumpus to win fabulous prizes… well, a trophy graphic…
Continue reading

How to share preferences between macOS/iOS apps

A couple of macOS releases or so ago, Apple introduced app extensions: self-contained modules that are bundled within apps to deliver functionality to the wider operating system. But how do apps and their extensions share information between themselves, in particular users’ preferences?

PreviewMarkdown’s new Preferences sheet
Continue reading

How to migrate to native Homebrew on an M1 Mac

Let the great Homebrew migration begin. Yes, Homebrew now has native support for Apple’s ARM64-based M1 chip. The latest version, 3.0.0, released 5 February, will run nicely on your Apple Silicon Mac. There’s a catch, of course. Well, several catches: first, not all of the tools you can install using Homebrew are M1 native yet and, second, Homebrew doesn’t offer explicit migration instructions, that I could find at least.

Apple Silicon Mac, now with native Homebrew support
Continue reading