24

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.)

5
  • 25
    You can open -a Skim foo.pdf.
    – user588
    Commented Nov 23, 2010 at 0:54
  • 1
    @mankoff: nice, I forgot you could use -a to tell open that you want an app from the Applications dir. you should change this to an answer :) Commented Nov 25, 2010 at 6:26
  • @Reid: could you edit your question to include an example command line that you are looking to be able to type? Commented Nov 25, 2010 at 6:28
  • @mankoff which means he needs aliases for sure or extension of tab completion Commented Nov 25, 2010 at 7:01
  • @Reid any issues with the solution?
    – user588
    Commented Jan 22, 2011 at 16:02

4 Answers 4

10

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()
4
  • The "grep -v iWork" is in there just because the apostrophe in "iWork '09" is messing things up. Note that if you store apps elsewhere (~/local/Applications) just add that to the ls command, and this will work there too.
    – user588
    Commented Nov 25, 2010 at 6:58
  • nice one. i like that it's dynamic so you don't end up with any maintenance issues like cleaning up old aliases. Commented Nov 25, 2010 at 7:04
  • Thanks @mankoff! Smells great - once I've tested it and it works I'll mark the answer accepted. Much appreciated.
    – Reid
    Commented Dec 13, 2010 at 0:25
  • OK, this works. It does break on apps with an apostrophe (and presumably other funny chars in the name). I riffed on your solution and did something similar in Python, which I'll post in another answer - though if that's not the right etiquette, I'm happy to add it to your answer instead (or whatever).
    – Reid
    Commented Jan 28, 2011 at 17:57
8

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 ;)

8
  • I think @Reid wants a shorter way w/o manually creating all the aliases.
    – user588
    Commented Nov 23, 2010 at 5:47
  • But he doesn't have to create an alias to the unix executable if he uses 'open' on the .app package. I got the impression that he was creating aliases because he thought the only way to start an app directly from the command line was by going all the way into the MacOS directory and executing the file that is the 'real' executable inside an app. So in that case, an alias would at least save typing the extra three levels of directory structure that you would need in order to start the app. Maybe I'm misunderstanding his question. Commented Nov 23, 2010 at 5:54
  • @calevara, thanks - @mankoff is right; it's about length.
    – Reid
    Commented Nov 23, 2010 at 16:48
  • 3
    This is a bit of a misuse of the edit functionality. If you later would like to propose a second answer, do just that. Do not edit your original answer, piggybacking the votes that it received onto your new idea. Commented Nov 24, 2010 at 14:00
  • 1
    I don't agree... the answer is still pretty similar because he'd be using the same syntax. Not only that, but if up-voters don't like the new answer they can change their vote because the answer has been edited. That's exactly why you can change your vote after edits. It's also why I put "on edit" in bold. Commented Nov 24, 2010 at 18:37
4

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.

0
2

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.

2
  • Doesn't support tab-completion. Requires remembering app names exactly. Don't see why AppleScript is required (especially if you hate it) if shell scripting can do it all.
    – user588
    Commented Nov 25, 2010 at 6:57
  • 1
    I never realized that open -a could be used without a file name as argument. And I've been using open since NextStep sometimes in the early 90's! Please submit that as an answer so I can vote on it. So yes, the correct answer is to use 'open -a <application name>'
    – eric
    Commented Nov 25, 2010 at 12:16

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .