11
OS X SCRIPTS

image

One of the most important changes in the world of Unix and Unix-like operating systems was the release of the completely rewritten OS X system, built atop a reliable Unix core called Darwin. Darwin is an open source Unix based on BSD Unix. If you know your Unix at all, the first time you open the Terminal application in OS X, you’ll doubtless swoon with delight. Everything you’d want, from development tools to standard Unix utilities, is included in the latest generation of Mac computers, with a gorgeous GUI quite capable of hiding all that power for people who aren’t ready for it.

There are significant differences between OS X and Linux/Unix, however, so it’s good to learn some OS X tweaks that can help you in your day-to-day interaction. For example, OS X has an interesting command line application called open, which allows you to launch graphical applications from the command line. But open isn’t very flexible. If you want to open, say, Microsoft Excel, entering open excel won’t work because open is picky and expects you to enter open -a "Microsoft Excel". Later in this chapter, we’ll write a wrapper script to work around this picky behavior.

#79 Automating screencapture

If you’ve used a Mac for any length of time, you’ve learned that it has a builtin screen capture capability that you access by pressing image-SHIFT-3. You can also use the OS X utilities Preview or Grab, located in the Applications and Utilities folders, respectively, and there are excellent third-party choices too.

But did you know that there’s a command line alternative? The super useful program screencapture can take shots of the current screen and save them to the Clipboard or to a specific named file (in JPEG or TIFF format). Enter the command with an undefined argument and you’ll see the basics of its operation, as shown here:

$ screencapture -h
screencapture: illegal option -- h
usage: screencapture [-icMPmwsWxSCUtoa] [files]
  -c         force screen capture to go to the clipboard
  -C         capture the cursor as well as the screen. only in non-interactive
modes
  -d         display errors to the user graphically
  -i         capture screen interactively, by selection or window
               control key - causes screen shot to go to clipboard
               space key   - toggle between mouse selection and
                             window selection modes
               escape key  - cancels interactive screen shot
  -m         only capture the main monitor, undefined if -i is set
  -M         screen capture output will go to a new Mail message
  -o         in window capture mode, do not capture the shadow of the window
  -P         screen capture output will open in Preview
  -s         only allow mouse selection mode
  -S         in window capture mode, capture the screen not the window
  -t<format> image format to create, default is png (other options include
pdf, jpg, tiff and other formats)
  -T<seconds> Take the picture after a delay of <seconds>, default is 5
  -w         only allow window selection mode
  -W         start interaction in window selection mode
  -x         do not play sounds
  -a         do not include windows attached to selected windows
  -r         do not add dpi meta data to image
  -l<windowid> capture this windowsid
  -R<x,y,w,h> capture screen rect
  files   where to save the screen capture, 1 file per screen

This is an application begging for a wrapper script. For example, to take a shot of the screen 30 seconds in the future, you could use this:

$ sleep 30; screencapture capture.tiff

But let’s make something more interesting, shall we?

The Code

Listing 11-1 shows how we can automate the screencapture utility so it captures screenshots a bit more stealthily.

   #!/bin/bash
   # screencapture2--Use the OS X screencapture command to capture a sequence of
   #   screenshots of the main window, in stealth mode. Handy if you're in a
   #   questionable computing environment!

   capture="$(which screencapture) -x -m -C"
 freq=60         # Every 60 seconds
   maxshots=30     # Max screen captures
   animate=0       # Create animated gif? No.

   while getopts "af:m" opt; do
     case $opt in
      a ) animate=1;                  ;;
      f ) freq=$OPTARG;               ;;
      m ) maxshots=$OPTARG;           ;;  # Quit after specified num of pics
      ? ) echo "Usage: $0 [-a] [-f frequency] [-m maxcaps]" >&2
          exit 1
     esac
   done

   counter=0

   while [ $counter -lt $maxshots ] ; do
     $capture capture${counter}.jpg   # Counter keeps incrementing.
     counter=$(( counter + 1 ))
     sleep $freq   # freq is therefore the number of seconds between pics.
   done

   # Now, optionally, compress all the individual images into an animated GIF.

   if [ $animate -eq 1 ] ; then
   convert -delay 100 -loop 0 -resize "33%" capture* animated-captures.gif
   fi

   # No exit status to stay stealthy
   exit 0

Listing 11-1: The screencapture2 wrapper script

How It Works

This will take a screenshot every $freq seconds for up to $maxshots captures (with a default of every 60 seconds for 30 captures). The output is a series of JPEG files sequentially numbered starting at 0. This could be very useful for training purposes or perhaps if you’re suspicious that someone has been using your computer while you’re at lunch: set this up, and you can review what occurred without anyone being the wiser.

The last section of the script is interesting: it optionally produces an animated GIF one-third the size of the original by using the ImageMagick convert tool . This is a handy way of reviewing the images all at once. We’ll use ImageMagick a lot more in Chapter 14! You may not have this command by default on your OS X system, but by using a package manager like brew, you can install it with a single command (brew install imagemagick).

Running the Script

Because this code is designed to run stealthily in the background, the basic invocation is easy:

$ screencapture2 &
$

That’s all there is to it. Easy. As an example, to specify how many shots to take (30) and when to take them (every 5 seconds), you could start the screencapture2 script like this:

$ screencapture2 -f 5 -m 30 &
$

The Results

Running the script results in zero output, but new files do show up, as shown in Listing 11-2. (If you specify the -a animate flag, you’ll get an additional result.)

$ ls -s *gif *jpg
 4448 animated-captures.gif      4216 capture2.jpg      25728 capture5.jpg
 4304 capture0.jpg               4680 capture3.jpg      4456 capture6.jpg
 4296 capture1.jpg               4680 capture4.jpg

Listing 11-2: The images of a screen that was captured over a period of time by screencapture2

Hacking the Script

For a long-term screen-monitoring tool, you’ll want to find some means of checking when the screen actually changes so you’re not wasting hard drive space with uninteresting screenshots. There are third-party solutions that should allow screencapture to run for much longer periods, saving the history of when the screen actually changes rather than dozens—or hundreds—of copies of the same unchanged screen. (Note that if you have a clock display on your screen, every single screen capture will be slightly different, making it much harder to avoid this problem!)

With this capability, you could have “monitor ON” and “monitor OFF” as a wrapper that starts the capture sequence and analyzes whether any of the images differ from the first capture. But if you were using this script’s GIFs to create an online training tutorial, you might use finer-grained controls to set the length of capture, using that period of time as a command line argument.

#80 Setting the Terminal Title Dynamically

Listing 11-3 is a fun little script for OS X users who like to work in the Terminal application. Instead of having to use the Terminal image Preferences image Profiles image Window dialog to set or change the window title, you can use this script to change it whenever you like. In this example, we’ll make the Terminal window’s title just a bit more useful by including the present working directory in it.

The Code

   #!/bin/bash
   # titleterm--Tells the OS X Terminal application to change its title
   #   to the value specified as an argument to this succinct script

   if [ $# -eq 0 ]; then
     echo "Usage: $0 title" >&2
     exit 1
   else
   echo -e "\033]0;$@\007"
   fi

   exit 0

Listing 11-3: The titleterm script

How It Works

The Terminal application has a variety of secret escape codes that it understands, and the titleterm script sends a sequence of ESC ] 0; title BEL , which changes the title to the specified value.

Running the Script

To change the title of the Terminal window, simply enter the new title you desire as the argument to titleterm.

The Results

There’s no apparent output from the command, as Listing 11-4 shows.

$ titleterm $(pwd)
$

Listing 11-4: Running the titleterm script to set the terminal title to that of the current directory

However, it instantly changes the title of the Terminal window to the present working directory.

Hacking the Script

With one small addition to your login script (.bash_profile or something else, depending on what login shell you have), you can automatically have the Terminal window title always show the current working directory. To make this code show your current working directory, for example, you can use this at tcsh:

alias precmd 'titleterm "$PWD"'                      [tcsh]

Or this at bash:

export PROMPT_COMMAND="titleterm \"\$PWD\""          [bash]

Just drop one of the commands above into your login script, and starting the next time you open up a Terminal window, you’ll find that your window title changes each time you move into a new directory. Darn helpful.

#81 Producing Summary Listings of iTunes Libraries

If you’ve used iTunes for any length of time, you’re sure to have a massive list of music, audiobooks, movies, and TV shows. Unfortunately, for all its wonderful capabilities, iTunes doesn’t have an easy way to export a list of your music in a succinct and easy-to-read format. Fortunately, it’s not hard to write a script that offers this functionality, as Listing 11-5 shows. This script does rely on the “Share iTunes XML with other applications” feature of iTunes being enabled, so before running this script, ensure that it’s enabled in the iTunes preferences.

The Code

   #!/bin/bash
   # ituneslist--Lists your iTunes library in a succinct and attractive
   #   manner, suitable for sharing with others, or for synchronizing
   #   (with diff) iTunes libraries on different computers and laptops

   itunehome="$HOME/Music/iTunes"
   ituneconfig="$itunehome/iTunes Music Library.xml"

 musiclib="/$(grep '>Music Folder<' "$ituneconfig" | cut -d/ -f5- | \
     cut -d\< -f1 | sed 's/%20/ /g')"

   echo "Your library is at $musiclib"

   if [ ! -d "$musiclib" ] ; then
     echo "$0: Confused: Music library $musiclib isn't a directory?" >&2
     exit 1
   fi

   exec find "$musiclib" -type d -mindepth 2 -maxdepth 2 \! -name '.*' -print \
     | sed "s|$musiclib/||"

Listing 11-5: The ituneslist script

How It Works

Like many modern computer applications, iTunes expects its music library to be in a standard location—in this case ~/Music/iTunes/iTunes Media/— but allows you to move it elsewhere if you want. The script needs to be able to ascertain the different location, and that’s done by extracting the Music Folder field value from the iTunes preferences file. That’s what the pipe at accomplishes.

The preferences file ($ituneconfig) is an XML data file, so some chopping is necessary to identify the exact Music Folder field value. Here’s what the iTunes Media value in Dave’s iTunes config file looks like:

file://localhost/Users/taylor/Music/iTunes/iTunes %20Media/

The iTunes Media value is actually stored as a fully qualified URL, interestingly enough, so we need to chop off the file://localhost/ prefix. This is the job of the first cut command. Finally, because many directories in OS X include spaces, and because the Music Folder field is saved as a URL, all spaces in that field are mapped to %20 sequences and have to be restored to spaces by the sed invocation before proceeding.

With the Music Folder name determined, it’s now easy to generate music lists on two Mac systems and then use the diff command to compare them, making it a breeze to see which albums are unique to one or the other system and perhaps to sync them up.

Running the Script

There are no command arguments or flags to this script.

The Results

If you have a large music collection, the output from the script can be large. Listing 11-6 shows the first 15 lines of the output from Dave’s music collection.

$ ituneslist | head -15
Your library is at /Users/taylor/Music/iTunes/iTunes Media/
Audiobooks/Andy Weir
Audiobooks/Barbara W. Tuchman
Audiobooks/Bill Bryson
Audiobooks/Douglas Preston
Audiobooks/Marc Seifer
Audiobooks/Paul McGann
Audiobooks/Robert Louis Stevenson
iPod Games/Klondike
Movies/47 Ronin (2013)
Movies/Mad Max (1979)
Movies/Star Trek Into Darkness (2013)
Movies/The Avengers (2012)
Movies/The Expendables 2 (2012)
Movies/The Hobbit The Desolation of Smaug (2013)

Listing 11-6: Running the ituneslist script to print the top items in an iTunes collection

Hacking the Script

All right, this isn’t about hacking the script per se, but because the iTunes library directory is saved as a fully qualified URL, it would be interesting to experiment with having a web-accessible iTunes directory and then using the URL of that directory as the Music Folder value in the XML file. . . .

#82 Fixing the open Command

One neat innovation with OS X is the addition of the open command, which allows you to easily launch the appropriate application for just about any type of file, whether it’s a graphics image, a PDF document, or an Excel spreadsheet. The problem with open is that it’s a bit quirky. If you want it to launch a named application, you have to include the -a flag. And if you don’t specify the exact application name, it will complain and fail. This is a perfect job for a wrapper script like the one in Listing 11-7.

The Code

   #!/bin/bash
   # open2--A smart wrapper for the cool OS X 'open' command
   #   to make it even more useful. By default, 'open' launches the
   #   appropriate application for a specified file or directory
   #   based on the Aqua bindings, and it has a limited ability to
   #   launch applications if they're in the /Applications dir.

   #   First, whatever argument we're given, try it directly.


 if ! open "$@" >/dev/null 2>&1 ; then
     if ! open -a "$@" >/dev/null 2>&1 ; then

       # More than one arg? Don't know how to deal with it--quit.
       if [ $# -gt 1 ] ; then
         echo "open: More than one program not supported" >&2
         exit 1
       else
         case $(echo $1 | tr '[:upper:]' '[:lower:]') in
           activ*|cpu   ) app="Activity Monitor"           ;;
           addr*        ) app="Address Book"               ;;
           chat         ) app="Messages"                   ;;
           dvd          ) app="DVD Player"                 ;;
           excel        ) app="Microsoft Excel"            ;;
           info*        ) app="System Information"         ;;
           prefs        ) app="System Preferences"         ;;
           qt|quicktime ) app="QuickTime Player"           ;;
           word         ) app="Microsoft Word"             ;;
           *            ) echo "open: Don't know what to do with $1" >&2
               exit 1
         esac
         echo "You asked for $1 but I think you mean $app." >&2
         open -a "$app"
       fi
     fi
   fi

   exit 0

Listing 11-7: The open2 script

How It Works

This script revolves around the zero and nonzero return codes, with the open program having a zero return code upon success and a nonzero return code upon failure .

If the supplied argument is not a filename, the first conditional fails, and the script tests whether the supplied argument is a valid application name by adding a. If the second conditional fails, the script uses a case statement to test for common nicknames that people use to refer to popular applications.

It even offers a friendly message when it matches a nickname, just before launching the named application.

$ open2 excel
You asked for excel but I think you mean Microsoft Excel.

Running the Script

The open2 script expects one or more filenames or application names to be specified on the command line.

The Results

Without this wrapper, an attempt to open the application Microsoft Word fails.

$ open "Microsoft Word"
The file /Users/taylor/Desktop//Microsoft Word does not exist.

Rather a scary error message, though it occurred only because the user did not supply the -a flag. The same invocation with the open2 script shows that it is no longer necessary to remember the -a flag:

$ open2 "Microsoft Word"
$

No output is good: the application launched and ready to use. In addition, the series of nicknames for common OS X applications means that while open -a word definitely won’t work, open2 word works just fine.

Hacking the Script

This script could be considerably more useful if the nickname list were tailored to your specific needs or the needs of your user community. That should be easily accomplished!