Question

I do a fair amount of work in the command line, and I find myself defining a lot of aliases of the form:

alias skim='/Applications/Skim.app/Contents/MacOS/Skim'

Is there a way to add magic such that asking for executable "foo" automatically uses executable /Applications/Foo.app/Contents/MacOS/Foo? This is on OS X 10.6.

(I'm aware that I could do open foo.pdf, but Skim is not the default PDF reader, and I'd like a general solution - in some cases, it's not appropriate to set the application in question as the default handler for the file.)

Was it helpful?

Solution

Finally I got it: Add this to your .bash_profile

function foomagick() {
    rm -f ~/.foomagick.tmp
    ls /Applications/ | grep "\.app" | grep -v iWork | while read APP; do
        # clean it up                                                           
        a=`echo $APP | sed s/\ //g`;
        a=`echo $a | sed s/\'//g`;
        echo alias ${a%.*}="'open -a \"${APP%.*}\"'" >> ~/.foomagick.tmp
    done
    source ~/.foomagick.tmp
    rm ~/.foomagick.tmp  
}
foomagick()

Now the following work:

Skim # open Skim.app
Skim foo.pdf
FireFox http://google.com
FireFox google.com # ERROR. Looks for local file.

Edit by Reid:

I implemented the above as a Python script that makes wrapper scripts instead of aliases. You will need to put ~/bin/mankoffmagic in your path. If you want the wrappers to be updated automatically, run it regularly from cron or somesuch.

#!/usr/bin/python
#
# This script automagically updates a set of wrapper shell scripts in
# ~/bin/mankoffmagic which call Mac apps installed in /Applications.
#
# Inspired by mankoff's shell alias posted on apple.stackexchange.com; see:
# http://apple.stackexchange.com/questions/4240/concisely-starting-mac-os-apps-from-the-command-line/4257#4257
#
# Notes/Bugs:
#
# 1. Does not follow symlinks (aliases?)
#
# 2. Assumes that application names do not contain double-quotes.
#
# 3. Not very smart about finding the actual binary (it guesses). This is
# wrong sometimes, e.g. Firefox. Probably a deeper understanding of the app
# package structure would fix this.

import copy
import glob
import os
import os.path
import re

BINDIR = os.path.expandvars("$HOME/bin/mankoffmagic")
APP_RE = re.compile(r'(.*)\.app$')
STRIP_RE = re.compile(r'[\W_]+')

def main():
   # We aggressively delete everything already in BINDIR, to save the trouble
   # of analyzing what should stay
   for f in glob.glob("%s/*" % BINDIR):
      os.unlink(f)

   # Walk /Applications and create a wrapper shell script for each .app dir
   for (root, dirs, files) in os.walk("/Applications"):
      dirs_real = copy.copy(dirs)  # so we can manipulate dirs while looping
      for d in dirs_real:
         #print "checking %s" % os.path.join(root, d)
         m = APP_RE.search(d)
         if (m is not None):
            #print "Found " + m.group()
            dirs.remove(d)  # no need to recurse into app
            create_script(root, d, m.group(1))

def create_script(path, appdir, appname):
   # remove non-alphanumerics and downcase it
   wrapper = STRIP_RE.sub('', appname).lower()
   wrapper = os.path.join(BINDIR, wrapper)
   fp = open(wrapper, "w")
   # Twiddle the comments in the script depending on whether you want to
   # invoke the binary or use "open" -- the former lets you use any
   # command-line args, while the latter is more Mac-like (app puts itself in
   # the front, etc.)
   fp.write("""
#!/bin/sh
exec "%s/%s/Contents/MacOS/%s" "$@"
#open -a "%s" "$@"
""" % (path, appdir, appname, appname))
   fp.close()
   os.chmod(wrapper, 0700)


if (__name__ == "__main__"):
   main()

OTHER TIPS

You don't need anything fancy, you already have the answer. Try:

open /Applications/Foo.app bar.pdf

on edit

In light of the comments below, I think my answer would still be relatively similar... I would make a function that overrides open, does a pushd to /Applications, calls /usr/bin/open $appName.app $args, does a popd and returns.

I suck at shell scripting but something like below which covers special cases and keeps you using pretty much the same syntax as apple provided for open. I like to keep my environment as clean as possible.

I'm sure the syntax is from outer space:

function open($appName, $args)
{
    #if we are calling open without a path like /Applications/TextEdit.app AND
    #    $appName ends in '.app'
    if (!hasParent($appName) && isApp($appName))
    {
        pushd /Applications
        /usr/bin/open ${appName}.app $args &
        popd
        return
    }
    #otherwise, use open as normal
    /usr/bin/open $appName $args
    return
}

on second edit

looking @mankoff's comment on the original question, most of the stuff in the above function would probably be a waste of time since you could just use open -a $appName. So mankoff probably has the easiest solution and should change his comment to an answer ;)

There are two solutions that I can think of:

The easier way - Using the Info.plist file in each .app Contents directory, create an index of the value for the keys CFBundleExecutable. Then add a short alias that calls a script (perl, python, whatever) with the name of an executable and the arguments you'd like to pass.

Your index would be pairs of executable names and paths to those executables. You'd have to write a recurring scheduled script to keep this updated.

You'd end up being able to call:

f foo file.txt

where f is an alias to a script that checks your index for an executable named foo.

All in all, not a lot of work to get the functionality you'd like.

The harder way - Extend your shell to supplement the handling of its command_not_found error. Essentially you would implement a Ruby style method_missing functionality within whatever shell you're using. When command_not_found was thrown, your method would check the executable names of your installed Applications.

Applescript to the rescue:

osascript -e 'tell application "iTunes"' -e "activate" -e 'end tell'

Replace the name of the application with the application you want to start and you're done. You can of course make it a shell function if needed:

function f() {
    osascript <<EOF
tell application "$1"
  activate
end tell
EOF
}

and use it that way:

f iTunes

I hate applescript, but it is useful sometimes, and I believe the only way to address an application simply by name on the command line. Everything else will require a full path.

Licensed under: CC-BY-SA with attribution
Not affiliated with apple.stackexchange
scroll top