A library of "Database Routines" for Apple Script

Hello.

This is a collection of handlers, as found in the now so venerable Scripting Additions Guide, while you hopefully still can get it. I just dump this here, for the hope that anybody can use it.

The stuff is copyright Apple I guess, but they never copyrighted it, I take the full responsibilty for any faults I may have introduced, and there are no warranties, but I hope someone may find them useful, while they read the Scripting Additions Guide.

It is rather unpolished, but it did work for me.

--Listing 2-1	Reading a specific record from a text-based database file
--first choose data file to work with
set pathToUse to choose file
try
	set x to open for access pathToUse
	set z to ReadRecord(16, 1, tab, "
", x)
	close access x
	z --display requested record
on error errString number errNum
	-- 72  Using Read/Write CommandsCHAPTER 2
	-- Scripting Addition Commands
	display dialog errString
	close access x
end try
on ReadRecord(numberOfFields, whichRecord, fieldDelimiter, recordDelimiter, fileRefNum)
	try
		(*
if there's a record delimiter, read all fields except for last using field delimiter, then
read last field using record delimiter *)
		if recordDelimiter is "" then
			set readxTimes to numberOfFields
		else
			set readxTimes to numberOfFields - 1
		end if
		repeat whichRecord times
			set recordData to {}
			repeat (readxTimes) times
				set recordData to recordData & {(read fileRefNum before fieldDelimiter)}
			end repeat
			if readxTimes is not numberOfFields then
				set recordData to recordData & {(read fileRefNum before recordDelimiter)}
			end if
		end repeat
		return recordData
	on error errString number errNum
		display dialog errString
		return errString
	end try
end ReadRecord

-- Listing 2-2	Deleting a record from a text-based database file
--choose data file to use

-- global startSize, idx, preRecordSize, accumulatedSize, readxTimes, fileBuffer, q
-- set q to 0
-- set startSize to 0
-- set preRecordSize to 0
-- set accumulatedSize to 0
-- set readxTimes to 0
-- Debugget MYE 3/6/2012 Tommy Bollman
-- A) sørget for at vi beholdt return på linja vi har lest inn.
-- B) Sørget for at den nye fil størrelse ble korrekt, med hensyn til beregning av EOF
-- C) Sørget for at vi ikke utfører ulovlige operasjoner ved purge. enten hele fra start, eller siste post.

set pathToUse to choose file
--74	Using Read/Write CommandsCHAPTER 2
-- Scripting Addition Commands
try
	set x to open for access pathToUse with write permission
	DeleteRecord(16, 1, ";", return, x)
	
	close access x
on error errString number errNum
	display dialog errString
	close access x
end try


on DeleteRecord(numberOfFields, whichRecord, fieldDelimiter, recordDelimiter, fileRefNum)
	try
		--initialize variables
		set startSize to get eof fileRefNum --current size
		set idx to 1 --counter
		set preRecordSize to 1 --offset of record to delete
		set accumulatedSize to 0 --total size of records read
		if recordDelimiter is "" then
			set readxTimes to numberOfFields
		else
			set readxTimes to numberOfFields - 1
		end if
		repeat with idx from 1 to whichRecord
			repeat (readxTimes) times
				set q to read fileRefNum as text until fieldDelimiter
				set accumulatedSize to accumulatedSize + (length of q)
			end repeat
			if readxTimes is not numberOfFields then
				set q to read fileRefNum as text until recordDelimiter
				set accumulatedSize to accumulatedSize + (length of q)
			end if
			
			-- Using Read/Write Commands	75
			-- CHAPTER 2
			--Scripting Addition Commands
			(*
if record to delete is the first record in file or the next record that will be read,
set preRecordSize *)
			if whichRecord is 1 or idx is whichRecord - 1 then
				if whichRecord is 1 then
					set preRecordSize to 0 -- var  1  BUG A korreksjon
				else
					set preRecordSize to accumulatedSize
				end if
			end if
		end repeat
		(* now that preRecordSize is determined, read the record to be deleted so file mark is
set to beginning of next record *)
		
		
		if (startSize - accumulatedSize) is not 0 then
			set fileBuffer to read fileRefNum from accumulatedSize + 1 -- BUG C korreksjon
			--next, overwrite record to be deleted with remainder of file
			write fileBuffer to fileRefNum as text starting at preRecordSize + 1 -- BUG A korreksjon
			set eof fileRefNum to (startSize - (accumulatedSize - preRecordSize)) -- BUG B korreksjon
		else
			(*
if the file contains only the record to be deleted,
set the end of the file to 0 *)
			if whichRecord is 1 then
				set eof fileRefNum to 0 (*
if record to be deleted is last record in file, just shrink the file *)
			else
				set eof fileRefNum to preRecordSize -- BUG A korreksjon INGEN;  recordskiller er med her
			end if
		end if
	on error errString number errNum
		display dialog errString
	end try
end DeleteRecord


-- Listing 2-3	Inserting a record in a database file
--choose file to work with
set pathToUse to choose file
-- Using Read/Write Commands	77CHAPTER 2
-- Scripting Addition Commands
try
	(* first put the record to be added into a variable; in this case the record to be added is actually an AppleScript list because the file on disk doesn't include label data *)
	set newRecord to {"Granny", "Knutsen", "123 Potato Chip Lane", "Palo Minnow", "CA", "98761", "Snackable Computer", "888-987-0987", "978 -234-5432", "123-985-1122"}
	set x to open for access pathToUse with write permission
	AddRecord(newRecord, -1, tab, return, x) -- addrecord # 5 uheldig når det ikke er 5 records i fil...
	close access x
on error errString number errNum
	display dialog errString
	close access x
end try

on AddRecord(recordToAdd, addWhere, fieldDelimiter, recordDelimiter, fileRefNum)
	try
		--initialize variables
		set idx to 1 --counter
		set preRecordSize to 1 --offset of byte at which to add file
		set accumulatedSize to 0 --total size of records read
		set numberOfFields to count of recordToAdd
		if recordDelimiter is "" then
			set readxTimes to numberOfFields
		else
			set readxTimes to numberOfFields - 1
		end if
		
		(* Addendum 9/6 2012 17:18 writing a record to the end of file, this can happen 
			when creating a file, or the item will be inserted at the end! *)
		
		if addWhere is -1 then
			set fileend to get eof of fileRefNum
			write "" to fileRefNum starting at fileend + 1 --sets write pointer to point at eof.
			WriteNewRecord(recordToAdd, fieldDelimiter, recordDelimiter, fileRefNum) --now add back the rest of the record
			return
		end if
		
		
		(*
if the record is to be added at the beginning of the file, this
If statement adds the record *)
		if addWhere is 1 then
			--read from beginning of file and store in postBuffer
			--offset of byte at which to add file 0 --total size of records read
			-- 78	Using Read/Write Commands
			--CHAPTER 2
			-- Scripting Addition Commands
			--		try
			-- for det tilfelle at fila var tom!
			set postBuffer to read fileRefNum from 1
			--	on error
			-- set postBuffer to ""
			--	end try
			(* before writing new record, file mark must be reset to beginning of file; to do this,
write an empty string to the beginning of file *)
			write "" to fileRefNum starting at 0
			WriteNewRecord(recordToAdd, fieldDelimiter, recordDelimiter, fileRefNum) --now add back the rest of the record
			write postBuffer to fileRefNum
			return
		end if
		
		(*
if the record is to be added somewhere other than at the beginning of the file, the rest of the AddRecord handler is executed *)
		repeat with idx from 1 to addWhere - 1
			repeat (readxTimes) times
				set q to read fileRefNum until fieldDelimiter
				set accumulatedSize to accumulatedSize + (length of q)
			end repeat
			if readxTimes is not numberOfFields then
				set q to read fileRefNum until recordDelimiter
				set accumulatedSize to accumulatedSize + (length of q)
			end if
		end repeat
		(* read from beginning of file to the byte at which the new record is to be added *)
		set postBuffer to read fileRefNum from accumulatedSize + 1
		(* before writing new record,
set file mark to byte at which new record is to be added; to do this,
write an empty string to that byte *)
		write "" to fileRefNum starting at accumulatedSize + 1
		WriteNewRecord(recordToAdd, fieldDelimiter, recordDelimiter, fileRefNum)
		-- Using Read/Write Commands	79
		-- CHAPTER 2
		--Scripting Addition Commands
		--now add back the rest of the record
		write postBuffer to fileRefNum
	on error errString number errNum
		display dialog errString
	end try
end AddRecord



on WriteNewRecord(recordToAdd, fieldDelimiter, recordDelimiter, fileRefNum)
	try
		set numberOfFields to count of recordToAdd
		if recordDelimiter is "" then
			set readxTimes to numberOfFields
		else
			set readxTimes to numberOfFields - 1
		end if
		repeat with idx from 1 to numberOfFields
			if idx ≤ readxTimes then
				write item idx of recordToAdd & fieldDelimiter to fileRefNum
			else
				(*
if file uses a record delimiter,
write delimiter after the last field in the record *)
				write item idx of recordToAdd & recordDelimiter to fileRefNum
			end if
		end repeat
	on error errString number errNum
		-- 80	Using Read/Write CommandsCHAPTER 2
		--Scripting Addition Commands
		display dialog errString
	end try
end WriteNewRecord

script textDatabase
	
	-- Start Clipping:
	-- clippingsname: OpenFileIfItExists({hfsPathAsText;theFile,blnIfWritingAllowed;writePermission,forceCreation;blnIfMakeFileSilently,procnameAsText;scriptName,appBundleId;frontappId}) -- Returns Errnumber, or File Reference Number
	-- clippingscontents: textDatabase's OpenFileIfItExists({hfsPathAsText:theFile,blnIfWritingAllowed:writePermission,forceCreation:blnIfMakeFileSilently,procnameAsText:scriptName,appBundleId:frontappId}) -- Returns Errnumber, or File Reference Number
	-- End Clipping:
	-- on OpenFileIfItExists({hfsPathAsText:theFile,blnIfWritingAllowed:writePermission,forceCreation:blnIfMakeFileSilently,procnameAsText:scriptName,appBundleId:frontappId}) -- Returns Errnumber, or File Reference Number
	on OpenFileIfItExists(R) -- Returns Errnumber, or File Reference Number
		-- R : {hfsPathAsText:theFile,blnIfWritingAllowed:writePermission,forceCreation:blnIfMakeFileSilently,procnameAsText:scriptName,appBundleId:frontappId}
		local AlertStopIconFile, x, theFirstErrMsg, theSecondErrMsg, firstErrorNum, secondErrorNum, thethirdErrMsg, thirdErrnumber, btnResult, failed, thefourthErrMsg, fourthErrnumber
		
		if not (forceCreation of R) then
			set AlertStopIconFile to a reference to file ((path to library folder from system domain as text) & "CoreServices:CoreTypes.bundle:Contents:Resources:AlertStopIcon.icns")
			-- set AlertStopIconFile to textDatabase's EnvironmentLIb's getResourceIconFile({icsFile:"AlertStopIcon.icns", appBundleId:(appBundleId of R)})
		end if
		set V to {emsg:"", num:0}
		try
			(*
if ( hfsPathAsText of R) doesn't exist, Info For returns error -43 *)
			set x to info for file (hfsPathAsText of R)
			if (blnIfWritingAllowed of R) is true then
				set num of V to (open for access file (hfsPathAsText of R) with write permission)
			else
				--82	Using Read/Write CommandsCHAPTER 2
				--Scripting Addition Commands
				set num of V to (open for access file (hfsPathAsText of R))
			end if
		on error theFirstErrMsg number firstErrorNum
			if not (forceCreation of R) then
				
				try
					--if error is -43, the user can choose to create the file
					set btnResult to ""
					tell application "SystemUIServer"
						activate
						set failed to false
						try
							set btnResult to button returned of (display dialog (my textDatabaseprivate's makePosixPath(hfsPathAsText of R) & "
Does Not Exist") with title (procnameAsText of R) buttons {"Create It For Me", "Cancel", "Ok"} default button 2 cancel button 2 with icon AlertStopIconFile)
						on error theSecondErrMsg number secondErrorNum
							if n is -128 then
								set failed to true
							else
								set emsg of V to theSecondErrMsg
								set num of V to secondErrorNum
								return V
							end if
						end try
					end tell
					if failed is true then
						set emsg of V to "User Aborted It"
						set num of V to -60000
						return V
					end if
					if btnResult is "Ok" then
						set emsg of V to theFirstErrMsg
						set num of V to firstErrorNum
						return V
					else
						--create the file
						if (blnIfWritingAllowed of R) is true then
							set num of V to open for access file (hfsPathAsText of R) with write permission
						else
							set num of V to open for access file (hfsPathAsText of R)
						end if
					end if
				on error thethirdErrMsg number thirdErrnumber
					set emsg of V to thethirdErrMsg
					set num of V to thirdErrnumber
				end try
				return V
			else -- just force the creation
				try
					if (blnIfWritingAllowed of R) is true then
						
						set num of V to open for access file (hfsPathAsText of R) with write permission
					else
						set num of V to open for access file (hfsPathAsText of R)
					end if
				on error thefourthErrMsg number fourthErrnumber
					set emsg of V to thefourthErrMsg
					set num of V to fourthErrnumber
				end try
				return V
			end if
			
		end try
		return V
	end OpenFileIfItExists
	
	
	-- 	on OpenFileIfItExists(theFile, writePermission)
	-- 		set AlertStopIconFile to my EnvironmentLIb's getResourceIconFile({icsFile:"AlertStopIcon.icns", appBundleId:frontappId})
	-- 		try
	-- 			(*
	-- if theFile doesn't exist, Info For returns error -43 *)
	-- 			set x to info for file theFile
	-- 			if writePermission is true then
	-- 				return (open for access file theFile with write permission)
	-- 			else
	-- 				--82	Using Read/Write CommandsCHAPTER 2
	-- 				--Scripting Addition Commands
	-- 				return (open for access file theFile)
	-- 			end if
	-- 		on error theErrMsg number errorNum
	-- 			try
	-- 				--if error is -43, the user can choose to create the file 
	-- 				set btnResult to ""
	-- 				tell application "SystemUIServer"
	-- 					activate
	-- 					set failed to false
	-- 					try
	-- 						set btnResult to button returned of (display dialog theFile & "
	-- Does Not Exist" with title "Open File If It Exists" buttons {"Create It For Me", "Cancel", "Ok"} default button 2 cancel button 2 with icon file AlertStopIconFile)
	-- 					on error e number n
	-- 						set failed to true
	-- 					end try
	-- 				end tell
	-- 				if failed is true then error number -6000
	-- 				if btnResult is "Ok" then
	-- 					return errorNum
	-- 				else
	-- 					--create the file
	-- 					if writePermission is true then
	-- 						return open for access file theFile with write permission
	-- 					else
	-- 						return open for access file theFile
	-- 					end if
	-- 				end if
	-- 			on error theErrMsg number theerrnumber
	-- 				return theerrnumber
	-- 			end try
	-- 		end try
	-- 	end OpenFileIfItExists
	
	
	on writeheadOfFileReferenceAsUtf8BOM(fRef, stuff) -- Returns record, to store any error message in.
		-- ! NB! det siste tegnet i stuff bør være en newline...
		-- hakket mitt besørger at fil blir til ut f8 selv om det bare havner ascii i den 
		-- etterpå!, så tegnsettet kommer korrekt ut  i preview!
		
		local stuff, fRef, Msg, n, mybuffer, startsize, fsz, V
		set V to {emsg:"", num:0}
		try
			set startsize to get eof fRef
			if not startsize is 0 then
				set mybuffer to read fRef from 1 to startsize as «class utf8»
			else
				write «data rdatEFBBBF» to fRef -- as «class utf8» -- BOM Thanks! to Nigel Garvey
			end if
			
			write stuff to fRef as «class utf8» starting at 4
			if not startsize is 0 then
				write mybuffer to fRef as «class utf8»
			end if
			set fsz to get eof fRef
			close access fRef
			set num of V to fsz
		on error Msg number n
			try
				close access fRef
			end try
			set emsg of V to Msg
			set num of V to n
		end try
		return V
	end writeheadOfFileReferenceAsUtf8BOM
	
	-- mark jfileLib¢writeTo<B
	-- >> jfileLib¢writeTo
	(*
writeTo
writes given data to given file as given mode, and returns an alias to the file

Parameters:
f: file path, alias, posix path (the file to be written, can exist or not)
stuff: stuff to write to the file
mode: how to write the data (text, list, record, «class utf8», C string, etc)

Example:
writeTo("path:to:file.txt", {1,2,3}, list) --> alias "path:to:file.txt"
*)
	
	to writeToFileReferenceAsUtf8BOM(fRef, stuff, mode)
		set V to {emsg:"", num:0}
		
		local stuff, fRef, Msg, n, startsize, fsz, V
		
		try
			set eof of fRef to 0
			write «data rdatEFBBBF» to fRef -- as «class utf8» -- BOM Thanks! to Nigel Garvey
			write stuff to fRef as «class utf8» starting at 4
			set fsz to get eof fRef
			close access fRef
			set num of V to fsz
		on error Msg number n
			try
				close access fRef
			end try
			set emsg of V to Msg
			set num of V to n
		end try
		return V
	end writeToFileReferenceAsUtf8BOM
	
	
	on writeheadOfFileReferenceAsUtf8(fRef, stuff)
		-- ! NB! det siste tegnet i stuff bør være en newline...
		-- hakket mitt besørger at fil blir til ut f8 selv om det bare havner ascii i den 
		-- etterpå! Virker i bbedit, ikke i preview!
		
		local stuff, fRef, Msg, n, mybuffer, startsize, fsz
		
		try
			set startsize to get eof fRef
			if not startsize is 0 then
				set mybuffer to read fRef from 1 to startsize as «class utf8»
			else
				write "æøå" to fRef starting at 0 as «class utf8» --utf8 no bom!
			end if
			set eof fRef to 0
			write stuff to fRef as «class utf8»
			if not startsize is 0 then
				write mybuffer to fRef as «class utf8»
			end if
			set fsz to get eof fRef
			close access fRef
			return fsz
		on error Msg number n
			try
				close access fRef
			end try
			return n
		end try
	end writeheadOfFileReferenceAsUtf8
	
	
	
	on writeTailOfFileReferenceAsUtf8(fRef, stuff)
		-- Denne brukes til begge utf8 variantene.
		local stuff, fRef, Msg, n, fsz
		try
			write stuff to fRef starting at eof as «class utf8»
			set fsz to get eof fRef
			close access fRef
			return fsz
		on error Msg number n
			try
				close access fRef
			end try
			return n
		end try
	end writeTailOfFileReferenceAsUtf8
	
	
	on writeheadOfFileReference(fRef, stuff)
		-- ! NB! det siste tegnet i stuff bør være en newline...
		-- Skriver ikke u-tf 8 automatisk. på min maskin...
		-- det avhenger vel av hva slags tegn som kommer inn der...
		-- mac roman kan ofte bli resultatet
		local stuff, fRef, Msg, n, mybuffer, startsize, fsz
		
		try
			set startsize to get eof fRef
			if not startsize is 0 then
				set mybuffer to read fRef from 1 to startsize
				set eof fRef to 0
			end if
			write stuff to fRef starting at 0
			if not startsize is 0 then
				write mybuffer to fRef
			end if
			set fsz to get eof fRef
			close access fRef
			return fsz
		on error Msg number n
			try
				close access fRef
			end try
			return n
		end try
	end writeheadOfFileReference
	
	
	on writeTailOfFileReference(fRef, stuff)
		local stuff, fRef, Msg, n, fsz
		try
			write stuff to fRef starting at eof
			set fsz to get eof fRef
			close access fRef
			return fsz
		on error Msg number n
			try
				close access fRef
			end try
			return n
			-- error msg number n
		end try
	end writeTailOfFileReference
	
	script textDatabaseprivate
		----------------------------------------------------------------------------------
		-- unixpath method:
		-- 		Get the posix path (unix path).
		-- 		Doesn't handle accented characters. I'm open to suggestions that are not based on shell wildcards (too dangerous when deleting file(s)).
		----------------------------------------------------------------------------------
		-- argument:
		--		macpath: an alias or path string
		-- return:
		--		posix path string
		----------------------------------------------------------------------------------
		-- nicked from BBEdit lib
		on makePosixPath(macpath)
			set unixpath to POSIX path of macpath
			set unixpathlist to every character of unixpath
			repeat with i from 1 to length of unixpathlist
				-- took badchars list from http://www.macosxhints.com/article.php?story=20011217040113759 
				set badchars to {" ", "'", "!", "$", "\"", "*", "(", ")", "{", "[", "|", ";", "<", ">", "?", "~", "`", "\\"}
				set thisItem to item i of unixpathlist as text
				if thisItem is in badchars then set item i of unixpathlist to ("\\" & thisItem)
			end repeat
			set unixpath to unixpathlist as string
			return unixpath
		end makePosixPath
	end script
	
end script

OS X has a built-in scriptable database application named Database Events (located in CoreServices folder)
which is quite unknown but easy to use

That is true, I have used it.

The code above makes for keeping a “database” in a CSV file, I should have mentioned that.

Actually at one point in time, I wondered about using the code above to export info from a database-events database into CSV. Which should be no problem really. :slight_smile:

An proper CSV uses string/text indicators (string fields are quoted) so it’s not 100% CSV compatible. Therefore I’ve written an C command line utility that reads an CSV file and prints out AppleScript code than only need eval. In AppleScriptObjC I’m using Objective-C (with C code) and put it all in an NSArray (never coerce to appleScript list).

Please share it!

We both know that if we have the files, we can just “cat” the code into a directory, and type make from a terminal window! (I think the make that ships with developertools, knows where to find the frameworks by itself, the code won’t be optimally compiled, but it should compile.) I volunteer! :smiley:

I’ll been abit lazy, I have the code to create “RFC-compliant” CSV, that is, you can specify a delimiter, the text are put between hyphens, and the delimiter is escaped within the string.

Maybe I should do that in Applescript? (I should add escapes for any hyphens too I guess.)

I’ll do this a little by little.