AppleScript is a great tool. It is awesome to be able to get the selected text from an application, grab the current URL from Safari, ask the user to choose a file, or show a dialog box requesting text. But writing AppleScript scripts is usually painful.
For anything mildly complicated, I would much rather write something in Python. So a lot of my AppleScripts look like this:
do shell script some_python_or_bash_script
For yet another time, I recently found myself making an AppleScript where part 3 of the process involved composing an email to someone. It is difficult to take the result of the shell script (which is just a single, structureless string) and parse out multiple fields (body, subject, recipient) to pass to a complicated
make new message command.
So instead, I made a Python wrapper around the
make new message AppleScript command. Yes, that means I am using AppleScript to call a shell script which runs an AppleScript, but I’m okay with that. (Others have done the same thing, but not with the full set of options that I wanted.)
There are already command line mail programs. Why not just use one of them? Two reasons.
First, getting mail to transfer properly is always a pain. Comcast won’t let you use their SMTP, and if they did, your message would probably be marked as spam. So you have to figure out how to hook authenticated SMTP up to Google, and then it breaks, and you just get sick of it. Currently, my best solution to this has been to pipe a message over SSH to my work computer, which has a fully functional transfer agent, just to send an email to myself!
Second, and more important, often you want to see the message and maybe edit it a little before you send it. This also minimizes the chance that a script will screw up and either not send the mail or send duplicates.
AppleScript to create a mail message looks about like this:
The first half of the Python script does nothing more than create an AppleScript and feed it to the
Dr. Drang recently complained about how inconvenient it is to send data to a subprocess in Python. I feel his pain, because I have spent plenty of time and trial and error to figure out how
communicate work. The official documentation is no help, either.
In the end, though, there is nothing terribly ugly about the three lines that run the AppleScript. If you want to send anything to the subprocess’s
stdin, you need the argument
=subprocess.PIPE, depending on your import statement). Running
communicate returns a tuple with the subprocess’s
stderr, but only if you use the arguments
stderr=PIPE. So my script, communicate only returns the
stdout (which I discard).
When you don’t specify
stderr=PIPE, the error output is just passed along to the main process’s
stderr (and so also with
stdout). If you run my script from the command line, any errors from the
osascript command will just be printed on your screen (unless, of course, you do something like
My newest rule to myself is “Never parse your own command line arguments.” Especially when I make something that I only ever plan to call from other scripts, and nobody but me is ever going to see, it is very tempting to do something stupid like require 8 positional arguments in a specific order.
Then you change some script somewhere and everything breaks. Or you want to use the script again and there is no
--help. So you have to jump into source that you wrote a year ago just to figure out what to do. Not good.
The argparse library is new and replaces the short-lived and now depreciated optparse. But it has lots of useful bells and whistles. For example, with the
type=argparse.FileType() option, you can add an argument that expects a filename and automatically opens the file for you. It also creates a
--help option automatically.
Here is the second half of the script.
When you run
parse_args, it returns a special
Namespace object, which has the parsed arguments as attributes. (Why didn’t they use a dictionary?) In my script, “recipient”, which is a positional argument because it lacks a leading hyphen, is stored in
args.recipient. The subject is stored in
args.s. If I wanted to, I could pass
add_argument, and then the subject would be stored in
args.subject, but could be specified on the command line as either
-s subject or
--subject subject. With the
args.send will be true if the user gives the
--send option, and false otherwise.
I call the script mailapp. Just run
$ ls | mailapp -s "Here's how my home directory looks"