Sunday, November 19, 2017
  • Index
  •  » Code Exchange
  •  » A generic Algorithm for scripting Finder Selections, and chosen files.

#1 2015-03-15 04:19:34 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

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. smile

Applescript:


-- 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
-- [url]http://www.macscripter.net/viewtopic.php?pid=179364#p179364[/url]
-- 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)
           -- [url]http://macscripter.net/viewtopic.php?id=43732[/url] 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.

Last edited by McUsrII (2015-03-17 05:24:59 pm)

Offline

 

#2 2015-03-18 12:02:51 pm

McUsrII
Member
Registered: 2012-11-21
Posts: 3046
Website

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

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! 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)

Applescript:

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:

Applescript:

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)
           -- [url]http://macscripter.net/viewtopic.php?id=43732[/url] 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:

Applescript:

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 [url]http://macscripter.net/viewtopic.php?id=38680[/url]
       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.")

Last edited by McUsrII (2015-03-18 12:05:04 pm)

Offline

 
  • Index
  •  » Code Exchange
  •  » A generic Algorithm for scripting Finder Selections, and chosen files.

Board footer

Powered by FluxBB

RSS (new topics) RSS (active topics)