MarksLib on GitHub

My MarksLib AppleScript library, which is featured in many of my Script Debugger tutorial videos, is now part of my AppleScript Libraries repository on GitHub.

MarksLib

MarksLib is a collection of handlers for everyday operations that I find myself using in almost every script I write. There are tools for reading and writing text files, string substitution, converting between strings and arrays and more.

Installation

Download and run this script application:

Install AppleScriptLibraries.zip (71.1 KB)

Installation (manually)

Enter the following command in the Terminal application to install the latest version of MarksLib on your machine:

curl https://raw.githubusercontent.com/alldritt/AppleScriptLibraries/master/MarksLib.applescript | osacompile -o ~/Library/Script\ Libraries/MarksLib.scpt

Usage

MarksLib provides the following handlers:

containerOf

The containerOf(theReference) handler returns the container of an object specifier. For example, containerOf(file 1 of folder 1 of application "Finder") would return file 1ā€™s container: folder 1 of application "Finder".

readFromFile

The readFromFile(theFile) handler reads the contents of a text file. The theFile parameter can take many forms:

  • full HFS path (e.g. readFromFile("Macintosh HD:Users:Mark:Desktop:file.txt"))
  • full POSIX path (e.g. readFromFile("/Users/Mark/Desktop/file.txt"))
  • relative POSIX path (e.g. readFromFile("~/Desktop/file.txt"))
  • alias (e.g. readFromFile(alias "Macintosh HD:Users:Mark:Desktop:file.txt"))
  • file reference (e.g. readFromFile(file "Macintosh HD:Users:Mark:Desktop:file.txt"))

writeToFile

The writeToFile(theFile, theData) handler writes the contents of a variable to a file encoded as UTF-8 text. The theFile parameter can take many forms:

  • full HFS path (e.g. readFromFile("Macintosh HD:Users:Mark:Desktop:file.txt"))
  • full POSIX path (e.g. readFromFile("/Users/Mark/Desktop/file.txt"))
  • relative POSIX path (e.g. readFromFile("~/Desktop/file.txt"))
  • alias (e.g. readFromFile(alias "Macintosh HD:Users:Mark:Desktop:file.txt"))
  • file reference (e.g. readFromFile(file "Macintosh HD:Users:Mark:Desktop:file.txt"))

The theData parameter should be a string.

writeDataToFile

The writeDataToFile(theFile, theData) handler writes the contents of a variable to a file as raw data. The theFile parameter can take many forms:

  • full HFS path (e.g. readFromFile("Macintosh HD:Users:Mark:Desktop:file.txt"))
  • full POSIX path (e.g. readFromFile("/Users/Mark/Desktop/file.txt"))
  • relative POSIX path (e.g. readFromFile("~/Desktop/file.txt"))
  • alias (e.g. readFromFile(alias "Macintosh HD:Users:Mark:Desktop:file.txt"))
  • file reference (e.g. readFromFile(file "Macintosh HD:Users:Mark:Desktop:file.txt"))

The theData parameter can be any type of data.

doesFileExist

The doesFileExist(theFile) handler tests to see if a file exists. The theFile parameter can take many forms:

  • full HFS path (e.g. readFromFile("Macintosh HD:Users:Mark:Desktop:file.txt"))
  • full POSIX path (e.g. readFromFile("/Users/Mark/Desktop/file.txt"))
  • relative POSIX path (e.g. readFromFile("~/Desktop/file.txt"))
  • alias (e.g. readFromFile(alias "Macintosh HD:Users:Mark:Desktop:file.txt"))
  • file reference (e.g. readFromFile(file "Macintosh HD:Users:Mark:Desktop:file.txt"))

replaceText

The replaceText(theString, fString, rString) handler replaces all occurrences of fString in theString with rString. If theString is a list of strings, all occurrences of fString are replaced in each string in the list.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use MarksLib : script "MarksLib" version "1.0"

MarksLib's replaceText("the quick brown fox jumped over the lazy dog", "the", "xxx")
-->"xxx quick brown fox jumped over xxx lazy dog"

trim

The trim(theString) handler trims leading and trailing whitespace from a string. The functional also works on arrays of strings, returning an array of trimmed strings.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use MarksLib : script "MarksLib" version "1.0"

MarksLib's trim("  hello world  " & return)
-->"hello world"
MarksLib's trim({" abc ", "1234   ", "   "})
-->{"abc", "1234", ""}

split

The split(theString, theSeparator) handler splits a string into an array of strings.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use MarksLib : script "MarksLib" version "1.0"

MarksLib's split("1, 2, 3, 4", ",")
-->{"1", " 2", " 3", " 4"}
MarksLib's trim(MarksLib's split("1, 2, 3, 4", ","))
-->{"1", "2", "3", "4"}

join

The |join|(theString, theSeparator) handler joins an array of strings together into a single string.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use MarksLib : script "MarksLib" version "1.0"

MarksLib's join({"hello", "world"}, " ")
-->{"hello world"}
MarksLib's join(MarksLib's trim(MarksLib's split("1, 2, 3, 4", ",")), "-")
-->{"1-2-3-4"} 

formatForJSON

The formatForJSON(theValue) handler formats a string so that it is suitable for use as a value in a JSON structure.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use MarksLib : script "MarksLib" version "1.0"

MarksLib's formatForJSON("My name is \"Mark\"")
-->"My name is \"Mark\""

formatForCSV

The formatCSVString(theValue) handler formats a string so that is suitable for use as a value in a CSV file.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use MarksLib : script "MarksLib" version "1.0"

MarksLib's formatCSVString("Nothing special")
-->"Nothing special"
MarksLib's formatCSVString("My name is \"Mark\"")
-->"My name is ""Mark"""
7 Likes

Thanks for sharing, Mark.
This looks like great stuff. :+1:

I like that installation trick. I never thought of doing that before! :+1:t3:

I like the ability to supply either a string or a list to your trim function. āˆš

I was wondering why you didnā€™t use the Cocoa methods for some of these functions, though

e.g., stringByTrimmingCharactersInSet

for trim and

componentsJoinedByString
componentsSeparatedByString

for the corresponding Join and Split handlers.

Is this doable from an appleScript? This didnā€™t work:

do shell script quoted form of "curl https://raw.githubusercontent.com/alldritt/AppleScriptLibraries/master/MarksLib.applescript | osacompile -o ~/Library/Script\\ Libraries/MarksLib.scpt"

Couldnā€™t find the path, so I think it would have to build the path on the fly or maybe set the home directory? (worked just fine from terminal)

Itā€™s just very old code that works and Iā€™ve not bothered to upgrade. This stuff has existed for almost 20 years in one form or another. I began by copy-pasting these handlers into scripts, then used SDā€™s old libraries mechanism and now use AppleScript libraries.

I was also late to the ASObjC train. Shane had to drag me kicking and screaming into our present.

To be fair, I donā€™t think there was a train till SD6. Maybe itā€™s just me, but Iā€™m sure Iā€™m seeing a lot more people on ASUsers list (largely SD users I recognise from here) posting ASObjC in their scripts.

The wagon might just be starting to pull out of the stationā€¦

No sure why this is failing for you. When I execute this code it works correctly:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

do shell script "curl https://raw.githubusercontent.com/alldritt/AppleScriptLibraries/master/MarksLib.applescript | osacompile -o ~/Library/Script\\ Libraries/MarksLib.scpt"

That worked here. Thank you!

So Iā€™ve been thinking about taking a fairly straight forward library and adding a SDEF to it, to help me learn how to write a good AppleScript dictionary.

Iā€™m thinking that MarksLib might be a good candidate. Would you mind if I did that publicly on the forum? (Either this thread or a new dedicated thread).

Go right ahead. If you like, you can clone the GitHub repository and do as you please. I suggest a separate thread with a link back to this one.

Ed,

I donā€™t think you should mix ā€œquoted form ofā€ and escaping spaces in the path.
I believe it is one or the other. Note Mark does NOT use ā€œquoted form ofā€.

Getting quotes right in shell scripts is always tricky (at least for me). :wink:

Phil, good point.
So, why not do a pull on Markā€™s Github and revise those handlers to use ASObjC?
Would be a great example for all of us.

Partly cos I donā€™t have the time and donā€™t use github, but mostly because ā€œif it ainā€™t broken, donā€™t fix itā€. I think the longevity of Markā€™s code is a good tesitmonial for sticking with vanilla AS unless thereā€™s a reason not to. Apple like to deprecate Cocoa methods willy-nilly, sometimes in as little as two versions, and thatā€™s one of the things that makes me nervous about using them in AppleScripts (that said, the particular APIs I was referrring to above are all fairly long in the tooth).

Since Iā€™m on that tack, Iā€™ll also throw another warning flag for people jumping into ASObjC: you need to check the minimum version of any new method you discover. A good example of that is a recent thread here which was discussing using NSProcessInfo's operatingSystemVersionString. If you look at the docs for that, youā€™ll see youā€™re fairly good to go: available from 10.2. But itā€™s closely related sibling operatingSystemVersion is only available from 10.10 onwards. Itā€™s easy to get caught by those gotchas* if you donā€™t get into the habit of checking every method you use.

As for examples, though, I do have a bunch that I already use. Iā€™m a bit slack regarding provenance, but I suspect these were largely written by Shane; however, any of them may have been written by or modified by myself, so if thereā€™s any errors then you can credit those to me.

!! Donā€™t forget to add the appropriate property if you cut and paste the handler into another script (like I just did!)

property NSArray : a reference to current application's NSArray
property NSString : a reference to current application's NSString
property NSMutableArray : a reference to current application's NSMutableArray

# remove characters from a string
on remove:remove_string fromString:source_string
	set s_String to NSString's stringWithString:source_string
	set r_String to NSString's stringWithString:remove_string
	return s_String's stringByReplacingOccurrencesOfString:r_String withString:""
end remove:fromString:

# get substring from a character to end of string
on getSubstringFromCharacter:char inString:source_string
	set s_String to NSString's stringWithString:source_string
	set find_char to NSString's stringWithString:char
	set rangeOf to s_String's rangeOfString:char
	return s_String's substringFromIndex:(rangeOf's location)
end getSubstringFromCharacter:inString:

# insert an item into a list
on insertItem:anItem atIndex:theIndex inList:theList
	set theArray to current application's NSMutableArray's arrayWithArray:theList
	theArray's insertObject:anItem atIndex:(theIndex - 1)
	return theArray as list
end insertItem:atIndex:inList:
# set theList to {5, 2, 9, 4, 2, 6, 3}
# set longList to theLib's insertItem:99 atIndex:5 inList:theList
# --> {5, 2, 9, 4, 99, 2, 6, 3}

# remove item from a list
on deleteItem:anItem fromList:theList
	set theArray to NSMutableArray's arrayWithArray:theList
	theArray's removeObject:anItem
	return theArray as list
end deleteItem:fromList:

# remove an item at a particular index
on deleteItemsAtIndexes:theIndexes fromList:theList
	set theArray to NSMutableArray's arrayWithArray:theList
	set theSet to current application's NSMutableIndexSet's indexSet()
	repeat with anIndex in theIndexes
		(theSet's addIndex:(anIndex - 1))
	end repeat
	theArray's removeObjectsAtIndexes:theSet
	return theArray as list
end deleteItemsAtIndexes:fromList:
# set theList to {1, 2, 3, 4, 5, 6, 7, 8, 9}
# theLib's deleteItemsAtIndexes:{3, 5, 7, 1} fromList:theList
# --> {2, 4, 6, 8, 9}

# get index of first occurrence of an item in a list
on getIndexOfItem:anItem inList:aList
	set anArray to NSArray's arrayWithArray:aList
	set ind to ((anArray's indexOfObject:anItem) as number) + 1
	if ind is greater than (count of aList) then
		display dialog "Item '" & anItem & "' not found in list." buttons "OK" default button "OK" with icon 2 with title "Error"
		return 0
	else
		return ind
	end if
end getIndexOfItem:inList:
# set thisList to {"I", "see", "a", "red", "door", "and", "I", "want", "to", "paint", "it", "black"}
# set aNum to its getIndexOfItem:"paint" inList:thisList
# if aNum is not 0 then
# 	-- do something
# end if
# 

  • speaking from personal experience

Thatā€™s pretty rare, though. And perhaps more importantly, they tend to support deprecated methods for a long time. Thereā€™s soft deprecation and hard deprecationā€¦

Thatā€™s true ā€“ but itā€™s another reason to use Script Debuggerā€™s code-completion. If your script includes a use AppleScript version... statement set to a version before the method was introduced, the method wonā€™t appear in code-completion. As of 6.04, this now also goes for any minimum version set in a bundleā€™s Info.plist.

(Actually, operatingSystemVersion is even more complex ā€“ because it returns a struct, and only a handful of structs were bridged before 10.11, itā€™s not really usable in 10.10 either. Themā€™s the breaks.)

Or use SDā€™s Edit -> AppleScriptObjC... -> Copy as Standalone Code :slight_smile:

Ah, nice! I hadnā€™t spotted that one. :+1:

Right, but thereā€™s two things with that (probably veering off-topic here).

One is that itā€™s fine when you write original code in SD6, but doesnā€™t help when you reuse code elsewhere. You (read: me and anyone else with my slapdash coding habits!) really need to keep good notes / comments on version compatibility with reusable code.

The second thing is a sort-of feature request: it can be disconcerting in SD6 when you know what method you want and code completion refuses to find it. My first assumption is that SD6ā€™s code completion isnā€™t working or responding properly. It only occurs to me after a bit of frustration (or more likely after I look it up in Dash) that its not available. It would be good if CC could throw an alert bubble for a method that gets entered that it can see is a valid method but isnā€™t valid for that version. I recognise you could only do that once the entire method had been typed in, but it would still be really helpful.

Extending that concept a bit further, a ā€˜Verify ASObjC code compatibilityā€™ button for an entire script would ameliorate the first problem, too.

Phil,

This is true. This is also true for for iOS and Mac OS developers and has been so for a long time. But it is certainly something that can be managed.

In AppleScript there are a lot of scripts written for a small group, a company, for a family, ā€¦ For these it is almost always know what the minimum system level is before even starting on a script. For this situation if it runs on the oldest system then youā€™re good to go.

The main thing is that system versions are not as much of a problem unless you sell software to the public. There are a lot of scripters that create scripts to be used on a known minimum Mac configuration. When I wrote things for companies I always knew the minimum system configuration every time. So I literally ran functional tests on the oldest system level first and then finished my testing with various version levels.

I wrote scripts that interfaced with a lot of UNIX systems and other larger systems that produced output that the Mac worked with. The data configuration was always changing. So I just wrote a script that was easy to change. I added preferences when so the specification of the data being received could be specified and allow both old and new data could be accessed. The point I am making is that system levels and configurations are very manageable. Selling scripts on the open market is when it gets hard. If Iā€™m selling to the public I can just developed something on the oldest system that will be used.

When it comes to Apple deprecating things that is something that all scripters would have to worry about. But again this can managed just like Apple developers do. Itā€™s true you donā€™t know what will be deprecated but it is not like every year a large percentage of ASObj-C methods are deprecated. Right now I see a lot of methods and constants being deprecated and then brought back with a new name. All that is needed is to check the OS version and then use the old or new version depending on the OS version number. These particular types of changes leave the interface to the new and old way the same.

Iā€™m not sure what your point is here as far as deprecations and minimum system configurations is. Is this a call for caution, a warning of danger or just some thoughts on something. I got the feeling it was a warning of danger but I Iā€™m not sure.

Bill

It was a heads-up for people new to ASObjC. For the most part, you can write AppleScript scripts without ever having to think about dependencies; you loose that innocence once you start getting into ASObjC.

Thatā€™s what confuses me. The problem with ASObj-C is that it isnā€™t documented beyond a few notes here and there and a couple of longer sources. Therefore ASObj-C has a high curve. Shaneā€™s book makes up a considerable portion of the documentation out there and thatā€™s just one book. Learn AppleScript: The Comprehensive Guide to Scripting and Automation on Mac OS X is the only other Applescript book Iā€™ve seen it mentioned in and it has some useful stuff in the last chapter. There was one other book but that book ended with some passing remarks about ASObj-C and was so tiny I couldnā€™t take it seriously.

That is the whole reason I started the ASObj-C database. It was to get rid of the steep learning curve. Changing languages wonā€™t make a difference when dealing with OS versions when you know the oldest OS version.

Applescript has dependencies. Perhaps not as complicated as those in Objective-C. AS has libraries that need to be present to do certain things, frameworks that have to be ā€œused,ā€ objects need to exist before they can be worked on, ā€¦ It seems like the same mental process to me. There is just a lot more of it in Objective-C and it is true the ASObj-C dependencies are less familiar to newer users.

If something breaks in a future OS release for ASObj-C then the scripter might have to find something new for a line in the script. But thatā€™s just like scripting additions going bad with a new OS release, Apple deprecating something used in shell script, something called in an application that no longer works in a new version of the application, the code for scripting the interface of an application doesnā€™t work with a new version of the application, ā€¦ How is the concept of making changes for new versions of things new to a scripter? Itā€™s been with AppleScript from the beginning.

Bill