Import scripts without worrying too much about their location

Hi,
the script below is my attempt at solving the long-standing problem of importing external AppleScript scripts easily. It is a lightweight solution, in the sense that it is made of just a handler and a property, but I have used it satisfactorily for some time, so I thought I might share it. For a more complete solution, you may want to look at Loader.

The script mainly aims at easing development (for deployment you may want somehow to package scripts together). The main advantage, apart from helping achieve code modularity, is that you can reorganize your scripts in the Finder without (probably) any need to change the code that imports them.

For example, say your project folder is organized like this:

and you also have ~/Library/Scripts/Reusable/Stuff/Lib3.scpt. Then, Main.scpt might look like this:

import("Lib1")
import("Lib2")
import("Lib3")
-- Use the imported scripts:
Lib1's f()
Lib2's g()
tell Lib3 to run somescript

-- Here goes import's definition

The script is thoroughly commented: if you have installed Xcode 4.1 or later, you may get pretty-printed documentation by typing the following in Terminal.app:

Import script:


(*!
@header
	Import
@abstract
	Easily import other scripts, without worrying too much about their location.
@discussion
	Copy and paste the code of this file into your main script (the best location is probably at the very bottom of the script, so that it will stay out of your sight). Then use
	<pre>
	import("Script Name")
	</pre>
	at the beginning of your script to import an external script. By default, @link import @/link searches the main script's directory and its subfolders, and the user's Scripts folder and its subfolders (in that order). It efficiently does so using Spotlight; hence, Spotlight must be on and your disk must have been indexed for @link import @/link to work. If the external script is located elsewhere, the @link libpath @/link property can be used to specify a list of additional search paths. For example, if <tt>MyLib.scpt</tt> is in (a subfolder of) the Downloads folder then it can be imported as follows:
	<pre>
	set libpath to {path to downloads folder}
	import("MyLib")
	</pre>
	The imported scripts can be in source form (<tt>.applescript</tt>) or compiled form (<tt>.scpt</tt>). When both variants of a script exist, the compiled script is preferred over the textual script. When multiple versions of the same script exist in different locations, the first one to be found is the one that is imported: user-defined locations are searched first, followed by the script's folder, followed by <tt>~/Library/Scripts/</tt>.

	<strong>Important</strong>: each external script loaded by @link import @/link as shown above should set a variable to itself and return itself. This can be achieved by defining the external script's <tt>run</tt> handler as follows:
	<pre>
	on run
	  global MyLib -- Optional
	  set MyLib to me
	  return me
	end
	</pre>
	(The <tt>global</tt> declaration is optional: variables defined in the top-level run handler are implicitly global.)
	This way, thanks to the somewhat convoluted scoping rules of AppleScript, the external script becomes automagically available at the top-level of the main script through the variable <tt>MyLib</tt> (it is not mandatory, but strongly recommended, that the variable's name and the script's file name be the same). Note, however, that, by the same mysterious rules, <tt>MyLib</tt> will not be accessible in nested scopes, that is, <tt>MyLib</tt> becomes <em>implicitly local</em> at the top-level of the main script. To make it global, you should add <tt>global MyLib</tt> at the top-level of your main script, or add a <tt>global MyLib</tt> declaration in each nested scope that must access <tt>MyLib</tt>.
	
	For scripts that you cannot modify, or if you want a script's name to be globally visible without an explicit <tt>global</tt> declaration, the script can be imported as follows:
	<pre>
	set ScriptName to import("ScriptName")
	</pre>
	Note, however, that, unless the external script returns itself, this will only work with compiled scripts.

@version 1.1.1
*)

(*!
@abstract
	<em>[list]</em> A list of paths where scripts are located.
@discussion
	By default, @link import @/link searches for scripts inside the directory where the loader script is located, then inside <tt>~/Library/Scripts/</tt>. This property can be used to specify a list of additional search paths, which take precedence over the default ones.
*)
property libpath : {}

(*!
@abstract
	Import another script.
@discussion
	This handler loads and runs the code from the given script. A pretty obvious constraint is that the external script should not raise errors upon running; this is easily satisfied by any sensible library code.
@param scriptName <em>[text]</em> The name of the script to be loaded, <em>without suffix</em>.
@return <em>[script]</em> The imported script (unless the imported script is a text file (<tt>.applescript</tt>) <em>and</em> it does not return itself, in which case import's return value will be whatever the imported script returns).
*)
on import(scriptName)
	local pathList, lib, searchPath, scriptPaths, pathname
	set pathList to my libpath & {folder of file (path to me) of application "Finder", path to scripts folder from user domain}
	repeat with searchPath in pathList
		set scriptPaths to the paragraphs of (do shell script "mdfind -literal -onlyin " & quoted form of POSIX path of (searchPath as alias) & " 'kMDItemFSName == " & quote & scriptName & ".*" & quote & "cd'")
		repeat with pathname in scriptPaths
			try
				if pathname ends with ".scpt" then
					set lib to load script pathname
				else if pathname ends with ".applescript" then
					set lib to run script pathname
				else
					error -- Not an AppleScript script
				end if
				log " Script loaded from " & pathname & " "
				exit repeat
			end try
		end repeat
		try
			run lib
			log " " & scriptName & " imported. "
			exit repeat
		end try
	end repeat
	try
		lib
	on error
		repeat with searchPath in pathList
			set the contents of searchPath to searchPath as text
		end repeat
		set AppleScript's text item delimiters to return
		error "No script named " & quote & scriptName & quote & " was found." & return & return & "Search path: " & return & (pathList as text)
	end try
end import

Finally, to avoid copying and pasting every time, you may define an AppleScript Editor’s shortcut. Just save the following script as “Import Script.scpt” and copy it into /Library/Scripts/Script Editor Scripts. It will become available in the contextual menu (which appears when you right-click on an editor’s window).

(* Import Script
Code adapted from Apple sample code, but changed by druido for Macscripter's users.
Still provided "AS IS".
*)

set the target_string to "X-X-X"
set the script_text to "property libpath : {}" & return ¬
	& "on import(scriptName) -- v1.1.1" & return ¬
	& "local pathList, lib, searchPath, scriptPaths, pathname" & return ¬
	& "set pathList to my libpath & {folder of file (path to me) of application \"Finder\", path to scripts folder from user domain}" & return ¬
	& "repeat with searchPath in pathList" & return ¬
	& "set scriptPaths to the paragraphs of (do shell script \"mdfind -literal -onlyin \" & quoted form of POSIX path of (searchPath as alias) & \" 'kMDItemFSName == \" & quote & scriptName & \".*\" & quote & \"cd'\")" & return ¬
	& "repeat with pathname in scriptPaths" & return ¬
	& "try" & return ¬
	& "if pathname ends with \".scpt\" then" & return ¬
	& "set lib to load script pathname" & return ¬
	& "else if pathname ends with \".applescript\" then" & return ¬
	& "set lib to run script pathname" & return ¬
	& "else" & return ¬
	& "error" & return ¬
	& "end if" & return ¬
	& "log \" Script loaded from \" & pathname & \" \"" & return ¬
	& "exit repeat" & return ¬
	& "end try" & return ¬
	& "end repeat" & return ¬
	& "try" & return ¬
	& "run lib" & return ¬
	& "log \" \" & scriptName & \" imported.\"" & return ¬
	& "exit repeat" & return ¬
	& "end try" & return ¬
	& "end repeat" & return ¬
	& "try" & return ¬
	& "lib" & return ¬
	& "on error" & return ¬
	& "repeat with searchPath in pathList" & return ¬
	& "set the contents of searchPath to searchPath as text" & return ¬
	& "end repeat" & return ¬
	& "set AppleScript's text item delimiters to return" & return ¬
	& "error \"No script named \" & quote & scriptName & quote & \" was found.\" & return & return & \"Search path: \" & return & (pathList as text)" & return ¬
	& "end try" & return ¬
	& "end import" & return ¬
	
tell current application
	activate
	tell the front document
		set the selected_text to contents of selection
		if the selected_text is not "" then
			if my display_message() is false then return "user cancelled"
		end if
		set the contents of selection to the script_text
		try
			check syntax
		end try
		my replace_and_select(target_string, "")
		try
			check syntax
		end try
	end tell
end tell

on display_message()
	display dialog "This script will delete the selected text." & return & return & "Do you want to continue?" buttons {"Help", "Continue", "Stop"} default button 3 with icon 2
	set the user_choice to the button returned of the result
	if the user_choice is "Help" then
		my script_help("ScriptEditor001")
		return false
	else if the user_choice is "Stop" then
		return false
	else
		return true
	end if
end display_message

on script_help(this_anchor)
	ignoring application responses
		tell application "HelpViewer"
			activate
			try
				lookup anchor this_anchor in book "Script Editor Help"
			end try
		end tell
	end ignoring
end script_help

on replace_and_select(target_string, replacement_string)
	tell current application
		tell the front document
			set this_text to the contents
			set this_offset to the offset of the target_string in this_text
			if this_offset is not 0 then
				set selection to characters this_offset thru (this_offset + (length of the target_string) - 1)
				set the contents of the selection to the replacement_string
			end if
		end tell
	end tell
end replace_and_select