Avoiding AppleScript through Ruby: rb-appscript or rubyosa?
Quoth kch:
That's nice, but now I'm curious about
how scripting bridge compares to
applescript. I guess I'll have some
reading to do.
SB omits some functionality found in AppleScript. For example, the following script moves all files from the desktop to the Documents folder:
tell application "Finder"
move every file of desktop to folder "Documents" of home
end tell
In SB, the SBElementArray class severely restricts your ability to apply a single command to multiple objects, so you either have to resort to the low-level API or else get a list of individual file references and move them one at a time:
require 'osx/cocoa'; include OSX
require_framework 'ScriptingBridge'
finder = SBApplication.applicationWithBundleIdentifier('com.apple.finder')
destination = finder.home.folders.objectWithName('Documents')
finder.desktop.files.get.each do |f|
f.moveTo_replacing_positionedAt_routingSuppressed(destination, nil, nil, nil)
end
In rb-appscript, you'd use the same approach as AppleScript:
require 'appscript'; include Appscript
app("Finder").desktop.files.move(:to => app.home.folders["Documents"])
...
SB obfuscates the Apple event mechanism much more heavily than AppleScript does. AppleScript can be a pain to get your head around, what with the weird syntax, tendency to keyword conflicts, and the like, but beyond that it largely presents Apple events as-is. The only really significant piece of magic in AS is its 'implicit get' behaviour when it evaluates a literal reference that doesn't appear as a parameter to a command. AppleScript's biggest sin is that its documentation doesn't better explain how it actually works, but there is a very good paper by William Cook that sheds a lot of light on what's actually going on.
SB, on the other hand, does its hardest to pretend that it is a genuine Cocoa API with Cocoa-style behaviour, so layers on a large amount of magic. The result is something superficially appealing to Cocoa developers, but as soon as those abstractions start to leak - as abstractions invariably do - you are completely at sea in terms of understanding what's going on. For example, SBElementArray claims to be an array - it even subclasses NSMutableArray - but when you actually try to use its array methods, half of them work and half of them don't. In fact, it isn't a real array at all; it's a wrapper around an unevaluated Apple event object specifier, faked up to pretend it's an NSMutableArray. So when it does something un-array-like, you're largely stuffed for understanding why. And, as mentioned in #1, some of these thick abstractions make it difficult to access standard Apple event functionality underneath.
SB firstmost tries to be a good Cocoa API rather than a good Apple event API, and ends up being not very good at either.
Appscript, incidentally, follows AppleScript's lead and takes the opposite approach: do Apple events right, and then worry about accommodating the host language. That's why some folks prefer RubyOSA over rb-appscript; while appscript is the more capable solution, if you've coming from a heavily object-oriented background, it will feel very strange. That's because Apple events use an RPC-plus-query-based paradigm, and any resemblance appscript may have to OOP is purely syntactic. The nearest analogy would be to sending XQueries over XML-RPC, and it takes some getting used to.
...
SB tends to suffer significantly more application compatibility problems than AppleScript.
Some of these problems are due to SB imposing its own ideas of how Apple event IPC ought to work on top of how it actually works. For example, SB creates a set of [pseudo] proxy classes representing the classes defined in the dictionary; it then imposes various restrictions on how you can interact with those objects based largely on classic object-oriented behavioural rules.
For example, the following script gets the names of all sub-folders of the Documents folder:
tell application "Finder"
get name of every folder of entire contents of folder "Documents" of home
end tell
If you try the same approach in SB:
finder.home.folders.objectWithName('Documents').entireContents.folders.arrayByApplyingSelector(:name)
it gets as far as the #folders method, then throws an error because the type of the 'entire contents' property in Finder's dictionary is declared as 'reference'. Since there isn't a 'reference' class with 'folder' elements defined in the dictionary, SB doesn't let you construct that particular query (unless you want to drop down to the low-level APIs and use raw AE codes). It's perfectly legal according to Apple event rules, but doesn't fit within the narrower OO-centric rule set imposed by SB.
Other bugs are due to SB making assumptions about how scriptable applications will implement certain commands and other features. For example:
tell application "iTunes"
make new playlist with properties {name:"test 1"}
end tell
SB doesn't let you take advantage of any shortcuts provided by iTunes though (you can omit the reference to the source object you want the playlist created in, in which case the main 'Library' source is used), so let's write that in full for a better comparison:
tell application "iTunes"
make new playlist at source "Library" with properties {name:"test"}
end tell
In SB you'd write this as:
itunes = SBApplication.applicationWithBundleIdentifier('com.apple.itunes')
playlists = itunes.sources.objectAtIndex(0).playlists()
newplaylist = itunes.classForScriptingClass(:playlist).alloc().initWithProperties({:name => 'test'})
playlists.addObject(newplaylist)
When you run it though, it barfs on #addObject. In its attempt to turn a single 'make' command into a multi-line exercise, SB assumes that the 'at' parameter will always be a reference of form 'end of <elements> of <object>', which is how Cocoa Scripting-based applications do it. Carbon applications don't have a single standard framework for implementing Apple event support though, so they tend to vary a bit more in their requirements. iTunes, for example, expects a reference to the container object, in this case 'source "Library"', and doesn't like it when SB passes 'end of playlists of source "Library"'. That's just how a lot of AppleScriptable applications are, but SB ignores that reality in its determination to be 'object-oriented'.
Yet more problems are caused when an application dictionary isn't 100% accurate or exhaustive in detail. Neither the aete nor sdef formats allow you to describe how an application's scripting interface works in 100% detail; some things just have to be guessed at by users, or described in supplementary documentation - the nature of Finder's 'entire contents' property being one example. Other information, such as which classes of objects can be elements of which other classes of objects, and what the type of each property is, is never actually used by AppleScript itself - it's solely there as user documentation. Since AppleScript doesn't rely on this information, any mistakes will be missed when testing the application's scripting support against AppleScript, since scripts work just fine despite it. SB does employ that information, so any typos there will result in missing or broken features that have to be circumvented by dropping down to the low-level APIs again.
Appscript, BTW, isn't 100% 'AppleScript-compliant' either, but it does come an awful lot closer. Early versions of appscript also tried to impose various OO rules on Apple events, such as enforcing the dictionary-defined object model, but after a year of running into application incompatibilities I ganked all that 'clever' code and spent the next few years trying to black-box reverse-engineer AppleScript's internal machinations and make appscript emulate them as closely as possible. "If you can't beat 'em (which you can't), join 'em", in other words. And where appscript does hit a compatibility problem, there are usually ways around it, including flipping internal compatibility settings, exporting application terminology to a module, patching it by hand, and using that instead, or dropping down to its low-level raw AE code APIs.
...
FWIW, I should also plug a few related appscript goodies.
First, the ASDictionary and ASTranslate tools on the appscript site are your friends. ASDictionary will export application dictionaries in appscript-style HTML format and also enables the built-in #help method in rb-appscript; great for interactive development in irb. ASTranslate will take an AppleScript command and (bugs willing) return the equivalent command in appscript syntax.
Second, the source distribution of rb-appscript contains both documentation and sample scripts. If you install the appscript gem, remember to grab the zip distribution for those resources as well.
Third, Matt Neuburg has written a book about rb-appscript. Go read it if you're thinking of using rb-appscript. And go read Dr Cook's paper, regardless of what you eventually decide on.
...
Anyways, hope that helps. (Oh, and apologies for length, but I've just written about 25000 words this week, so this is just some light relaxation.)
p.s. Ned, your shiny dollar is in the post. ;)
Translating AppleScript into rb-appscript
Like this :
puts Appscript.app.by_name("System Events").processes["Dock"].lists[0].size.get[1]
lists and size are array.
Rewriting AppleScript to appscript-rb, setting variable in Keyboard Maestro
You have the syntax of your command wrong (hard to imagine with AppleScript's clear and well defined syntax, I know!).
The command should be something like:
#!/usr/bin/ruby
require "rubygems";
require "appscript";
kme = Appscript.app('Keyboard Maestro Engine');
kme.make(:new => :variable, :with_properties => {:name => "My New Variable", :value => "New Value 2"});
I found this draft book of Scripting Mac Applications With Ruby helpful in figuring out how to translate the AppleScript code to ruby.
BTW, if you know the variable already exists, it's easier to use just the simple reference get/set commands:
kme = Appscript.app('Keyboard Maestro Engine');
p kme.variables["My Variable"].value.get;
kme.variables["My Variable"].value.set("Next Value");
p kme.variables["My Variable"].value.get;
Learning AppleScript
I learned from AppleScript: the Definitive Guide. The free documentation available online at that time was quite confusing and incomplete, but that book taught me everything I needed to know. I'm not sure if the docs have improved since then (2005-ish).
As for tips on getting documentation, the script editor's "Open Dictionary" command is about the only documentation you'll get for most applications.
AppleScript - is enterprise-ready?
- Is it Silent, depends on the app and your scripts, a lot of app will want to open a window on the screen etc, the level and type of support apps can have for AppleScripts can vary greatly.
- Is it stable and durable, sort of, the issue is AppleScripts is not very fast, it does everything with AppleEvents, and so you can get timeout issues with AppleEvents a lot, or AppleScripts that take a long time, though I have to admit I haven't used AppleScript much recently but when I have with the modern faster CPUs, this is not as big an issue as it was in the past.
- Is it mature, its been around longer than Mac OS X, the biggest issue with AppleScripts I have found is the language looks like natural language which gives you the impression that you can just write something that match its natural language pattern and it will work, but this is not the case, and because of its natural language like syntax it takes a while to know what will work, it mot like other languages where you can work out the syntax and then pretty much any combination that obeys that syntax will work. I still get frustrated with operation that work on list, the language give you the impression you can do some filtering ops on any list when in fact its up to the app you are AppleScripting to supply that functionality for you, for example you can ask finder to get every running app that has some property from every active application, but if you have your own list you have to enumerate over it manual and inspect each item individually.
AppleScript is a language you can experiment with easily so I would just try some stuff out to see if it works the way you want, and then flesh out if its successful. People have used AppleScripts to write full Cocoa application, but I have never thought it was suitable for something like that.
Applescript (via appscript) stops processing in loop
I figured it out. The page that I was adding tags to could not be the currently active page.
vp = app("VoodooPad.app")
doc = vp.open vpdoc
page_names = Array.new
if self.class.to_s.match('Stake::Stack')
# Only the parent stack has a main page
page_names.push @name
# Create the release notes page. Only run on parent stack
notes = "#{@name} Release Notes"
page_names.push notes
doc.remove :with_name => notes
doc.create :new => :page, :with_name => notes, :with_content => self.release_notes
end
# Create the settings page
settings = "#{@name} Settings"
page_names.push settings
doc.remove :with_name => settings
doc.create :new => :page, :with_name => settings, :with_content => self.to_md
# Have to open to different page to ensure its not open when I add meta tags.
doc.open_page :with_name => 'index'
page_names.each do |page_name|
# Add the meta tags to the product page
page = doc.pages[page_name]
puts page_name
page.remove_meta_record :with_key => "description"
page.remove_meta_record :with_key => "url"
page.remove_meta_record :with_key => "name"
page.remove_meta_record :with_key => "image"
page.remove_meta_record :with_key => "version"
page.remove_meta_record :with_key => "stacks_version"
page.add_meta_record :with_value => {'version' => @version_str}
page.add_meta_record :with_value => {'stacks_version' => @stacks_version}
page.add_meta_record :with_value => {'subtitle' => @subtitle}
page.add_meta_record :with_value => {'url' => @info_url}
page.add_meta_record :with_value => {'image' => "#{@basename}@128.png".downcase}
page.add_meta_record :with_value => {'name' => @name}
# Open current page to ensure next page is not open or else cannot add tags
doc.open_page :with_name => page_name
end
Using ScriptingBridge framework for communicating with Entourage
A few things:
If you're stuck, first figure out how to do it in AppleScript. That's what most application scripters use (i.e. the folks best able to help you) and what almost all documentation is written for. Realistically, if you want to do much application scripting, you really need to learn some AppleScript (just as you really need to pick up a bit of ObjC to use Cocoa from Python, Ruby, etc).
Scripting Bridge is clunky, obfuscated and prone to application compatibility problems, so translating working AppleScript code to it can be tricky, if not impossible, depending on the application you're targeting, the commands you're using, and so on. From memory, I think Entourage is one of the apps it trips up on, in which case you're out of luck unless you resort to using raw Apple event codes. Other options are objc-appscript (m'baby), which is much less prone to such problems, and AppleScriptObjC (10.6+), which lets you call ObjC classes directly from AppleScript and vice-versa.
Have you looked at CSMail?
Entourage is going away in Office 2010 in favour of Outlook, so you may not want to invest a huge of time figuring out how to write SB code for it anyway.
Related Topics
Confusion With the Assignment Operation Inside a Falsy 'If' Block
Disable Activerecord For Rails 4
Why Does Ruby Have Both Private and Protected Methods
How to Have Methods Inside Methods
What's the Best Way to Use Soap With Ruby
Adding a Directory to $Load_Path (Ruby)
Where Is Ruby'S String Literal Juxtaposition Feature Officially Documented
How to Generate a List of N Unique Random Numbers in Ruby
How to Avoid "Cannot Load Such File - Utils/Popen" from Homebrew on Osx
Difference Between a Class and a Module
Limitations in Running Ruby/Rails on Windows
Error to Install Nokogiri on Osx 10.9 Maverick
Calling a Method from a String With the Method'S Name in Ruby
How to Extract Url Parameters from a Url With Ruby or Rails