TweetFollow Us on Twitter

Writing Contextual Menu Plugins for OS X, part 2

Volume Number: 19 (2003)
Issue Number: 1
Column Tag: Mac OS X

Writing Contextual Menu Plugins for OS X, part 2

Running Unix commands and handling text selections

by Brent Simmons

In part one of this article (August 2002) we built a simple OS X contextual menu plugin that works in the Finder and provides a Copy Path command. It demonstrated the basics of implementing the COM-based interfaces for contextual menu plugins.

In this article we won't revisit the COM interface or other basics of contextual menu plugins--instead we'll go further, show how to run a Unix command from a contextual menu, how to send an update event to the Finder when a file changes, how to handle text selections, how to create a submenu with multiple commands, how to open a URL in a browser, and more.

The project file and source, built using the OS X 10.2 Developer Tools, can be downloaded from

Plugin Overview

This plugin (imaginatively named SamplePlugin) will provide three commands:

    1. Touch--when one or more files (or folders) are selected, a Touch command will appear in the contextual menu. It will run the Unix touch command, which sets the modification date of the selected files to the current date and time.

    2. Copy--when text is selected, this command will appear in a Text Samples submenu. It copies the selected text to the clipboard. (Note that while some applications already supply a Copy command in their contextual menu, many do not, and it's useful to have this command.)

    3. Search with Google--when text is selected, this command will appear in a Text Samples submenu. It runs a search for the selected text in Google in one's default Web browser.

Touch command

Figure 1. Touch command in Finder's contextual menu

The examineContext function in the plugin is called to give the plugin a chance to add commands to the contextual menu that's about to be displayed. This plugin does one of three things:

    1. It adds a Touch command if one or more files or folders are selected.

    2. It adds a Text Samples submenu, with Copy and Search with Google commands, if text is selected.

    3. It adds no commands if neither files nor text is selected.

Listing 1: checking the context

static OSStatus examineContext (void *pluginInstance,
   const AEDesc *context, AEDescList *commandList)
   if (isFileOrListOfFiles (context))
      addFileCommandsToMenu (commandList);
   else if (isTextSelection (context))
      addTextCommandsToMenu (commandList);
   return (noErr);

examineContext first checks to see if one or more files or folders is selected. If so, it adds the file commands (the Touch command) to the contextual menu.

If the selection is not files or folders, then it checks to see if text is selected. If so, then it adds the Text Samples submenu and the Copy and Search with Google commands.

If neither of the above are true, it just returns noErr, having added no commands to the contextual menu.

The check to see if files are selected first checks to see if a single file or folder is selected. If not, then it checks to see if multiple files or folders are selected.

Listing 2: checking if files are selected

static Boolean isFileOrListOfFiles (const AEDesc *desc)
   if (isFileOrFolder (desc))
      return (true);
   return (isListOfFiles (desc));

isFileOrFolder checks a single AEDesc to see if it refers to a file or folder.

Listing 3: checking an AEDesc to see if it's a file or folder

static Boolean isFileOrFolder (const AEDesc *desc)
      return (descIsOfType (desc, typeFSRef));

If the descriptor type is of typeFSRef, or can be coerced to typeFSRef, it's a file or folder, and so it returns true. descIsOfType performs this check. It's general--it can check for types other than typeFSRef. (In fact, this plugin also calls descIsOfType to check for a text selection.)

Listing 4: checking the type of an AEDesc

static Boolean descIsOfType (const AEDesc *desc,
   OSType desiredType)
   AEDesc tempdesc = {typeNull, NULL};
   if ((*desc).descriptorType == desiredType)
      return (true);
   if (AECoerceDesc (desc, desiredType, &tempdesc)
      == noErr) {
      AEDisposeDesc (&tempdesc);
      return (true);
      } /*if*/
   return (false);

Why check for typeFSRef instead of typeAlias or typeFSS? Because Apple's message is that FSRefs are the way to go in OS X. It would probably work as well to substitute one of these other types--but when your platform vendor tells you to do it a certain way, it's probably a good idea to listen.

Lists of files

If it's not just one file or folder selected, isFileOrListOfFiles next checks to see if it's a list of files or folders selected.

isListOfFiles loops through each item in the AEDescList and calls isFileOrFolder for each item. If any calls to isFileOrFolder return false, then the selection therefore contains items other than files or folders, and so isListOfFiles returns false.

Listing 5: looping through a list

static Boolean isListOfFiles (const AEDesc* desc)
   long numitems, i;
   OSErr err;
   err = AECountItems (desc, &numitems);
   if (err != noErr)
      return (false);
   for (i = 1; i <= numitems; i++ ) {
      AEKeyword keyword;
      AEDesc tempdesc = {typeNull, NULL};
      Boolean flFile = true;
      err = AEGetNthDesc (desc, i, typeWildCard,
         &keyword, &tempdesc);
      if (err != noErr)
         return (false);
      flFile = isFileOrFolder (&tempdesc);
      AEDisposeDesc (&tempdesc);
      if (!flFile)
         return (false);
   return (true);

AECountItems gets the number of items in the list. Then a for loop visits each item, calling isFileOrFolder with each. If isFileOrFolder returns false, then isListOfFiles returns false.

Otherwise it returns true, and then isFileOrListOfFiles returns true, and we're back to examineContext, which then calls addFileCommandsToMenu to add the Touch command to the contextual menu.

Adding the Touch menu command

To add the Touch command, this function calls pushCommand with the title of the command, its command ID, and the command list that will become the contextual menu that appears. The final parameter sent to pushCommand is NULL--if this item had a submenu it would be specified in the last parameter.

Listing 6: adding the localizable Touch command

static Boolean addFileCommandsToMenu
   (AEDescList *commandList)
   CFStringRef touchCommand =
      CFCopyLocalizedString (CFSTR("Touch"),
      "Touch Menu Text");
   return (pushCommand (touchCommand, touchCommandID,
      commandList, NULL));

Note that it gets the name of the Touch command by calling CFCopyLocalizedString. This looks up the name in MenuNames.strings (a resource that's included with the project you downloaded). This way you can easily localize the plugin by editing a strings file--which is definitely preferred to hard-coding the names of menu items in the source code.

We'll otherwise skip pushCommand for now--we'll talk about it later when we show how to build submenus. For now just know that it adds the Touch command to the contextual menu.

touchCommandID is defined in SamplePlugin.h. The command ID, since it's just used internally, does not have to be localized, of course.

After this function returns, examineContext returns noErr, and the system handles displaying and tracking the contextual menu.

Handling a command

handleSelection is called by the system to actually run a command chosen by the user.

Listing 7: handling the user's command

static OSStatus handleSelection (void *pluginInstance,
   AEDesc *context, SInt32 commandID)
   switch (commandID) {
      case touchCommandID:
         runTouchCommand (context);
      case copyCommandID:
         runCopyCommand (context);
      case searchCommandID:
         runSearchCommand (context);
   return (noErr);

Based on the command ID associated with the given command, it calls the corresponding function. If Touch is chosen, it calls runTouchCommand. There are similar cases for the Copy and Search with Google commands (more about those later).

Running the Touch command

The goal of this function is to pass to the system a string of text as if typed on the command line. We're calling the Unix touch command with one or more files or folders as arguments. The string that gets passed to the system will look something like this:

/usr/bin/touch "/path/to/some/file" "/path/to/some/other/file"

Each separate file path is enclosed in quotes because there may be spaces in the name.

So the first thing runTouchCommand does is get the list of selected files as a string suitable for passing to the system as command line arguments. Then it actually calls the system to run the Touch command. Finally it sends an update event to the Finder so it updates its display.

Listing 8: running the command

static void runTouchCommand (const AEDesc *desc)
   CFStringRef commandLineParams;
   if (!getFileListAsText (desc, &commandLineParams))
   callSystem (CFSTR("/usr/bin/touch"), commandLineParams);
   CFRelease (commandLineParams);
   sendUpdateEventToFinder (desc);

getFileListAsText is somewhat similar to the isListOfFiles function--it loops through the selected files in the same way. It also double-checks that each item actually is a file or folder.

Listing 9: getting the list of selected files as text

static Boolean getFileListAsText (const AEDesc *desc,
   CFStringRef *fileList)
   long numitems, i;
   OSErr err;
   Boolean flSuccess = false;
   CFMutableStringRef s = CFStringCreateMutable
      (kCFAllocatorDefault, 0);
   if (s == NULL)
      return (false);
   err = AECountItems (desc, &numitems);
   require_noerr (err, getFileListAsText_fail);
   for (i = 1; i <= numitems; i++ ) {
      AEKeyword keyword;
      AEDesc tempdesc = {typeNull, NULL};
      Boolean flFile = false;
      err = AEGetNthDesc (desc, i, typeWildCard, &keyword,
      require_noerr (err, getFileListAsText_fail);
      flFile = (tempdesc.descriptorType == typeFSRef);
      if (!flFile) {
         err = AECoerceDesc (&tempdesc, typeFSRef, &tempdesc);
         require_noerr (err, getFileListAsText_fail);
         flFile = (tempdesc.descriptorType == typeFSRef);
      if (!pushFileAsText (&tempdesc, s))
         flFile = false;
      AEDisposeDesc (&tempdesc);
      if (!flFile)
         return (false);
   flSuccess = true;
   if (flSuccess)
      *fileList = CFStringCreateCopy
         (kCFAllocatorDefault, s);
      *fileList = NULL;
   CFRelease (s);
   return (flSuccess);

One of the parameters, fileList, is a pointer to a CFStringRef that will contain the list of files as quote-enclosed arguments. For each item, pushFileAsText is called, which adds each item to a CFMutableString created at the top of this function.

At the end of the function the CFMutableString is copied to the fileList parameter, which is a non-mutable CFString. (That is, if everything went well. In case of a problem the function returns false and fileList is NULL.)

Adding a single file to the list

pushFileAsText takes one AEDesc that references a file or folder, gets its Unix path, escapes double quotes in the path, escapes $ characters in the path, puts double quotes around the path, adds a space, then adds the text to the passed-in CFMutableString.

Listing 10: adding a single file

static Boolean pushFileAsText (const AEDesc *desc,
   CFMutableStringRef s)
   CFStringRef pathString, escapedPathString;
   if (!getPathStringFromFSRef (desc, &pathString))
      return (false);
   if (!escapeShellText (pathString, &escapedPathString)) {
      CFRelease (pathString);
      return (false);
   CFStringAppend (s, CFSTR("\""));
   CFStringAppend (s, escapedPathString);
   CFStringAppend (s, CFSTR("\" "));
   CFRelease (pathString);
   CFRelease (escapedPathString);
   return (true);

It calls getPathStringFromFSRef to get the path to the file as a CFString.

Then it escapes quotes and $ characters inside the path string--if there are quotes anywhere in the path they must be escaped, otherwise there would be quote mis-matches in the command line text. $ characters must be escaped to avoid variable interpolation. (For instance, if you had a file named $USER for some strange reason, the system would replace $USER with your actual user name. We don't want that to happen here.)

It then adds a quote to the mutable string, then adds the escaped path, then adds another quote and a space. You end up with a string like "/path/to/some/file" or "/path to some/file" or "/path to/\$some\" file/."

Getting the path string

getPathStringFromFSRef gets a Unix path string from an FSRef and puts it into a CFString.

Listing 11: getting a path string from an FSRef

static Boolean getPathStringFromFSRef (const AEDesc *desc,
   CFStringRef *pathString)
   FSRef fileRef;
   Size dataSize = AEGetDescDataSize (desc);
     OSErr err;
   CFURLRef fileURL;
   err = AEGetDescData (desc, &fileRef, dataSize);
   if (err != noErr)
      return (false);
   fileURL = CFURLCreateFromFSRef (kCFAllocatorDefault,
   if (fileURL == NULL)
      return (false);
   *pathString = CFURLCopyFileSystemPath (fileURL,
   return (*pathString != NULL);

First it gets the FSRef object from the AEDesc by calling AEGetDescData. CFURLCreateFromFSRef gets a CFURLRef from the FSRef--and from that it gets the Unix-style path by calling CFURLCopyFileSystemPath.

getCFString is a simple wrapper around CFStringCreateWithCString, which creates a CFString based on a C string.

Escaping text for the command line

Back to pushFileAsText: it next calls escapeShellText. It does a couple find-and-replace operations in a CFString. It escapes double quotes and $ characters to avoid quote mis-matches (when a file contains a double quote in its name) and variable interpolation (on the off chance you have a file named something like $USER).

Listing 12: escapeShellText

static Boolean escapeShellText (CFStringRef source,
   CFStringRef *dest)
   CFStringRef tempString;
   Boolean flSuccess = false;
   if (!replaceAll (CFSTR("\""), CFSTR("\\\""),
      source, &tempString))
      return (false);
   flSuccess = replaceAll (CFSTR("$"), CFSTR("\\$"),
      tempString, dest);
   if (flSuccess)
      CFRelease (tempString);
   return (flSuccess);

replaceAll actually performs the replacements.

Listing 13: replaceAll

static Boolean replaceAll (CFStringRef searchFor, CFStringRef replaceWith, CFStringRef source, 
CFStringRef *dest)
   CFMutableStringRef mutableString;
   CFRange range;
   mutableString = CFStringCreateMutableCopy
      (kCFAllocatorDefault, 0, source);
   if (mutableString == NULL)
      return (false);
   range = CFRangeMake (0,
      CFStringGetLength (mutableString));
   CFStringFindAndReplace (mutableString, searchFor,
      replaceWith, range, 0);
   *dest = CFStringCreateCopy (kCFAllocatorDefault,
   CFRelease (mutableString);
   return (*dest != NULL);

The first parameter to replaceAll is the string to find, the second parameter is the replacement string, the third parameter is the source string in which to search, and the last parameter is a pointer to a CFStringRef that will contain the result.

Aside: CFStrings

Question: why use CFStringRefs and CFMutableStringRefs? Why not traditional C strings or Pascal strings?

Because, as with FSRefs, Apple's message is that CFStrings are the way to go. But beyond that they have several benefits:

    1. They're easy to manipulate. Functions like CFStringAppend make it very easy, for instance, to add text to a string. Check out CFString.h--there is a gold mine of functions which make string manipulation easy.

    2. CFStrings are "toll-free bridged" with Cocoa NSStrings. This means that, among other things, when writing Cocoa code, you can call CFString functions with NSStrings as parameters. This is an example of a kind of convergence, where you can write code that works in Carbon apps as well as Cocoa apps.

    3. CFStrings take some of the headache out of supporting various character encodings. Support for Unicode and other text encodings is an important part of a modern operating system, and CFStrings can store Unicode text as well as other encodings.

Back to runTouchCommand

Now it's time to actually call the system to run the Touch command.

If getFileListAsText succeeds, runTouchCommand calls callSystem, which takes two parameters: the path to the command to execute and the command line parameters (as a single string).

The path to the Touch command is /usr/bin/touch. The command line parameters in this case is the list of files created in getFileListAsText.

Note the line (in runTouchCommand): callSystem (CFSTR("/usr/bin/touch"), commandLineParams);

The CFSTR("/usr/bin/touch") part is a shortcut for creating a CFStringRef from quoted text. (Cocoa developers will note that it's the equivalent of typing @"/usr/bin/touch" to create an NSString.)

Calling the system

It's simple to call the system to execute a command-line string. There's a function actually named system that takes a C string and executes the command as if typed in the Terminal.

(The man page for system is pretty short and worth checking out.)

Listing 14: calling the system

static void callSystem (CFStringRef command,
   CFStringRef params)
   char *buffer;
   CFMutableStringRef fullCommand;
   fullCommand = CFStringCreateMutableCopy
      (kCFAllocatorDefault, 0, command);
   if (fullCommand == NULL)
   CFStringAppend (fullCommand, CFSTR(" "));
   CFStringAppend (fullCommand, params);
   if (!getCString (&buffer, fullCommand,
      goto callSystem_exit;
   printf ("System call: ");
   printf (buffer);
   printf ("\n");
   system (buffer);
   CFRelease (fullCommand);
   if (buffer != NULL)
      free (buffer);

Our callSystem function takes two CFStringRef parameters--the path to the command and the command-line parameters. In order to call the system command it needs to create a C string of this form: /path/to/command param1 param2 param3.

In this plugin it will look something like this: /usr/bin/touch "/path/to/file1" "/path/to/anotherFile"

The fullCommand string is a mutable CFString. It starts by copying the command (the /usr/bin/touch part) to itself.

Then it appends a space, since there needs to be a space between the command and the parameters. Then it appends the parameters.

At this point we have the string in the right form for the system, except that it's a CFString rather than a C string. So it calls getCString to get a C string.

Listing 15: getCString

static Boolean getCString (char **cStringToGet,
   CFStringRef cfString, CFStringEncoding encoding)
   UInt32 lenText = sizeof (UniChar) *
      CFStringGetLength (cfString) + 1;
   *cStringToGet = malloc (lenText);
   if (*cStringToGet == NULL)
      return (false);
   if (!CFStringGetCString (cfString, *cStringToGet,
      lenText, encoding)) {
      free (*cStringToGet);
      *cStringToGet = NULL;
      return (false);
   return (true);

This function is a wrapper for CFStringGetCString, which gets a C string from a CFString. First this function allocates a buffer that's the length of the CFString plus one for zero-termination. Then it calls CFStringGetCString. If that call fails, the buffer is freed and the function returns false. If all's well it returns true.

Back to callSystem

Before calling the system function, notice the trio of printf commands. An important point: one way to debug contextual menus is to use printf commands. The output appears in the Console. (Which lives at /Applications/Utilities/Console.) If you launch it, then run the Touch command from a contextual menu, you'll see a copy of what gets passed to the system command in the Console.

Though not used here, another important function, similar to printf, is the CFShow command. This prints a description of a CoreFoundation object--such as a CFString--to the Console. (See CFShow and CFShowStr in CFString.h and in the documentation.)

Okay, now it finally calls the system with the C string from getCString. This runs the Unix touch command with the list of files as parameters.

This function cleans up, then control goes back to runTouchCommand, then back to handleSelection which returns noErr.

That's it for running a Unix command. You know the basics--you can do just about anything Unix-y from a contextual menu plugin now.

But it didn't work...

Oh yes it did. But it might not have appeared to work.

Here's why: sometimes the Finder doesn't update its display right away when you make a change to a file from a contextual menu. And so it may look like the command didn't work, even though it actually did.

So runTouchCommand sends the Finder a kAESync event via the sendUpdateEventToFinder function:

Listing 16: sending a kAESync event to the Finder

static void sendUpdateEventToFinder (const AEDesc *desc)
   AEAddressDesc finderAddressDesc = {typeNull, NULL};
   AppleEvent event = {typeNull, NULL};
   AppleEvent reply = {typeNull, NULL};
   OSType finderSignature = 'MACS';
   OSErr err;
   err = AECreateDesc (typeApplSignature, &finderSignature,
      sizeof (finderSignature), &finderAddressDesc);
   if (err != noErr)
   err = AECreateAppleEvent (kAEFinderSuite, kAESync,
      &finderAddressDesc, kAutoGenerateReturnID,
      kAnyTransactionID, &event);
   require_noerr (err, sendUpdateEventToFinder_fail);
   err = AEPutParamDesc (&event, keyDirectObject, desc);
   require_noerr (err, sendUpdateEventToFinder_fail2);
   AESend (&event, &reply, kAENoReply + kAECanInteract,
      kAENormalPriority, kAEDefaultTimeout, NULL, NULL);
   AEDisposeDesc (&event); 
   AEDisposeDesc (&finderAddressDesc);

It creates an Apple event that just asks the Finder to update its display. (See FinderRegistry.h for this and other commands you can send the Finder.)

This function passes to the Finder, as the direct object parameter, the same AEDesc context received in handleSelection. One assumes that the Finder knows what to do with this AEDesc since it came from the Finder in the first place.

kAENoReply is specified, since a reply wouldn't be useful, and we don't really care if the Finder failed to handle the event. We hope it handled the event, but if it didn't it's no big deal, since the Touch command succeeded anyway.

Text commands and submenus

Let's back all the way up to examineContext and show how to add a submenu to the contextual menu and how to handle our two text commands (Copy and Search with Google).

Recall that if it's not one or more files or folders that are selected, then examineContext checks to see if it's text that's selected. isTextSelection performs this check. If the check returns true, then examineContext calls addTextCommandsToMenu to add the submenu and its commands.

Figure 2. Text commands in BBEdit's contextual menu


This function checks the AEDesc that examineContext got from the system to see if it's of type text or can be coerced to type text.

Listing 17: checking if a selection is a text selection

static Boolean isTextSelection (const AEDesc *desc)
      return (descIsOfType (desc, typeChar));

typeChar, defined in AEDataModel.h, is 'TEXT.' This function calls descIsOfType, which was also called by isFileOrFolder to determine if an AEDesc refers to a file or folder.

If isTextSelection returns true, then examineContext calls addTextCommandsToMenu with the AEDescList from the system that will become the contextual menu.

Adding text commands

The plugin will build a menu item named Text Samples with a submenu. The submenu will contain two commands: Copy and Search with Google.

Here things are done in what may seem like a backward order. First the submenu (Copy and Search with Google commands) is created, then the menu item is created that will contain the submenu.

Listing 18: adding text commands to the menu

static Boolean addTextCommandsToMenu (AEDescList*
   AEDescList submenuCommands = {typeNull, NULL};
   OSErr err;
   Boolean flSuccess = false;
   CFStringRef textSubmenuName =
      CFCopyLocalizedString (CFSTR("Text Samples"),
         "Text Samples Menu Text");
   err = AECreateList (NULL, 0, false, &submenuCommands);
   if (err != noErr)
      return (false);
   if (!buildTextSubmenu (&submenuCommands)) {
      AEDisposeDesc (&submenuCommands);
      return (false);
   flSuccess = pushCommand (textSubmenuName, nil,
      commandList, &submenuCommands);
   AEDisposeDesc (&submenuCommands);
   return (flSuccess);

The submenu is, like the commandList passed to examineContext from the system, an AEDescList that will specify one or more commands. submenuCommands will hold the submenu: buildTextSubmenu creates this submenu.

Listing 19: building a submenu

static Boolean buildTextSubmenu (AEDescList *commands)
   CFStringRef searchCommand = CFCopyLocalizedString
      (CFSTR("Search with Google"), "Search Menu Text");
   CFStringRef copyCommand = CFCopyLocalizedString
      (CFSTR("Copy"), "Copy Menu Text");
   if (!pushCommand (copyCommand, copyCommandID,
      commands, nil))
      return (false);
   return (pushCommand (searchCommand, searchCommandID,
      commands, nil));

It calls pushCommand twice, with the command names, command IDs, and the AEDescList that will contain the commands.

If it succeeds, then addTextCommandsToMenu calls pushCommand to add the Text Samples menu item that will contain the submenu.

Here's the important point: both the main contextual menu and any submenus are AEDescLists. That means that you add commands the same way, whether it's the main menu or any submenus. pushCommand is thus a reusable piece of code, useful whether you're building one or more submenus or not.

Listing 20: adding a command to a menu

static Boolean pushCommand (CFStringRef commandName,
   SInt32 commandID, AEDescList* commands,
   AEDescList *submenuToAttach)
   OSStatus err = noErr;
   AERecord commandRecord = {typeNull, NULL};
   Boolean flSuccess = false;
   char *commandNameCString;
   err = AECreateList (NULL, 0, true, &commandRecord);
   require_noerr (err, pushCommand_fail);
   if (!getCString (&commandNameCString, commandName,
      goto pushCommand_fail;
   err = AEPutKeyPtr (&commandRecord, keyAEName,
      typeUTF8Text, commandNameCString,
      strlen (commandNameCString) + 1);
   free (commandNameCString);
   require_noerr (err, pushCommand_fail);
   if (commandID != NULL) {
      err = AEPutKeyPtr (&commandRecord,
         typeLongInteger, &commandID, sizeof (commandID));
      require_noerr (err, pushCommand_fail);
   if (submenuToAttach != NULL) {
      err = AEPutKeyDesc (&commandRecord,
         keyContextualMenuSubmenu, submenuToAttach);
      require_noerr (err, pushCommand_fail);
   err = AEPutDesc (commands, 0, &commandRecord);
   if (err == noErr)
      flSuccess = true;
   AEDisposeDesc (&commandRecord);
   return (flSuccess);

When using pushCommand to add a menu item that contains submenu, just call it with a NULL commandID and a non-NULL AEDescList that specifies the submenu it contains (submenuCommands, in this case).

When using pushCommand to add a menu item that does not contain a submenu, specify its command ID, and send a NULL AEDescList, since it will have no submenu.

When addTextCommandsToMenu is finished, control returns to examineContext, which returns noErr. Then the system displays and tracks the contextual menu with the Text Samples menu item and its submenu.

Handling the text commands

Back to handleSelection--if the command ID is the ID of the copy command, runCopyCommand is called. If it's the command ID of the search command, runSearchCommand is called. Let's do runCopyCommand first.

The Copy command copies the selected text to the clipboard (unsurprisingly).

Listing 21: running the Copy command

static void runCopyCommand (const AEDesc *desc)
   CFStringRef selectedText;
   if (!getTextFromDesc (desc, &selectedText,
   writeStringToClipboard (selectedText);
   CFRelease (selectedText);

First it gets the selected text from the AEDesc via getTextFromDesc, then it writes that text to the clipboard via writeStringToClipboard, then it cleans up and returns to handleSelection.


Any time you're handling text selections you need to get the selected text. getTextFromDesc gets it as a CFString.

Listing 22: getting text from an AEDesc

static Boolean getTextFromDesc (const AEDesc *desc,
   CFStringRef *text, CFStringEncoding encoding)
   long len = AEGetDescDataSize (desc);
   char *s;
   Boolean flSuccess = false;
   s = malloc (len + 1);
   if (s == NULL)
      return (false);
   AEGetDescData (desc, s, len);
   s [len] = '\0';
   flSuccess = getCFString (text,
      (const char *) s, encoding);
   free (s);
   return (flSuccess);

AEGetDescDataSize tells us how long the selected text is. It then allocates a buffer to get that text, then calls AEGetDescData to put it into the buffer. It zero-terminates the buffer so that it's a C string.

The function then calls getCFString to get a CFString based on the C string it got from the AEDesc.

After freeing the buffer (which is no longer needed) it returns true if the CFString was created and false otherwise.

getCFString is a simple wrapper around CFStringCreateWithCString.

Listing 23: getting a CFString from a C string

static Boolean getCFString (CFStringRef *stringToGet, const char *cString, CFStringEncoding encoding)
   *stringToGet = CFStringCreateWithCString
      (kCFAllocatorDefault, cString, encoding);
   return (*stringToGet != NULL);

runCopyCommand then writes the text to the clipboard via writeStringToClipboard.

Listing 24: writing text to the clipboard

static void writeStringToClipboard (CFStringRef s)
   ScrapRef scrap;
   char *cString;
   if (!getCString (&cString, s, kCFStringEncodingMacRoman))
   ClearCurrentScrap ();
   GetCurrentScrap (&scrap);
   PutScrapFlavor (scrap, kScrapFlavorTypeText,
      kScrapFlavorMaskNone, strlen (cString),
      (const void *) cString);
   free (cString);

Note that this seems a little crazy--in getTextFromDesc we created a CFString from a C string, then in writeStringToClipboard we turn around and get a C string from a CFString. Why?

Well, in the interests of maintainability, I like having just one function that gets the selected text. Instead there could be one function that returns it as a CFString and another that returns it as a C string. I prefer getting a CFString because CFStrings are so easy to manipulate, and chances are you're going to want to do some operation on the selected text.

In this case, however, we're not manipulating the selected text in any way, just writing it to the clipboard.

Again, the writeStringToClipboard function could take a C string rather than a CFString, but my bet is that most of the time I'll have a CFString, since I use CFStrings so much.

In other words, what I've done is standardize on using CFStrings internally, so it's only at the edges--when getting text from an AEDesc, writing text to the clipboard, or calling the system function--where it's sometimes necessary to convert to or from a CFStringRef. (And, as you'll note later in the Search with Google command, there are cases where no conversion is necessary as more and more APIs take CFStrings.)

So writeStringToClipboard works in some ways like the callSystem command--it takes the CFStringRef and gets a C string. A difference here is what it does with it--it clears the current scrap (the clipboard), gets a reference to the current scrap, then writes the C string as text to the clipboard. Finally it frees the C string it got.

That's it for the Copy command. On to Search with Google.

Running the Search with Google command

This command gets the selected text then creates a search URL string based on a base URL. The search URL is then opened in one's default Web browser.

Listing 25: running the Search command

static void runSearchCommand (const AEDesc *desc)
   CFStringRef selectedText, urlString;
   if (!getTextFromDesc (desc, &selectedText,
   if (!createEncodedSearchURLString
      (CFSTR(googleSearchURL), selectedText, &urlString)) {
      CFRelease (selectedText);
   openInBrowser (urlString);   
   CFRelease (selectedText);
   CFRelease (urlString);

As in runCopyCommand, it calls getTextFromDesc to get the selected text as a CFString.

Then it creates the URL string to pass to the browser by calling createEncodedSearchURLString. Then it calls openInBrowser with that URL string to open it in the browser. Then it cleans up and returns.

Encoding a URL string

This function demonstrates some of the utility of CFStrings. It takes a string like as the base URL and a string like "some selected text" as the search arguments. It creates a string that a Web browser would understand, as in

In our case the base URL is (defined in SamplePlugin.h), so the final URL string will look something like

Listing 26: Encoding a URL string

static Boolean createEncodedSearchURLString (CFStringRef
   baseURL, CFStringRef searchArgs, CFStringRef *dest)
   CFStringRef urlString, urlStringPlusEncoded;
   urlString = CFStringCreateWithFormat
      (kCFAllocatorDefault, NULL, ("%@%@"),
      baseURL, searchArgs);
   if (!replaceAll (CFSTR(" "), CFSTR("+"), urlString,
      &urlStringPlusEncoded)) {
      CFRelease (urlString);
      return (false);
   *dest = CFURLCreateStringByAddingPercentEscapes
      (kCFAllocatorDefault, urlStringPlusEncoded, NULL, NULL,
   CFRelease (urlStringPlusEncoded);
   CFRelease (urlString);
   return (*dest != NULL);

First it concatenates the base URL and the search args via CFStringCreateWithFormat. (Cocoa developers, note how similar this is to NSString's stringWithFormat method.)

Then it calls replaceAll to replace spaces with + characters (which is what Web browsers and servers want us to do).

Then it's necessary to URL-encode the URL string--characters such as e and so on have to be converted to percent-encoding. CFURLCreateStringByAddingPercentEscapes does this.

Finally it cleans up, and *dest is the encoded string.

Opening a URL in the default Web browser

runSearchCommand then calls openInBrowser to actually open the URL in one's default Web browser.

Listing 27: opening a URL

static void openInBrowser (CFStringRef urlString)
   CFURLRef urlRef = CFURLCreateWithString
      (kCFAllocatorDefault, urlString, NULL);
   if (urlRef != NULL) {
      LSOpenCFURLRef (urlRef, NULL);
      CFRelease (urlRef);

The sole parameter is a CFStringRef, the encoded URL string created in createEncodedSearchURLString. The system call for opening a URL in a browser wants a CFURLRef, which is created via CFURLCreateWithString. (Cocoa developers: this is like NSURL's URLWithString method.)

Then the LaunchServices function LSOpenCFURLRef is called. This useful function also opens local URLs, doing different things depending on what kind of URL it has. In this case it's an HTTP URL, and so it opens the URL in the default browser. Note that the code doesn't even know or care what the default browser is: the system handles it for us. (Cocoa developers note how this is like NSWorkspace's openURL method.)

You can get more control over how the default browser opens the URL by calling LSOpenFromURLSpec--for instance, you can tell the browser not to come to the front. But in this sample we use LSOpenCFURLRef because the default behavior is what we want anyway.

After calling openInBrowser, runSearchCommand cleans up and returns control to handleSelection, which returns noErr.

Room for improvement

There some things this plugin could do better--but since this an article rather than a book, we'll leave that up to you. A couple obvious things:

When getting text to write to the clipboard or run a search on, it naively assumes MacRoman text encoding, which is not necessarily the case. It's unlikely the results will be satisfactory on, for instance, Japanese systems.

Another issue is error reporting--this plugin just fails silently when something goes wrong. At a minimum it could write an error message to the Console. But since most people don't leave their Console running, a better choice would be to display a dialog box letting the user know the error.

Bonus Tips


I mentioned earlier that printf, CFShow, and CFShowStr are useful for debugging--you can print to the Console to help you figure out what's going on.

But what if you want to really debug--that is, set breakpoints and step through your code? A plug-in isn't an application, so you can't just run and debug it as-is. Here's what you do:

From the Project menu choose New Custom Executable. Click the Set... button in the dialog that appears. Choose an application that supports system contextual menus. Me, I always use Script Editor, just because it launches very quickly on my machine. Click the Finish button.

Figure 3. Attaching an executable to the plugin

Then, before debugging your plugin, set a breakpoint somewhere--in examineContext, for instance. To begin debugging, choose Debug Executable from the Debug menu. Script Editor (or whatever app you chose) will launch.

Type some text in the app you chose, select it, then ctrl-click (or right-click) on the selected text. Project Builder's debugger should come to the front with execution stopped at your breakpoint. From there you can debug as normal.

(Unfortunately I have not figured out how to get this to work with the Finder, but it works very well for other apps.)

So your cycle becomes like this: build your plugin, delete the old version from your ~/Library/Contextual Menu Items/ folder, copy the new build to your Contextual Menu Items folder, choose Debug Executable, then debug. Lather, rinse, repeat.

Quitting apps

Remember that, when you rebuild a plugin and put it in the Contextual Menu Items folder, you need to quit any app you're testing with in order to force it to reload the plugin. What's the fastest way to quit an app that's not in front? Ctrl-click (or right-click) on its icon in the Dock and choose Quit from the pop-up menu.

This doesn't work with the Finder. However, you can enable a Quit menu item for the Finder so that you can type Cmd-Q in the Finder. In Terminal, type: defaults write QuitMenuItem Enabled

You will have to log out then log back in for the changes to take effect. If later on you want to disable the Quit menu item in the Finder, in Terminal type: defaults write QuitMenuItem Disabled

Quit the Finder and re-start it (by clicking its icon in the Dock) and the Quit menu item will be gone.


To summarize a few important points from this article:

    - To run a Unix command, call the system function with a C string, with text that looks like what you would type on the command line. Remember to add and escape quotes and $ characters as needed.

    - CFStrings are good to use for lots of reasons, but the best one is that it makes string manipulation easy.

    - Whether adding a command to the main contextual menu or to a submenu, you're doing the same thing, adding an item to an AEDescList. There's no difference.

    - There are other high-level routines like LSOpenCFURLRef that do things like open a URL in the default browser. If you find yourself doing things like writing code to support different browsers, there's a good chance you're working way too hard. Pay special attention to the CoreFoundation and LaunchServices frameworks. Even Cocoa developers can benefit by looking at these frameworks, since there are useful functions there that have no Cocoa equivalent but that work with NSStrings and NSURLs and so on.

    - printf, CFShow, and CFShowStr are useful for debugging--but, even better, you can truly debug a plugin by attaching an executable and setting a breakpoint.

    - Remember to quit apps you're testing with after rebuilding your plugin. You can even enable a Quit item for the Finder via the defaults Terminal command.

Thanks to...

For review and feedback for this article, thanks to Jim Correia, Quentin D. Carnicelli, and George Warner. Any remaining errors or oddities are mine.


Brent Simmons is a Seattle-based independent Mac OS X developer and writer. He runs a Mac developer news weblog at; he can be contacted at


Community Search:
MacTech Search:

Software Updates via MacUpdate

Dropbox 22.4.24 - Cloud backup and synch...
Dropbox is an application that creates a special Finder folder that automatically syncs online and between your computers. It allows you to both backup files and keep them up-to-date between systems... Read more
Posterino 3.3.5 - Create posters, collag...
Posterino offers enhanced customization and flexibility including a variety of new, stylish templates featuring grids of identical or odd-sized image boxes. You can customize the size and shape of... Read more
Kodi 17.1. - Powerful media center tool...
Kodi (was XBMC) is an award-winning free and open-source (GPL) software media player and entertainment hub that can be installed on Linux, OS X, Windows, iOS, and Android, featuring a 10-foot user... Read more
Kodi 17.1. - Powerful media center tool...
Kodi (was XBMC) is an award-winning free and open-source (GPL) software media player and entertainment hub that can be installed on Linux, OS X, Windows, iOS, and Android, featuring a 10-foot user... Read more
Bookends 12.8 - Reference management and...
Bookends is a full-featured bibliography/reference and information-management system for students and professionals. Bookends uses the cloud to sync reference libraries on all the Macs you use.... Read more
Apple iTunes 12.6 - Play Apple Music and...
Apple iTunes lets you organize and stream Apple Music, download and watch video and listen to Podcasts. It can automatically download new music, app, and book purchases across all your devices and... Read more
Default Folder X 5.1.4 - Enhances Open a...
Default Folder X attaches a toolbar to the right side of the Open and Save dialogs in any OS X-native application. The toolbar gives you fast access to various folders and commands. You just click on... Read more
Amazon Chime 4.1.5587 - Amazon-based com...
Amazon Chime is a communications service that transforms online meetings with a secure, easy-to-use application that you can trust. Amazon Chime works seamlessly across your devices so that you can... Read more
CrossOver 16.2 - Run Windows apps on you...
CrossOver can get your Windows productivity applications and PC games up and running on your Mac quickly and easily. CrossOver runs the Windows software that you need on Mac at home, in the office,... Read more
Adobe Creative Cloud - Access...
Adobe Creative Cloud costs $19.99/month for a single app, or $49.99/month for the entire suite. Introducing Adobe Creative Cloud desktop applications, including Adobe Photoshop CC and Illustrator CC... Read more

The best deals on the App Store this wee...
Deals, deals, deals. We're all about a good bargain here on 148Apps, and luckily this was another fine week in App Store discounts. There's a big board game sale happening right now, and a few fine indies are still discounted through the weekend.... | Read more »
The best new games we played this week
It's been quite the week, but now that all of that business is out of the way, it's time to hunker down with some of the excellent games that were released over the past few days. There's a fair few to help you relax in your down time or if you're... | Read more »
Orphan Black: The Game (Games)
Orphan Black: The Game 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Dive into a dark and twisted puzzle-adventure that retells the pivotal events of Orphan Black. | Read more »
The Elder Scrolls: Legends is now availa...
| Read more »
Ticket to Earth beginner's guide: H...
Robot Circus launched Ticket to Earth as part of the App Store's indie games event last week. If you're not quite digging the space operatics Mass Effect: Andromeda is serving up, you'll be pleased to know that there's a surprising alternative on... | Read more »
Leap to victory in Nexx Studios new plat...
You’re always a hop, skip, and a jump away from a fiery death in Temple Jump, a new platformer-cum-endless runner from Nexx Studio. It’s out now on both iOS and Android if you’re an adventurer seeking treasure in a crumbling, pixel-laden temple. | Read more »
Failbetter Games details changes coming...
Sunless Sea, Failbetter Games' dark and gloomy sea explorer, sets sail for the iPad tomorrow. Ahead of the game's launch, Failbetter took to Twitter to discuss what will be different in the mobile version of the game. Many of the changes make... | Read more »
Splish, splash! The Pokémon GO Water Fes...
Niantic is back with a new festival for dedicated Pokémon GO collectors. The Water Festival officially kicks off today at 1 P.M. PDT and runs through March 29. Magikarp, Squirtle, Totodile, and their assorted evolved forms will be appearing at... | Read more »
Death Road to Canada (Games)
Death Road to Canada 1.0 Device: iOS Universal Category: Games Price: $7.99, Version: 1.0 (iTunes) Description: Get it now at the low launch price! Price will go up a dollar every major update. Update news at the bottom of this... | Read more »
Bean's Quest Beginner's Guide:...
Bean's Quest is a new take on both the classic platformer and the endless runner, and it's free on the App Store for the time being. Instead of running constantly, you can't stop jumping. That adds a surprising new level of challenge to the game... | Read more »

Price Scanner via

Apple Chip Foundry TSMC To Begin A11 System-o...
Digitimes’ Steve Shen is reporting today that according to the Chinese-language Economic Daily News (EDN), chipmaker and major Apple supplier foundery Taiwan Semiconductor Manufacturing Company (TSMC... Read more
MacX MediaTrans 3.5 iOS Data Transfer Spring...
MacXDVD Software has announced general availability of the latest MacX MedTrans 3.5, featuring a new user interface (UI). MacX MediaTrans is ann iPhone manager that enables free data transfer between... Read more
Regular Price $19.95 DupeZap 4 Finder For OS...
Hyperbolic Software has announced the release of DupeZap 4.0.2, their modern duplicate finder developed exclusively for macOS. DupeZap 4 is an utility for Mac owners seeking to reclaim disk space... Read more
B-Eng Releases SSD Health Check for MVNe for...
Fehraltorf, Switzerland based B-Eng has announced the release and immediate availability of SSD Health Check for MVNe for MacBook Pro, their app that delivers important data and insights for MVNe... Read more
Apple iMacs on sale for up to $200 off MSRP,...
B&H Photo has 21″ and 27″ Apple iMacs on sale for up to $200 off MSRP, each including free shipping plus NY sales tax only: - 27″ 3.3GHz iMac 5K: $2099 $200 off MSRP - 27″ 3.2GHz/1TB Fusion iMac... Read more
Apple Certified Refurbished iMacs available f...
Apple has Certified Refurbished 2015 21″ & 27″ iMacs available for up to $350 off MSRP. Apple’s one-year warranty is standard, and shipping is free. The following models are available: - 21″ 3.... Read more
1.4GHz Mac mini on sale for $419, $80 off MSR...
B&H Photo has the 1.4GHz Mac mini on sale for $80 off MSRP including free shipping plus NY sales tax only: - 1.4GHz Mac mini: $419.88 $80 off MSRP Read more
Apple refurbished Mac minis available for up...
Apple has Certified Refurbished Mac minis available starting at $419. Apple’s one-year warranty is included with each mini, and shipping is free: - 1.4GHz Mac mini: $419 $80 off MSRP - 2.6GHz Mac... Read more
Updated iPad Price Trackers
Scan our Apple iPad (and iPod touch) Price Trackers for the latest information on sales, bundles, and availability on systems from Apple’s authorized internet/catalog resellers. We update the... Read more
12-inch 32GB Space Gray iPad Pro on sale for...
B&H Photo has 12″ Space Gray 32GB WiFi Apple iPad Pros on sale for $50 off MSRP including free shipping. B&H charges sales tax in NY only: - 12″ Space Gray 32GB WiFi iPad Pro: $749 $50 off... Read more

Jobs Board

Fulltime aan de slag als shopmanager in een h...
Ben jij helemaal gek van Apple -producten en vind je het helemaal super om fulltime shopmanager te zijn in een jonge en hippe elektronicazaak? Wil jij werken in Read more
*Apple* Mobile Master - Best Buy (United Sta...
**492889BR** **Job Title:** Apple Mobile Master **Location Number:** 000886-Norwalk-Store **Job Description:** **What does a Best Buy Apple Mobile Master do?** Read more
*Apple* Mobile Master - Best Buy (United Sta...
**492472BR** **Job Title:** Apple Mobile Master **Location Number:** 000470-Seattle-Store **Job Description:** **What does a Best Buy Apple Mobile Master do?** Read more
*Apple* Mobile Master - Best Buy (United Sta...
**492562BR** **Job Title:** Apple Mobile Master **Location Number:** 000853-Jackson-Store **Job Description:** **What does a Best Buy Apple Mobile Master do?** Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.