Tagging files from the macOS command line

[Tweet : nvALT]

Let’s address the headline first. This post is about the tags on files that Apple started supporting in Mavericks. Up until iOS 11, they didn’t work on iOS devices, so they eventually became “Finder tags.” I think “Apple Tags” is going to have to be the nomenclature moving forward (now that they’re starting to work on iOS as well), but I don’t think it’s a widely accepted phrase yet. So I’ll use “Finder tags” for a little while longer.

I have a handful of scripts for manipulating tags from the command line, including the most complete (and useful to me) one, vitag. There’s an excellent CLI from James Berry called “tag” that I use frequently, but I sometimes implement more “down and dirty” techniques in scripts.1 If you’re just looking for a ready-to-go tool, grab tag and skip the rest of this.

Last weekend I wrote a script to handle cleaning up my system’s tags, merging synonymous tags, fixing spacing and punctuation, making casing and pluralization consistent, and various other nitpicks that have gotten messy in my taxonomy over time. It used the same basic Ruby classes that I used in vitag, which you can reference on GitHub for a more full-fledged version of these tips. I’m not ready to publish this last script yet, but I thought I’d point out a few simple tricks for those working on their own solutions.

Reading tags on a file

Tags are stored in extended attributes on the files, in a metadata attribute with the key kMDItemUserTags.

Not ideal:

Trying to view them using the xattr tool almost always results in a hex dump, and converting it results in a binary plist, and converting that gives you messy results.

$ xattr -px com.apple.metadata:_kMDItemUserTags "2015-04-01-intrepid-command-line-directory-traversal.md" | perl -wane 'print chr hex for @F'| plutil -p -
[
  0 => "bash
0"
  1 => "terminal
0"
  2 => "unix
0"
]

More ideal:

I’ve found it better to just get the raw output from mdls:

$ mdls -raw -name kMDItemUserTags "2015-04-01-intrepid-command-line-directory-traversal.md"
(
    bash,
    terminal,
    unix
)

I can parse that response, split lines, remove commas, etc., and turn it into an array of tags I can work with.

Writing tags to a file

Tags are written to files using xattr. They need to be passed to xattr in Plist format (XML) with an array of string elements containing the tags.

When you write tags to the file using xattr, it will obliterate any existing tags, so note that if you want to add tags instead of replacing them, you need to read the tags into an array as shown above, modify and update the array, then write the whole thing back to the file.

The command for doing this is:

xattr -w com.apple.metadata:_kMDItemUserTags '[plist data]' [file]

The xml string you need looks like this (line breaks for display, easiest to pass as one long string:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<array>
		<string>tag1</string>
		<string>tag2</string>
	</array>
</plist>

So the command for writing “tag1” and “tag2” to /file/path would be:

xattr -w com.apple.metadata:_kMDItemUserTags '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><array><string>tag1</string><string>tag2</string></array></plist>' /file/path

Here’s a Ruby snippet showing the building of the XML string and shelling out to write it to the file:

# File to operate on
path = "~/Dropbox/nvALT2.2/ruby shellwords module.md"

# Test array of tag strings
tags_xml = [':snippet:ruby', 'shell']

# begin valid PLIST XML string
plist = %Q{<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">}
# array of tag names to array of xml string

tags_xml.map! {|tag|
  "<string>#{tag.title}</string>"
}

# Join the array strings and append to the plist
plist += %Q{<plist version="1.0"><array>#{tags_xml.join()}</array></plist>}

# Shell out and run xattr to write the plist to the file's attributes
%x{xattr -w com.apple.metadata:_kMDItemUserTags '#{plist}' '#{File.expand_path(path)}'}

Color labels

The colors associated with tags like “Blue” and “Orange” (default label names) are stored in a different attribute (com.apple.FinderInfo). This dates back a ways, and there’s really no point in directly writing to this attribute anymore. It’s easiest just to change the tags. Remove “Blue” and add “Orange.”

Writing a color to a file, e.g.:

xattr -wx com.apple.FinderInfo "0000000000000000000900000000000000000000000000000000000000000000" filename

…will simply apply the Blue tag to the file automatically. So just apply the Blue tag and skip the hex strings.

In closing

I’ll stop there because I know this isn’t all of widespread interest. If you are looking into scripting any of this directly, I do recommend checking out the vitag code and James Berry’s tag project.

  1. Often just for the sake of not having any external dependencies…