Lessons & Fun with "do shell script" in AppleScripts

With the introduction of OS X, Apple’s operating system was built on a BSD Unix foundation. Scary stuff for those of us who had never dealt with Unix before even if we were happy with AppleScript. We found a new application in the Utilities folder of the Applications folder called Terminal - a program that permitted direct interaction with the system through an interface called a shell; a program for talking to Unix via command lines or shell scripts. Visions of mangling my system danced through my head. For AppleScripters, however, “do shell script …” provides a vital access to the system, just as “tell application “Finder” to …” is our access to those file related tasks that only the Finder can do for us.

For those readers who have never used a shell or a do shell script instruction because Unix remains a dark mystery to them, the best “short introduction” to Unix I’ve encountered is a Unix FAQ assembled by Cameron Hayne, a moderator and resident guru in the MacOSXhints forums. If you want a quick introduction to Unix, that’s a very good place to start, although for the purposes of this article, reading it is not essential. This article is about recipes for use in an AppleScript, and doesn’t really delve into Unix at all. You can let your breath out, now.

As I just said, this tutorial is not about Unix per se; it is a small collection of useful “do shell script” snippets for accessing a small fraction of the power of shell scripts in your AppleScripts. None of these examples are dangerous in the slightest - running them from your own script editor will not wipe out your system or do evil things to your files (although there are certainly instructions that will). To get “Apple’s word” on shell scripting in AppleScript, you should at least glance through Tech Note TN2065 sometime. It’s on Apple’s Developer Connection.

Before we begin, there is a caveat about using do shell script in your AppleScripts – if there is an AppleScript way to do what you want to do, then the AppleScript route is almost certainly faster. A do shell script “call” has a latency of about 100 milliseconds on a fast machine and a complex AppleScript can often beat that hands down, so if you’re tempted to put a do shell script function in a repeat loop, first ask yourself (or the bbs) if there’s an AppleScript way to do it. This point is driven home more clearly in a comparison later.

Recipes in Do Shell Script “PleaseDoThis”

How Long Idle? On occasion, you’d like to trigger a maintenance script of some kind (or something as simple as saving all open documents and logging out) when your machine has been idle for some time (but isn’t asleep). The second line of the script below returns the time in seconds since the last keyboard or mouse event in Tiger. (If you are running Panther, then see this thread in the bbs if the script below doesn’t work for you).To make the version here testable, it’s in a repeat loop that displays the idle time every time the dialog times out. Note that it is not fantastically accurate because of latencies in the call to the shell and reply to your script. To use it productively however, you would just use the line “set idleTime to…” and discard the rest. To see the effect, notice that if you don’t touch your mouse or keyboard, the dialog just counts up by approximately 5 seconds, but try wiggling the mouse sometime after a dialog appears. Click “Cancel” to stop it.


repeat
	set idleTime to (do shell script "ioreg -c IOHIDSystem | perl -ane 'if (/Idle/) {$idle=(pop @F)/1000000000; print $idle,\"\";last}'")
	display dialog idleTime as string giving up after 5
end repeat

As an example of how to use this, consider this alternative form of the shell script (to be saved as a stay-open application for testing):


property howLong : 600 -- ten minutes
on idle
	set idleTime to (do shell script "ioreg -c IOHIDSystem | awk '/HIDIdleTime/ {print int($NF/1000000000); exit}'") as number
	if idleTime > howLong then
		-- Post a notice that expires, say something, make sounds, play music, or do your own thing here.
	end if
	return howLong
end idle

How Long Since the Last Restart? This is called “system uptime” – the time elapsed since the last reboot. For folks like me who sleep their Macs but only restart them after an installation or upgrade that requires it, this can be a long time. For a server, it can be months. I’ve added some AppleScript to “prettify” the answer. Useful for starting periodic tasks too.


set UT to (do shell script "uptime")
set tid to AppleScript's text item delimiters
set AppleScript's text item delimiters to space
set now to text item 1 of UT
set AppleScript's text item delimiters to "up"
set UpT to words 1 thru 4 of text item 2 of UT
set AppleScript's text item delimiters to tid
tell UpT to set msg to item 1 & space & item 2 & ", " & item 3 & " hours, and " & item 4 & " minutes."
display dialog "At " & now & " this system has been up for " & return & msg buttons {"OK"} default button 1 with title "System Uptime"

File Useage “info for” and the Finder can supply creation dates and modification dates which are properties of files, but can’t supply the history of use of a file. The system records this, however, and it can be extracted with a small shell script:


set tFile to (choose file) -- or use an alias to any file
set tPath to quoted form of (POSIX path of tFile) -- the shell's form. quoted form of is required if the path might include spaces.
set tName to name of (info for tFile) -- standard AppleScript
-- Get the file's usage --
set MD to do shell script "mdls -name kMDItemUsedDates " & tPath
-- Parse the answer (or don't, it's readable) --
set tid to AppleScript's text item delimiters
set AppleScript's text item delimiters to "= ("
set tDates to text item -1 of MD
set AppleScript's text item delimiters to ")"
set tDates to text item 1 of tDates
set AppleScript's text item delimiters to ", "
set tDates to (text items of tDates) as text
set AppleScript's text item delimiters to tid
display dialog "The file \"" & tName & "\" was used as follows:" & return & tDates with title "File Usage Dates"

Ever Have a CD or DVD Stuck in the burner? This little script will eject it even when the eject button won’t. Happens sometimes when an optical disk is in the drive, but for some reason the system doesn’t recognize it and won’t mount it. If it’s not mounted, it might not eject. You could go to the Disk Utility, of course, but:


-- if all else fails and you only have one burner....
do shell script "drutil tray eject"

Comparison of Shell and Vanilla AppleScript: In the introduction, I mentioned that a shell script, while a cryptic one-liner quite often, is probably not faster than plain vanilla AppleScript to do the same thing if there is an “AppleScript way” to do it that isn’t too long. The following pair of scripts give two examples for finding the duration of an MP3 without resorting to iTunes to tell you. One uses a shell script and the other calls System Events in a vanilla (no osax required) AppleScript that relies on QuickTime to find out:


tell application "System Events" to set {duration:d, time scale:t} to QuickTime file named ((choose file with prompt "Choose a music file or movie" without invisibles) as Unicode text) -- short but not fastest way, see comparison later.
set duration to round (d / t) -- in seconds

tell application "Finder" to set M to (choose file with prompt "Choose a music or movie file" without invisibles)
set TM to quoted form of POSIX path of M -- the shell's form
try
	set T to last word of (do shell script "mdls " & TM & " | grep kMDItemDurationSeconds")
on error
	display dialog "Duration not available for this file or file type"
	return
end try
display dialog "The duration is " & T & " seconds"

Now to compare the “guts” of these two methods we use Nigel Garvey’s “Lotsa” form. Note that I’ve also added a Test 3 and Test 4 to the “Lotsa” as an illustration of how extreme this comparison can get.

The second runoff compares set date_str to do shell script “date "+%m/%d/%y"” with the plain Applescript set thedate to get short date string of (current date) as string. These timing tests require an osax called “GetMilliSec” which is available from osaxen.com, part of MacScripter.


set Tests to RunTest() -- this kicks it off
----
on RunTest()
	set lotsa to 500 -- the number of times the test will repeat
	-- Any other preliminary values here.
	-- We need a file, so find one for use in tests 1 & 2.
	set tFile to ((choose file with prompt "Choose a music file or movie" without invisibles) as Unicode text)
	-- Dummy loop to absorb a small observed time handicap in the first repeat.
	repeat lotsa times -- warming up the engine.
	end repeat
	-- Test 1.
	set t to GetMilliSec -- returns a large number
	repeat lotsa times
		-- First test code or handler call here.
		tell application "System Events" to tell QuickTime file named tFile
			set d to duration -- splitting these is faster than bundling them in a record
			set ts to time scale -- because of the coercions required (more is less)
			set Dur1 to round d / ts
		end tell
		-- End of first code fragment
	end repeat
	set t1 to ((GetMilliSec) - t) / 1000
	-- Test 2.
	set t to GetMilliSec
	repeat lotsa times
		-- Second test code or handler call here.
		set TM to quoted form of POSIX path of tFile
		set Dur2 to last word of (do shell script "mdls " & TM & " | grep kMDItemDurationSeconds")
		-- end of second code fragment
	end repeat
	set t2 to ((GetMilliSec) - t) / 1000
	-- Now the unrelated quickie time tests 3 & 4.
	-- Test 3
	set t to GetMilliSec
	repeat lotsa times
		-- Second test code or handler call here.
		set thedate to get short date string of (current date) as string
		-- end of second code fragment
	end repeat
	set t3 to ((GetMilliSec) - t) / 1000
	-- Test 4
	set t to GetMilliSec
	repeat lotsa times
		-- Second test code or handler call here.
		set date_str to do shell script "date \"+%m/%d/%y\""
		-- end of second code fragment
	end repeat
	set t4 to ((GetMilliSec) - t) / 1000
	-- Timings.
	return {t1, t2, t2 / t1, t3, t4, t4 / t3}
end RunTest

On my dual-core G5/2.3 (Tiger 10.4.8), the Test 1 takes 9.7 seconds and the Test 2 takes 33.6 seconds - the plain vanilla AS calling QuickTime is 3.5 times faster than the shell script! In the second comparison between methods of getting a short date, the Test 3 takes 298 milliseconds for 500 runs and the shell version, Test 4, takes 7.9 seconds. AppleScript beats Shell by a factor of 26.6 in this comparison because the AppleScript version is intrinsic to AppleScript - no other applications are called to do it – AppleScript “knows” how. You will think about putting shell script calls in long repeat loops if AppleScript alternatives exist, won’t you? Clearly, for a one-of-a-kind, it doesn’t matter, but in loops it sure does.

The next example is one of several in a Code Exchange contribution by Nigel Garvey. It’s worth reading the whole thing because he explains it well. He says there: “

There have been a few posts in the BBS recently about creating pre-defined folder hierarchies. The most obvious method, for an AppleScripter, would be a recursive process involving the Finder. But the same thing can be achieved with a single Unix ‘mkdir’ command. This is incredibly fast and is (for a Unix command) quite easy to understand.
” Note: I enjoyed the “for a Unix command” aside – I agree.
HINT: If you’d like an AppleScript/Preview method for reading MAN pages and storing them as PDFs for quick reference, then see the “Handy Hint for MAN Pages” thread in the Code Exchange section of bbs.applescript.net.

Suppose you have the task of creating a folder and sub-folders for a book you are writing. Inside this folder, to be called “Scripting the Shell”, you would like to have a numbered folder for each of four chapters (Chapter 1, Chapter 2, etc.), and within each chapter folder, you need folders called “Figures”, “References”, and “Text”. That would be a complex AppleScript. The shell function “mkdir” simplifies it immensely as can be seen in this example following Mr. Garvey’s examples. Notice in the script that the words “Scripting the Shell” is in single quotes and that the word Chapter is followed by a space in single quotes. Spaces in shell scripts must either be escaped or enclosed in single quotes. Notice also that you can set up the script as a variable, and then execute it by invoking the variable.


set desktopPath to POSIX path of (path to desktop) -- the where
set BookFolders to "mkdir -p " & desktopPath & "'Scripting the Shell'/Chapter' '{1,2,3,4}/{Figures,References,Text}/" -- the what
do shell script BookFolders -- the action

A Shell Script File Finder One way, though possibly not the fastest, to find a file in your Music folder say (whether in an iTunes Library or loose outside a library), is to use mdfind. Here’s a sample using one of the options: -onlyin, which focuses the search in my Music folder. The first return is an MP3 and the second is in an iTunes Library. Note the single quotes.


set F to POSIX file (do shell script "mdfind -onlyin " & POSIX path of (path to music folder) & " 'Bobby McGee'") -- only part of the name is required. Note that the "Posix file" converts this from a posix path to a standard Mac path that you could then use in the rest of a script.
--> "myHD:Users:shortName:Music:GDead:2.07 Me & Bobby McGee.mp3

set F2 to POSIX file (do shell script "mdfind -onlyin " & POSIX path of (path to music folder) & " 'Dug Up a Diamond'")
--> "myHD:Users:shortName:Music:iTunes:iTunes Music:Mark Knopfler & Emmylou Harris:All The Roadrunning:02 I Dug Up A Diamond 1.m4a"

And Now the Fun Part: Playing Music without iTunes

Wouldn’t it be nice sometimes to be able to play some music without having to run iTunes to do it? This would be especially nice if you wanted to play the music from a script and didn’t want iTunes to open. Patrick Machielse of Hieper Software thought so too. He wrote an executable shell script called “Playfound here, a 35K freeware Unix executable that will do just that. It will play anything Quicktime can play, in fact. If you go to the Heiper page in the link above and download Play, it’s ready to go as a shell executable but then how do we use it from an AppleScript? First, Be sure to read the Readme.html that comes with download, particularly the Usage link. In the meantime, here’s a simple way to proceed.

  • Open a new document in your Script Editor.
  • Type: [b]space & set tMusic to quoted form of "[/b] and leave the cursor there.
  • Navigate in the Finder to your Music folder and drag an MP3 from there to the end of the line above in the Script Editor Window.
  • Close the quotes on the path that appears and type a return.
  • Next type: [b]set Play to quoted form of "[/b], and leave the cursor there.
  • Again, Navigate in the Finder to your download of "play" and drag it to the Script Editor window. Close the quote and type return.
  • Now type: [b]do shell script Play & tMusic[/b], compile and run
[b]Note[/b]: To stop play in progress, press the escape key.

As a final set of shell scripts, let’s first consider combining our “find” shell script using mdfind with the player to both find a song and then play it. (Sure we could write a script to have iTunes do it, but this is more fun, right?)

Because with this “piping” of one instruction to another, the escape key will no longer stop the song, I’ve added a “stopper” to the next script. In order to get back to the script so the stopper will be active while the song is playing, we have to start the player in the background. This is accomplished by the phrase "&> /dev/null & at the end of the do shell script line; > /dev/null means “direct any messages from the script to vapor land” and the ampersands (&) before and after the redirection inside a shell script mean “do it in the background”. When you download the script, you’ll have to point it at your copy of the play executable by properly filling in the line that begins “set Play to…” with the Posix path to wherever you put it. Remember you can get this by dragging the file to your script.


set tSong to quoted form of text returned of (display dialog "Enter a song name or part of one" default answer "Part of Title Here") -- an MP3 file or a song in any iTunes Library
set TuneP to ((path to "apps" as Unicode text) & �
	":iTunes.app:Contents:Resources:iTunesHelper.app:Contents:Resources:iTunesHelper.icns") as alias

set Play to POSIX path of ("pathToYourCopyofThePlayExecutable") -- Fix This path!!!
-- A path in slash delimited form doesn't need the "POSIX path of", in colon delimited form, it does.

do shell script "mdfind -onlyin  ~/Music/ " & tSong & " | " & Play & " &> /dev/null &"
-- sending any output to /dev/null in the background gives control back to the script immediately.
set B to button returned of (display dialog "Do you want to quit Play?" & return & return & "Is there no sound?" buttons {"Stop Playing", "No Sound"} with icon TuneP)
if B = "Stop Playing" then
	try -- in case you press "Stop Playing" when there is no sound.
		do shell script "killall process play"
	end try
else
	display dialog "Probably didn't find the song"
end if

If you have read the “Usage” part of the read me, you’ll know that you can arrange for more than one song to play, and you can play snippets of all your iTunes song from a particular album.

The following script will play 10 seconds of each song in an album of your choice in your iTunes Library (presuming you haven’t moved the Library database). The Album title has to be exactly what it is called in iTunes. When the tunes have finished playing, the variable “Tunes” will contain a list of the titles of what was played, so if you’re looking for a particular piece and can tell you’ve found it from the first 10 seconds of play, then note the number as you listen and you’ll know the title of the piece.


set play to "/Users/bellac/Library/Scripts/ShellScpt/PlayScript/play"
set Album to quoted form of text returned of (display dialog "Enter an exact Album Title as it appears in iTunes" default answer "An Album Title")
set TenSecs to "mdfind -onlyin ~/Music/iTunes/'iTunes Library' \"kMDItemAlbum == " & Album & "\" | " & play & " -vt 10"
set Tunes to do shell script TenSecs

In that script, the -v means list them, and the -t num is how long to play in seconds. They’re combined here. You can aim it at other places by changing the posix path immediately following -onlyin.

A FINAL HINT: to find out what kMDItem… parameters you can search for in a particular type of file (except for stuff in iTunes Libraries) run this:


set P to quoted form of (POSIX path of (choose file without invisibles))
set V2 to {}
set vars to (do shell script "mdls " & P)
set tid to AppleScript's text item delimiters
set AppleScript's text item delimiters to "kmDitem"
set V1 to text items of vars
set AppleScript's text item delimiters to space
repeat with I in V1
	set end of V2 to "kmDitem" & text item 1 of I
end repeat
set AppleScript's text item delimiters to tid
set kmDs to items 2 thru -1 of V2

Have fun, I have. At some point, I might do a follow-up to this article introducing some more “quickies”. One I’ve omitted here is cscreen; an executable for getting and setting the screen resolution on one or more monitors, but I figured one download was enough. Lots of others come to mind. Let me know by commenting on this article or emailing me via the link in the bbs.