GUI-up your Script
Volume Number: 22 (2006)
Issue Number: 8
Column Tag: Mac in the Shell
GUI-up your Script
OK - Perhaps the GUI is nice every now and then...
by Edward Marczak
Introduction
While I can extol the virtues of life in the shell, I do realize that sometimes, just sometimes, a GUI is more appropriate. Why would I say such a thing? Because there will be scripts that you write primarily to be used by other people. The kind of people that don't keep a terminal window open 100% of the time. On one hand, you've found a great solution to their issue. On the other, you don't want the cure to be more painful than the disease. The wonderful thing about Apple's XCode development environment is that you can take practically any shell solution and turn it into a GUI-based one without too much effort. That's where we'll be heading this month.
What's New in AppleScript
Did I just say AppleScript? Yes, yes I did. The history of AppleScript is outside the scope of this column; it is very lengthy. It is now, however, a mature technology. Even more exciting is the inclusion of "AppleScript Studio" into OS X and XCode.
Now, you won't find an application, or even a submenu that launches anything called "AppleScript Studio." AppleScript Studio refers to the ability of XCode to take an AppleScript app, tie it into all of the other XCode technologies and compile it into a native Cocoa app. This makes creating a basic application mind-blowingly simple. We're going to create a small sample app to prove this.
Fire Up XCode
If you've never launched XCode, now's your chance to dig in. If you're an XCoder already, you may have missed (or ignored) some of the AppleScript options. More importantly, if you've never installed XCode, you'll need to for the rest of this article. While the XCode environment ships with each copy of the OS, the retail disc is a little dated at this point. Bring yourself to <http://developer.apple.com>, sign up for a free account, and download the latest version of XCode (2.3, as of this writing, weighing in at 915MB).
With XCode installed, it's time to launch it. You'll find it at /Developer/Applications:
Figure 1: XCode icon
You may not notice that it's up and running. In its minimalist way, there's no splash screen or other trumpeting of its arrival. Choose New Project from the File menu:
Figure 2: New Project
When presented with the choice of the kind of project, choose "AppleScript Application" and click "Next". Name your project - the example this month is "Backup", if you're following along - and where you want it saved. XCode will create directories for you if necessary. Once you give it the go ahead, you'll be staring at a fresh new project.
Figure 3: Default Project
Lot of stuff - stay cool. In the "Groups & Files" pane, you'll see a 'Scripts' folder. Since we're going to be writing an AppleScript that will front-end a shell script, as you can imagine, this is where the bulk of the action will take place for us. Of course, we need to begin at the beginning. Let's write the shell script.
The Heart of the Matter
The real work will be done by a shell script, so we need to have that in place. This will be a 'lite' version of a solution I really have put in place. However, there were enough things that were specific to the environment in question, that I don't want to reproduce here. Additionally, I won't be getting into any heavy error checking or correction. Nor will I make the claim that this is a perfect fit for any other particular situation. That said, I'll offer this basic script as our base:
#!/bin/bash
ditto -rsrc /Files /Volumes/Backup_Drive/Files
This one-liner uses the OS X native ditto command to copy all files from /Files to /Volumes/Backup_Drive/Files. I'll stick the '-rsrc' switch in there as it only became the default behavior as of 10.4. What if we want to run this on a Panther machine? Use your favorite text editor to create this script, and then save it. Call it "backup.sh". Mark it executable with chmod 770 and test it. You may need to modify the path names, or, for testing purposes, simply copy between two folders on your local drive.
Have it working? Great. Now, we can drag-and-drop this script right into our XCode project. Locate this script in the Finder, and drag it into the Scripts folder. You'll be asked how to reference this file. Ensure that the "Copy items into destination group's folder" is checked, and click add.
Figure 4: Copying a script into XCode
You should see your backup.sh script listed alongside the preloaded AppleScript file. Like so:
Figure 5: Your script has been added
If you've never used XCode before, please do this now: click on the build icon in the toolbar. This will help you understand how XCode lays out your project. Open up the folder where you saved this project. Inside, you'll find a folder named 'build'. Inside of that, you'll find one named 'Debug', and it will contain your Backup.app. See how easy that was? OK, not so fast, right? Although this app actually will run (go on, try it), it won't do anything of use. Right-click (control-click) on the app and select "Show Package Contents". Open the resulting 'Contents' folder, and the 'Resources' folder beneath that. You should see three objects: backup.sh, English.lproj and Scripts. Well, there's our 'backup.sh' file! Now we know where it lives and how to access it.
Get GUI
Back in XCode, toggle the disclosure triangle next to the 'Resources' group. Double-click on the 'MainMenu.nib' file. This will launch Interface Builder, and even more windows will now litter your screen. Click once on the window labeled 'Window'. Let's immediately name it something more relevant. Change the Window Title in the inspector pane, and title the window "Backup". Also, we need to give it an AppleScript name. Change the drop-down menu in the inspector to "AppleScript". Name the window "wMain". Now, look for a button in the 'Cocoa-Controls' pane. Drag it to the lower right-hand corner of our window. You'll see guidelines appear to help you position it. With the button still selected, let's make it say and do something appropriate. Back on the attributes screen, update the button title in the inspector pane. (Change the drop-down menu to get back to the attributes screen). Additionally, change the 'Key equiv.' to '\R' - type it in or use the drop down menu to select 'Return', and check the 'Selected' checkbox. Flip the inspector back to AppleScript, and give this button a name. How about "bBackup"?
Now to begin to tie it together: Making sure that you're still in the AppleScript portion of the inspector, place a check-mark in the 'Action' box, and select the 'Backup.applescript' in the list at the bottom of the Inspector. Save and quit. Not too terrible, right? Of course, now we have to make that AppleScript do something.
I am not, nor will I pretend to be an AppleScript guru. I can deal with the basics well enough, and they are easy to pick up. However, once you start doing this, and you get hooked on it, do yourself a favor and hit some of the many fine AppleScript resources available. This includes Ben Waldie's AppleScript column right here in MacTech. He's also written some excellent books, available through SpiderWorks (http://www.spiderworks.com). There's also <http://www.macscripter.net>, another web-based resource. However, to simply front-end a shell script - no matter how complex - you'll find all you need in this one column. Just do realize that you can do some very complex scripting with AppleScript, and in an all Mac environment is well-worth learning.
With that out of the way, let's continue. Back in XCode proper, double-click on the 'Backup.applescript' in the 'Scripts' group. You'll see that XCode give you some pre-populated comments for you to tailor to this project. But comments don't cause any action! Place these lines into the script:
on clicked theObject
-- setup
set myPath to POSIX path of (path to me) as string
-- Do it!
do shell script quoted form of myPath & "Contents/Resources/backup.sh"
end clicked
As you type this in, all text will be set in a purple mono-spaced font (by default, anyway - you may have changed this preference). Once you save, the parser comes alive to check the validity of the script. Valid strings and recognized keywords get color-coded and set in a different font. What did we just tell our application to do? Actions performed on the button will be sent here since we used Interface Builder to tie an action to this particular script. In this case, we're interested to know when someone clicks on the button.
The first line (set myPath...) sets the "myPath" variable to the path of the application. This lets the user place this app wherever they want on their file system, and we can deal with it at runtime. Next, we call our shell script. AppleScript gained the ability to call scripts with the "do shell script" command. Currently, we know where our app lives and have stored that in a variable, myPath. Earlier, we verified where our script lives inside our application bundle. Now we know where to go relative to the app. We can launch our script from the "Contents/Resources" folder below "myPath".
Is it really this simple? Let's find out. First: go load up your /Files directory with some files and folders. Then, back in XCode, click on the "Build and Go" icon in the toolbar. A build window will appear and give you build status. Once complete, you'll see the window that we created earlier in Interface Builder with its one lonely button. As you can see, it is the default button, and has the Aqua default pulsing glow. Go ahead and click the "Backup" button. Our app will run our shell script buried in the application bundle. Depending on how much stuff you loaded into the /Files directory, this may take a minute. Do notice, though, that while the underlying script is running, the button stops pulsing. Once the script finishes up, the button will pulse again. Press Apple-Q or select "Quit NewApplication" from the "Backup" menu.
The answer, then, is "yes! It is that simple." Of course, not only did we not write a very friendly application, there are some other things we need to take into consideration. However, if all you ever want is a one-button method of running a shell-script, now you have the framework to do so. This isn't anything I'd hand a client, of course.
Version 1.1
Before I continue to spill letters onto the page, there's a much more important source before we continue. "do shell script" is addressed in detail in Apple's Technote TN2065. This is a must read. URL in the references at the end of this article.
Moving along, we need to create better feedback for the client when they run our application. Double-click on the MainMenu.nib file once again (in XCode). You should see our application's window displayed by default. If not, double-click the "Window" icon in the "MainMenu.nib (English)" window. Drag a progress indicator from the Cocoa-Controls window onto our main window - place it just above the backup button, and stretch it horizontally to the width of the window. Then, with it still selected, change the drop-down menu in the inspector to "AppleScript". Give this control a name...like, "pMainProgress". (I used to code in VB and RB and, yes, Hungarian notation is driven pretty deep into my brain). We need this name so we can refer to the control in code. Save and quit Interface Builder.
Once again, we're in the position of writing code that will actually make that progress bar do something. Fortunately, AppleScript makes it pretty trivial. You'll see why we need to name objects so AppleScript can reference them. Here's one of the lines we'll need to add:
set uses threaded animation of progress indicator "pMainProgress" of window "wMain" to true
(While this may wrap in print, you should type it as all one single line with no line break).
Since we can have multiple windows, each with multiple progress bars and other objects, we need to be specific. We need to set the "uses threaded animation" property of the progress bar. Ah, which progress bar? The one we called "pMainProgress". Well, on which window? "wMain"! AppleScript's goal of being English-like works in its favor most of the time. Other times, you can get some pretty bizarre sentences. We'll place all of our code right in the on clicked section, because we only want the progress bar to animate while the backup is running. The fully updated code follows (and look for the start and stop of the progress bar):
on clicked theObject
-- setup
set myPath to POSIX path of (path to me) as string
-- Start progress bar
set uses threaded animation of progress indicator "pMainProgress" of window "wMain" to true
tell progress indicator "pMainProgress" of window "wMain" to start
-- Do it!
do shell script quoted form of myPath & "Contents/Resources/backup.sh"
-- Stop progress bar
tell progress indicator "pMainProgress" of window "wMain" to stop
set uses threaded animation of progress indicator "pMainProgress" of window "wMain" to false
end clicked
(Again, these lines wrap in print, but they should not be artificially broken when you type them).
Our progress bar commands "wrap" the do shell script. Time for the test: Apple-S (save) and Apple-R (build and Run). Make sure that your source folder is populated, and click on "Backup". Progress bar moves while it's working, and stops once it's done. Fantastic.
What if there's a problem? We really want to trap for errors. Change the "Do it" section to read thusly:
-- Do it!
try
do shell script quoted form of myPath & "Contents/Resources/backup.sh"
on error number 255
display dialog "Sorry, an error occurred. I can't make the copy."
end try
As you can surmise, this allows us to try a command that may fail, without bombing out - we get to control the process. Here, I show a basic try block. We try to run the script. If it runs properly, returning a zero, all's good and we continue on. If there's a problem, we show a dialog box that explains that there has been a problem. Try blocks can certainly get much more elaborate.
What might be a reason that we'd have an error in our script? Perhaps we're asking it to backup areas of the file system that we don't have access to? In that case, you can have your script run with administrator privileges. You do this with the....wait for it..."with administrator privileges" parameter. Alter the do shell script line to read like this:
do shell script quoted form of myPath & "Contents/Resources/backup.sh"
with administrator privileges
By itself, with no additional parameters, this will present the user with the familiar dialog box asking for an admin password when the app is run.
Figure 6: Our app asking for authentication
You can preload your script with a username and password. However, I DO NOT RECOMMEND THIS. Clear? To tie this back into last month's column, it would be trivial for someone to run strings across the resulting binary and gather the admin level id and password you provided. If you're daring enough to risk it, find the instructions in the Apple 'do shell script' technote referenced earlier (but seriously: don't).
One little thing that TN2065 does not mention: there is a default timeout on all AppleScript commands, including do shell script, of two minutes. If you believe your script may run for longer - for whatever reason - use a with timeout block:
with timeout of 300 do
do shell script ...
end timeout
Unfortunately, there is no setting that will just let it do its thing indefinitely (OK, perhaps not a terrible thing). If a command does timeout, a timeout error is thrown (-1712). So do make sure you use a value large enough to let your script run to completion. Even if that's "99999".
One for the road
We'll add one more object to our window: A text field with the current status. Double-click on MainMenu.nib, and once Interface Builder is running, click on the Cocoa-Text icon in the object palette:
Figure 7: Cocoa-Text icon
Drag two "System Font Text" fields to the main window. Place one in the upper left (use the guides), and the second right next to it. Double-click on the first, and type "Status:" into the text field. Double-click on the second and type "Idle" into its text field. With the second still selected, Apple-8 your way to the AppleScript attributes, and name this field "efStatus". We should also clean this window up a bit while we're here, no? Drag the progress bar straight up until the guides appear under the text fields we just created. Do the same for the single button, and stop when the guides appear under the progress bar. Size the window itself appropriately, again using the guides. Save and quit Interface Builder.
Again, we need to make that text field perform an appropriate action. Here's how we can do so. In the "setup" section, add this line:
set the contents of text field "efStatus" of window "wMain" to "Copying files..."
...and just after the try block, add this line:
set the contents of text field "efStatus" of window "wMain" to "Backup Complete."
Save (Apple-S), build and run (Apple-R) the project with the changes. Nice.
Finally
Last thing I'll mention: If you really want to make this professional, you'll probably want to fix up the app menus to show the app's real name (not "New Application") and add a custom icon. Let's tackle the icon first. For this, you'll need Icon Composer. You'll find that with the developer tools in /Developer/Applications/Utilities. You'll need to have your artwork complete before using this utility - Icon Composer is not a drawing program. It will, though, read in Photoshop files directly. Drag your artwork to the 128x128 pane, scale it if needed. From there, drag the 128x128 to the 48x48, scale it and let the program extract the mask. Rinse and repeat for the last two sizes. Apple-S to save. Make sure "Save into bundles" is checked, and save the file. You can then take this file and drag it into the "Resources" section of the XCode project. Choose the "Project" menu and click on the "Properties" tab. Type in the name of your icon file (I left mine "icons.icns" in the example below), and as soon as you press return, you'll see a thumbnail representation of your application icon.
Figure 8: Project properties and setting an application icon.
To update the menus, we need to double-click on the MainMenu.nib once again and launch Interface Builder. You should notice a small menubar floating just underneath the window we've been editing. If not, simply double-click on the "Main Menu" icon in the main window of Interface Builder. Editing the menu is as simple as double-clicking on each item you want to rename. You can alternatively accomplish this with the inspector pane, but this method will update the inspector as well.
Figure 9: Menu editing in action.
Quit and save. Let's build this thing into a 'real' app. In XCode, choose the "Project" menu, and then change the "Active Build Configuration" to "Release". Save and build (Apple-B) the app. The last line of the Build Results window will tell you where to pick up your Universal Binary application! Go get it and double-click to run. Exiting, huh? You can now distribute this application, and it contains everything that it needs to run - your shell script (the point of this entire exercise, remember) is bundled inside.
Figure 10: Our final application, running as a universal binary.
Keep it simple
The goal of this article was to introduce the oft-overlooked capability to easily create a GUI for a shell script using tools that Apple provides for free. While many are aware of this capability, I find it "oft-overlooked" by shell scripters, anyway. As mentioned earlier in the article, this is certainly not an exhaustive look at everything you can do, nor does it even begin to approach "best practices" (such as exhaustive error checking in a try-block, creating a proper info.plist, or even dealing with script output). If this introduction intrigues you, there are many, many places to increase your knowledge and further this path.
While I realize that this will run very much after the fact, this publication will appear during WWDC, which will be less lively without Michael Bartosh. Friend, fellow tech, fellow author and more, he will be sorely missed. Let's all remember to raise a glass - even a virtual one - in remembrance. Speaking of WWDC, I'm present to raise a real glass with anyone interested. See you at Dave's.
Media of the month: I've been too tied up in Citrix tech docs and Microsoft Group Policy to recommend anything appropriate here from a technical perspective. Actually...that may provide the perfect segue: OS X provides Windows interoperability by using the open source Samba project. By itself, Samba is an ambitious and deep project. Cleverly, Apple provides O'Reilly's entire "Using Samba, Second Edition" preloaded onto your Mac! Launch Safari and point your browser to <file:///usr/share/swat/using_samba/toc.html>. This tends to be another 'oft-overlooked' resource.
I was fortunate to meet many MacTech readers at MacWorld in January, and I hope to meet more at WWDC 2006. If you're unable to attend, look for our reports from the show. In any case, I'll see you in print next month!
References
Just about everything at http://developer.apple.com
Especially Tech Note 2065: http://developer.apple.com/technotes/tn2002/tn2065.html
The Tao of AppleScript, Derrick Schneider (yes, this came out in 1994 and shipped with a floppy of examples. AppleScript basics just haven't changed that much).
Resources
Mac Scripter: http://macscripter.net/
Ed Marczak owns and operates Radiotope, a technology consulting company. Radiotope helps separate technology issues from policy issues, cool-tech from needed-tech. Guide your decision at http://www.radiotope.com