MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Scripting Mac OS X

by James Reynolds
11/07/2003

Editor's Note: James Reynolds recently spoke at the O'Reilly Mac OS X Conference as part of a panel titled "Migration to Mac OS X, A Case Study of Higher Education Institutions." In that session, James talked about the value of automating tasks in the educational environment. Of course, when you think of automation, you think of scripts. So as a follow up, James put together this hands-on tutorial to help you master scripting Mac OS X, complete with a helpful appendix of sample scripts (on page 3). This is one you'll want to have handy for reference.

Mac OS X is an awesome platform to administer. The ability to control the user experience with scripts is astounding. This OS has everything a scripter wants: a command line, languages, remote login, and dependability. And it's all built in.

This tutorial strives to share tips for administrators who are relatively new to the platform (but there are some good tips for veterans too). First I'll cover learning Perl and shell scripts. Then I'll discuss how to execute Perl, shell, or AppleScript commands from a script that is written in a different language. As part of this bargain, I'll show three common scripting mistakes. Finally, I'll go through the steps of setting up a script that runs every login and show some of the problems that might be encountered. At the end of this tutorial, there is a side-by-side comparison of common script tasks in Perl, BASH, and AppleScript.

In order to work through this tutorial, you will at least need to know how to create and execute either a Perl or shell script. It also helps to know the Terminal command line, some Terminal commands, and some Perl. You can refresh your Terminal hacks by taking a look at Chris Stone's Learning the Terminal series right here on Mac DevCenter.

Related Reading

AppleScript: The Definitive Guide
By Matt Neuburg

Shell Scripts

The Terminal is a powerful tool, and every administrator should develop some basic skills using it.

It is important to learn how to use the Terminal because shell scripts are the same as typing commands in the Terminal. There are only a few Terminal commands that cannot be placed straight into a script. If you can perform your tasks using the Terminal, you have already written your script! Just copy the commands that you type in the Terminal and paste them into a text document. With a little extra formatting and cleanup, you have a shell script.

There are several shell script languages. In Mac OS X, the most common shell script is BASH. The default Terminal shell language is TCSH in 10.1 and 10.2, and BASH in 10.3. You should set the Terminal to use the same language you are going to script in so that you have experience using that shell and know exactly how a command will behave. Most shell scripts are written using /bin/sh, so you should change the command line to /bin/sh by just typing /bin/sh. You can write shell scripts in any language. But you need to be aware that each language behaves differently.

Perl

The other common administrator non-shell language is Perl. Perl syntax is very different than that of shell scripts. To write scripts using Perl, you should read Learning Perl, 3rd Edition. This is the book I read when I learned Perl.

Executing Shell Commands from Perl

If you know a little Perl and a little shell, you can actually mix both languages in a script by calling the system command in Perl or executing code using Perl -e ... in a shell script.

For example, a Perl script can read the contents of a file without using Perl's file commands with this code:

#!/usr/bin/perl

$system_crontab = `cat /etc/crontab`;
print $system_crontab;
The backticks (``) tell Perl to execute the shell command cat /etc/crontab. The output of the cat command is stored in the $system_crontab variable and printed in the last line.

Executing Perl from Shell Scripts

Likewise, a shell script could call Perl to get the fifth file of the /etc folder with:

#!/bin/sh

file_six=`/usr/bin/perl -e '@file_list=\`ls -1 /etc\`;' -e 'print 
$file_list[5];'`
echo $file_six

The backticks in this example perform the same function that they do in Perl. They are used to execute a shell command (in this case, the command is the Perl interpreter) and save the output into the file_six variable.

The -e flag that follows the Perl command tells the Perl interpreter to execute the string '@file_list=\`ls -1 /etc\`;' -e 'print $file_list[5];'. Rolling this out, Perl code that is executed looks like this:

@file_list=`ls -1 /etc`;
print $file_list[5];

The first line, '@file_list=`ls -1 /etc`;', has another pair of backticks, which execute ls -1 /etc in the shell environment. The output is assigned to the file_list array. Notice that these backticks have to be escaped with the backslash (\), because the code is already inside of a pair of backticks.

The second line prints the sixth element of the file_list array. Perl array indexes begin with 0, so the sixth element has an index of 5.

That print statement doesn't print to the command line, though. The output of the Perl script is "printed" back to the shell script and it is assigned to the file_six shell variable. It is printed to the command line with the echo command that is in the last line of the shell script.

Executing AppleScript from Shell and Perl Scripts

One of the jewels of Mac OS X scripting is the osascript command. It is used just like the example above with the shell script that executes Perl. The following shell script will display a dialog:

#!/bin/sh

osascript -e 'tell application "Finder"' -e "activate" -e "display 
dialog \"hello\"" -e 'end tell'

In this example, the shell script executes the osascript command. The osascript command executes this AppleScript:

tell application "Finder"
         activate
         display dialog "hello"
end tell

To execute an AppleScript from Perl, simply put the osascript command in backticks, or execute it with the system command. This Perl script will use Text-To-Speech to "say" a message.

#!/usr/bin/perl

$message = "hello";
system "osascript -e 'say \"$message\"'";

Executing Shell and Perl Scripts from AppleScript

Finally, it is even possible to execute shell scripts with the do shell script AppleScript command.

The following AppleScript command will display a dialog that contains all of the files in /etc that contain the letter "b."

display dialog (do shell script "ls /etc | grep b")

Similarly, if the do shell script contained the Perl -e command, then you could execute Perl from AppleScript, as well.

Common Script Mistakes: Line Endings

Perhaps the most common scripting error occurs when a script is created with BBEdit or some other text editor and the file is saved with Macintosh line endings. There is a Macintosh new line character, ASCII 13, and there is a UNIX new line character, ASCII 8. They are both valid in text documents, but UNIX will not treat a Macintosh line ending as the end of a line.

If you try to execute a script that has Macintosh line endings, you will get a "Command not found" error:

/path/to/script: Command not found.

There are a few ways to change the line endings from Macintosh to UNIX. The easiest is to open the document in BBEdit. Each document has a tool bar at the top. The fifth tool looks like a small document icon. Click on it and a pop-up menu appears. Scroll down and select "UNIX," and then save the script.

To change the line endings from the command line, execute this command in the Terminal:

tr \\r \\n < /path/to/script > /path/to/new_script

Replace /path/to/script with the path to the script with Macintosh line endings, and /path/to/new_script to where you want to save the new file. The two paths cannot be the same, or you will end up creating an empty file.

Common Script Mistakes: Execute Permissions

Another common mistake is incorrect execute permissions. In order to execute a script, the file must have execute permissions. If you try to execute a file that does not have execute permissions, you will get the following error:

/path/to/script: Permission denied

By default, all new text documents do not have execute permissions. To add execute permissions to a script, open the Terminal and type the following command:

"chmod u+x /path/to/script"

If you do not want to type the script path, you can always type chmod u+x and drag the script file to the Terminal window, and the Terminal will fill in the path. Then hit return.

Common Script Mistakes: Bad White Space

Related Reading

Learning Perl
By Randal L. Schwartz, Tom Phoenix

Another problem occurs when copying script text from web pages. Many web pages that show scripts contain the special HTML code &nbsp;. This code creates a whitespace character and it is used for script formatting on a web page. The problem with this is that Safari and Internet Explorer display this space with ASCII character 194 or 202 instead of the regular space character, ASCII character 32. While on the web page it may look like a space, if you copy it straight from the browser and paste the text into BBEdit or the Terminal, it will still look like a space, but when you try to execute the script, you will get an error.

A shell script with a bad whitespace character will produce an error that looks like this:

</path/to/script>:  <command>: command not found

</path/to/script> would be the path to your script, and <command> would be replaced with the line in your code that has the bad whitespace character.

A Perl script will give one of these errors:

Unrecognized character \xC2 at <filename> line <number>

or

Unrecognized character \xC4 at <filename> line <number>

To fix this, you need to remove all of the bad whitespace characters and replace them with ASCII 32, the character produced by the space bar on your keyboard. In BBEdit, you can do this by finding one of the bad characters and performing a find and replace. Paste the bad character into the find field, and type a normal space character in the replace field. Then check "Start at Top" and click the "Replace all" button.

Real-World Scripting Example

Now I would like to set up a script to perform a common computer-lab task that is performed at login. It will remove the contents of the folder /Users/labuser. Then the script will restore /Users/labuser with a brand-new home folder and give ownership of the folder to the user who is logging in. There are many hidden problems just in this simple task.

Here is the script, in BASH:


#!/bin/sh

rm -rf "/Users/labuser"
ditto -rsrcFork "/System/Library/User Template/English.lproj" "/Users/labuser"
chown -R $1:staff "/Users/labuser"

First of all, let's try to execute each command, one line at a time in the Terminal. Before you do this, you need to create a home folder that the script will erase. Open up System Preferences and create a user account named "labuser". This will create a folder at /Users/labuser. Don't create a folder in the Finder or the Terminal, because then the folder will not have the correct ownership.

Second, enter the same shell as your script by typing /bin/sh in the terminal. This should guarantee that everything you type will behave exactly the same as when you run the script for real.

When you try to execute the first line, you should get this error:

rm: /Users/labuser: Operation not permitted

The solution to this is to execute the command with elevated permissions. You can do this by typing

sudo /bin/sh

Then enter your password.

This brings up a point that you need to be aware of when writing scripts. Most system tasks require root permission. You cannot place sudo in any script, because it will ask for a password, and you should never hardcode a password in a script. So you need to execute the script by the root user. In a few paragraphs, I will show you how to execute the script every login as a LoginHook. The LoginHook will execute as root; that is how you can avoid using sudo. LogoutHooks, startup items, and system cron jobs also execute as root. For now, to finish testing the script, just type sudo /bin/sh and your password to become root.

Start entering each line again. This time, you should not get the "Operation not permitted" error. However, the last line will produce an error. $1 is a special variable that is set when the script is executed. Since we aren't executing this as a script yet, you need to replace $1 with labuser, like this:

chown -R labuser:staff "/Users/labuser"

Then it executes as expected.

Save the commands in a text file and make sure the file has user execute permissions. Attempt to execute the script from the Terminal. Just type in the path to the text file, either the full path or a relative path (/Users/yourname/Desktop/loginhook, ~/Desktop/loginhook, or whatever the path is). You will need to give it a value for $1. Do this by placing labuser after the path, like so:

/Users/yourname/Desktop/loginhook labuser

The script executes as expected. You can make sure it is deleting files by adding extra files to the labuser folder and checking to see if they are gone after running the script. You can confirm that the ownership of the folder is also correct by typing:

ls -ld "/Users/labuser"

It should be:

drwxr-xr-x  2 labuser  staff  68 Sep 30 02:07 /Users/labuser/

Now, let's actually set this up as a LoginHook and execute it by logging in to the computer.

The LoginHook functionality of Mac OS X is built into Mac OS X's loginwindow application. Apple had the foresight to add this feature and to document it in "Mac OS X: System Overview." Before you set up the LoginHook, you should decide where to place all your administrator scripts. Everyone has a different preference. Most administrators create a dedicated administration folder in /Library. The folder could get quite cluttered as you add more functionality, so you may want to save it in a scripts folder as well, such as /Library/Admin/scripts/loginhook.sh.

To set up the LoginHook, type the following command into the Terminal, replacing /path/to/script with the path to your script.

sudo defaults write com.apple.loginwindow LoginHook /path/to/script

Now log out, and log back in as the admin user. When you log in, the LoginHook should execute. Now we need to check to see if it worked! Examine the contents of /Users/labuser by typing the following in the Terminal:

ls -ld "/Users/labuser"

You should see:

drwxr-xr-x  2 root  wheel  68 Sep 30 02:15 /Users/labuser/

The folder is replaced, but the permissions are incorrect. For some reason, the chown command did not execute.

Why didn't the script execute the chown command? To find the solution, you need to capture the output of the command. Edit your LoginHook and change the chown line to:

chown -R $1:staff "/Users/labuser" &> /var/log/loginhook.debug

Now log out and log back in as admin. Look at the file /var/log/loginhook.debug by typing this in the terminal:

less /var/log/loginhook.debug

You will see:

/path/to/script: chown: command not found

The shell could not find the chown command when it ran at login. However, it can find the chown command when running in the Terminal. That is because the PATH environment variable is different when running at login.

So how do you fix this? One way is to modify the LoginHook's PATH environment variable. But the best way is to just tell the script where the command is, then it doesn't even have to go look. To find a command, type "whereis " and then the command. Here is the above script with all of the paths filled out:

#!/bin/sh

/bin/rm -rf "/Users/labuser"
/usr/bin/ditto -rsrcFork "/System/Library/User 
Template/English.lproj" "/Users/labuser"
/usr/sbin/chown -R $1:staff "/Users/labuser"

After saving the change, log out and then back in. The LoginHook should work now. You can now log in as labuser, make changes, log out, and log back in as labuser and notice that the changes you made are gone.

There are many tasks that can be performed at login. The ditto command is quite time-intensive as well. It is possible to run the ditto command before the LoginHook, such as at startup, or even create an idle script that runs constantly, detects when the computer is idle, and creates a cache of home folders.

In a public lab setting, there are many tasks that you want to perform at logout, startup, and other times. While still in its infancy, ULabMin is a project that contains scripts that run at login, logout, and startup, and when idle or at night.

Appendix: List of Common Administrative Commands for Shell, Perl, and AppleScript, Listed Side-by-Side

Creating an Executable Script

To create an executable AppleScript, you must open /Applications/AppleScript/Script Editor, write the script that passes the Check Syntax operation, then select File->Save As... and change its format to Application. You also want to check Never Show Startup Screen.

To create a Perl script, create a new plain text file and on the first line put the "shebang" (#!) and then the path to the Perl interpreter, like so:

#!/usr/bin/perl

# code goes here
# etc

You must also make sure it has the execute permission set.

To create a shell script, make it just like you would a Perl script, except replace the path to the Perl executable with the path to the shell-script language you will use. For example:

#!/bin/sh

# code goes here
# etc
Executing a Script

Perl, shell, and AppleScripts can all be launched in the Terminal by typing in the path to the script, either a full path or a relative path. For example, if your script is on your Desktop:

/Users/yourname/Desktop/myscript

or if you open a new Terminal window, it defaults to /Users/yourname, so you can just type:

./Desktop/myscript

AppleScript files saved as Application format can also be double-clicked. Perl and shell scripts cannot be double clicked, unless you add .command to the end of the script. However, a script that is opened in this way will open the Terminal, and then execute in a new Terminal window.

There are third party utilities such as DropScript, Pashua, and Platypus that will turn a script into an application.

Perl and shell scripts that do not have execute permissions can also be executed in the Terminal by typing the path to the Perl or shell interpreter, and then the path to the script. For example, type this in the Terminal:

/bin/sh /path/to/script

or

/usr/bin/perl /path/to/script
Creating Variables

In AppleScript:

set theVariable to "something"

In Perl:

$theVariable = "something";

In Perl, variables always start with $ when you create them and when you access the contents.

In a BASH shell script, create a variable using the variable name, without a $, and with no spaces after the =, like this:

theVariable="something"

To get at the contents of a BASH variable, you must place a $ before the variable, like this:

echo $theVariable

Other script environments, such as TCSH, set variables differently. TCSH is as follows:

set theVariable="something"
echo $theVariable
Comments

In AppleScript, a comment begins with --.

-- this is a comment

In Perl and shell scripts, a comment begins with #.

# This is a comment
Arrays (or Lists)

In AppleScript, an array is called a List and is created like this:

set theList to { "item1", "item2", "item3"}

Access an item of the list like so:

display dialog item 1 of theList

Get the number of items like so:

set x to count of every item of theList

Loop with the list like this:

repeat with i in theList
	display dialog i
end repeat

In Perl, you create a list like this:

@theArray = ("item1", "item2", "item3");

Access an item of the array like so:

print $theArray[0];

To get the number of items in the array, use this:

$number_of_items = @theArray;

Loop with the list like this:

foreach $i (@theArray) {
   print "$i\n";
}

In a BASH shell script, create an array like so:

theArray=(item1 item2 item3)

Print out the first item like so:

echo "${theArray[0]}"

Get the number of items like so:

echo "${#theArray[@]}"

Loop with the list like this:

for i in ${theArray[@]}
do
   echo $i
done
if-then Condition Blocks

In AppleScript, an if-then block looks like this:

set something to "blah"
if (something is equal to "bla") then
   display dialog "something is bla"
else if (something is equal to "blah") then
   display dialog "something is blah"
else
   display dialog "something is not bla or blah"
end if

In Perl, it looks like this:

$something = "blah";
if ($something eq "bla") {
   print "something is bla\n";
} elsif ($something eq "blah") {
   print "something is blah\n";
} else {
   print "something is not bla or blah\n";
}

Also, in Perl, text comparisons are done with eq (equal), or ne (not equal). Number comparisons are done with == (equal to), != (not equal to), etc.

In BASH, it looks like this:

something="blah"
if [ "$something" = "bla" ]; then
   echo "something is bla"
else
   if [ "$something" = "blah" ]; then
     echo "something is blah"
   else
     echo "something is not bla or blah"
   fi
fi

In Perl, spaces in condition statements don't always matter. In BASH, you must have the spaces around the brackets and the equal sign.

Check to See if a File Exists

In AppleScript, to check to see if a file exists, you do this:

tell application "Finder"
   if (file "Hard Disk:path:to:file" exists) then
     -- do something
   end if
end tell

In Perl, you do this:

if ( -f "/path/to/file" ) {
   # do something
}

In BASH, you do this:

if [ -f "/path/to/file" ]; then
   # do something
fi
Combining Strings

In AppleScript, you combine strings with &:

set theVariable to "c"
set x to "a " & "b " & theVariable

In Perl, you do it with a period:

$theVariable = "c";
$x = "a " . "b " . $theVariable;

If you use double quotes, you can place variables straight in the quotes and it will be replaced with the variable value, like this:

$x = "a b $theVariable";

or, to put text at the end of the variable:

$x = "a b ${theVariable}d";

If you really want a $, you have to escape it:

$x = "a b \$theVariable";

Or use a single quote:

$x = 'this that $theVariable';

You have to be careful with special characters in a double quote. If you are worried about using special characters, you can either use a single quote or just test escaping it.

# this is a test
$x = "test: &@!$?";
print $x;

In BASH, you combine variables like so:

variable1="a "
variable2="b "
variable3=${variable1}${variable2}c
echo ${variable3}

James Reynolds is a member of the University of Utah's Student Computing Labs Mac Group. His main duty is the deployment of Mac OS X. Most of his responsibilities include the OS customizations, scripts, and security of the Mac OS X lab and kiosk computers supported by SCL.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.