A library for persistent lists

Note:

Storing values in script objects, are not securing them from prying eyes, you don’t store passwords, or account numbers, and anything you deem sensitive, in a script object.

Description Of The Script Cache Storage Library

This Library contains handlers and acts like a module for for storing and retrieving lists
from script objects stored on the disk. The motivation, is to be able to store any kinds of
lists permanently on disk, and retrieve values from them as well. The script library also
supports storing lists in one central location, and provides a means for letting you see the
contents of the lists, to easily, see what is in which list.
It is totally up to you what kind of items you store in the list, be it records, or single chuncks of text, or other lists for instance.

The library itself only provides for basic the operations like loading a script object with the list from storage, or saving the contents of the script object back to disk, along with a handler that dumps the list as text, and a handler, for just adding something you want to have added to a script object. The items are added at the end of the list.

The library in itself does not provide any means for duplicating, editing, inserting or deleting values in alist, a library at a higher level tailor made for your needs are supposed to do this for
you.

The handlers are intended do be as easy to use as possible, so if you just specify a filename, the handlers, assumes they are going to find that file, in the centralized location you have specify. You must create and specify a centralized location for this to work however.

The handlers will however, take aliases, hfspaths, posix paths, also like ‘~/misc/myobj.scpt’ or a finder reference, say from a selection of a finder window. All of this are good and well, as long as the filename ends with “.scpt”. The handlers will then ignore the centralized location, and use the supplied path.

Important

A cache is made when you first try to retrieve it from disk, so there are no warnings about non-existent caches. You are encouraged to test for its existance on your own, at this point in time. Because, the addItemToScriptStorage, and storeScript handlers, writes over any contents, or creates it indiscriminantly with the same name, and a cache that has disappared, might be a situation you’d want to resolve before storing something new.

Advantages of persistent lists

The advantages of having persistent lists are many:

  • You don’t loose list values if you have to edit your script.

  • You can share lists between scripts.

  • You can have lists of lists, or list with records, this is where using textfiles for list values
    may become awkward.

It can provide for some flexibility during scripting:

You can have one script construct the list for you, that you let (an) other script(s) use,
keeping as much scaffolding code away from the end product as possible.
For example: Having a list with say 1000 random integers you want to use for testing between several algorithms stored in different scripts. Then you can just create it once, and load it into the ‘client scripts’, there is a script supplied with this library, that makes the load statement for the library, and call statements of the handlers for you

Accessing your list items from the module.

When you load the contents of your list, you are returned a script object, inside that
script object is a property named itemsList, that is the property that holds your list,
so, if you have called your scriptObject for say cache then you get at the list with
cache's itemsList.

  • to loadScript from cacheName
    If this doesn’t find the cache, it will create a new cache object, with an empty list
    in itemslist, that it returns.

  • to storeScript from cacheName against changed_cache
    I call this, whenever I have modified a list, by editing an item, added or deleted an
    item. Returns true if success, false if not, so that you can dump the list to the clipboard,
    if you like save your work.

  • to addItemToScriptStorage for newItem against cacheName
    Saves an item directly to a ScriptObject, and creates the list if it doesn’t exist.
    Returns true if success, false if not, so that you can dump the list to the clipboard,
    if you like save your work. I save the list back whenever I have done some changes to it.

  • to returnListAsText from cacheName
    It is well worth noting that this returns what is stored in the itemsList on disk,
    not the list you may have changed in memory.

A suite of accompanying scripts

I have stored this suite of scripts in the /Library/Scripts/ScriptEditor Scripts folder (I have named it CacheStorage) , so that they are easily accessible from the context menu of Script Editor.

CacheStorage:
Driver for cacheStorage’s basic functionality.scpt
Open ScriptCache’s Storage Folder.scpt
Put load Statement on clipboard.scpt
Show contents of a script cache object.scpt
Show ScriptCache’s StoragePath.scpt

The various scripts in that folder are there to assist using or implementing the script objects.

One script lets you see the contents of the various script objects thru a choose from
file dialog, (Open ScriptCache’s Storage Folder.scpt) that are pointed to the folder with the script objects for storage thru the path you have entered in the library file.

  • One script that tests that the library is correctly set up with the path, to the centralized
    storage (Driver for cacheStorage’s basic functionality.scpt) run it from the Script editor.

  • One script, as mentioned above, that puts a template, based on your choice of script object onto the clipboard, that you can paste into your script.

  • One script shows the path of your script objects in a display alert (Show ScriptCache’s StoragePath.scpt).

  • One script that shows the contents of a chosen script object in a display alert (Show contents of a script cache object.scpt).

Where to store the script objects

Obviously, it is the best thing to store script objects in one central location,
that you “hard code” into the library, that way, as maybe months has gone by without giving
this any thought, it is easy to retrace where they are. You may like to do this differently,
and trust that you have some value in the script, that you can use in a Spotlight search
like this kind:osa somevalue though.

I prefer to use ~/Library/Application Support/Script Cache Storage/ that I will refer to as the “caches folder” hereafter, to keep it somewhere in line with the policies of the folder structure of the ~/Library folder.
(Your local library folder).

Installing the Script Cache Storage Library

  1. Open the Library in Script Editor, save it as “Script Cache Storage” in the ~/Library/Script
    Libraries folder. N.B! You will get an error message during the first save, because the contents of the placeholder for the ScriptCacheStoragePath doesn’t point to a valid folder at this moment. Thi is ok, we are going to change that soon.

# http://macscripter.net/viewtopic.php?pid=178732#p178732 
# Copyright © 2015 McUsr
property parent : AppleScript
use AppleScript version "2.3"
use scripting additions

-- Once you have created a folder to hold your centralized script objects for storing lists,
-- you drag that path into the commented out propety line below , from Finder, and then uncomment the property line.


property ScriptCacheStoragePath : POSIX file "INSERT-PATH-TO-YOUR-SCRIPT-CACHE-FOLDER-HERE" as alias
(*
 If this doesn't find the cache, it will create a new cache object, with an empty list
 that is, a list that is owned by the property itemslist. It returns the cache object.
 (See the private handler __makeEmptyListStorage() below.)
  *)
to loadScript from cacheName
	
	set probe to my __hfsPathOfAnythingAsText(cacheName) -- we may have hit an alias
	if probe contains ":" then
		set cachePathName to probe
	else
		set cachePathName to (my ScriptCacheStoragePath as text) & cacheName
	end if
	if cachePathName does not end with ".scpt" then error "loadScript Error : Only filenames that ends with \".scpt are supported. (May have been called from my addItemToScriptStorage or my returnListAsText.)\""

	local script_cache
	try
		set script_cache to load script alias cachePathName
	on error
		set script_cache to my __makeEmptyListStorage()
	end try
	return script_cache
end loadScript

(*
  Stores a modifed list back onto the disk. 
  I call this, whenever I have modified a list, by editing an item, added or deleted an 
  item. Returns true if success, false if not, so that you can dump the list to the clipboard,
if you like save your work, should something have gone wrong.
*)
to storeScript from cacheName against changed_cache
	local probe, cachePathName
	
	set probe to my __hfsPathOfAnythingAsText(cacheName) -- we may have hit an alias
	if probe contains ":" then
		set cachePathName to probe
	else
		set cachePathName to (my ScriptCacheStoragePath as text) & cacheName
	end if
	if cachePathName does not end with ".scpt" then error "storeScript Error : Only filenames that ends with \".scpt are supported.\" (May have been called from my addItemToScriptStorage.)"
	
	try
		store script changed_cache in file cachePathName replacing yes
		return true
	on error e number n
		tell application (path to frontmost application as text)
			display alert "storeScript:
Couldn't store script " & cachePathName & "
" & e & "number : " & n
		end tell
		try
			tell application id "MACS" to reveal file cachePathName -- Finder 
		end try
		(* If you are happy with this behaviour, uncomment the lines below
		set the clipboard to (__literalList of me from (changed_cache's itemslist))
		display notification "storeScript :Something went wrong during saving the list, it is copied to the clipboard as text"
		*)
		return false
	end try
end storeScript

-- You can use this handler to create a new list.
(*
  Saves an item directly to a ScriptObject, and creates the list if it doesn't exist.
  Returns true if success, false if not, so that you can dump the list to the clipboard,
  if you like save your work. I save the list back whenever I have done some changes to it.
*)
to addItemToScriptStorage for newItem against cacheName
	set curCache to loadScript of me from cacheName
	set end of curCache's itemslist to newItem
	
	return (storeScript from cacheName against curCache)
	
end addItemToScriptStorage
(*
  It is well worth noting that this returns what is stored in the itemsList **on disk**,
  not the list you may have changed in memory.
*)
to returnListAsText from cacheName
	set tempCache to loadScript from cacheName
	
	set listAsText to __literalList of me from (tempCache's itemslist)
	return listAsText
end returnListAsText



(* Private Handlers, grab them and store them somewhere else, if you see a need for them.*)
on __hfsPathOfAnythingAsText(anyFileOrFolderPath)
	-- Thanks to Yvan Koenig and Nigel Garvey for suggestions. ("'~/tmp:Nick\\'s files'")
	-- Constraints: 
	-- Some (unusual) things regarding paths in unix doesn't work here:
	--And you aren't creative ~bin, goes wrong
	-- returns:
	-- the full hfs pathname of anything if the file exists
	local tids, theFile, lastItem, tidsToUse, singleQuoteCheck -- Thanks to Yvan Koenig :)
	local firstPiece
	set singleQuoteCheck to false
	set tidsToUse to ":"
	try
		(get class of anyFileOrFolderPath)
	on error number -1728 -- it was a filereference
		set fileOrFolderPath to fileOrFolderPath as alias as text
		return fileOrFolderPath
	end try
	
	set anyFileOrFolderPath to "" & anyFileOrFolderPath -- doesn't harm it may have been a list with one item.
	if anyFileOrFolderPath starts with "'" and anyFileOrFolderPath ends with "'" then
		set anyFileOrFolderPath to text 2 thru -2 of anyFileOrFolderPath
		set singleQuoteCheck to true
	end if
	if anyFileOrFolderPath does not start with "/" and anyFileOrFolderPath does not start with "~" then
		return anyFileOrFolderPath -- we had a hfspath
	else
		set tidsToUse to "/"
	end if
	
	set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, tidsToUse}

	if singleQuoteCheck is true then
		set AppleScript's text item delimiters to "'\\''"
		set anyFileOrFolderPath to text items of anyFileOrFolderPath
		set AppleScript's text item delimiters to "'"
		set anyFileOrFolderPath to anyFileOrFolderPath as text
		-- just an escaped tick this time
		set AppleScript's text item delimiters to "\\'"
		set anyFileOrFolderPath to text items of anyFileOrFolderPath
		set AppleScript's text item delimiters to "'"
		set anyFileOrFolderPath to anyFileOrFolderPath as text
	end if
	-- We check for ":"'s in posix paths,those needs to be converted into "/"'s in hfsPaths!
	--  thanks to Nigel Garvey for reminding me!
	
	set AppleScript's text item delimiters to ":"
	set anyFileOrFolderPath to text items of anyFileOrFolderPath
	set AppleScript's text item delimiters to character id 0
	set anyFileOrFolderPath to anyFileOrFolderPath as text
	-- character id 0 temporarily replaces ":"'s in a posix path, to avoid conflicts with "real" ":"'s
	
	set AppleScript's text item delimiters to tidsToUse
	if "~" is in text item 1 of anyFileOrFolderPath then
		set firstPiece to text item 1 of anyFileOrFolderPath
		if (length of firstPiece) is 1 then
			set anyFileOrFolderPath to {item 1 of (list disks)} & text items 2 thru -2 of (POSIX path of (path to home folder) as text) & text items 2 thru -1 of anyFileOrFolderPath
		else
			set anyFileOrFolderPath to {item 1 of (list disks)} & {text 2 thru -1 of firstPiece} & text items 2 thru -2 of (POSIX path of (path to home folder) as text) & text items 2 thru -1 of anyFileOrFolderPath
		end if
	else if text item 2 of anyFileOrFolderPath is "Volumes" then
		set anyFileOrFolderPath to text items 3 thru -1 of anyFileOrFolderPath
	else
		set anyFileOrFolderPath to {item 1 of (list disks)} & text items 2 thru -1 of anyFileOrFolderPath
	end if
	set AppleScript's text item delimiters to ":"
	set anyFileOrFolderPath to anyFileOrFolderPath as text
	-- we exchange the character id 0's with "/"'s in order to have a corresponding hfs path.
	set AppleScript's text item delimiters to character id 0
	set anyFileOrFolderPath to text items of anyFileOrFolderPath
	set AppleScript's text item delimiters to "/"
	set anyFileOrFolderPath to anyFileOrFolderPath as text
	
	set AppleScript's text item delimiters to tids
	return anyFileOrFolderPath
end __hfsPathOfAnythingAsText


to __makeEmptyListStorage()
	script newScriptCache
		property parent : AppleScript
		property itemslist : {}
	end script
end __makeEmptyListStorage

on __literalList from someList
	-- Thanks to a clever suggsetion by Nigel Garvey
	-- I were able to remove some ugly, more time consuming code.
	-- This handler is his, with copyright and everything!
	--  I just borrow.
	if class of someList is not list then
		error "'__literalList' requires a list" number -1703
	else
		try
			text 0 of someList
		on error e
			set astid to AppleScript's text item delimiters
			set AppleScript's text item delimiters to "{"
			set textOfSomeList to "{" & text from text item 2 to -1 of e
			set AppleScript's text item delimiters to "}"
			set textOfSomeList to text 1 thru text item -2 of textOfSomeList & "}"
			set AppleScript's text item delimiters to astid
			return textOfSomeList
		end try
	end if
end __literalList
  1. Create a folder named ‘Script Cache Storage’ in your ~/Library/Application Support Folder.

  2. Drag that Folder from the Finder window, and into the Script Cache Storage script in the
    Script Editor, so that it end up between the pair of quotes in the property declaration of the
    ScriptCacheStoragePath Property, which instigated the previous error message when you saved it.

  3. Compile, Save & Close it, unless you want to take a closer look. (I lock my library scripts, when there is nothing to edit).

  4. Install the script below into the /Library/Script/Script Editor Scripts folder, somwhere, (I have a folder I have remarkably have called CacheStorage.

Run it, and see if it works, or see to that it will!


-- Name: Show ScriptCache's StoragePath
-- Store this script in /Library/Scripts/Script Editor/Script Cache Storage
-- Or whatever you think denotes those scripts best!
-- Purpose: Test that the _ScriptCacheStoragePath is initialized properly
-- Author: McUsr 

use AppleScript version "2.3"
use scripting additions


use store : script "Script Cache Storage"

tell application (path to frontmost application as text)
	try
		display alert "The Cache storage path is: 
" & POSIX path of store's ScriptCacheStoragePath
	on error e number n
		display alert "Show ScriptCache's StoragePath: The \"_ScriptCacheStoragePath\" variable  seems to not be initalized, (run then save the library: " & "
Error: " & e & " Number: " & n
		
	end try
end tell

  1. Maybe fool around with the driver script below, or use that as a vantage point. At least run it once, so you have some contents to view with the scripts in #7.
    (It should be stored the same place.)

-- Name: Driver for cacheStorage's basic funcitionality
-- Store this script in /Library/Scripts/Script Editor/Script Cache Storage
-- Or whatever you think denotes those scripts best!
-- Purpose: Usage demonstration of the list library.
-- Author: McUsr 

use AppleScript version "2.3"
use scripting additions
use cacheLib : script "Script Cache Storage"


if (addItemToScriptStorage of cacheLib for "^.*" against "TestCache.scpt") then
	display notification "Added an item to the storage"
else
	display alert "something went wrong during adding and item"
	return
end if


set asText to returnListAsText of cacheLib from "TestCache.scpt"

set myCache to loadScript of cacheLib from "TestCache.scpt"
set end of myCache's itemslist to "!$"

if (storeScript of cacheLib from "TestCache.scpt" against myCache) then
	display notification "Saved the cache to disk"
else
	display alert "something went wrong during saving the list back to disk."
	return
end if
set myCache to loadScript of cacheLib from "TestCache.scpt"

  1. Save the two scripts below to the same the same folder as in #5: ( /Library/Script/Script Editor Scripts folder), play with them, and you should be good to go.

-- Name: Open ScriptCache's StoragePath
-- Store this script in /Library/Scripts/Script Editor/Script Cache Storage
-- Or whatever you think denotes those scripts best!
-- Purpose: Test that the _ScriptCacheStoragePath is initialized properly
-- Author: McUsr 

use AppleScript version "2.3"
use scripting additions


use store : script "Script Cache Storage"

tell application id "MACS" to open store's ScriptCacheStoragePath
do shell script "open -b \"com.apple.finder\"  &>/dev/null &"


--- Name: Put load Statement on clipboard 
-- Store this script in /Library/Scripts/Script Editor/Script Cache Storage
-- Or whatever you think denotes those scripts best!
-- Purpose: Create template code, ready to use as a vantage point.
-- Author: McUsr 

use AppleScript version "2.3"
use scripting additions
use cacheLib : script "Script Cache Storage"
property scriptTitle : "Put Load Statment on Clipboard"
on run
	set startText to "
use AppleScript version \"2.3\"
use scripting additions
use cacheLib : script \"Script Cache Storage\"

"
	
	
	set callStatements to "
set myCache to loadScript of cacheLib from \"--PLACEHOLDER--\"
storeScript of cacheLib from \"--PLACEHOLDER--\" against myCache
addItemToScriptStorage of cacheLib for \"Some list item\" against \"--PLACEHOLDER--\"
set asText to returnListAsText of cacheLib from \"--PLACEHOLDER--\"
-- >> You access the list thru myCache's itemslist <<
"
	
	
	tell application id "sevs" to set bid to bundle identifier of first process whose frontmost is true and visible is true
	
	tell application id "MACS"
		set targetFolder to (cacheLib's ScriptCacheStoragePath as text)
		set ids to get id of every Finder window
		script o
			property l : ids
		end script
		repeat with i from 1 to (count ids)
			try
				if (target of Finder window id (item i of o's l as integer) as text) = targetFolder then
					close (Finder window id (item i of o's l))
				end if
			end try
		end repeat
		tell (make new Finder window)
			set target of it to folder (cacheLib's ScriptCacheStoragePath as text)
			-- this is the simple solution
		end tell
	end tell
	do shell script "/usr/bin/open -b \"com.apple.finder\" ; sleep 0.3 " -- ; /usr/bin/open -b \"" & bid & "\""
	tell application (path to frontmost application as text)
		set answ to button returned of (display dialog "Is there a already a ScriptObject that you want to reuse?" buttons {"Make New", "Cancel", "Yes"} cancel button 2 default button 3 with title my scriptTitle with icon 1)
	end tell
	do shell script "/usr/bin/open -b \"" & bid & "\"  ; sleep 0.3 "
	tell application (path to frontmost application as text)
		
		if answ = "Yes" then
			set chosenScriptObject to (choose file default location cacheLib's ScriptCacheStoragePath of type {"osas"} with prompt "Choose a Script Object to use as Cache for the Call statements") as text
			
		else
			set chosenScriptObject to text returned of (display dialog "Enter the Name of your new ScriptCacheObject" default answer "ScriptObjectName" with title scriptTitle with icon 1)
			if chosenScriptObject is "" then
				display alert "You need to enter a name for your script object, please try again."
				return
			else
				if ".scpt" is not in chosenScriptObject then
					set chosenScriptObject to chosenScriptObject & ".scpt"
				end if
			end if
		end if
	end tell
	
	set astid to my text item delimiters
	set my text item delimiters to ":"
	set theFn to text item -1 of chosenScriptObject
	set my text item delimiters to "--PLACEHOLDER--"
	set trueCalls to text items of callStatements
	set my text item delimiters to theFn
	set trueCalls to trueCalls as text
	set my text item delimiters to astid
	set the clipboard to (startText & trueCalls)
	tell application (path to frontmost application as text)
		display alert "Load statements and call statements  for the script object: " & chosenScriptObject & " have been put onto the clipboard."
	end tell
	
end run

The script that lets you view the contents of the script objects that you have stored in a centralized location!


-- Name: Show contents of a script cache object
-- Store this script in /Library/Scripts/Script Editor/Script Cache Storage
-- Or whatever you think denotes those scripts best!
-- Purpose: Show the contents of the list of  a script cache object
-- Author: McUsr 

use AppleScript version "2.3"
use scripting additions
use cacheLib : script "Script Cache Storage"


tell application (path to frontmost application as text)
	set chosenScriptObject to (choose file default location cacheLib's ScriptCacheStoragePath of type {"osas"} with prompt "Choose a Script Object to display the List of.")
	set asText to returnListAsText of cacheLib from chosenScriptObject
	display alert asText
end tell


8.) This was just presentation of the basic functionality, read the code in the library, and see how you can override the standard path (and what kind of paths it can take), for your convenience. (Then you must modify other scripts, should you wish to peruse their contents, and such.)

Edit
27th of february 2015:

  • Removed a redundant comment, (a comment that was a copy of the line above).
  • Removed a constraint from the hfsPathOfAnything, as the constraint isn’t there anymore, you can have a quoted tick, inside a a filename surrounded by ticks: '~/tmp:Nick\‘s files’
  • Removed a line of code, that was redundant, and had been formerly commented out.

Hello.

Unfortunately, I had to remove a bug from the Cache Storage Library. The bug was that I used alias as an object specifier for the Store Script command, and not file.

This resulted in that no script object could be created with the Cache Storage Library.

It is fixed now, and I am sorry for any inconvenience.

Hello.

I have improved the documentation in post #1.

Thanks to Nigel Garvey who read thru it, and found several things that could be done better.

Hello.

The handler __hfsPathOfAnythingAsText(anyFileOrFolderPath) is now hardened in that it now deals correctly with “:”'s in posix paths, which Nigel Garvey reminded me of.

The handler __literalList from someList consists now of Nigel Garveys work, since he had such a much better and more elegant solution, than in addition to more readable, and faster, should work with Os X versions from way back when in addition.

Many thanks to Nigel Garvey, for constructive questions, comments, and solutions. :slight_smile:

Hello.

I had made a copy/paste error yesterday, which is now fixed, it was in __literalList, where I accidentally copied over the try error block, with the improved code that should have been inside it! I am very sorry!

I have also improved the “MakeLoadStatement” handler so you now can enter a name of a script object, if you don’t have any you want to use.

Hello.

I have improved the Put Load Statement Script further, to make it really user friendly.

Below are a demo, on its usage, which I find helpful when I have to swap items of text, and it is not happening in an app that has multiple clipboards, it consists of two scripts, and the idea is that you copy text or something to the clipboard, then you run SmarterCopy, to put that item a persistent list. When I start pasting, then I paste the items in same order as they went in, by running Smarter Paste, that way, I get the text rearranged.

Example

I copy item a to the clipboard, and run smarter copy.

I copy item b to the clipboard

I paste item b over item a

I run smarter paste

I paste item a over item b.

-- SmarterCopy
(*
	This script puts the last item entered in the cache onto the clipboard
		Illustrates the use of persistent storage
*)

use AppleScript version "2.3"
use scripting additions
use cacheLib : script "Script Cache Storage"

addItemToScriptStorage of cacheLib for (the clipboard as record) against "SmarterPaste.scpt"


-- SmarterPaste

(*
	This script puts the last item entered in the cache onto the clipboard.
	Illustrates the use of persistent storage
*)

use AppleScript version "2.3"
use scripting additions
use cacheLib : script "Script Cache Storage"

tell application id "sevs"
	if not (exists item ((my cacheLib's ScriptCacheStoragePath as text) & "SmarterPaste.scpt")) then
		set notFound to true
	else
		set notFound to false
	end if
end tell
if notFound then
	tell application (path to frontmost application as text)
		display alert "You must \"SmarterCopy\" something from the Clipboard before running \"SmarterPaste\"."
	end tell
	return
end if
set myCache to loadScript of cacheLib from "SmarterPaste.scpt"
if myCache's itemslist is {} then -- underflow
	beep
	tell application (path to frontmost application as text)
		display alert "There are no items left to move to the clipboard"
	end tell
else
	set the clipboard to (item -1 of myCache's itemslist) as record
	set item -1 of myCache's itemslist to missing value
	set myCache's itemslist to myCache's itemslist's records -- remove it
	storeScript of cacheLib from "SmarterPaste.scpt" against myCache
end if