13.5. ScriptingIn addition to Automator, Mac OS X has several scripting languages to help you automate tasks on your machine. At first glance, creating your own scripts may seem like a daunting task. The goal of this section is to take some of that dread away by introducing you to scripting in a friendly manner. There are some concepts that can Figure 13-11. The Confidential Watermark workflowbe a bit hard to grasp at first, especially if you've never programmed before. But with a bit of luck and some know-how gained from the following sections, you'll be writing your own scripts in no time. 13.5.1. Scripting LanguagesPicking a scripting language can be one of the most difficult parts of scripting, especially when you have as many choices as those supplied by Mac OS X. Sometimes, a certain language just works better for a job. For example, when you're working with text, Perl will prove much more versatile than AppleScript. It's the nature of the languageits included features and functionsthat helps you determine if it's up to the task. Mac OS X comes with quite a few different scripting languages: Perl, Python, Ruby, AppleScript, C shells, Bourne shells...the list goes on. Some of these languages are much more sophisticated than others. Entire books have been dedicated to each, but in this chapter, we're just going to dabble with two: AppleScript and Bourne shell (that is, bash) scripting. These two languages are easier to grasp and flexible enough to accomplish a wide array of tasks. 13.5.2. Essential Programming ConceptsWhen you're writing a programand a script is really a small programthere are certain concepts that you'll frequently encounter. The terminology might seem a little strange at first, but once you understand what they do and when to use them, these concepts are applicable across languages.
Now that you're a little more familiar with the terminology, the following sections discuss how to put these concepts to practical use. 13.5.3. Shell ScriptingChapter 4 discussed some of the basics behind shell scripts, but shell scripts can actually be quite complicated. The bash scripting language is pretty flexible, and there are a variety of command-line tools to help you accomplish just about any task. The major difference between scripting for the command line and scripting in an environment like AppleScript is that shell scripts are really just a bunch of commands saved in a file. The same commands that you enter at the command line can be strung together and saved for later use as a shell script.
13.5.3.1. A simple scriptTo make a shell script, launch a basic text editor, such as nano, and kick things off with a shebang . "Shebang" is Unix parlance for a hash symbol (or number sign), followed by a bang (or exclamation point): #!. A shebang tells the shell which interpreter should be loaded to process the script. An interpreter is a program capable of processing the code found in the script. Conveniently, the interpreter for bash shell scripts is bash. So, the first line of any bash script you write should start with a shebang, followed by the path to the bash executable: #!/bin/bash In programming circles, it's customary to have one's first program simply print "Hello, World! " to the screen. On the Unix command-line, there's a utility called echo that prints whatever arguments you supply it out to the screen. So, for our Hello World example, we need only two lines of code: #!/bin/bash echo 'Hello, World!' Once you've typed these two lines of code into nano, press Control-O to save the file. Give it a name like HelloWorld.sh (The .sh extension will help you remember it's a shell script). After you've saved the file, press Control-X to exit nano and return to the command line. You've just created a shell script. It might not be a particularly complicated shell script, but all of your future shell scripts will be created in a similar fashion. However, your shell script isn't quite ready to go yet. You must first set its execute bit, to indicate to the shell that the script is a valid command to be used on the command line. To set the execute bit on your script, use the chmod command: $ chmod +x HelloWorld.sh
After its execute bit has been set, the script can be called like any other command. Because the script has been saved to your Home directory and not a directory that is in your shell's PATH, you must specify the absolute path to the script to run it, as shown in Example 13-1. Example 13-1. Running the HelloWorld.sh script$ ./HelloWorld.sh Hello, World! 13.5.3.2. Tying in variablesChapter 4 also discussed the concept of environment variables . If you recall, an environment variable is a value stored in memory that is used to set certain runtime parameters for shell commands. The variables found in scripts are used for similar purposes, with the exception that they only exist while the script is running and within the context of that script. Even though a script can view and manipulate environment variables, other scripts running in the same environment cannot see other scripts' variables. This is what is known as the scope of a variable. To create a variable for your script, you must use an assignment statement. An assignment statement starts with the name of the variable you'd like to instantiate, an equals sign to indicate the assignment, and then the value you'd like to assign. For example, to assign the value "Hello, World!" to a variable named MESSAGE, use this statement: MESSAGE="Hello, World!" The resulting variable, $MESSAGE, can be substituted for the original message in the HelloWorld.sh example. Remember that to use a variable, you must preface its name with a dollar sign. So, the HelloWorld.sh script rewritten to use a variable is: #!/bin/bash MESSAGE='Hello, World!' echo $MESSAGE A nice thing about variables is that they can be assigned a value that is the result of a command (or series of commands). Using command substitution , you can call one command and have its results placed in a variable (or even used as a parameter for another command). Command substitution is processed using one of two code conventions. You can use either $(command) or `command` (note that a back tick is used, not a single quote). For example, to have the results of calling the whoami command placed in the variable ME, use the statement: ME=$(whoami) The command is processed when the interpreter encounters your assignment statement, and the command results are placed in your variable. This variable can then be used just like any other. Expanding upon our HelloWorld.sh example: #!/bin/bash ME=$(whoami) MESSAGE="Hello, $ME!" echo $MESSAGE The value from whoami can also be entered directly into the MESSAGE variable without assigning it to the ME variable: MESSAGE="Hello, $(whoami)!" Or, you can skip the variables altogether: echo "Hello, $(whoami)!"
13.5.3.3. Conditionals in bashNow that you know how to assign a value to a variable, you're probably wondering about ways to access that value. In most cases, you'll use a variable either in some form of output (like the echo statements discussed in the previous section) or in a conditional statement. As mentioned in the "Essential Programming Concepts" section, conditionals are used to perform an action based on the value of a condition. When writing shell scripts, the most common type of conditional you'll use is the if statement. Using if statements, you can determine whether your desired condition evaluates to true. If it does, then the shell will follow the code until an else or fi statement is encountered. The else statement is used to define alternate actions that should be performed if your desired condition evaluates to false. Finally, the fi statement marks the end of the conditional logic. Example 13-2 shows a simple if statement. Example 13-2. A block of conditional codeif [ "$UID" == "0" ]; then # It's true, do something as root echo "You're root! " else # It's false, the user is not root echo "Sorry, you're not root. " fi The other major conditional statement available in bash is the case statement. A case statement allows you to test a condition for several possible values. Each value has its own block of logic to be processed if the condition equals that value. A case statement starts out by calling case and supplying your condition, followed by the keyword in. Each possible outcome is then listed, with the potential value, the block of code to be processed, and two semicolons to mark the end of that value's code. Finally, the esac keyword marks the end of all of the case conditional logic. Example 13-3 shows a case statement with three possible outcomes. Example 13-3. A case statement with three potential outcomescase "$UID" in 0) # The UID is 0, do something as root echo "You're root!" ;; 501) # The UID is 501; the first user echo "You're the first user on this machine." ;; *) # Some other user echo "You're not root nor the first user." ;; esac You might have noticed that the last potential value is an asterisk (*). In a case statement, the asterisk branch of code is used if the condition does not trigger one of the other code branches. The case statement is an excellent alternative to using nested blocks of if and else statements. When creating a conditional, bash offers many different types of tests to perform. For example, you can check whether the value stored in a variable is equal to another value. You can check whether a file or directory exists. You can determine if you have read access to a file. These tests enable you to create scripts that react to a variety of different scenarios. Table 13-2 shows some of the more useful tests. To see what else is available, take a look at the manpage for test.
13.5.3.4. Send in the loopsLoops are where the real power of scripting comes into play. Being able to perform a series of commands on a series of files with just a few keystrokes can turn a tedious project into an easy job. Loops (and scripting in general) shift the burden of monotonous tasks onto the computer. As mentioned earlier, there are two types of programming loops. For incremental loops, bash uses the for command. For conditional looping, there are two options: while and until . The bash shell handles for loops a bit differently from most other programming languages. It's still used for iterating through a block of code a certain number of times. The main difference, however, is that bash's for is designed to step through a series of values within a string. Chapter 4 provided an example of such a loop, which is repeated here in Example 13-4. Example 13-4. A simple for loopfor i in $(ls ~/Desktop/*.txt); do say -f $i done The earlier discussion of command substitution mentioned that the resulting value can be used as an argument for another command. That's exactly what's happening in Example 13-4. The ls command is run first, and its results are then supplied to the for loop. Example 13-5 applies the command substitution and shows what the resulting values might look like. Example 13-5. The command substitution expandedfor i in "/Users/jldera/Desktop/One.txt /Users/jldera/Desktop/Two.txt"; do say -f $i done The for loop then takes the first value from the list and inserts it into the variable i. The shell considers a blank space to be the demarcation between entries, so in the first iteration of the loop, i would have a value of /Users/jldera/Desktop/One.txt. Subsequent iterations of the loop step through the list of values until the entire list has been processed. The other two loop commands, while and until, are based around the use of conditionals. They have a similar syntax, and both operate under the same basic principle. A conditional statement is checked and the loop either processes again or it doesn't. Depending on which command you're using, the when and why of stopping is handled differently. For the while command, the code inside the loop is processed as long as the condition holds true. Example 13-6 shows a while loop. Example 13-6. T.G.I.F. while loopwhile [ $(date +%a) == "Fri" ]; do # As long as it's Friday... osascript -e 'say "T G I F"' sleep 360 done An until loop is the reverse. The loop code is processed while the condition is not true. Example 13-7 shows a simple until loop. Example 13-7. An until loop# Use the shell's random number generator VALUE=$RANDOM until [ $VALUE -lt 1000 ]; do # We do this til the value is less than 1k echo Nope, $VALUE is greater than 1000. VALUE=$RANDOM Done # Now print something after the loop is done echo Success! $VALUE is less than 1000. 13.5.3.5. Interacting with the userAnother important aspect of scripting is the ability to get parameter information and other feedback from the user. The read command and some special script environment variables are more than up to the task of supplying interaction with the user. When a script is launched, it has a few variables that are defined and populated with information about how the script was called. These variables can be used in your scripts just like any other variable. They just contain data that is supplied by the shell. To get some information about the command-line execution of your script, look no further than the positional parameter variables. These variables start with the name of the script itself in position 0. Position 1 will contain the first parameter; position 2, the second; and so forth. You refer to the variable by using its numerical position, prefaced by a dollar sign. For example, the first parameter is $1. For positions beyond $9, enclose the number in curly brackets; e.g. ${13}. There are a few other variables related to the positional parameters. The $* variable contains all of the position parameter values, combined into a single value. The $@ variable contains all of them as well, but the parameters are passed as individual values. Finally, the $# variable contains the number of command-line arguments that were supplied. For those scripts that want more interaction with the user than command-line arguments offer, the bash shell includes the read command. Using read, the shell will wait for keyboard input from the user. Once the user presses Enter, the input is stored in the variable specified in the read call. For example, to get input from the user and store it in the variable AGE, use code like that in Example 13-8. Example 13-8. Reading keyboard inputecho -n "Please enter your age: " read AGE echo "Your age is $AGE!"
13.5.3.6. Sample scriptsNothing beats experience. If you haven't already, go back and try out some of the code examples. Most of them should work for you with fewif anychanges. The best way to learn to script is to jump in headfirst. Start out by trying a couple of little tests, even if they aren't practical. Even just working on redirecting output and piping when using the Terminal can help you build your scripting foo. Here are a few scripts for you to pick apart and try out. Feel free to use them as a starting point for your own scripting efforts. The script in Example 13-9 uses piping and the grep command to display information about a hard disk. Example 13-9. Report essential information about a disk#!/bin/bash # disk_info.sh: Displays summary data about a disk DISK=/dev/disk0 # The disk to check echo "Drive status for $DISK:" diskutil info $DISK | grep "SMART" # Display SMART status echo; echo "Mount points:" mount | grep $DISK # Display mount points echo; echo "Disk Space:" df | head -n 1 # Print out the column headers df | grep $DISK # Display only relevant results Example 13-10 is a script that redirects the output from the system_profiler command into a file. It then uses the diff command to compare the output from the current run and output from the prior run. If a difference is found, it notifies the user and then displays the differences. Example 13-10. Alert user to changes in system hardware#!/bin/bash # watch_profiler.sh: Alerts user if basic hardware is changed # Store the data from System Profiler temporarily system_profiler -detailLevel mini > /tmp/newest_system_profile # Check for changes if [ -f ~/.last_system_profile ]; then DIFF=$(diff -s /tmp/newest_system_profile ~/.last_system_profile | grep -c "identical") # We have a change if [ $DIFF == 0 ]; then echo There have been changes to the hardware! diff /tmp/newest_system_profile ~/.last_system_profile fi fi # Setup for the next run mv /tmp/newest_system_profile ~/.last_system_profile The script in Example 13-11 uses command substitution to get the current date, create a temporary directory using mktemp , and find out the directory path for a file. It also uses the find command to locate files that have been modified within the past day. After find's output is stored in a file, a while loop uses the read command to read the file back in. The results are then copied to the temporary folder, after which the hdiutil command is invoked to create a disk image of the workspace and place it on the desktop. Finally, the script removes its workspace to keep the disk clean. Example 13-11. A simple backup script#!/bin/bash # daily_diskimage.sh: Creates a disk image of files found in # FOLDER that have changed within the past day. # The image is then placed in OUTDIR. FOLDER=~/Documents # The folder to search OUTDIR=~/Desktop # Where to place image TODAY=$(date +"%Y%m%d") # Get today's date WORK=$(mktemp -d /tmp/diskimage.XXXXXX) # Create a work path find $FOLDER \( -mtime 1 -or -ctime 1 \) -and -type f \ -print >> $WORK/.BackupList # Find files to backup # Copy our files to the work directory while read FILE; do DEST="$WORK"$(dirname "$FILE") mkdir -p "$DEST" cp -v "$FILE" "$DEST" done<"$WORK/.BackupList" # Create a disk image of the work directory hdiutil create -fs HFS+ -srcfolder $WORK -volname $TODAY \ "$OUTDIR"/$TODAY.dmg # Remove our workspace rm -rf $WORK As you can see, shell scripts can perform a variety of tasks and have a wide number of commands at their disposal. For more information on scripting, take a look at some of the books and manpages mentioned in the "Further Explorations" section at the end of this chapter. 13.5.4. AppleScriptLong before Mac OS X and its scriptable shell, the tool with which Mac power users automated their systems was AppleScript. AppleScript has persisted as a part of today's Mac OS, Tiger. In Tiger, AppleScript is still going strong, with more and more functionality and refinement being built into the language itself, its editor, and its interaction with the OS and user.
13.5.4.1. Another simple scriptUnlike shell scripts, which are simply text files with a series of commands, AppleScripts have a special file format. So, to work with AppleScripts, you need to use a special editor, called Script Editor (/Applications/AppleScript), shown in Figure 13-12. Figure 13-12. Script EditorMuch like shell scripts, the commands available to AppleScript are dependent on what software you have installed. When you install an application that supports AppleScriptwhich isn't all Mac applications, but many of themthe application implements an AppleScript dictionary . Using the app's dictionary, you can determine what kind of tasks you can perform in your script by calling that application. Figure 13-13 shows the Finder's dictionary. To open an application's dictionary, open up Script Editor and click File Open Dictionary. A dialog box containing all of the available dictionaries will appear, allowing you to select the one youre interested in. You can keep as many dictionaries open as you'd like, which is convenient when you're looking up commands for multiple applications. Figure 13-13. The Finder's AppleScript dictionaryNow that you're a little more familiar with AppleScript, its editor, and its dictionaries, let's take a look at its syntax. AppleScript is often described as a very verbose language. In their quest to make AppleScript more accessible to new users, Apple's developers made the language very English-like. This is both a blessing and a burden, as the code is far easier to read, but can be a bit more tedious to write. Example 13-12 is our Hello World script, revised for AppleScript. Example 13-12. Hello World, AppleScript styledisplay dialog "Hello, World! " Once you've entered that line of code into your script in Script Editor, click the Run button to execute it. A dialog like that shown in Figure 13-14 is displayed, letting you know it worked. 13.5.4.2. Variables: AppleScript styleLike any good scripting language, AppleScript has variables as well. AppleScript has more variable types than most shells, however, allowing you to store different types Figure 13-14. The Hello World script in actionof values more efficiently. Assigning a variable in AppleScript is done using the set command, followed by the variable name, the keyword to, and then the value to assign to the variable. For example: set message to "Hello, World!" assigns the value "Hello, World!" to the variable message. So, the Hello World script rewritten with a variable is: set message to "Hello, World!" display dialog message Depending on the type of value you're storing and how you're going to use it, you can specify a variable type in its declaration. As an example, to declare the message variable as a string, use: set message to "Hello, World!" as string You can also change a variable's type on the fly as follows: set num to 3 as number set message to (num as string) & " is a magic number." display dialog message In addition to individual variables, variables can be combined into lists and records. A list is simply a series of values stored in a single list variable (often called an "array" in programming parlance). You can refer to individual items in the list, add items to the list, etc. For example, to create a new list, define it as follows: set animals to {"cat","dog","dogcow") This line of code creates a new list named animals and populates it with three values. To refer to a specific item in the list, use the item...of keyword. For example, to refer to the second item in the list: display dialog item 2 of animals The line above displays a dialog with the word "dog," which is the value stored in the second position of the animals list. To change a specific element in the list, use the item...of keyword in conjunction with the set command: set item 2 of animals to "horse" display dialog item 2 of animals This time around, the resulting dialog displays the word "horse," since the first line has replaced the "dog" value in position two with "horse". Records are quite similar to lists. The biggest difference between them is that a record stores multiple properties . Properties are essentially variables that have a persistent value. Most programmers would call such variables constants . Some properties are defined by an application (and thus listed in its dictionary). Others are user-defined using the property keyword, similar to using set. The other unique quality of records is that their values are stored in name-value pairs. This allows you to then refer to a given property within a record by its name, instead of using a positional number. For example, here is a definition of a simple record: set pet to {name: "Charlie", owner: "Jason", type: "Turtle"} Instead of using the item...of keyword, you'll want to refer to the property using its name. To get the owner property out of the pet record, use the statement: display dialog owner of pet 13.5.4.3. AppleScript conditionalsAlso like the bash shell, AppleScript includes conditionals for diverting program logic. AppleScript has a variety of comparison operators, all of which can be easily used with an English-like syntax. Here are some example comparisons, each on a separate line: if type of pet is equal to "Turtle" then display dialog "Turtle!" if num is greater than 5 then display dialog "It's bigger than 5!" if passwd is not equal to "secret" then display dialog "Sorry, wrong password." When working with strings, records, and lists, there are some special comparisons at your disposal. These comparisons make it easy to look for a specific substring or value. The starts with comparison is used to examine the beginning of a string. The reverse of starts with is the ends with comparison; use it to check the end of a string. Lastly, the contains comparison evaluates true when the substring is found within your variable. Here are some examples using these comparisons: if name of pet starts with "C" then display dialog "The name starts with a C" if animals ends with "dogcow" then display dialog "Clarus was here" if owner of pet contains "Jobs" then display dialog "Are you Steve's relative?" 13.5.4.4. Looping in AppleScriptIn AppleScript, all loops are defined using the repeat keyword. Depending on whether you want the loop to be incremental or conditional, the syntax for using repeat is a little bit different. For example, to create an incremental loop in AppleScript, use code similar to Example 13-13. Example 13-13. An incremental AppleScript loopset nums to "" repeat with i from 1 to 10 set nums to nums & " " & (i as string) end repeat display dialog nums Like bash, there are two ways to use a conditional loop in AppleScript. Conveniently, the keyword and purpose for both types are the same as those found in the shell. Example 13-14 uses a while loop. Example 13-14. A loop using while logicset today to the current date repeat while (today as string) starts with "Monday" display dialog "Sounds like somebody's got a case of the Mondays" set today to the current date end repeat Example 13-15 shows a loop using until logic. Example 13-15. Using until logic in a loopset WorldPeace to false as boolean -- Sad, but true repeat until WorldPeace is equal to true display dialog "All we are saying is, give peace a chance." end repeat 13.5.4.5. User interactionAppleScript is much more flexible than shell scripts when it comes to interacting with the user. Though it is not possible to use parameters quite the way you would with a command line, it is possible to use several different dialog windows for soliciting information from the user. In the previous code examples, the display dialog command has been used to provide feedback about a script's progress. This command can also be used to obtain basic text input from the user. For example, to get some text from the user and store it in a variable called nom, use this code: display dialog "Please enter your name" default answer "" set nom to text returned of the result
You can also give the user up to three different buttons to accompany the text input (or, just use the buttons for input and skip the text). To do this, use the buttons parameter of the display dialog command, as follows: set reply to display dialog "Enter pet's name and type" default answer "" buttons {"Dog", "Cat", "Turtle"} set nom to the text returned of reply set type to the button returned of reply display dialog nom & " is a " & type If you want to offer the user more than three choices, you can use the choose command and implement a list. For example, to have the user choose an option from a list of fruits, use the command: choose from list {"apple", "orange", "pear", "banana"} with prompt "Pick one:" set fruit to the (result as string) display dialog fruit & "s are good" The choose command offers several other dialog windows for different types of choices. The file keyword is used for files, folder for folders, and application for applications. As an example, you can have the user pick a file with the command: choose file with prompt "Pick a PDF file:" of type {"PDF"} 13.5.4.6. Sample scriptsHere are a couple of simple AppleScripts to help you get started on your own scripting projects. Example 13-16 is an excellent candidate for a script to tie to iCal. The script activates iTunes and then plays a playlist named "Wake-Up." To use this script, create the playlist in iTunes and name it "Wake-Up." Populate the playlist with your desired tracks and then use this script as a musical alarm clock for iTunes. Example 13-16. iTunes playlist script-- Opens up iTunes and plays a playlist named "Wake-Up" tell application "iTunes" activate play playlist "Wake-Up" end tell Example 13-17 is an example of a folder action. Using folder actions, you can create a script that is launched upon changes to the folders contents. Example 13-17 displays a simple dialog window that notifies you when a file has been placed in a folder. This is a great script to use with your Mac's Drop Box. To configure the folder action, you'll first need to save your script in ~/Library/Scripts/Folder Action Scripts. Then, use the Folder Action Setup tool (/Applications/AppleScript) to select the folder to attach the script to, followed by your new script. Figure 13-15 shows a folder and its associated action scripts. Example 13-17. A folder action-- Displays a simple dialog whenever a file is added to the folder on adding folder items to this_folder after receiving added_items display dialog "Someone has placed an item in " & this_folder end adding folder items to Figure 13-15. Configuring folder action scripts13.5.5. Bridging the GapA convenient command for moving between the AppleScript and shell script worlds is osascript . Using osascript, you can execute an AppleScript from the command line, either by specifying the path to an existing script or using osascript's -e switch to enter the AppleScript line by line. Example 13-18 is a script that will control iTunes playback. Example 13-18. A script to control iTunes playback#!/bin/bash osascript - <<EOF tell application "iTunes" playpause end tell EOF |