I am Polish and I have files whose names contain Polish characters. One day I was horrified to find that I could not pass such a file to an AppleScript. After much work, I finally figured out a way to pass arguments containing international characters to an AppleScript. Along the way, I learned many other things as well. I will explain this starting with simple examples, then expanding on them. In this hint you will learn the the magic shebang for an AppleScript, and some useful rules for Makefiles, in addition to the main hint itself, with an example script that opens files in Preview.
I wanted a way to run an AppleScript from the command line using the Unix shebang approach. An example of what I wanted is having the first line of a Bourne shell script be #!/bin/sh. In this way, you can simply type the name of the script in Terminal to launch it. I wanted a similar approach for an AppleScript.
[kirkmc adds: Wow, what a hint! I have to admit, my AppleScript skills are too limited to follow all of this one, but the poster went to no end to explain this in detail. What follows are nearly 4,000 words on the subject, so read on if you're interested in the very fine details.]
Here's how the code looks:
#!/bin/sh
exec <"$0" || exit; read v; read v; exec /usr/bin/osascript - "$@"; exit
-- Your AppleScript here
How does this work? First the file is read by /bin/sh according to the first line (the shebang #!/bin/sh). In the next line, many things happen. First, standard input is redirected to the script itself (exec <"$0"). Then the first two lines of the script are read (the two read v bits). Then /bin/sh is replaced by osascript (exec /usr/bin/osascript - "$@"), which reads the script from standard input. Remember that standard input is the third line of the file itself, by this point, so osascript will interpret anything from that point forward.
% cat > shebang
#!/bin/sh
exec <"$0" || exit; read v; read v; exec /usr/bin/osascript - "$@"; exit
on run argv
argv
end run
^D
% chmod +x shebang
% ./shebang a b c
a, b, c
Sadly, you cannot pass arguments to your AppleScript in 10.3 or earlier in this manner. But if you have an AppleScript that does not take any arguments, it will work perfectly on 10.3 or earlier using this technique. If you need to pass arguments to your AppleScript on 10.3 or earlier, then you can pass them as environment variables like in the following example.
#!/bin/sh
# check args
case "$#" in
1)
arg=$1; export arg
;;
*)
echo 'Usage: shebang arg' >&2
exit 2
;;
esac
# redirect stdin
exec <"$0" || exit
# find the start of the AppleScript
found=0
while read v; do
case "$v" in --*)
# file offset at start of AppleScript
found=1; break
;;
esac
done
case "$found" in
0)
echo 'shebang: AppleScript not found' >&2
exit 128
;;
esac
# run the AppleScript
exec /usr/bin/osascript -; exit
-- AppleScript starts here
set arg to system attribute "arg"
First the shell script does some argument checking to see if there was an argument passed to the script (the case statement). If there was, it sets the value of the argument to an environment variable named "arg" (arg=$1; export arg). Then it reads itself until it finds a line starting with an AppleScript comment (the while loop). It will find the line "-- AppleScript starts here". It is very important that such a comment be the first line in your AppleScript embedded at the end of the shell script. The AppleScript then gets the argument from the environment variable arg (set arg to system attribute "arg"). Thereafter, your AppleScript can use the variable arg throughout.
% make -f /dev/null shebang
cat shebang.sh >shebang
chmod a+x shebang
Basically, that is all you need for an AppleScript embedded in a Bourne Shell script; give your source a filename of foo.sh, and make foo will create it. You do not have to add anything more to your Makefile.
% /usr/bin/osacompile -o shebang.scpt
on run argv
argv
end run
^D
% chmod +x shebang.scpt
% echo \#\!'/usr/bin/osascript' > shebang.scpt
% ./shebang.scpt a b c
a, b, c
First you compile the AppleScript into the resource fork of shebang.scpt (/usr/bin/osacompile -o shebang.scpt). Then you type the AppleScript itself, finishing by pressing Control-D on your keyboard. Then you make the compiled AppleScript executable (chmod +x shebang.scpt). Finally, you replace the (currently empty) data fork of the compiled AppleScript with the shebang #!/usr/bin/osascript.
#!/bin/sh
# check args
case "$#" in
1)
arg=$1; export arg
;;
*)
echo 'Usage: shebang arg' >&2
exit 2
;;
esac
# run the AppleScript
exec /usr/bin/osascript -- "$0"
That was the Bourne shell script part that was previously described. Then create this file and name it shebang.applescript:
set arg to system attribute "arg"
Now if you do the following from Terminal, it will put the two halves together and run:
% osacompile -o shebang.scpt shebang.applescript
% chmod +x shebang.scpt
% cat shebang.shosa > shebang.scpt
% ./shebang.scpt 'Hello, world!'
Hello, world!
So the only additional trick is to replace the /bin/sh with osascript (exec /usr/bin/osascript -- "$0") using the compiled script itself.
.SUFFIXES : .applescript .scpt
.applescript.scpt : ; osacompile -o '$(subst ','\'',$*)'.scpt -- '$(subst ','\'',$<)'
% : %.scpt %.shosa
ditto -rsrc '$(subst ','\'',$*)'.scpt '$(subst ','\'',$@)'
chmod a+x '$(subst ','\'',$@)'
cat -- '$(subst ','\'',$*)'.shosa > '$(subst ','\'',$@)'
The lines under % : %.scpt %.shosa must all begin with tabs instead of spaces. That is how make knows what are make commands and what are shell commands. Also, since we know we are using AppleScript on a Mac, we might as well use some GNU-specific make extensions such as subst. This is particularly useful for AppleScript, because I love to give my AppleScripts names containing all sorts of strange characters, such as spaces and apostrophes. The idea is for my AppleScripts to have meaningful names in the AppleScript menu, and using subst is what gets everything quoted correctly for the shell.
-- toggle the sound on/off from the command line
set myVolume to get volume settings
if output muted of myVolume is false then
set volume with output muted
else
set volume without output muted
end if
This AppleScript does not take any arguments, so you might as well use this as the contents of your mute.shosa:
#!/usr/bin/osascript
Then type this make command:
% make mute
osacompile -o 'mute'.scpt -- 'mute.applescript'
ditto -rsrc 'mute'.scpt 'mute'
chmod a+x 'mute'
cat -- 'mute'.shosa > 'mute'
rm mute.scpt
% ./mute
Now I can use the mute command just created to turn off email notifications from the Mac across the room via ssh, without moving my butt from my chair.
.PRECIOUS : %.scpt
You may often you like to create .scpt AppleScripts to drop into your ~/Library/Scripts folder. This same bunch of rules in a Makefile will allow you to do that as well. Just type make foo.scpt, and the foo.scpt compiled AppleScript will be generated from foo.applescript.
#!/bin/sh
case $# in
0)
echo "Usage: ${0##*/} file [ file... ]" >&2
exit 1
;;
esac
i=0
for arg in "$@"; do
let 'i = i + 1'
case "$arg" in
/*)
;;
*)
arg=$PWD/$arg
;;
esac
eval export ARG$i=\$arg
done
ARGN=$# exec /usr/bin/osascript -- "$0"
Again, this is AppleScript, so we might as well use features common to zsh and bash in our script, since those are what /bin/sh is on a Mac. This shell script also shows a couple of useful tricks. In bash and zsh, ${0##*/} is the basename of the script. You can do integer arithmetic like this: let 'i = i + 1'. Relative paths often do not work in AppleScript, so you need to change all the relative paths to absolute paths. All absolute paths start with a slash (/); otherwise you can put the current working directory in front of the relative path to make it absolute. That is all that is done in the case statement.
tell application "Preview" to activate
set argn to system attribute "ARGN"
set arg to system attribute "ARG" & argn
set argv to {}
repeat argn times
set arg to system attribute "ARG" & argn
set arg to POSIX file arg
set the beginning of my argv to arg
set argn to argn - 1
end repeat
tell application "Preview" to open my argv
Let's see if it works: create this Makefile (remembering to replace what looks like eight spaces with tabs):
.SUFFIXES : .applescript .scpt
.PHONEY : install
preview :
PREFIX = /usr/local
BINDIR = ${PREFIX}/bin
install : preview
mkdir -p ${BINDIR}
ditto -rsrc $< ${BINDIR}
.applescript.scpt : ; osacompile -o '$(subst ','\'',$*)'.scpt -- '$(subst ','\'',$<)'
% : %.scpt %.shosa
ditto -rsrc '$(subst ','\'',$*)'.scpt '$(subst ','\'',$@)'
chmod a+x '$(subst ','\'',$@)'
cat -- '$(subst ','\'',$*)'.shosa > '$(subst ','\'',$@)'
In Terminal type this:
% make preview
osacompile -o 'preview'.scpt -- 'preview.applescript'
ditto -rsrc 'preview'.scpt 'preview'
chmod a+x 'preview'
cat -- 'preview'.shosa > 'preview'
rm preview.scpt
% sudo make install
Password:
mkdir -p /usr/local/bin
ditto -rsrc preview /usr/local/bin
% rehash; hash -r
% /usr/local/bin/preview /Library/Desktop\ Pictures/*.[Jj][Pp][Gg] \
/Library/Desktop\ Pictures/*/*.[Jj][Pp][Gg] \
/System/Library/Screen\ Savers/*.slideSaver/Contents/Resources/*.[Jj][Pp][Gg]
This AppleScript creates a list, argv, that gets arguments from the environment variables, but, as I alluded to in the very beginning of this hint, such a script will not work with pathnames that contain international characters. The problem is that system attribute returns a "string" and not a "Unicode text" in AppleScript. Therefore, pathnames with international characters get munged according to your text encoding, in my case macroman. What is even worse is that the argument passing ability of AppleScript added in 10.4 also treats the arguments as "string": real scripting languages like perl, python, and even the Bourne shell treat arguments as a raw string of bytes and you need to ask for the arguments to get munged on your behalf.
#!/bin/sh
case $# in
0)
echo "Usage: ${0##*/} file [ file... ]" >&2
exit 1
;;
esac
{
case "$1" in
/*)
arg=$1
;;
*)
arg=$PWD/$1
;;
esac
echo -nE "$arg"
shift
for arg in "$@"; do
case "$arg" in
/*)
;;
*)
arg=$PWD/$arg
;;
esac
echo -ne '\x00'; echo -nE "$arg"
done
} | /usr/bin/osascript -- "$0"
So what is this shell script doing that is new? It writes all of the arguments to standard output with a NULL byte between each argument. That is what the echo lines do. Now since the NULL byte is used to signify the end of a C-style string, you should not find any pathnames with a NULL byte in them. Also, because of how UTF-8 is defined, you will never have a NULL byte appear in a multibyte character. So it is perfectly safe to do this.
tell application "Preview" to activate
set argv to do shell script "/bin/cat"
set AppleScript's text item delimiters to ASCII character 0
set argv to argv's text items
set AppleScript's text item delimiters to {""}
repeat with i from 1 to number of items in my argv
set this_item to item i of my argv
set item i of my argv to POSIX file this_item
end repeat
tell application "Preview" to open my argv
The AppleScript does not touch standard input in any way. In fact, it calls the Unix command cat with set argv to do shell script "/bin/cat". Remember that the standard input of the AppleScript is the output of the pipe in the previous shell script. Therefore, the input of /bin/cat is that same output. cat just spits its input back out into the result of do shell script in the AppleScript. This return value is a raw string of bytes as a "unicode string." Then we set the text delimiter with set AppleScript's text item delimiters to ASCII character 0 to the NULL byte, and use that to break up the long string into a list of the arguments separated by NULL bytes with set argv to argv's text items.
% make preview
osacompile -o 'preview'.scpt -- 'preview.applescript'
ditto -rsrc 'preview'.scpt 'preview'
chmod a+x 'preview'
cat -- 'preview'.shosa > 'preview'
rm preview.scpt
% sudo make install
Password:
mkdir -p /usr/local/bin
ditto -rsrc preview /usr/local/bin
% rehash; hash -r
% /usr/local/bin/preview ~/Pictures/*.[Jj][Pp][Gg]
So we have basically reached the end of this hint. You now know how to pass arguments with international characters into an AppleScript. Along the way you might have picked up a few tidbits too. I hope you had fun.
#!/bin/sh
case $# in
0)
echo "Usage: ${0##*/} file [ file... ]" >&2
exit 1
;;
esac
argio() {
case "$1" in
/*)
arg=$1
;;
*)
arg=$PWD/$1
;;
esac
echo -nE "$arg"
shift
for arg in "$@"; do
case "$arg" in
/*)
;;
*)
arg=$PWD/$arg
;;
esac
echo -ne '\x00'; echo -nE "$arg"
done
}
pv=`/usr/bin/sw_vers -productVersion`
case "$pv" in
10.[0123].*)
;;
*)
argio "$@" | /usr/bin/osascript -- "$0"
exit
;;
esac
unset fg; unset pid; unset app
app=`fg=1 /usr/bin/osascript -- "$0"`
trap : USR1
argio "$@" | pid=$$ /usr/bin/osascript -- "$0" &
wait
export app;
/usr/bin/osascript -- "$0"
set sysv to system attribute "sysv"
if sysv is less than 4160 then
set env to system attribute
if my env contains "fg" then
tell application "System Events" to name of first application process whose frontmost is true
return result
end if
if my env contains "app" then
set arg to system attribute "app"
tell application arg to activate
tell application "Preview" to activate
return
end if
end if
tell application "Preview" to activate
set argv to do shell script "/bin/cat"
set AppleScript's text item delimiters to ASCII character 0
set argv to argv's text items
set AppleScript's text item delimiters to {""}
repeat with i from 1 to number of items in my argv
set this_item to item i of my argv
set item i of my argv to POSIX file this_item
end repeat
if sysv is less than 4160 then
try
try
set pid to system attribute "pid"
do shell script "/bin/kill -USR1 " & pid
tell application "Preview" to open my argv
on error e number -609
-- Connection is invalid.
end try
on error e number -1712
-- AppleEvent timed out.
end try
else
tell application "Preview" to open my argv
end if
set fifodir to system attribute "fifodir"
set fifo to fifodir & "/fifo"
set the_input to open for access POSIX file fifo
-- We can't do this because AS does an open for every read!
-- do shell script "rm -rf " & quoted form of fifodir
set linefeed to (ASCII character 0)
set argv to {}
repeat
try
-- «class utf8»
set linebuf to read the_input before linefeed as text
set end of my argv to POSIX file linebuf
on error e number -39 -- End of file error.
close access the_input
exit repeat
end try
end repeat
my argv
But unfortunately, this does not work. It seems that when I use as text or as «class utf8» in the set linebuf to read the_input before linefeed line, the line is munged (at least on my intel iMac). I think that what is going on is that AppleScript only really understands UTF-16 and is converting incorrectly, but that is only a hunch. If I use as unicode text that is even worse; I just get garbage that looks Chinese. But anyway, AppleScript seems to be pretty slow at reading, so there probably would not have been much improvement to the version the uses /bin/cat . But if anyone knows how to make the fifo/file approach work, please let me know.
Mac OS X Hints
http://hints.macworld.com/article.php?story=20060425140531375