Submit Hint Search The Forums LinksStatsPollsHeadlinesRSS
14,000 hints and counting!

osascript, shebang, and unicode arguments UNIX
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.

I realize that I could use a Here Document to do something similar, but if I am passing arguments, then I would need to escape them properly before they get to the AppleScript. Regardless of whether I pass arguments or not, using a Here Document makes the shell create a temporary file. This approach is therefore slightly more elegant (in my mind, you might consider it more perverse).

Here is some interaction in Terminal on 10.4:
% 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.

The sky is the limit for whatever you would like to add before your AppleScript code, because the Bourne shell can parse its own input. The approach that I use is putting an AppleScript comment in the file, and the Bourne shell parses its own input until it reaches that comment. After that point lies your AppleScript itself. As a concrete example, suppose you wanted to verify that your script was called with exactly one argument, printing some usage information to standard error as a reminder if not.
#!/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.

Makefiles

I promised some rules for a Makefile. Well, for AppleScripts embedded in Bourne shell scripts in this manner, you do not need to add anything to your Makefile, since there are predefined suffix rules to handle this case. Suppose the above script was named shebang.sh; here is some Terminal interaction:
% 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.

So that is all fine and dandy, but with this approach, the AppleScript source is in your shell script. That is good for readability, but you still have to compile the AppleScript every time you run the shell script. Is there a way to compile an AppleScript and have it run from the command line with a shebang? Sure, just make use of the resource fork in addition to the data fork. Here is some terminal interaction on 10.4 to illustrate what I mean:
% /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.

So the only line in the the data fork is the shebang. The contents of the resource fork are the compiled AppleScript.

Let's say you want to do some argument checking in the shell script, or want argument passing to work in 10.3 as well; here is a recipe. Create this file and name it shebang.shosa.
#!/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.

You may be wondering why I used the strange .shosa extension. That is for the benefit of make. Just add this to your Makefile:
.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.

So, let's say you wanted to compile foo. You would create foo.shosa and foo.applescript. foo.shosa would contain your shebang or Bourne shell script, and foo.applescript would contain your AppleScript source. Here is an example. Say you have this AppleScript, mute.applescript:
-- 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.

If you do not want your .scpt files to be automatically deleted by make, just add this line to your Makefile:
.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.

Finally, I have lots of .applescript source files in a directory. It is a good idea to make one file named shebang.txt with the shebang line #!/usr/bin/osascript, and then link all of the .shosa files for scripts that do not take arguments to that one. Then with ls -l *.shosa or file *.shosa, you can quickly see which of your scripts do not take arguments, or you might surprise yourself with which scripts you thought take arguments but will not work correctly on 10.3 or earlier.

Now we are getting ready for the main hint. Suppose we wanted to create a script that would open all its arguments in Preview. Knowing what we know now, we might expect this script to work as follows:

Contents of preview.shosa:
#!/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.

Finally, this shell script shows how to pass an arbitrary number if arguments to an AppleScript via environment variables (eval export ARG$i=$arg in the for loop). So the first argument is passed in the environment variable "ARG1", the second in "ARG2", and so on. Also the environment variable "ARGN" is set to the number of arguments passed.

Contents of preview.applescript:
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.

So what can you do? Fortunately, do shell script in AppleScript returns the output of a Unix command as a raw stream of bytes as "Unicode text." The trick is to get the arguments out of /bin/cat and into your AppleScript:

Contents of preview.shosa:
#!/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.

This output is passed into osascript with a pipe. You might think that the AppleScript would read that, but it doesn't.

Contents of preview.applescript:
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.

I have some JPEGs with Polish characters in the filenames in my ~/Pictures folder. Amazingly (to me at least), this script now works:
% 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.

There is a bit more though. For one thing, Preview in 10.3 is broken. If there are not at least two or more files without international characters in the pathname, along with ones with international characters, then the window does not come forward and the script hangs. Preview does not show the window even when the files are launched from the Finder. The workaround is to switch apps a few times and all is well. Preview in 10.4 does not have this problem. Here are some modified versions of the previous scripts that address this issue.

Contents of preview.shosa:

#!/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"

This version checks to see if you are using Mac OS X prior to 10.4, and if so, it implements the workarounds. You can read the script for the details if you are interested; it is not perfect but it usually works. At least the command no longer hangs on 10.3.

Contents of preview.applescript:

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

This may seem specific, but it illustrates the general technique of calling different subroutines in an AppleScript via the use of environment variables to chose which subroutine to call.

A final tidbit has to do with a dead end I ran into. Initially, I intended to create a fifo and write the arguments into it with NULL bytes separating the arguments like this:
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.

Finally, there were a ton of places I found useful info that led me along. The most important of which were these references: I wish I could open /dev/fd/n and /dev/random in AppleScript like that last reference claims. That would be useful. Does any one know how to do that?
    •    
  • Currently 2.00 / 5
  • 1
  • 2
  • 3
  • 4
  • 5
  (3 votes cast)
 
[23,362 views]  

osascript, shebang, and unicode arguments | 7 comments | Create New Account
Click here to return to the 'osascript, shebang, and unicode arguments' hint
The following comments are owned by whoever posted them. This site is not responsible for what they say.
osascript, shebang, and unicode arguments
Authored by: wgscott on May 02, '06 08:52:54AM
Your life might get a bit easier with a "here document" construct. The following example runs applescript code for a directory browser from within a shell script, and returns the full posix path of the chosen directory:
    link


[ # ]
osascript, shebang, and unicode arguments
Authored by: jctull on May 02, '06 11:11:36AM

He mentioned this, and his reason it was not adequate, about two paragraphs after the summary post.



[ # ]
osascript, shebang, and unicode arguments
Authored by: wgscott on May 02, '06 01:45:31PM
You are right -- I missed it. Damn the NyQyl (TM) hangover. But either I don't understand the objection, or else it is mistaken. Here is another, slightly more complex example:
    osa_display_dialog
Invoke with the command:
osa_display_dialog  foo  bar  foobar
This gives three buttons, the third as the default, and returns the user's choice. I didn't escape anything...

[ # ]
osascript, shebang, and unicode arguments
Authored by: mzs on May 02, '06 06:44:10PM
Your first script works, your second does not. Here is what I am talking about. (I just figured-out how to use Polish characters on this site.) Suppose I have this directory:

~/Ł

And I choose it with your first script:


% /bin/zsh -f osa_direc_browser | xxd
0000000: 2f55 7365 7273 2f6d 7a73 2fc5 812f 0a    /Users/mzs/../.
See the c5 81 in the hexdump? That is the UTF-8 for "LATIN CAPITAL LETTER L WITH STROKE". Replacing "choose folder" with "choose file" in your script does the right thing for files too. But what is the point of a shell script that makes you choose the files one by one in a dialog box, I want them from the command line and besides the arguments need not be files, but simply strings.

Now I have a file DŁN.JPG and your second script has this output:


% /bin/zsh -f osa_display_dialog bar.jpg *.JPG | xxd
0000000: 44e2 8988 c385 4e2e 4a50 470a            D.....N.JPG.
See the hex e2 89 88 c3 85, that is all messed-up, since my encoding is macroman. Also the text is munged in the dialog box itself. The hint offers a way for passing arguments to an AppleScript from the command line without arguments with international characters such as Ł getting munged.

[ # ]
osascript, shebang, and unicode arguments
Authored by: wgscott on May 02, '06 07:44:08PM

The point is to use them as building blocks for more complicated GUI scripting. By themselves, they aren't worth much.

Anyway, I now understand the problem. Thanks!



[ # ]
simple explanation
Authored by: mzs on May 02, '06 07:34:33PM
I just figured-out how to post to macosxhints.com using Polish characters. Therefore I think I can more simply explain what this hint is all about, since there seems to be some confusion. This web page shows a bunch of Central European international characters. Suppose you have this AppleScript named shebang.applescript:

set argv to do shell script "/bin/cat"

set AppleScript's text item delimiters to ASCII character 0
set argv to my argv's text items
And this shell script named shebang:

#!/bin/sh

{
	nullsep=''
        for arg in "$@"; do
                echo -ne "$nullsep"; echo -nE "$arg"
		nullsep='\x00'
        done
} | exec /usr/bin/osascript -- "$0".applescript
Then this works:

$ /bin/sh shebang 1 2 3 4
1, 2, 3, 4
$ /bin/sh shebang ? ?
?, ?
$ /bin/sh shebang ? ? | xxd
0000000: c581 2c20 c582 0a                        .., ...
c5 81 is the hex for "LATIN CAPITAL LETTER L WITH STROKE"
c5 82 is the hex for "LATIN SMALL LETTER L WITH STROKE"

This works correctly, all of the other methods of passing arguments to AppleScript from the command line that I know of munge the arguments according to your character encoding, in my case macroman.

The rest of the hint is about the following:

  • How to have the same file contain both a shell script and an AppleScript.
  • How to have the same file contain both a shell script and a compiled AppleScript (so that it does not need to be compiled each time it is run).
  • Useful Makefile rules to utilize the recipes for the above two items plus magic shebang lines to use so that you can run any AppleScript from the command line.
  • A few helpful examples along the way
  • Some notes about dead ends that did not work.


[ # ]
argh, macosxhints stripped-away my Polish characters
Authored by: mzs on May 02, '06 07:36:43PM
Here is the terminal interaction with the international characters again:

$ /bin/sh shebang 1 2 3 4
1, 2, 3, 4
$ /bin/sh shebang Ł ł
Ł, ł
$ /bin/sh shebang Ł ł | xxd
0000000: c581 2c20 c582 0a                        .., ...


[ # ]