Create a Finder Service with Automator and Ruby

Learn how to use Automator to create a Service to automate the process of duplicating a file in Finder and incrementing a number in its filename.

I work with weekly publications. With many of these there are associated files – recurring ads with updated content, for example. Since I like systems, most of them have the issue number in their filenames. It can look something like Activity Calendar 1202.indd, for example. And for next issue, this would be updated to Activity Calendar 1203.indd.

I found myself doing this with several different files every week, and one day I thought it might be fun to figure out a way to do it with one simple command so that instead of doing this:

  1. Select the file.
  2. Duplicate the file (Cmd-D).
  3. Edit the file name (Return or Enter).
  4. Hit right arrow and delete the word “copy”.
  5. Move to wherever the number is and edit it.
  6. End the editing (Return or Enter again).

… I could end up with just this:

  1. Select the file.
  2. Hit a keyboard shortcut.

Besides, scripting is much more fun than editing filenames in Finder.

So which tools should we use to do this? We want to find a number in a string (the filename), increment it and insert it back into the string to get the new filename. Sounds like the perfect job for Ruby and regular expressions! And fortunately, Automator can run Ruby scripts (in addition to shell, Perl and Python scripts).

This article is going to assume that you have a basic understanding of Ruby scripting. Unfortunately, the topic of regular expressions (regexps for short) is much too huge to even begin explaining in an article like this, so if you want to learn more about them, check out regular-expressions.info or The Pickaxe (Pragmatic Programmers’ online version of their Ruby book).

Creating the Service in Automator

Start Automator, and choose the Service template.

Start by setting what kind of input this service accepts in the top right bar. It should say: Service receives selected files or folders in Finder

Then select the Utilities library section and drag the action Run Shell Script and drop it below the first action. In this action, set Shell: /usr/bin/ruby and Pass input: as arguments. This last option ensures that we get a list of full file paths as strings.

The Ruby Script

Here’s the code for the script. Copy it and paste it into the script action.

require 'fileutils'
ARGV.each do |f|
  afile = File::basename(f)
  adir = File::dirname(f)
  number_re = /\b1\d\d\d/
  amatch = number_re.match(afile)
  if amatch!=nil
    newnumber = amatch[0].to_i + 1
    afile.sub!(number_re, newnumber.to_s)
    newfilename = adir+"/"+afile
    FileUtils.copy f, newfilename if !File.exists?(newfilename)
  end
end

Your Service should now look something like this in Automator:

Let’s go through the script to learn what it does:

require 'fileutils'

We’re using the FileUtils class later, so we need to require that module.

ARGV.each do |f|
  # ...
end

ARGV stands for argument vector and is an array (one of the meanings of the word vector is “a one-dimensional array”) containing the arguments that are sent to the script. In this case it’s a list of the full file paths of the currently selected files in Finder.

The whole script is contained in an each loop, which means something like this: “For each item in the ARGV array, set the put this item in the variable f and then run the code.”

afile = File::basename(f)
adir = File::dirname(f)

Regular expressions are really advanced searches. If we want to find something specific in a piece of text, we want to make sure we don’t find more than we want. The only thing we want to change is the file’s name, not anything else in the path. We therefore use the File class to help us quickly parse the file path. The basename method takes a file path as an argument and gives back the actual filename without the directories, and conversely, dirname gives us just the directories.

number_re = /\b1\d\d\d/

This is the regular expression that actually specifies what to search for. We store it in a variable called number_re. The _re appendix to the variable name is just so it’s obvious that it contains a regular expression.

The forward slashes delimit the expression. \d will match a single digit, while \b means we’re looking for word boundaries – white space or punctuation, for example. This regexp will not match something like filename1234.doc or 1234filename.doc, because there aren’t word boundaries on both sides of the four digit number. But filename 1234.doc or 1234-filename.doc will match.

Like I just mentioned, it is a good idea to ensure that our regexps are as specific as possible. My four-digit issue number codes start with the year as two digits, which means they will start with 1 for the foreseeable future (the next eight years) and are followed by three other digits (\d matches 0–9). If I searched for just four digits of any kind (\d\d\d\d), I could end up matching something I didn’t want to. A worst case scenario might be accidentally hitting the shortcut keystroke while having lots of picture files selected with filenames like P123852.jpg and ending up with a copies named things like P123952.jpg.

So we search for numbers with exactly four digits, of which the first digit has to be a 1. Of course, you would probably want to modify this regular expression to match your needs. To create your own, I highly recommend using Rubular to test your expression against your filenames. You can even put in a list of filenames, one per line, to ensure you don’t match more than you want.

amatch = number_re.match(afile)

To do the actual search, we invoke the match method of the regexp, with the filename as an argument. We’re saying: “Hey, regexp, see if you match this string.” The returned result – a MatchData object – is stored in the variable amatch. If there is no match, the returned value is nil, so …

if amatch!=nil
  ...
end

… we enclose the rest of the script in an if block which ensures that we only make the new file if we do have a match.

newnumber = amatch[0].to_i + 1

The MatchData object acts as an array, and the first element of the array is what we’re interested in – it containes the whole string that was found by the regexp. We know it’s a number, since we’ve searched for only digits, so we can safely convert it to an integer with the to_i method. We add 1 and store the result in a variable called newnumber.

newfilename = adir + "/" + afile.sub(number_re, newnumber.to_s)

We have to make the new filename – that is, the full file path – by concatenating the directory and the new filename. And we must put a forward slash in between, or else we end up with a file named [parentdirectory][filename] in the directory above.

The sub method is Ruby’s String class’ method for search and replace. The first argument is a string or regular expression to search for, and the second argument is the replacement string. It returns a copy of the string with the replacement done. (There’s also a sub! method which modifies the original string, but since we just need to use the replaced string once, we can make do with a one-off.)

FileUtils.copy f, newfilename if !File.exists?(newfilename)

Finally, we use FileUtils.copy to do the actual copying of the old file, f, to the new file, newfilename – but only if the new file doesn’t exist already. It would be bad if we accidentally ran our Service on the wrong file and accidentally replaced an already existing file.

Brevity vs. clarity

It is entirely possible to write the newfilename line like so:

newfilename = adir+"/"+afile.sub(number_re, (amatch[0].to_i + 1).to_s)

It is shorter, but to the detriment of readability and ease of debugging. If the script doesn’t work as expected, we want to be able to comment out some parts or insert a puts statement somewhere to see what’s going on in the console (which you can display in Automator by clicking the Results button in the Run Shell Script action).

Our Service is ready! Save it (Cmd-S) and give it a name (mine is called Duplicate and increment).

Setting the keyboard shortcut

To set up the keyboard shortcut, open System Preferences, select Keyboard, switch to the Keyboard Shortcuts pane, select Services in the list on the left, and you’ll find your new action under Files and Folders.

Doubleclick to the right of the Service name so that you get an input box with a text cursor, then press your preferred keyboard shortcut. (I chose Command-Option-Shift-I.) Then click the checkbox to the left of the Service name to enable your new shortcut.

Test it by selecting some files in Finder and hitting your shortcut key combo. If everything works: Congratulations! If not: Double-check that you haven’t forgotten anything.

And regardless: Feel free to leave a comment!