Please join me at my new location bryankyle.com

Monday, February 27, 2012

Converting Shell Scripts into Applications

OS X doesn’t allow shell scripts to run as Login Items. Adding a shell script as a login item causes the script to be opened in an editor; not exactly what I had in mind. Login items need to be applications, but how do you convert a shell script into an application? Easy: Platypus.

Platypus is an open source project that packages up scripts as Mac applications. It works with pretty much any scripting language you can think of. It’s a really simple concept and it works well.

To convert a simple shell script into an application using Platypus you’ll need to follow these steps:

  1. Enter the name of the application you want to generate. This will be the name of the file and what appears in the Finder, so be as descriptive as you need to be.
  2. Select the type of script being wrapped and configure any arguments to the interpreter. If you’re wrapping a shell script you won’t need to do this as the default will work just fine.
  3. Select the path to the script to wrap up as an application either by using the Select button or dragging and dropping the script into the field.
  4. If the script generates output you might want to configure the output format. If there is no output from the application, or you don’t care about the output you can safely select “None”.

There are a bunch of other options such as the icon to use for the generated application, additional files to bundle, etc. Since I wanted my application to be used as a Login Item I wanted it to be as unintrusive as possible. The configuration options for my application were:

  • Set “Output Type” to “None”.
  • Check the “Run with Administrator privileges” checkbox.
  • Check the “Run in background” checkbox.
  • Uncheck the “Remain running after initial execution” checkbox.

Thursday, February 23, 2012

Shell Command to Unmount Volumes

I recently decided that I wanted to have a 2nd installation of OS X laying around for testing purposes. Initially I thought of using VMWare Fusion or something similar. Being that my machine only has 4GB of RAM I didn’t think this would make for the best experience in the world so I discounted it.

The only other option was to either partition my hard drive or use an external drive. External drives are horrendously slow (when compared to the internal SSD of the MacBook Air) and aren’t portable. So partition the hard drive it was. For my own sanity I wanted to keep each of the installations as separate as possible. I really didn’t want each installation to see the other’s data. In a pinch I wrote up a quick shell script that takes care of this problem.

Here’s the script:

#!/usr/bin

mount | grep “/dev/” | grep -v “on / ” | awk ‘{ print $1}’ | xargs -n1 diskutil umount

It’s pretty simple when you break it down into its parts. This single command works because the Unix shell allows the output of one command to become the input for the next. Here’s a breakdown of what’s happening:

The mount command is executed which outputs a bunch of lines, one for each mounted volume. Each line looks similar to the following:

/dev/disk0s2 on / (hfs, local, journaled)

This is saying that the disk /dev/disk0s2 is mounted at the root of the file system, /, with the hfs, local, and journalled options. Of these pieces of information all I really care about is the name of the disk. I really just want to isolate the local disks from other disks and file systems. That’s where the first grep command comes in.

The listing of volume information is used as the input to grep, a program that filters out the lines that don’t match a regular expression. In this case we’re looking for lines that contain /dev/ somewhere on them. This initial processing with grep takes care of all of the non-local volumes. But there’s a problem. The system is booted from a local volume. If that volume isn’t filtered out then there will be problems later on. Likely one of the commands will fail. That’s what the 2nd invocation of grep is for.

The output of the grep command can be inverted so that it will only output lines that do not match a regular expression. Since I wanted the script to be generic, and the device name will be different for each installation I couldn’t filter based on the name. Luckily however, I could just filter out the line that contains the expression on /. The reason I can do that is because the volume that contains the system will always be mounted at the root of the file system. Note here that the expression contains a trailing space; this is important because without it all of the lines would match and therefore there would be no output.

With the output of the mount command successfully trimmed down to just the lines containing local volumes that aren’t the startup volume I can remove the pieces of information from the lines that I don’t care about. Or, to put it more precisely, just output the data I do care about: the device name. Say hello to awk.

The awk command is a handy tool that can perform all sorts of manipulations to its input. awk works well in situations where the data being processed is columnar. In the case of the output of mount the data could be considered as space-separated-values with the device name in the first column. In order to get awk to just print out the first column of each record we use the script { print $1}. If you were to just execute the command pipeline up to the end of the awk command you would see a listing of all of the device names that contain currently mounted volumes that are not the startup volume. The last thing we need to do is unmount them.

There are several ways to unmount volumes on a Mac. I’ve chosen to use the diskutil command but others I’m sure would work fine. One of the shortcomings of the diskutil command is that it will only allow you to pass a single device name to the umount subcommand. It also expects that the device name will be passed on the command line; it won’t read device names from standard input. That’s where the wonderful and magical xargs command comes in.

xargs takes as arguments a the first part of a command to execute. To that it appends the contents of each line that it reads from standard input. By default xargs tries to batch the data into as few commands as possible but since diskutil will only take a single device name at a time the -n1 switch needs to be used. -n1 tells xargs to execute the command for each line.

xargs is an amazing command. It took me a while to get my head wrapped around it but once I did it opened up a world of possibilities. It’s the most complicated piece of the pipeline but it’s also the one that makes it work as a single command. Without xargs I would have had to resort to some kind of a loop and variables. That may be easier to read but it’s certainly less concise.

But this script alone doesn’t solve the problem. It needs to run at login, and that requires a different solution.