A generic Algorithm for scripting Finder Selections, and chosen files.

Hello.

Some of the scripts we write, are scripts that does something on the Finder selection, or lets us do something on files we select. I have tried to merge those two cases into one; if nothing is selected, or valid, by filetype, or other parameters, then choose files, (or folders).

When we do have something, to work on, then do the work on the items, one, or many, and do any postprocessing too.

I have chosen to split the work above into an algorithm, and a script object, with several mandatory handlers, that you must fill in with your details. It may seem daunting at first, but when you have finished off your first implementation, then you have a good grasp, and you can just copy your previous object as a “blue-print” for the next script.

The algorithm, that controls the selection, and what is to be done, is totally transparent, to the type of file reference you elect to work with, and what you do with the files in the script, so it is highly reusable, which has the advantage, that you or others will have a much higher degree of confidence in your file-handling scripts, since they all behave alike.

The code below, is working, but it is all mocked up into a single file for simplicity, so, I have a working demo. Feel free to organize it as you please, if you think that this is something for you, after having played with it. (I have it all arranged in different AS 2.3 Libraries.)

Enjoy. :slight_smile:


-- Copyright © 2015 McUsr, you are free to use, and share per email, but not to publish on a webpage or in a book please refer to the link below
-- http://www.macscripter.net/viewtopic.php?pid=179364#p179364
-- You are free to derive the algorithm, and modify it to suit your own needs, but you are not free to publish them as is, as  your own work, or any derivations, as  your own idea.
property TopLevel : me

property scripttitle : "Make ul>li Listing of selected Files."

script fileListObj
	property nameExts : {} -- Contains any extensison , {} means "Anything goes"
	property fileUti : {"public.item"}
	
	on obtainFileList(scripttitle)
		-- Wrapper for getting the selection from Finder
		return TopLevel's selectionListMaybeIncludingFoldersAsPxPath(scripttitle)
	end obtainFileList
	
	on preparateFileList(theList)
		-- use this handler, to convert the alias list into something different.
		-- if you choose to use theAlias list, let this handler be empty!
		script o
			property l : theList
		end script
		-- We'll have to create the list of aliases into a list of posix path
		repeat with i from 1 to (count theList)
			set item i of o's l to POSIX path of item i of o's l
		end repeat
	end preparateFileList
	
	on okToProcessFile(aFile)
		-- A wrapper for writing a test if the file is something we'll process
		-- This works in tandem with obtainfileList and preparateFileList, alias-alias, posix-posix, and so on.
		return TopLevel's validFileExtOfPxPath(aFile, my nameExts)
		
	end okToProcessFile
	
	on processOneFile(theList, aFile)
		processManyFiles(theList, aFile, 0)
		return 1 -- mandatory
	end processOneFile
	
	on processManyFiles(theList, aFile, theItemNUmber)
		if theItemNUmber is 0 then set end of theList to "<ul>"
		-- To keep the "Algorithm" clean from implementation details
		set end of theList to (tab & "<li>" & (tildeContractPxPath of TopLevel for aFile) & "</li>")
		set theItemNUmber to theItemNUmber + 1
		return theItemNUmber
	end processManyFiles
	
	on postProcess(theList, numberOfItems)
		set end of theList to "</ul>"
		set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, linefeed}
		set the clipboard to theList as text
		set AppleScript's text item delimiters to tids
	end postProcess
	
	on aliasOfFolder(aFile)
		-- this mandatory handler, is for returning an alias to a parent folder, 
		-- that is to be the default location for the choose file dialog, for the case that nothing
		-- that was selected was valid for processing,, so that we have a connection to where we were,
		-- either from te Finder window, or from the choose file dialog.
		tell application id "MACS"
			return (container of (POSIX file (aFile) as alias) as alias)
		end tell
	end aliasOfFolder
end script


selectAndProcessFiles(scripttitle, fileListObj, "Putting ul>li list onto Clipboard")

(* the main library, consisiting of "the Algorithm", and any handlers that returns the selection *)

on selectAndProcessFiles(scripttitle, jobObject, doneMessage)
	set fileListReceived to {}
	set defLoc to missing value
	-- initial selection of files, from a finder window or the desktop
	
	set fileListReceived to jobObject's obtainFileList(scripttitle)
	
	repeat
		if length of fileListReceived = 0 then
			if defLoc is missing value then tell application id "MACS" to set defLoc to (insertion location as alias)
			-- http://macscripter.net/viewtopic.php?id=43732 juanfal
			try
				tell application (path to frontmost application as text)
					set fileListReceived to (choose file with prompt scripttitle & ":
Choose the file(s) you want to make an ul>li listing of:" of type jobObject's fileUti default location defLoc with multiple selections allowed)
				end tell
				jobObject's preparateFileList(fileListReceived)
			on error e number n
				return
			end try
		end if
		set resultList to {}
		if length of fileListReceived = 1 then
			set workFile to item 1 of fileListReceived
			if jobObject's okToProcessFile(workFile) then
				set numProcessed to jobObject's processOneFile(resultList, workFile)
				exit repeat
			end if
		else
			set numProcessed to 0
			repeat with workFile in fileListReceived
				if jobObject's okToProcessFile(workFile) then
					set numProcessed to jobObject's processManyFiles(resultList, workFile, numProcessed)
				end if
			end repeat
			if numProcessed > 0 then exit repeat
			-- else, we are going back and finding something.
		end if
		-- if we end up here, then it seems we may have to choose some objects to work on
		-- after all, or cancel the whole thing as the finder selection didn't do the trick
		-- for us, so, below we prepare to receive something.
		set defLoc to jobObject's aliasOfFolder(item 1 of fileListReceived)
		set fileListReceived to {}
	end repeat
	
	display notification doneMessage with title scripttitle
	jobObject's postProcess(resultList, numProcessed)
end selectAndProcessFiles

(*the handlers below are necessary for "the Algorithm" *)

(* Two different handlers that gets file lists without, or with folder/contents *)

on selectionListingAsPxPath() --  not in use in this demo.
	-- returns the current selection as a text-list of px paths
	local itemList, tids
	script o
		property l : missing value
	end script
	-- fetch the selected items
	tell application id "MACS" to set o's l to selection as alias list
	-- make a list of posix paths
	set itemList to {}
	repeat with i from 1 to (count o's l)
		set end of itemList to POSIX path of item i of o's l
	end repeat
	return itemList
end selectionListingAsPxPath


on selectionListMaybeIncludingFoldersAsPxPath(scripttitle)
	-- returns the current selection as a text-list of px paths
	local itemList, tids
	script o
		property l : missing value
	end script
	script p
		property l : missing value
	end script
	
	-- fetch the selected items
	tell application id "MACS" to set o's l to selection as alias list
	-- make a list of posix paths
	set itemList to {}
	repeat with i from 1 to (count o's l)
		tell application id "sevs" to set wasFolder to (class of disk item ((item i of o's l) as text) is folder)
		if wasFolder then
			try
				tell application (path to frontmost application as text)
					display dialog "One of the selected items was a folder, do you want to include any Contents of the Folder, or to include the Folder itself?" buttons {"Cancel", "Contents", "Folder"} default button "Folder" cancel button 1 giving up after 300 with icon 1 with title scripttitle
				end tell
				copy {button returned, gave up} of result to {whatToList, gaveUp}
				if gaveUp then return {}
			on error
				return {}
			end try
			
			if whatToList is "Contents" then
				tell application "Finder"
					set probe to folder (item i of o's l as text)
					set p's l to (items of folder (item i of o's l as text)) as alias list
				end tell
				repeat with j from 1 to (count p's l)
					set end of itemList to POSIX path of item j of p's l
				end repeat
			else
				set end of itemList to POSIX path of item i of o's l
			end if
		else
			set end of itemList to POSIX path of item i of o's l
		end if
	end repeat
	
	return itemList
end selectionListMaybeIncludingFoldersAsPxPath

on validFileExtOfPxPath(fileNm, extList)
	local fileExt
	if extList is {} then return true
	set fileExt to my fileExtOfPxPath(fileNm)
	ignoring case
		if fileExt is in extList then
			return true
		else
			return false
		end if
	end ignoring
end validFileExtOfPxPath

on fileExtOfPxPath(pxPath)
	local astid, basicFn, fileExt
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to "/"
	set basicFn to (item -1 of text items of pxPath)
	set AppleScript's text item delimiters to "."
	set fileExt to (item -1 of text items of basicFn) as Unicode text
	set AppleScript's text item delimiters to astid
	return fileExt
end fileExtOfPxPath


(* ======================================================== *)

(* Handlers that really belong to other libraries, but are included here  for demo  purposes *)
-- This handler is here, just for this particular job.
on tildeContractPxPath for aPxPath
	set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, POSIX path of (path to home folder)}
	set aPxPath to text items of aPxPath
	set AppleScript's text item delimiters to "~/"
	set aPxPath to aPxPath as text
	set AppleScript's text item delimiters to tids
	return aPxPath
end tildeContractPxPath



Edit
Removed two typos, in the description.
Added credit to juanfal for the insertion location.
Moved the comment about necessary handlers to a more logical place than the bottom.

Hello.

This was a highly successful project for me, and if you have suites of scripts that may use the same kind of algorithm, I recommend such an approach. I have re-used this for some 5-6 scripts now, where the task involved are from create an UL>LI list in html, to list contents of archieves from a variety of archieve formats, (1 script per format, except for gzip), and to count the numbers of pages in PDF documents, and sum them.

One of the advantages of this approach, is if I am going to change the processing algorithm, then I just have to change the algorithm in one place, and just recompile the scripts that uses them! :slight_smile:

The methods in the object, stands the test, whether you require no post processing, as is the case with UL>LI, or a lot of post processing, as with count and sum PDF pages, where the regular processMany handler just is used for finding the max length of base names, so that I can format the report properly, (filenames and basenames) are shoveled over to a new structure in processMany(), which then are iterated over in postProcess()

Along the way, I have changed the library a little, so that I have a “prototype object” with methods, so that I don’t have to override more of them than I need in a new script.

The AS 2.3 Library mm: (MY MACS)

use AppleScript version "2.3"
use scripting additions
(*

My collection of coalesced handlers for scripting finder, aka MACS.
Every finderLibrary I have is to turn up here!

*)


-- selectionListing()
on selectionListingAsPxPath() --  for delimiter
	-- returns the current selection as a text-list of px paths
	local itemList, tids
	script o
		property l : missing value
	end script
	-- fetch the selected items
	tell application id "MACS" to set o's l to selection as alias list
	-- make a list of posix paths
	set itemList to {}
	repeat with i from 1 to (count o's l)
		set end of itemList to POSIX path of item i of o's l
	end repeat
	(*
	-- create a textual file list with the pxPaths
	set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delimiter}
	set itemList to items of itemList as text
	set AppleScript's text item delimiters to tids
	*)
	return itemList
end selectionListingAsPxPath


(*
	Sligthly more complicated than above.
	
*)

on selectionListMaybeIncludingFoldersAsPxPath(scripttitle)
	(*
	Mny ways to do it with a folder:
	 folder and contents if is any contents
	 just the contents, if is any contents.
	 just folder in any case.
	 skip the folder all together
	
	*)
	-- returns the current selection as a text-list of px paths
	local itemList, tids
	script o
		property l : missing value
	end script
	script p
		property l : missing value
	end script
	
	-- fetch the selected items
	tell application id "MACS" to set o's l to selection as alias list
	-- make a list of posix paths
	set itemList to {}
	repeat with i from 1 to (count o's l)
		tell application id "sevs" to set wasFolder to (class of disk item ((item i of o's l) as text) is folder)
		if wasFolder then
			try
				tell application (path to frontmost application as text)
					display dialog "One of the selected items was a folder, do you want to include any Contents of the Folder, or to include the Folder itself?" buttons {"Cancel", "Contents", "Folder"} default button "Folder" cancel button 1 giving up after 300 with icon 1 with title scripttitle
				end tell
				copy {button returned, gave up} of result to {whatToList, gaveUp}
				if gaveUp then return {}
			on error
				return {}
			end try
			
			if whatToList is "Contents" then
				tell application "Finder"
					set probe to folder (item i of o's l as text)
					set p's l to (items of folder (item i of o's l as text)) as alias list
				end tell
				repeat with j from 1 to (count p's l)
					set end of itemList to POSIX path of item j of p's l
				end repeat
			else
				set end of itemList to POSIX path of item i of o's l
			end if
		else
			set end of itemList to POSIX path of item i of o's l
		end if
	end repeat
	(*
	-- create a textual file list with the pxPaths
	set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, delimiter}
	set itemList to items of itemList as text
	set AppleScript's text item delimiters to tids
	*)
	return itemList
	
	
end selectionListMaybeIncludingFoldersAsPxPath

The latest version of the library “SelectAndProcessFiles” that is the cog-wheel here:

use AppleScript version "2.3"
use scripting additions
use mm : script "MY MACS"
use fn : script "FileNames"
(*
	It works like this: 
		if there wasn't a selection, then we  give the option of selecting any archives we want to open.
		
		if there was a selection in Finder then we grab that selection, and we check to see if the items is of 
		the kind we want.
		
		If we have processed any files, then we are done, and exits the repeat loop.
		
		if we didn't process any files, then we try again, and again, and again, until the user hits
		"Cancel" on the "choose file dialog".
*)


to makeFileObj()
	-- You can override any method you like in here, but you must be cautios.
	script fileObject
		property nameExts : missing value
		property fileUti : missing value
		
		on obtainFileList(scriptTitle)
			-- Wrapper for getting the selection from Finder
			-- This could really be the default implementation
			return mm's selectionListingAsPxPath()
		end obtainFileList
		
		on preparateFileList(theList)
			-- use this handler, to convert the alias list into something different.
			-- if you choose to use theAlias list, let this handler be empty!
			-- This handler is called exclusively after choose file, that is,
			-- when the Finder selection was empty.
			script o
				property l : theList
			end script
			-- We'll have to create the list of aliases into a list of posix path
			repeat with i from 1 to (count theList)
				set item i of o's l to POSIX path of item i of o's l
			end repeat
		end preparateFileList
		
		on okToProcessFile(aFile)
			-- A wrapper for writing a test if the file is something we'll process
			-- This works in tandem with obtainfileList and preparateFileList, alias-alias, posix-posix, and so on.
			return fn's validFileExtOfPxPath(aFile, my nameExts)
			
		end okToProcessFile
		
		on processOneFile(theList, aFile)
			-- This is just a filler method, we could have inherited
			processManyFiles(theList, aFile, 0)
			return 1 -- mandatory
		end processOneFile
		
		on processManyFiles(theList, aFile, theItemNUmber)
			error "At least you'll have to implement this one!"
			set theItemNUmber to theItemNUmber + 1
			return theItemNUmber
		end processManyFiles
		
		on postProcess(theList, numberOfItems)
			-- Empty method in this context, but we could have given an account of 
			-- the number of Archieves prosessed, had that been interesting.
		end postProcess
		
		on aliasOfFolder(aFile)
			-- this mandatory handler, is for returning an alias to a parent folder, 
			-- that is to be the default location for the choose file dialog, for the case that nothing
			-- that was selected was valid for processing,, so that we have a connection to where we were,
			-- either from te Finder window, or from the choose file dialog.
			tell application id "MACS"
				return (container of (POSIX file (aFile) as alias) as alias)
			end tell
		end aliasOfFolder
		
		
	end script
end makeFileObj


on selectAndProcessFiles(scriptTitle, jobObject, chooseFileMessage, doneMessage)
	set fileListReceived to {}
	set defLoc to missing value
	-- initial selection of files, from a finder window or the desktop
	
	set fileListReceived to jobObject's obtainFileList(scriptTitle)
	
	repeat
		if length of fileListReceived = 0 then
			if defLoc is missing value then tell application id "MACS" to set defLoc to (insertion location as alias)
			-- http://macscripter.net/viewtopic.php?id=43732 juanfal
			try
				tell application (path to frontmost application as text)
					set fileListReceived to (choose file with prompt scriptTitle & ":
" & chooseFileMessage of type jobObject's fileUti default location defLoc with multiple selections allowed)
				end tell
				jobObject's preparateFileList(fileListReceived)
			on error e number n
				return
			end try
		end if
		set resultList to {}
		if length of fileListReceived = 1 then
			set workFile to item 1 of fileListReceived
			if jobObject's okToProcessFile(workFile) then
				set numProcessed to jobObject's processOneFile(resultList, workFile)
				exit repeat
			end if
		else
			set numProcessed to 0
			repeat with workFile in fileListReceived
				if jobObject's okToProcessFile(workFile) then
					set numProcessed to jobObject's processManyFiles(resultList, workFile, numProcessed)
				end if
			end repeat
			if numProcessed > 0 then exit repeat
			-- else, we are going back and finding something.
		end if
		-- if we end up here, then it seems we may have to choose some objects to work on
		-- after all, or cancel the whole thing as the finder selection didn't do the trick
		-- for us, so, below we prepare to receive something.
		set defLoc to jobObject's aliasOfFolder(item 1 of fileListReceived)
		set fileListReceived to {}
	end repeat
	
	display notification doneMessage with title scriptTitle
	jobObject's postProcess(resultList, numProcessed)
end selectAndProcessFiles

Example implementation of relatively “advanced” file processing script:

use AppleScript version "2.3"
use scripting additions
-- use fn : script "FileNames"
-- use mtxt : script "TextEdit"
use fileProc : script "SelectAndProcessFiles"
-- necessary properties for an archive 


property scripttitle : "List page numbers of Pdf-Files"


(*
 If there is just one file, we'll just show the number in a dialog.
 if there are many, then we'll generate a list, which we'll postprocess
 and sum the total of.

The criteria for postprocessing is that there are more than one file.
*)

script fn
	on baseNameOfPxPath for pxPath
		local tids, basename, i
		-- NG http://macscripter.net/viewtopic.php?id=38680
		set {tids, text item delimiters, i} to {text item delimiters, "/", ((pxPath ends with "/") as integer) + 1}
		set {basename, text item delimiters} to {text item -i of pxPath, tids}
		return basename
	end baseNameOfPxPath
	
	to parentFolOfPxPath for pxPath
		-- Assumes no superfluous slashes
		local tids, parFol, i
		set {tids, text item delimiters, i} to {text item delimiters, "/", ((pxPath ends with "/") as integer) + 1}
		set {parFol, text item delimiters} to {text 1 thru text item -(i + 1) of pxPath, tids}
		return parFol
	end parentFolOfPxPath
	
end script

script mtxt
	on spaces(numberOf)
		local astid
		set astid to AppleScript's text item delimiters
		set AppleScript's text item delimiters to ""
		local i, s, theTabs, theSpac
		set s to ""
		set i to 1
		repeat while i < numberOf
			set s to s & space
			set i to i + 1
		end repeat
		set AppleScript's text item delimiters to astid
		return s
	end spaces
	
	on pages(nrPages)
		if nrPages ≠ 1 then
			return " pages."
		else
			return " page."
		end if
	end pages
	
	
	on rgthJustifyNumber(anum)
		if anum div 1000 ≠ 0 then
			return anum
		else if anum div 100 ≠ 0 then
			return space & anum
		else if anum div 10 ≠ 0 then
			return space & space & anum
		else
			return space & space & space & anum
		end if
	end rgthJustifyNumber
	
	on showList(theList)
		tell application id "ttxt"
			make new document
			set {tids, AppleScript's text item delimiters} to {AppleScript's text item delimiters, linefeed}
			set text of its document 1 to theList as Unicode text
			set AppleScript's text item delimiters to tids
			tell document 1
				-- set properties of its text to {size:16.0, color:{0, 0, 0}, font:"CourierNewPSMT", class:text}
				set size of its text to 13.0
				set color of its text to {0, 0, 0}
				set font of its text to "CourierNewPSMT"
			end tell
			tell its window 1
				set its bounds to {200, 100, 1000, 800}
			end tell
			-- set the font of every character of every paragraph of (its document 1) to "Courier"
			activate
		end tell
	end showList
	
end script

property fileObj : fileProc's makeFileObj()
--  Mandatory, for building an inheritance chain.

script listAndSumPdfPageNumbers
	property parent : fileObj
	property nameExts : {"pdf"}
	property fileUti : {"com.adobe.pdf"}
	
	-- Properties that the original "prototype" of our script object is augemented with
	
	script PdfPageCounter
		property totalPages : 0
		property baseNameList : {}
		property maxLen : 0
		
		on updateMaxLen(fnLen)
			if fnLen > maxLen then set maxLen to fnLen
		end updateMaxLen
		
		on missingSpaces(fnLen)
			if fnLen < maxLen then
				set numspaces to (maxLen - fnLen) + 1
			else
				set numspaces to 1
			end if
			return numspaces
		end missingSpaces
		
		on pgCount(posixFn, basename, spacing)
			local fileName, pageCount
			set fileName to quoted form of posixFn
			set pageCount to last word of (do shell script "mdls " & fileName & " | grep kMDItemNumberOfPages")
			set totalPages to totalPages + pageCount
			return basename & mtxt's spaces(spacing) & ": " & mtxt's rgthJustifyNumber(pageCount) & mtxt's pages(pageCount)
		end pgCount
	end script
	
	
	on processOneFile(theList, aFile)
		-- This is just a filler method, we could have inherited
		tell application (path to frontmost application as text)
			try
				display dialog my PdfPageCounter's pgCount(aFile, (baseNameOfPxPath of fn for aFile), 1) with title scripttitle buttons {"Ok"} cancel button 1 with icon 1
			end try
		end tell
		return 1 -- mandatory
	end processOneFile
	
	on processManyFiles(theList, aFile, theItemNUmber)
		set basename to baseNameOfPxPath of fn for aFile
		set fnLen to length of basename
		my PdfPageCounter's updateMaxLen(fnLen)
		set end of theList to {aFile, basename}
		set theItemNUmber to theItemNUmber + 1
		return theItemNUmber
	end processManyFiles
	
	on postProcess(theList, numberOfItems)
		if numberOfItems > 1 then -- We have a job to do
			set produce to {}
			set docPath to parentFolOfPxPath of fn for (item 1 of theList)
			set end of produce to "Pages in Pdf  documents on " & docPath & ":" & linefeed
			repeat with i from 1 to (count theList)
				
				set fnLen to (length of (item 2 of (item i of theList)))
				set numspaces to PdfPageCounter's missingSpaces(fnLen)
				set textLine to my PdfPageCounter's pgCount((item 1 of (item i of theList)), (item 2 of (item i of theList)), numspaces)
				set end of produce to textLine
			end repeat
			set textLine to mtxt's spaces((PdfPageCounter's maxLen) - 4) & "Total: " & mtxt's rgthJustifyNumber(PdfPageCounter's totalPages) & mtxt's pages(PdfPageCounter's totalPages)
			set end of produce to textLine
			mtxt's showList(produce)
		end if
	end postProcess
	
end script

fileProc's selectAndProcessFiles(scripttitle, listAndSumPdfPageNumbers, "Choose the Pdf file(s) you need the page count for:", "Counted PDF pages.")