Common Script Mistakes: Bad White Space
Another problem occurs when copying script text from web pages. Many web pages that show scripts contain the special HTML code
. 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>
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
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 will execute as root; that is how you can avoid using
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
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:
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.
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
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:
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
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
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
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.