Automating Handler Selection from Script Library File -- Ideas?

Hey guys, I’m looking for ideas/tools that will help me find a handler I need that is in one of my Script Libraries. I have far more handlers than I can remember, and I’d like to search for them by:

  • Name
  • Keyword/Tag (identifies the category)

This would present me with a list of choices, and the call to the one I select would be pasted into SD, much like how SD’s native code-complete works.

I don’t have any SDEFs, so the tool would have to rely on the text of the handler’s name in the Script Lilbrary file.

I have setup my handlers to include “text tags” (an idea I got from @ccstone) in the name line.
These are just keywords prefixed by an “@” symbol, like this:

on getUniqueTagNames(pTagObjectList) -- @EN @Tags @List @Merge
on filterList(pList, pMatch, pMatchAction) -- @List @Filter
on getPOSIXPath(pAnyPath) -- @Path @Posix
on getItemName(pPOSIXpath) -- @Path @Posix @Name
on convertASDateToNSDate(pASDate) -- @Date @NS @ASObjC @Convert

Before I begin the process of building a new tool to do this, I just thought I’d check with you guys to see if:

  • SD can do this?
  • Anyone has already built a similar tool they are willing to share?
  • Any ideas on how to proceed?

All suggestions welcome.
If I end up building this tool myself, then I’ll definitely share here.

Thanks.

Is this what you mean?

set myDoc to alias "Macintosh HD:Users:stocklys:Library:Script Libraries:List.scptd:" --an alias to a library script
tell application "Script Debugger"
   set openLib to open myDoc showing script source
   set myHandlers to name of every script handler of openLib
end tell
return myHandlers

I think this is an SD bug.

In this script, when the variable thisHandler is set to a value, the text of the hander is in the “contents” property. Any comments following the handler declaration end up at the end (which is fine).

The bug is when you try to coerce the handler contents to text. Doesn’t work.

If you ask for the contents you get a subset of the handler properties.

Once this bug is finished I think Jim’s script will be pretty easy to do.

set myDoc to alias "Macintosh HD:Users:stocklys:Library:Script Libraries:List.scptd:"
tell application "Script Debugger"
   set openLib to open myDoc showing script source
   
   set myHandlers to every script handler of openLib
   repeat with thisHandler in myHandlers
      set handlerName to the name of thisHandler
      set handlerContents to the contents of thisHandler
      
      set handlerinfo to the last item of paragraphs of handlerContents
   end repeat
end tell
return myHandlers

This is the age-old AppleScript problem where contents is being interpreted as the contents of the reference, not the contents property. Here’s a simple workaround:

set handlerContents to the contents of (get properties of thisHandler)

Thanks for the suggestion, Ed.

Perhaps I’m missing something, but I don’t see how to get the first line of the handler from your script:

on getUniqueTagNames(pTagObjectList) -- @EN @Tags @List @Merge

I don’t see how to get the argument list.

I got this to run without error:

set myDoc to alias "Macintosh HD:Users:Shared:Dropbox:Mac Only:Alias Folders:Script Libraries:[LIB] JMichael Lib AS.scpt"

tell application "Script Debugger"
  set openLib to open myDoc showing script source
  
  set myHandlers to every script handler of openLib
  
  --repeat with thisHandler in myHandlers
  set thisHandler to item 2 of myHandlers
  set handlerName to the name of thisHandler
  set propHandler to properties of thisHandler
  set handlerContents to the contents of propHandler
  
  set handlerinfo to the last item of paragraphs of handlerContents
  --end repeat
  
  set handlerHeader to handlerName & "(parameters?) " & handlerinfo
  
end tell
return handlerHeader

-->getUniqueTagNames(parameters?) -- @EN @Tags @List @Merge

BUT, this script is extremely slow. It took > 5 sec to just read my Script Library (~60 handlers).
Even if that can be solved, we are a long, long way from having a process that allows the user to type the name and/or keywords of the handler, and have it filter.

Honestly, my thought is to use BBEdit RegEx to do the extraction.
I have done this manually and it is very fast to produce a new document with the filtered results using RegEx.

OR, use a Keyboard Maestro Macro called “Spotlight Search Prompt”.

Not sure how much faster that can work, but it is doable from SD.

I don’t change my Script Libraries that often, so I would simply run this once and build a silt of handlers that would only be updated if f any of the script libraries have been modified or if new ones added, and then just working with the new, modified or deleted files.

Chris Stone did some stuff on this a couple of years ago – he’ll probably pipe in. One of the scripts I have from then is this:

use scripting additions
use framework "Foundation"
use framework "OSAKit"

on doIt()
	set thePath to current application's NSString's stringWithString:"~/Library/Script Libraries"
	set thePath to thePath's stringByExpandingTildeInPath()
	set theURL to current application's |NSURL|'s fileURLWithPath:thePath
	set fm to current application's NSFileManager's new()
	set theLibs to (fm's enumeratorAtURL:theURL includingPropertiesForKeys:(missing value) options:(((current application's NSDirectoryEnumerationSkipsPackageDescendants) as integer) + ((current application's NSDirectoryEnumerationSkipsHiddenFiles) as integer)) errorHandler:(missing value))'s allObjects()
	set theArray to current application's NSMutableArray's array()
	repeat with oneURL in theLibs
		try
			set theScript to (current application's OSAScript's alloc()'s initWithContentsOfURL:oneURL |error|:(missing value))
			set theSource to theScript's source()
			(theArray's addObject:(theSource))
		end try
	end repeat
	set allSource to the theArray's componentsJoinedByString:(linefeed & " *" & linefeed)
	set allSource to allSource's stringByReplacingOccurrencesOfString:return withString:linefeed
	-- return allSource as text
	set theRegEx to current application's NSRegularExpression's regularExpressionWithPattern:"(?m)^on (\\w.*?(?=[\\(:]))(.+)" options:0 |error|:(missing value)
	set theFinds to theRegEx's matchesInString:allSource options:0 range:{location:0, |length|:allSource's |length|()}
	set theHandlers to current application's NSMutableArray's array()
	repeat with oneFind in theFinds
		if (oneFind's numberOfRanges()) as integer > 1 then
			set theRange to (oneFind's rangeAtIndex:1)
			set theRange2 to (oneFind's rangeAtIndex:2)
			(theHandlers's addObject:(allSource's substringWithRange:(current application's NSUnionRange(theRange, theRange2))))
		end if
	end repeat
	return (theHandlers's componentsJoinedByString:linefeed) as text
end doIt

doIt()

That should be reasonably speedy – a couple of seconds for my lib folder – although it may launch some apps if your libraries target them.

It might be a suitable starting point.

1 Like

Thanks Shane, and Chris (@ccstone)

This is much, much faster, taking on 0.13 sec. :smile:
And, it returns exactly what I expect and need, the full first line of the handler:

getUniqueTagNames(pTagObjectList) -- @EN @Tags @List @Merge

Just one small issue. This statement does not work with a file sym link:

set thePath to current application's NSString's stringWithString:"~/Library/Script Libraries"

My “Script Libraries” folder is a sym link.
How can I evaluate this and convert to full POSIX path before using in the above statement?

This is a very fast script to get me the list of handlers, now the hard work: Provide an GUI to search/select the handler to use.
Any ideas?

You don’t – you resolve the URL. Something like:

	set theURL to current application's |NSURL|'s URLByResolvingAliasFileAtURL:theURL options:0 |error|:missing value

Thanks, Shane.

It was not obvious to me where to put this statement, but after some trial-and-error, I found this worked. So for the benefit of others, to make it explicity clear, here’s the revised code snippet:

set thePath to current application's NSString's stringWithString:"~/Library/Script Libraries"

set thePath to thePath's stringByExpandingTildeInPath()
set theURL to current application's |NSURL|'s fileURLWithPath:thePath

--- RESOLVE ALIAS OR SYM LINK TO FOLDER ---
set theURL to current application's |NSURL|'s URLByResolvingAliasFileAtURL:theURL options:0 |error|:(missing value)

set fm to current application's NSFileManager's new()

Hey Folks,

Actually I’ve been doing this for well over a decade – first by pulling the recovery resource out of (SD) compiled script libraries – and now by converting the recovery rtf file (thanks be to Shane).

The appended script is Satimage.osax-dependent but is virtually instant and will NOT launch any apps by accident. (Easy enough to convert it to AppleScriptObjC.)

NOTE:

Libraries MUST be script bundles saved by Script Debugger.

You’ll want to change glob “Lb” to something else to find the correct files – I use “Lb” as a filter to find only the specific libraries I want to search (not test-libs for instance).

Currently it finds only these libraries: ELb.scptd, FLb.scptd, GLb.scptd, NLb.scptd, NLbD.scptd, which are in order: Error-lib, Find-lib, General-lib, Net-lib, NetDownloader-lib.

Due to a bug in Discourse “\\1” is not displayed property in code-blocks.

I have written it as \\\1 in the code, and the user must remove one of the backslashes to make it work properly.

-Chris

-----------------------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2014/02/09 06:02
# dMod: 2017/01/30 09:31
# Appl: Finder, Script Debugger, Script Debugger
# Task: Extract and Search Source of Script Libraries
# Libs: None
# Osax: Satimage.osax
# Tags: @Applescript, @Script, @Finder, @Script_Debugger, @Script_Debugger, @Extract, @Search, @Source, @Script_Libraries, @Libraries
-----------------------------------------------------------------------
use framework "AppKit"
use framework "Foundation"
use framework "OSAKit"
use scripting additions
-----------------------------------------------------------------------

set scptLibFldr to alias ((path to library folder from user domain as text) & "Script Libraries:")
set myPath to (path to me as text)
set recvFilePath to "Contents/Resources/Scripts/main.recover.rtf"
set tempSrc to ""

--» GET LIBRARY FILES
set libFileList to glob "*Lb*" from scptLibFldr as POSIX path of extension {"scptd"}

if libFileList = {} then
	error "Make sure your libraries are script bundles, and check the glob specifier!"
end if

repeat with _path in libFileList
   set scptRcvrFilePath to _path & recvFilePath
   set tempSrc to tempSrc & rtf2Text(scptRcvrFilePath) of me & linefeed & "•• NEWLIB ••" & linefeed
end repeat

### CHANGE THE 3 BACKSLASHES (\\\) IN THE NEXT LINE TO 2 BACKSLASHES ###
set outputList to fndUsing("^on (.+)", "\\\1", tempSrc, true, true) of me

-----------------------------------------------------------------------
--» HANDLERS
-----------------------------------------------------------------------
--» rtf2Text:
-----------------------------------------------------------------------
--  Auth: Shane Stanley
--  Task: Extract plain text from a RTF file.
--  dMod: 2014/03/23 14:22
-----------------------------------------------------------------------
on rtf2Text(thePath)
   set attString to current application's NSAttributedString's alloc()'s initWithPath:thePath documentAttributes:(missing value)
   return (attString's |string|()) as text
end rtf2Text
-----------------------------------------------------------------------
--» HANDLERS
-----------------------------------------------------------------------
on fndUsing(_find, _capture, _data, _all, strRslt)
   try
      set findResult to find text _find in _data using _capture all occurrences _all ¬
         string result strRslt with regexp without case sensitive
   on error
      false
   end try
end fndUsing
-----------------------------------------------------------------------

EDITED 2017/01/31 19:05 CST
- Code changed to allow for a bug in Discourse’s code-blocks.
- Added basic error-handling if no libraries are found.

Many thanks, Chris.
I had a feeling we’d be hearing from you.

I’m getting one compile error on this line:

### COMPILE ERROR ON THIS LINE ###
set outputList to fndUsing("^on (.+)", "\`", tempSrc, true, true) of me

### CHANGE "\`" TO "\\`" ### 
set outputList to fndUsing("^on (.+)", "\\`", tempSrc, true, true) of me

Is this a correct change?

In case it’s not obvious to anyone following along, this requires that your library files are all saved as .scptd, and not simple .scpt files.

@ShaneStanley, I’m getting an error with your handler:

use scripting additions
use framework "AppKit"
use framework "Foundation"
use framework "OSAKit"


set scriptPath to "/Users/Shared/Dropbox/Mac Only/Alias Folders/Script Libraries/[LIB] JMichael Lib AS.scptd"

set scriptStr to my rtf2Text(scriptPath)


--» rtf2Text:
-----------------------------------------------------------------------
--  Auth: Shane Stanley
--  Task: Extract plain text from a RTF file.
--  dMod: 2014/03/23 14:22
-----------------------------------------------------------------------
on rtf2Text(thePath)
  set attString to current application's NSAttributedString's alloc()'s initWithPath:thePath documentAttributes:(missing value)
  return (attString's |string|()) as text
end rtf2Text
-----------------------------------------------------------------------

This error:

missing value doesn’t understand the “string” message.

at this line:

Running Script Debugger 6.0.3 (6A191) on macOS 10.11.4.

Any ideas on how to fix?

I did try restarting SD6.

That’s telling you that attString is missing value, which is not surprising because the handler is expecting you to pass it the path to an rtf file, not a script. Look at Chris’s script and see how he’s building a path to the bundle’s main.recover.rtf file.

It would probably help to rewrite the handler as:

on rtf2Text(thePath)
	set {attString, theError} to current application's NSAttributedString's alloc()'s initWithPath:thePath documentAttributes:(reference)
	if attString is missing value then error (theError's localizedDescription() as text)
	return (attString's |string|()) as text
end rtf2Text

Then you will hopefully get some enlightenment if something goes wrong.

Thanks. I overlooked that.

Thanks for the update.

Here’s my revised script, which works as expected, returning a clean, plain-text version of my Script Library (.scptd):

use scripting additions
use framework "AppKit"
use framework "Foundation"
use framework "OSAKit"


set scriptPath to "/Users/Shared/Dropbox/Mac Only/Alias Folders/Script Libraries/[LIB] JMichael Lib AS.scptd"

set recoverRTFSubPath to "Contents/Resources/Scripts/main.recover.rtf"
set scriptRTFPath to scriptPath & "/" & recoverRTFSubPath

set scriptStr to my rtf2Text(scriptRTFPath)
set the clipboard to scriptStr

--- REVISED HANDLER FROM SHANE ---

on rtf2Text(thePath)
  set {attString, theError} to current application's NSAttributedString's alloc()'s initWithPath:thePath documentAttributes:(reference)
  if attString is missing value then error (theError's localizedDescription() as text)
  return (attString's |string|()) as text
end rtf2Text

Here is an alternate version able to do the job with every library including those saved from an other editor or even being flat files.

[code] use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property storeInClipboard : true

true = store the text in the clipboard

false = store the text on the Desktop

set POSIXSource to POSIX path of ((path to library folder from local domain as text) & “Script Libraries:General Lib.scptd:”)

if not storeInClipboard then
set pathNSString to current application’s NSString’s stringWithString:POSIXSource
set bareNSString to (pathNSString’s stringByDeletingPathExtension())
set POSIXName to (bareNSString’s lastPathComponent()'s stringByAppendingPathExtension:“txt”)
set HfsName to POSIXName’s stringByReplacingOccurrencesOfString:":" withString:"/"
set destURL to current application’s |NSURL|'s fileURLWithPath:(POSIX path of (path to desktop))
set destURL to destURL’s URLByAppendingPathComponent:HfsName
end if

set theString to do shell script "osadecompile " & quoted form of POSIXSource

if storeInClipboard then
set the clipboard to theString
else
set theString to current application’s NSString’s stringWithString:theString
set theResult to theString’s writeToURL:destURL atomically:true encoding:(current application’s NSUTF8StringEncoding) |error|:(missing value)
end if
#===== [/code]

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) mercredi 15 mars 2017 16:02:44

Hey Folks,

I originally wrote scripts to extract Script Debugger AppleScript-Library text back in the mid-1990’s using one of the osaxen that would read file resources (AKUA Sweets?). The major design criteria was speed, and even on that old hardware with MacOS 8-9 the resource-reading-method was very, very fast.

I had some scripts that employed an osax with regular expression support and the Dialog Director osax to do all kinds of nice things with that source-text.

When I finally moved to OSX in 2002 (Jaguar) I was fairly heartbroken to lose all of my osaxen (outside of the Classic environment), but the following year (2003) the Satimage.osax came out and solved most of my problems. It had regEx support and could read/write file resources.

I’ve used osadecompile on and off since it became available, but I’ve always avoided it like the plague for regular operations – because it has the bad habit of launching certain applications when decompiling scripts containing their terminology.

Having 20 apps launch unexpectedly whenever you run a script was (and is) NOT supportable, so I kept using the file-resource-reading method.

Fast-forward to Script Debugger 6 — Mark discontinues Script Debugger libraries in favor of the new AppleScript library structure that debuted in Mavericks.

I would have to change my ways — again…

I tried osascript again, and found it still had a penchant to launch apps – so I looked for another method.

Shane gave me the idea to use the main.recover.rtf files in Script Debugger’s script-bundle scripts and helpfully provided some AppleScriptObjC code to do the job.

This didn’t work for anyone who didn’t have Script Debugger, but it was outstanding for me – because it was lightning fast and never, ever launched an app I didn’t want launched.

I’ve used this method for years to do all kinds of useful things.

Now – let’s revisit osadecompile.

Sometime in the last few years I asked Shane if that job could be done with AppleScriptObjC, and the answer was yes. Unfortunately it still launched apps I didn’t want launched, so I gave up on it again.

But — I tested recently on Sierra and didn’t have any app-launching problems. I don’t know if this has something to do with OS changes, or whether more developers are using the modern sdef format that stops that misbehavior. In any case osascript is working on my system (so far) without launching apps without my consent.

The AppleScriptObjC script below replicates the sort of thing I’ve been doing for just about 20 years — the last 14 ± using the Satimage.osax.

  • It filters the libraries to only the ones I want using a regular expression.

  • It extracts the source-text of all found libraries.

  • It has regular expression support for filtering the output text.
      – There’s a pre-built filter to output only handler-calls.

------------------------------------------------------------------------------
# Auth: Christopher Stone
# dCre: 2017/03/12 22:04
# dMod: 2017/03/15 16:52
# Appl: AppleScriptObjC
# Task: Osadecompile from ASObjC - decompile AppleScript Libraries.
#     : RegEx filter to select only desired libraries.
#     : RegEx filter to output only hander-calls.
# Libs: None
# Osax: None
# Tags: @Applescript, @Script, @ASObjC, @OSAdecompile, @Decompile, @Library, @Libraries
------------------------------------------------------------------------------
use framework "Foundation"
use framework "OSAKit"
use scripting additions
------------------------------------------------------------------------------

set scptLibFldrPath to "~/Library/Script Libraries/"
set scptLibFldrPath to current application's NSString's stringWithString:scptLibFldrPath
set scptLibFldrPath to scptLibFldrPath's stringByExpandingTildeInPath() as string

set libFileList to its findFilesWithRegEx:".*Lb.*" inDir:scptLibFldrPath -- filter files using a regEx

set scriptLibraryText to {}

repeat with libFile in libFileList
   set end of scriptLibraryText to linefeed & "••••• LIBRARY SEPARATOR •••••" & linefeed
   set end of scriptLibraryText to (its extractScriptSourceFrom:libFile)
end repeat

set AppleScript's text item delimiters to linefeed

set scriptLibraryText to scriptLibraryText as text -- entire text of all libraries

# return scriptLibraryText

# Filter library text using a regular expression – extract handler-calls:
set handlerList to its regexMatch:"(?m)^on (\\w.+)" fromString:scriptLibraryText captureTemplate:"$1"
set handlerList to handlerList as text

return handlerList

------------------------------------------------------------------------------
--» HANDLERS
------------------------------------------------------------------------------
on extractScriptSourceFrom:scriptPath
   set aURL to current application's |NSURL|'s fileURLWithPath:scriptPath
   set theScript to current application's OSAScript's alloc()'s initWithContentsOfURL:aURL |error|:(missing value)
   return theScript's source() as text
end extractScriptSourceFrom:
------------------------------------------------------------------------------
on findFilesWithRegEx:findPattern inDir:srcDirPath
   set fileManager to current application's NSFileManager's defaultManager()
   set sourceURL to current application's |NSURL|'s fileURLWithPath:srcDirPath
   set theURLs to fileManager's contentsOfDirectoryAtURL:sourceURL includingPropertiesForKeys:{} options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
   set theURLs to theURLs's allObjects()
   set foundItemList to current application's NSPredicate's predicateWithFormat_("lastPathComponent matches %@", findPattern)
   set foundItemList to theURLs's filteredArrayUsingPredicate:foundItemList
   set foundItemList to (foundItemList's valueForKey:"path") as list
end findFilesWithRegEx:inDir:
------------------------------------------------------------------------------
on regexMatch:thePattern fromString:theString captureTemplate:templateStr
   set theString to current application's NSString's stringWithString:theString
   set theRegEx to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
   set theFinds to theRegEx's matchesInString:theString options:0 range:{0, theString's |length|()}
   set theResult to current application's NSMutableArray's array()
   
   repeat with aFind in theFinds
      set foundString to (theRegEx's replacementStringForResult:aFind inString:theString |offset|:0 template:templateStr)
      (theResult's addObject:foundString)
   end repeat
   
   return theResult as list
   
end regexMatch:fromString:captureTemplate:
------------------------------------------------------------------------------

I have a script that uses a similar method to export all of my handler-calls to a couple of sets in Typinator with one keystroke. It takes less than 1/2 a second to refresh the sets when I’ve changed my libraries.

Typinator’s Quick Search feature gives me very quick, smart-searchable access to my handlers.

I keep the ones I use every day in Script Debugger’s own text-substitutions, but this lets me find what I want on demand and is especially convenient when my memory is fuzzy.

-Chris

Whatever it is, if an app says it has a dynamic dictionary, there’s no getting around launching it. There might be an element of luck skill in your selection of applications :slight_smile:

1 Like

Hey Shane,

Please explain briefly what a dynamic dictionary is, or point to some informative documentation.

Possibly (and I did consider this).

On the other hand if I add an OmniWeb handler to one of my libraries the script I just posted will NOT launch it — even though:

  1. An OmniWeb script will immediately launch the app when compiled or saved.
  • osadecompile run from the shell on a library with an OmniWeb handler in it will launch the app.

This behavior is definitely different than when I first tested your ASObjC decompile code (in 2014 if memory serves).

Any ideas about the change in behavior?

-Chris