![]() |
Delivering Davros, or "(ex)term innate"
Greetings folks,
I have a horrible feeling that this is going to turn into a long tale, so I’ll try from the beginning to keep it under control. [Having now read back through what follows, it's obvious that I failed miserably! Still, things are very quiet around here, so a coupla posts shouldn't be causing too much grief. The destination's not spectacular, but I hope some of you might enjoy a bit of the view along the way.] A few days ago I was humming along with the lovely "open" command, and in particular spouting my usual nonsense in telling folk how cool "open ." is , and even more so "open –a". You know the routine; how it handles case sensitivity for you, doesn’t need the ".app" nonsense for such apps, and how it hunts through the /Applications directory hierarchy. In essence, how grand it is to be able to enter % open –a console and have the application inside /Applications/Utilities/Console.app/ just, well, "open". And then I thought, "who needs that stinking 'open –a' " at the start? Why can't I just type % console and have it happen by magic. Or at least have tab completion on the application names, so that, say "open -a exc" followed by a tab to complete the word to "excel" would open up excel. Doubtless that's possible by writing a completion routine for tcsh or zsh. But given that I wanted to shorten the whole procedure I thought: how about a piss-weak command line version of what LaunchBar offers. That is, an abbreviating facility so that --say-- "ie" would specify internet explorer. And with this in mind I envisioned a new command called "oa" which would allow me to do this sort of thing: % oa ie # open internet explorer % oa exc<tab> # open excel Well that sounded like a cool project, so I scripted it up and worked out a not entirely unreasonable way to make default abbreviations for applications. It used a tab-delimited configuration file in ~/ that contained (1) a unique identifying command for each app, and (2) the full path to the .app that was being pointed at. The "oa" prototype worked pretty well, and I was starting to think it was approaching a public showing, and then it hit a couple of speed bumps. I realised there were a couple of problems. (1) It didn't actually find all the GUI-based apps (that is, those residing in the /Applications hierarchy for my purposes), and (2) the abbreviating mechanism was far trickier than I'd imagined and I was too slack to figure out how to write a completion mechanism for the proposed command. Add to that a final, killer, realisation: having to type "oa" really sucked. Laziness demanded that I should only have to enter "excel", for example –with tab completion available-- as per my original idea. While musing about further developments I wrote a cheery little "choose me" mechanism to cope with (2), and started musing about finding a solution to (1). ((( Sidebar: The difficulty with not finding all apps originates in the fact that not all apps have the .app extension on their package; some carbon apps (eg Microsoft's office apps and BBEdit) are simply files with the traditional APPL type signature instead. Now while you *can* test for the latter using GetFileInfo from the developer tools % /Developer/Tools/GetFileInfo -t /Applications/BBEdit\ 6.5/BBEdit\ 6.5 => "APPL" (or pgetfinfo that comes with MacOSX::File), it's not something that can be called portable, and scanning the whole directory hierarchy in this way is horribly slow. Harumph. Not pretty. And how do we avoid namespace clashes with existing executables in my PATH? Or existing aliases for that matter? I might quickly add in here a quick reply to "why not just use launchbar"? Truth be told I was a staunch launchbar advocate for a few weeks after purchasing it last year. Then I just started to use it very little, and eventually it didn't even warrant being in my login items: can't really say why, but maybe the start-up scan annoyance was a contributing factor. Maybe it's simply doing more than I need? I still think it's an excellent product, but it just "didn't fit". Another one of those things from which I use 15% of the functionality from and end up annoyed by the overhead attached to the other 85% of the app.))) Anyway, back to the story: the next stage in the evolution of a "solution" quickly dawned on me. Why not just make a new executable for each of the GUI apps, in a pale attempt to mimic what BBEdit had done with their bbedit command? That is, for each such app simply throw a simple shell script into a place in the user's PATH. Notably, into that users' ~/bin directory. The shell script for Disk Copy, for example, would be called something like "disk_copy" and comprise the two lines: #!/bin/sh open –a '/Applications/Utilities/Disk Copy.app/' Or, slightly better, slot in the arguments to the script: #!/bin/sh open –a '/Applications/Utilities/Disk Copy.app/' "$@" That way you can open files in the application from the command line interface. This pretty much works, and has a couple of virtues; ~/bin is usually a long way down the PATH's pecking order, so if there is a CLI command with the same name as that given to the command-line version of the GUI then the former will take precedence. But that's a bit of a cop out, and I still needed to take conflicts into account somehow. Another positive feature was that existing aliases wouldn't be trampled over. But again, there's not really any canonical ranking re what should be preserved, and the way that this precedence problem is worked out should really be user configurable within any satisfactory solution. The chief *vice* of this approach is that it pollutes the user's ~/bin directory with circa 100 files, all of which are essentially doing the same thing. So editing them becomes a one by one proposition; what I really wanted to do was to condense the configuration information into one or two files. Hmm: stand back, go for a walk, and abstract the two-liner above. It's really nothing more than the provision of an alias. Bingo; why hadn't I thought of that earlier? What "oa" was really trying to do was provide a heap of aliases for opening GUI applications. So why not take it all the way and actually **use** aliases. Thud. Clunk! (in retrospect). Indeed, I even had a couple of aliases like this in my .cshrc: alias ie 'open –a "internet explorer"' And that, dear (patient) reader, condenses into one line the essential content of what follows. So why is the script that I'm going to post over 200 lines long? Well, to be fair, almost half these lines are either comments or "heredocs" that print out information on the fly to the user of the script. But still… The only way to justify it is to describe its action: so here's a summary of what it attempts to achieve: and for that you should see the next post... |
Delivering Davros, or "(ex)term innate"
(1) (find_all_apps()): This scans through the users /Applications directory hierarchy *using applescript* in order to find all the applications contained therein. I never thought I'd be using the applescript word in earnest, but it seems to be the right tool for the job. Unless, that is, someone points out an easy way to get at the list that the "open" command uses directly. Go on, make my day! It's got to be cached somewhere in some format. Anyway, if nothing else this embedded applescript should show reasonably clearly how you can call applescript from perl in a very transparent manner, and use the return values from that applescript within the enclosing perl script. I think it's extremely cute, and in this case it's refreshing to be able to use the Finder for something useful. Back in the olden days (that's Mac OS 7,8,9 for you newbie mac users; heck, even in 10.0.x) scripting the Finder was pretty much a recipe for a wet week, but things are much better now. Almost makes me regret losing my touch at applescript, which when it works is a wonderful wand to wave, and when it doesn't work makes you feel like throwing your machine at the company that made the application you're trying to script. Oftentimes that would be "Apple"! Drifting again I see: back on track then. There are a few charming little bits and pieces in the applescript, including the "POSIX path" call, the "path to startup disk" call, the simple recursion, and the way in which you can exclude directories from the scan. One weirdness is that osascript definitely requires the subroutine to be declared before it's used: --other environments (eg script editor) don't seem to be so fussy. This had me foxed for a while there-- hence the rather odd structure of the applescript fragment.
(2) (find_existing_executables()) The script proper then finds the users $PATH, makes a list of all the executables in that path, and appends to that all the aliases that your $SHELL produces. This is probably one of the script's weakest points, and shows my old tcsh bias (even if I'm in the process of escaping to zsh): bash/zsh users tend to have functions and not aliases at all. Add to that the fact that it doesn't add all the built-ins for each shell to the list. Sigh… Another good chance to invoke Hofstatder's Law: Hofstadter's Law ============ It always takes longer than you think, even when you take Hofstadter's Law into account. ============ (3) process_apps() just does what you might expect: travels one by one through the application list found earlier, and condenses the full path to the app into a candidate command line invocation by ditching the directory info off the front, culling the .app/ from the back (if present), ditching any "Microsoft " off the front, lower-casing, replacing shell-significant characters with an underscore and removing any 8-bit characters. You can easily alter stuff here; maybe change the underscore to something else (maybe nothing would work OK; that could be achieved by replacing the single "$app=~tr/……" line with the two lines: $app=~tr/[A-Z]/[a-z]/; #lower case $app=~tr/!$&\"'*()]}{[|;<>?~`\ //; # remove barnacles to help your friendly shell Then you'd have "/Applications/Utilities/Disk Copy.app" => "diskcopy" and so forth. Hmm, not bad… Maybe this should be the default? Harder to read the commands, but easier to type. Quoting those characters is another option, but not one I'm going to be getting into.) The script prints a warning message if two applications in the list reduce to the same abbreviation. I've sorted the application locations by length so that the app residing at the end of the shortest path is the one that will get the nod in such cases. This doesn't seem unreasonable; if you've got an old_apps directory or something similar within /Applications it'll usually end up burying its contents further down the directory hierarchy than the "live" versions, and you'll wanting to be using those closer to the top. Don't forget the availability of the exclude_dirs variable in the applescript; that's what it's there for. Any directories in that list are simply "pinched off" from the /Applications directory hierarchy. You'll also get a warning if there's a conflict with an existing CLI app or an alias. In this case the potential mapping *is* produced, but when written out to file it'll be prefaced by a comment marker. For example, it's almost certain that your PATH will include /usr/bin, and hence that "mail" will point to /usr/bin/mail. So when the script tries to point "mail" at "/Applications/Mail.app/" there's a conflict. In this case I would choose to have mail point at the GUI app, so I've manually pasted the commented alias from the auto-configured .opena_auto into my "user configuration" file .opena and have uncommented it there. If I then wish to use the "/usr/bin/mail" command in the terminal the easiest way is to preface it with a backslash (which means "ignore any aliases", and is very handy to know in any case: for example, I sometimes tell newcomers to unix to alias rm to 'rm –i', which works well to start with. But then they get impatient with confirming all deletions. \rm does the trick nicely there.) The .opena_auto and .opena duplication is supposed to separate the script-generated file from that which the user has configured. Unalias anything you don't really want from the .opena_auto file --say a mapping from "register" to one of the registration applications-- by including, say, unalias register in your .opena file. (I've killed "bbedit_6.5" since "bbedit" is far cooler.) Aliases made in the .opena file will overwrite those in .opena_auto, so if you really do want to map to an app with a longer path length just include it here. The idea is that running the script again won't overwrite all your customisations. In fact I haven't found all that much need for very much customisation: you remember the name better than you remember any abbreviations, and tab completion is your very, very good friend here. (4) Post all this it's just a matter of getting the script to write out the aliases into .opena_auto, and then including the final two lines in that file: touch ~/.opena source ~/.opena The touch just ensures there's something in that location so that the following source line doesn't fail. (Note also that the alias format is slightly different for bash/zsh than it is for tcsh. There's an equals sign that has to to be inserted between the two arguments to the "alias" command.) (5) Finally we have install_init() : for the aliases to work the user will need the "source ~/.opena_auto" line in his or her shell start-up file. This part of the script makes a token effort to guess the correct start-up file and throws up its hands otherwise, pointing the user to what needs to be done. Might work OK for most people, and those for whom it gives trouble definitely won't be terribly worried by the prospect of adding a line to the appropriate file! Phew, enough narrative already. Except to say that while writing this I thought "hmm, should open stickies to jot down things I must remember to include in this write-up. Now where is stickies again? But before my hand had reached my mouse I drew it back to the keyboard in horror and entered "sti<tab>" and BOOM, up she came. Very, very, sweet! As you can probably tell, I *like* this one; it makes the terminal just a little bit more like Stazione Centrale (but without all the pickpockets). Hmm, maybe I'm simply delirious, having stared at it for far too long. Of course in the time spent writing this script I could have written all the aliases out by hand several dozen times over. But (I just keep repeating, trying to convince myself) "it's all about the journey"! Truth be told I learnt some little things that gladdened my heart along the way, and the bundles of frustration that presented themselves weren't all that awful. Hope some of you enjoy it too. Apologies to alias-haters. (err, "Hi PEZ"!) And to bash/zsh users, who don't get treated particularly well with respect to functions. The shell script route is always available if the idea sounds interesting but you don't like the execution; while the structure of my script isn't that nice it does outline pretty much all the heavy lifting you'd need to accomplish this. Oh yeah, uninstall really reduces to the simply act of deleting the "source ~/.opena_auto" line at the end of your shell initialisation file (and maybe deleting .opena_auto and .opena should you be deadly keen to purge this evil connivance). Hmm, I think I'm reading too many Irish novels at the moment… maybe something Orstrayan to finish. Avagoodone, Paul ps the way in which subroutines and their return values are used is a little funky. I started rejigging the whole arrangement of the script but didn't quite get it to a stage that I'm proud of before tiring of the exercise. Blame any syntactic messiness (and poorly chosen variable names) on that. OK then, time to show you my dirty linen. pps this addendum's really boring: if the script is giving errors and not running, check that those "heredocs" don't have a space at the end. See the last paragraph of the big comment region in the script itself. Bloody browsers!! |
The script itself
Code:
#!/usr/bin/perl -w |
pmccann,
Thanks a lot for the "big mamma...!" Will try it out as soon as I finish digesting the banquet. Cheers... |
Hmm, amazing how letting something onto other folks' machines yields cases that don't show up on your own. First real attempt to use this "in the wild" produced two problems. (1) There were directories in the user's path that didn't actually exist! If you look a the script above you'll notice that in
opendir DIR,$path or die "opening $path: $@"; # (original line) Given the possibility of the above situation that might be better converted to a warning message: alert the user to the problem, but there's no real need to abort the script. opendir DIR,$path or warn "opening $path: does it exist?\n $@"; # (new version) The other, more annoying, problem is that some paths to applications have quotation marks (single or double, but more likely single) within them. And when you "source" one of the alias items produced by the script this will produce an --unmatched " -- messsage. Yuck, but you might think: "well just backslash the slippery little suckers". NOPE. If you backslash the quote marks then "open" gets grumpy about the items name. GRRR! Quoting, you've got to love it. (What's that old saw?: "The best thing about standards is that there are so many to choose from".) Anyway, politely bowing out of such a situation is the best I can recommend for now. So, directly after the line that reads "my $orig = $app;" you can strap on the following safety belt: if ($orig=~/['"]/){print "Quote mark in $orig : no alias set.\n\n";next} I'm betting that not many people would run into this situation, but it's safety first for now. Cheers, Paul |
Paul, you're are a nut! I mean a genius! I mean, uh, this is really useful, and it must have taken some time to put together (even taking Hofstadter's Law into account). And it works in zsh, too, whoohoo! Thank you so much.
|
pmccann,
I tried the script, and obviously I made some mistake, as it shows the following: Code:
153 [Sao @/Users/pm/bin] % perl opena.plCheers... |
One day, one fine day, I'll remember to compose a reply outside the browser window and paste it inside. Then it might even be possible that I'll stop losing nearly-completed responses. Grumble...
Sao: I've no idea what's going on there. Two checks: do you get the same warnings when you just compile the thing (I'd imagine "yes": as you can see, there's precious little happening before the print statement that signals the scan of the apps directory.) That is, what does the following produce? perl -c opena.pl At the risk of you punching me through your monitor: have you tried cutting and pasting again? As should be obvious by now, I have no idea where these "subroutine redefined" warnings are coming from (or where "line 382" might be pointing to. The script is *long* but not that long! It must be some feedback loop in which it's eating its own tail somehow, and thus becoming considerably more substantial. Hmmm.) Titanium Man: thanks for the field report. Nice to know it's working OK for someone. I should fess up to a few problems that I'm smoothing over at the moment. There should be a new script by the end of the weekend, with all the current bug-fixes, and a couple of necessary corrections. (As it stands, if you run the script a second time it'll "see" all of the aliases from the first run, and thus protect them when it writes out the new .opena_auto, effectively disabling those some aliases thereafter. So the new version will "subtract" the .opena_auto aliases from the list of protected aliases. The other thing is that the script just appends the "source ~/.opena_auto" line to the startup file, possibly leading to multiple sources of the same file. Not an error, but decidedly infelicitous. And Ms Kendell must be obeyed. Aaah, the good life. [[Please ignore those last two sentences if you've never watched trashy British television series from the 70s/80s]]) Anyway, if you're thinking about trying this thing it might be worth waiting for the cleaned up version. Cheers, Paul |
Yes, please post the finished script. That is, if a script ever really is finished. Thanks.
|
The following message contains a revised version of the big script above. It just sands off a few rough edges that cropped up --as discussed in earlier posts-- and corrects a couple of other embarrassing omissions from the original.
In the end I've decided against the whole protection racket: instead I've plonked the "source ~/.opena_auto" call at the *start* of the shell initialization file, consequently allowing the aliases installed by the script can be overwritten by any user-defined aliases found later in the .tcshrc/.cshrc/.profile/.zshrc. That should work OK, and might even have the added bonus of letting me retain a modicum of sanity; protecting existing aliases was starting to lead me to a very unpleasant place, where the vagaries of the quoting conventions for each of the candidate shells had to be taken into account; and that was as sure sign as any that a better approach to the problem had to be found! I've not included the big comment at the start of the script: it's up above, so cut and paste from there if you want it kept with the new version! Cheers, and good luck to anyone taking the plunge: the water's fine *over here*:-) Paul ps Same as always: save as, say, opena.pl anywhere you like. Call it using "perl opena.pl" (or by making it executable, rah rah rah). The motivation is described above at great length, and the rest should be self-explanatory. Watch those heredocs if you get a compilation error. |
Working script (err, "individual results may vary")
Code:
#!/usr/bin/perl -w |
pmccann,
I get the following when trying the new script: Code:
170 [Sao @/Users/pm/bin] % perl opena.pl |
This is great stuff, Paul, thank you! FYI, I'm still not getting any aliase for my MS Office apps (easily fixed by putting them in ~/.opena), and the script posted above didn't work for me until I changed:
print <<"REPT "; to this: print <<"REPT"; (ie removed the space after REPT). Thanks again! |
A couple of other things:
1) Regarding MS Office apps not appearing, I have my MS Office folder in ~/Applications, so maybe that's why? Again, it's not a big deal to add: alias entourage='open -a ~/Applications/Microsoft\ Office\ X/Microsoft\ Entourage' alias excel='open -a ~/Applications/Microsoft\ Office\ X/Microsoft\ Excel' alias powerpoint='open -a ~/Applications/Microsoft\ Office\ X/Microsoft\ PowerPoint' alias word='open -a ~/Applications/Microsoft\ Office\ X/Microsoft\ Word' to ~/.opena, but I'm just curious. 2) The first time I ran the (original) script, I had some duplicates in ~/.opena_auto, but that's been resolved with this version. (And just when I was going to spend hours trying to learn how to fix it with 'uniq') 3) I'm kinda lazy (I use zsh afterall) and I hate having to type the underscore character, so I did this: perl -i~ -pe 's/_//g' ~/.opena_auto For those who know as much about perl as I do (NONE), that'll back up your original ~/.opena_auto as ~/.opena_auto~, and remove all the underscores from a copy of ~/.opena_auto. Man, I like this script :-) |
Timan,
Thanks, I removed the space ( print <<"REPT"; ) as you said, and the script worked with no problems. Pmccann, The execution is clean and impeccable. It saves so much time (I can't believe it). Great script! Thanks a lot for sharing. Cheers... |
Timan,
I have the MS Office folder at /Applications and got all the aliases, so it is probably as you said. Cheers... |
Interesting. Thanks sao!
|
On overcompensating
Hi again,
nice catch Titanium Man: I've edited the revised script to remove that pesky space. You might guess what was happening, but the short version is that it was left over from a test aimed at fooling the "space-happy" browsers. [Test failed!] Without boring you to tears, I syntax-checked the thing, pasted it into the input box, previewed the output, cut and pasted, and then noticed a *&^%ing extra space sitting after the REPT and thus edited the script to remove it (without remembering why this thing had been added in the first place, and without having the nous to realise that this would then cause the error you saw. Ah well... Re your Microsoft applications: yep, if you read through the applescript chunk you'll find that the Finder is only asked to search the "startup disk:Applications" folder, which is the Mac OS 9-ish way to say "the /Applications directory". We could scan the whole startup disk (or even *all* disks!) but it'd be dog slow. I do agree re the underscore: it's a toss-up between making the aliases easier to read and making them easier to type. Maybe things should fall toward the latter. Your perl one-liner will work fine as long as there are no underscores in the paths to the applications. If such things do exist you'll get broken aliases for those items. Not a big deal as not many such things will exist. If you wade through the second message I posted in this thread you'll find a way to change the script so that it uses "nothing" instead of underscores, as per your modification (but without the danger to your aliases, as it only affects the name by which you invoke the application). I was musing up there about making this the default, but obviously haven't. Anyway, great to hear that it's working OK for at least two people! I'm still enjoying conjuring forth GUI apps from the terminal; in fact I'm such an addict that for easily-typed apps I use it as a cheap application switcher. (Note that if an app is already open the alias simply brings it to the foreground.) It's getting a little silly, but sometimes just feels right. If you're in the terminal and want to get back to chimera "nav<TAB>" is about as quick as anything. Cheers, Paul |
I'm all set. I had already figured out that I could change
set starting_dir to (path to the startup disk as string) & "Applications:" to set starting_dir to "Users:timan:Applications:" and run the script again (moving my original opena* files out of the way so they wouldn't be affected), but I hadn't noticed that I could just change $app=~tr/[A-Z]!$&\"'*(){}][|;<>?~`\ /[a-z]_/; to $app=~tr/[A-Z]!$&\"'*(){}][|;<>?~`\ /[a-z]/; to get rid of those underscores. Thanks for the heads up, Paul, and don't hesitate to share your future gems, please. I still use bman to view manpages. |
Whoah there! Whoah here! You've just woken me from a bit of a slumber. You wrote:
$app=~tr/[A-Z]!$&\"'*(){}][|;<>?~`\ /[a-z]/; Unfortunately that's not equivalent to the two lines that I posted above, and even they don't seem to work as expected. Hmm, here we go; this is better. $app=~tr/[A-Z]/[a-z]/; #lower case $app=~tr/!$&\"'*()]}{[|;<>?~`\ //d; # remove barnacles. (Note extra d) The difference is due to the way that tr handles its arguments. If there are more characters between the first pair of / / than between the second pair tr will use the last character between the second set to replace all of the extras. That is, #!/usr/bin/perl -w use strict; my $string="abcdefg"; $string=~tr/abcd/wx/; print $string,"\n"; This produces ------------------ wxxxefg ------------------ Now for a confession. Those square brackets that I've been placing within the "tr" calls aren't really necessary. They don't harm anything, but they're not doing what I thought they were doing, in the sense that they're not providing "character classes" at all. They're just hangin' out, bein' 'emselves. So where's this leading? Well Titanium Man's line quoted above will cause any alphabetical characters to be lower-cased, as desired, but will end up with any of the "barnacles" appearing as a "]" in the result. Ugh! Here's an illustration of what I'm raving about: #!/usr/bin/perl -w use strict; my $string="a<>{}90()()()'''"; print "string starts as $string\n\n"; $string=~tr/[A-Z]!$&\"'*(){}][|;<>?~`\ /[a-z]/; print "string ends as $string\n\n"; This gives: ------------------ string starts as a<>{}90()()()''' string ends as a]]]]90]]]]]]]]] ------------------ So either use my two lines above (together with redundant []'s in the first line, which don't cause anyone any grief (except for the slight embarrassment that they engender in the author!)), or use the following two-liner in which I've excised the tautologous beasts: $app=~tr/A-Z/a-z/; #lower case $app=~tr/!$&\"'*()]}{[|;<>?~`\ //d; # remove barnacles I hope this makes sense: feel free to bug me about anything that's not clear. Moral of the story. Use these last two lines!!! Cheers, Paul |
| All times are GMT -5. The time now is 05:36 PM. |
Powered by vBulletin® Version 3.8.7
Copyright ©2000 - 2014, vBulletin Solutions, Inc.
Site design © IDG Consumer & SMB; individuals retain copyright of their postings
but consent to the possible use of their material in other areas of IDG Consumer & SMB.