-
Notifications
You must be signed in to change notification settings - Fork 0
/
BatchRenamer-utf-8-encoding.txt
949 lines (690 loc) · 26.7 KB
/
BatchRenamer-utf-8-encoding.txt
1
(*********************************BatchRenamer, Copyright © 2021 - 2030 by Steve Thompson.MIT License. (Note: this file is plain text and encoded in utf-8 so text will appear correct when viewing on Github. To run the script, it must be saved in Script Editor as a script with extension '.scpt', or as an application.)Renames batch of items in Mac Finder (anything that can be renamed).Can be saved as an application, or can be run as is.## How to use### 1. Select items in the Finder.Remember: Items appear in the renaming queue in the order they are selected.So for example, the items you want to rename appear in a Finder window in this order:- image.jpg- essay.doc- folder1But you want to rename them (with numbers at the beginning) in this order:- 01 folder1- 02 image.jpg- 03 essay.docSo you must select the items in that order (folder1, image.jpg, essay.doc) before you runthe app.### 2. Run the app.## Useful Tips1. If items you want to rename are in different folders from one another: - Create a Finder tag to assign to those items. - Put that tag in window sidebar so all those items appear in the window. - Select items and run BatchRenamer.**********************************)--static globalsproperty additionals : {}--all global constants (follows naming convention of: |CONSTANT| )-- initial choicesglobal |FR|, |NS|, |NSACN|, |OA|, |OAACN|, |APS|, |IRC|global |CE|, |CAPITAL|, |CHANGECASE|global |MAIN_CHOICES| -- list containing initial choices.global |INSERT|, |REMOVE| -- 2 choicesglobal |INSERT_REMOVE| -- list containing 2 choicesglobal |LEFT|, |RIGHT| -- 2 choicesglobal |LEFT_RIGHT| -- list containing 2 choicesglobal |PROCEED|global |CANCEL_PROCEED|global |GO_BACK|(*********************************|UNIQUESTRING| is needed to ensure the renaming is reliable.There's a possible scenario where you're renaming a batch which includes an itemthat already has a name you're assigning to a different item. If this happens, thescript will error. To solve this, the script first renames all selected items with|UNIQUESTRING| prepended to their new names. This prevents any error. Then itremoves |UNIQUESTRING| from all the names.**********************************)global |UNIQUESTRING|--Handler runs when app is double-clicked:on run tell application "Finder" to set selectedItemPaths to selection if selectedItemPaths's length = 0 then return (display dialog ¬ "Items must be selected in the Finder window before launching me." & ¬ return & return & "I'm going to quit now. Bye." with title ¬ "BatchRenamer" buttons {"OK"} default button 1 giving up after 5) mainRoutine(selectedItemPaths)end runon mainRoutine(itemPaths) try setGlobalConstants() repeat set {mainChoice, additionals} to ¬ get_mainChoice_additionalChoices() set newNames to getNewNames(itemPaths, mainChoice, additionals) if userApproves(newNames) then renameAll(itemPaths, newNames) exit repeat end if end repeat on error return end tryend mainRoutineon setGlobalConstants() --The only option that affects the name extension is "Change Extension". set {|FR|, |NS|, |NSACN|, |OA|, |OAACN|, ¬ |APS|, |IRC|, |CE|, |CAPITAL|, |CHANGECASE|} to ¬ {"Find and Replace", "Number Sequentially", ¬ "Number Sequentially, attach to current name", "Order Alphabetically", ¬ "Order Alphabetically, attach to current name", "Add Prefix/Suffix", ¬ "Insert/Remove Characters", "Change Extension", "Capitalize Each Word", ¬ "Change Case"} set |MAIN_CHOICES| to ¬ {|FR|, |NS|, |NSACN|, |OA|, |OAACN|, |APS|, |IRC|, |CE|, |CAPITAL|, ¬ |CHANGECASE|} set {|INSERT|, |REMOVE|} to {"Insert", "Remove"} set |INSERT_REMOVE| to {|INSERT|, |REMOVE|} set {|LEFT|, |RIGHT|} to {"The Left", "The Right"} set |LEFT_RIGHT| to {|LEFT|, |RIGHT|} set |PROCEED| to "Proceed" set |CANCEL_PROCEED| to {"Cancel", |PROCEED|} set |GO_BACK| to "No, Go Back" set |UNIQUESTRING| to "Ç◊¯&¯*¯#¯%¯"end setGlobalConstantson get_mainChoice_additionalChoices() set choice to ¬ listChoice(|MAIN_CHOICES|, "What To Do With These Names?", |PROCEED|) set additionals to getAdditionalChoices(choice) return {choice, additionals}end get_mainChoice_additionalChoiceson getAdditionalChoices(choice) script private set additionals to getDefaultParameters(choice) if choice is |FR| then set additionals to setFindReplaceParams(additionals) else if choice is in {|NS|, |NSACN|} then set additionals to setNumberSequentiallyParams(additionals, choice) else if choice is |APS| then set additionals to setAddPrefixSuffixParams(additionals) else if choice is |IRC| then set additionals to setInsertRemoveParams(additionals) else if choice is |CE| then set additionals's newExt to text returned of ¬ dialogWithTextInput("", "Enter the new extension:", ¬ |CANCEL_PROCEED|) else if choice is in {|OA|, |OAACN|} then set additionals to setOrderAlphabeticallyParams(additionals, choice) else if choice is |CHANGECASE| then set additionals to setChangeCaseParams(additionals) end if return additionals on getDefaultParameters(choice) if choice is in {|NS|, |NSACN|, |OA|, |OAACN|} then return {curN:"", additionalTxt:"", attachBeginOrEnd:""} else if choice is |IRC| then return {listChoice:"", startPosition:0, fromWhichEnd:¬ "", removeNum:0, insertTxt:""} else if choice is in {|CE|, |APS|} then return {pfx:"", sfx:"", newExt:""} else if choice is |FR| then return {caseBool:"", searchStr:"", replaceStr:""} else if choice is |CHANGECASE| then return {upperOrLower:""} end if return {} -- must at least return empty list. end getDefaultParameters end script run privateend getAdditionalChoiceson getNewNames(itemPaths, choice, additionals) script private set itemPaths to getEachAsString(itemPaths) return __getNewNames(itemPaths) on __getNewNames(itemPaths) set newNames to {} repeat with i from 1 to count of itemPaths set end of newNames to ¬ getNewName(LI(i, itemPaths), choice, additionals) end repeat return newNames end __getNewNames on getNewName(itemPath, choice, additionals) set {theName, theExt} to get_theName_theExtension_fromPath(itemPath) if choice is |CE| then return getNewName_CE(additionals, theName) if choice is |FR| then set newName to getNewName_FR(additionals, theName) else if choice is |NS| then set newName to getNewName_NS(additionals) else if choice is |NSACN| then set newName to getNewName_NSACN(additionals, theName) else if choice is |OA| then set newName to getNewName_OA(additionals) else if choice is |OAACN| then set newName to getNewName_OAACN(additionals, theName) else if choice is |APS| then set newName to (additionals's pfx & theName & additionals's sfx) else if choice is |IRC| then set newName to getNewName_IRC(additionals, theName) else if choice is |CAPITAL| then set newName to getNewName_Capitalize(theName) else if choice is |CHANGECASE| then set newName to getNewName_ChangeCase(additionals, theName) end if return (newName & theExt) end getNewName end script run privateend getNewNameson userApproves(newNames) script private set previewString to getPreviewString(newNames) --Show the user a preview of the new names: set theResult to ¬ dialogWithButtons("Is this what you wanted?" & return & return & ¬ previewString, {"Cancel", |GO_BACK|, "Yes"}) if button returned of theResult is "Cancel" then error "User cancelled" if button returned of theResult is |GO_BACK| then return false return true on getPreviewString(newNames) set previewString to "" repeat with i from 1 to count of newNames -- We don't need to see more than 20 lines of new names: if i = 21 then set previewString to previewString & "and so on...." exit repeat end if set newName to replace(|UNIQUESTRING|, "", LI(i, newNames)) set previewString to (previewString & newName & return) end repeat return previewString end getPreviewString end script run privateend userApproveson renameAll(itemPaths, newNames) script private renameIncludingUniqueString(itemPaths) set renamedPaths to getRenamedPaths(itemPaths) renameWithoutUniqueString(renamedPaths) on renameIncludingUniqueString(itemPaths) repeat with i from 1 to count of itemPaths renameItem(LI(i, itemPaths), (|UNIQUESTRING| & LI(i, newNames))) end repeat end renameIncludingUniqueString on renameWithoutUniqueString(renamedPaths) repeat with i from 1 to (count of renamedPaths) set theName to getNameFromPath(LI(i, renamedPaths)) set newName to replace(|UNIQUESTRING|, "", theName) renameItem(LI(i, renamedPaths), newName) if renameUnsuccessful(LI(i, renamedPaths)) then --the renaming failed because there was a naming conflict. set {nm, ext} to get_theName_theExtension_fromName(newName) set newName to (nm & "-renaming-conflict" & ext) renameItem(LI(i, renamedPaths), newName) end if end repeat end renameWithoutUniqueString on renameItem(itemPath, newName) tell application "System Events" to ¬ set name of item (itemPath) to newName end renameItem on renameUnsuccessful(itemPath) -- The rename is unsuccessful because itemPath should no longer exist. tell application "System Events" to return (exists item itemPath) end renameUnsuccessful on getRenamedPaths(itemPaths) set itemPaths to getEachAsString(itemPaths) set renamedPaths to {} repeat with i from 1 to count of itemPaths -- must include |UNIQUESTRING|: set end of renamedPaths to ¬ getNewPath(LI(i, itemPaths), (|UNIQUESTRING| & LI(i, newNames))) end repeat return renamedPaths end getRenamedPaths on getNewPath(oldPath, newName) set pathParts to explode(oldPath, ":") if pathParts ends with "" then ¬ set pathParts to getWithoutTail(pathParts, 1) setLI(-1, pathParts, newName) return implode(pathParts, ":") end getNewPath end script run privateend renameAllon setFindReplaceParams(additionals) set additionals's caseBool to button returned of ¬ dialogWithButtons("Should the Find and Replace be case-sensitive?", ¬ {"Cancel", "Yes", "No"}) if additionals's caseBool is "Cancel" then error "User canceled" set additionals's searchStr to text returned of ¬ dialogWithTextInput("", "Enter the text to find:", |CANCEL_PROCEED|) set additionals's replaceStr to text returned of ¬ dialogWithTextInput("", "Enter the text to replace it with:", |CANCEL_PROCEED|) return additionalsend setFindReplaceParamson setNumberSequentiallyParams(additionals, choice) set additionals's curN to text returned of ¬ dialogWithTextInput("001", "Enter a starting number:", |CANCEL_PROCEED|) if choice is |NSACN| then set additionals's attachBeginOrEnd to button returned of ¬ dialogWithButtons("Attach at beginning or end of current name?", ¬ {"Cancel", "End", "Beginning"}) end if if choice is |NS| or additionals's attachBeginOrEnd is "Beginning" then set befOrAft to "after" else set befOrAft to "before" end if set additionals's additionalTxt to text returned of dialogWithTextInput("", ¬ "Enter any additional text you want placed " & befOrAft & " the number:", ¬ |CANCEL_PROCEED|) return additionalsend setNumberSequentiallyParamson setAddPrefixSuffixParams(additionals) set listChoice to listChoice({"Prefix", "Suffix", "Both"}, ¬ "Add a Prefix, Suffix, or Both?", |PROCEED|) if listChoice is "Both" or listChoice is "Prefix" then set additionals's pfx to ¬ text returned of dialogWithTextInput("", "Enter the Prefix:", |CANCEL_PROCEED|) end if if listChoice is "Both" or listChoice is "Suffix" then set additionals's sfx to ¬ text returned of dialogWithTextInput("", "Enter the Suffix:", |CANCEL_PROCEED|) end if return additionalsend setAddPrefixSuffixParamson setInsertRemoveParams(additionals) set additionals's listChoice to ¬ listChoice(|INSERT_REMOVE|, (|INSERT| & " or " & |REMOVE| & " Text?"), ¬ |PROCEED|) set additionals's fromWhichEnd to getStartingFromWhichEnd(additionals's listChoice) set additionals's startPosition to getStartPosition() if additionals's listChoice is |REMOVE| then set additionals's removeNum to text returned of ¬ dialogWithTextInput("", "Enter the number of characters to remove:", ¬ |CANCEL_PROCEED|) as integer else if additionals's listChoice is |INSERT| then set additionals's insertTxt to text returned of ¬ dialogWithTextInput("", "Enter the text to insert:", |CANCEL_PROCEED|) end if return additionalsend setInsertRemoveParamson setOrderAlphabeticallyParams(additionals, choice) set additionals's curN to text returned of dialogWithTextInput("aaa", ¬ "Enter a starting letter combo, including total number of characters desired:", ¬ |CANCEL_PROCEED|) if choice is |OAACN| then set additionals's attachBeginOrEnd to button returned of ¬ dialogWithButtons("Attach at beginning or end of current name?", ¬ {"Cancel", "End", "Beginning"}) end if if (choice is |OA|) or (additionals's attachBeginOrEnd is "Beginning") then set befOrAft to "after" else set befOrAft to "before" end if set additionals's additionalTxt to text returned of dialogWithTextInput("", ¬ "Enter any additional text you want placed " & befOrAft & " the letters:", ¬ |CANCEL_PROCEED|) return additionalsend setOrderAlphabeticallyParamson setChangeCaseParams(additionals) set additionals's upperOrLower to button returned of ¬ dialogWithButtons("Change to Upper or Lower case?", {"Cancel", "Upper", "Lower"}) return additionalsend setChangeCaseParamson getStartingFromWhichEnd(action) return listChoice(|LEFT_RIGHT|, (action & " starting from which end?"), |PROCEED|)end getStartingFromWhichEndon getStartPosition() return text returned of ¬ dialogWithTextInput("1", "Enter the starting position:", |CANCEL_PROCEED|) as integerend getStartPosition--Increments theNum that theItem will be renamed with, and converts it back to text.--If the num requires zeros at the beginning, it adds them.on incrementNum(theNum) if class of theNum is not text then set theNum to (theNum as text) --Increment curN: set numLength to (count of theNum) set theNum to (theNum as integer) set theNum to ((theNum) + 1) set theNum to (theNum as text) --If there need to be zeros at beginning of curN, make them: if (count of theNum) is not numLength then set numZeros to (numLength - (count of theNum)) set theZeros to "" repeat numZeros times set theZeros to (theZeros & "0") end repeat set theNum to (theZeros & theNum) end if return theNumend incrementNumon get_theName_theExtension_fromPath(itemPath) set theName to getNameFromPath(itemPath) return get_theName_theExtension_fromName(theName)end get_theName_theExtension_fromPathon get_theName_theExtension_fromName(theName) set theList to explode(theName, ".") if (count of theList) = 1 then set theExt to "" else set theExt to "." & (getTail(1, theList) as text) set theName to (getWithoutTail(theList, 1) as text) end if return {theName, theExt}end get_theName_theExtension_fromName--Replaces searchStr with replaceStr inside theString:on replace(searchStr, replaceStr, theString) set item_list to explode(theString, searchStr) return implode(item_list, replaceStr)end replace-- This function separates pieces of a string into list items, using theDelimit-- as the separator. theDelimit can be either string or list of strings.on explode(theString, theDelimit) set origDelimit to AppleScript's text item delimiters set AppleScript's text item delimiters to theDelimit set theResult to every text item of theString set AppleScript's text item delimiters to origDelimit return theResultend explode--This function re-assembles a list of strings into a single string,--using theDelimit as glue to reconnect each string. theDelimit must be a string.on implode(textlist, theDelimit) set origDelimit to AppleScript's text item delimiters set AppleScript's text item delimiters to theDelimit set theString to (textlist as string) set AppleScript's text item delimiters to origDelimit return theStringend implode--This function is just for creating a short-hand way of accessing a list item.--ItemNum can be a single integer, or a list of two integers for accessing a range of items:on LI(itemNum, theList) if class of itemNum is integer then return (item itemNum of theList) else if class of itemNum is list then return (items (item 1 of itemNum as integer) thru ¬ (item 2 of itemNum as integer) of theList) end ifend LI--This function is for assigning a value to a list item:on setLI(itemNum, theList, theValue) set item itemNum of theList to theValueend setLIon getIndex(theItem, theList) if class of theList is not in {integer, real, text, list} then return false --If theList is a number then coerce into text: if (count of theList) is 0 then set theList to (theList as text) if theItem is not in theList then return false -- function stops. --Else, theItem must be in theList, so: set indexList to {} set itemLength to (count of (theItem as text)) if (count of theList) is 1 then -- Then theItem IS theList. set end of indexList to 1 return indexList -- function stops. end if if class of theList is list then repeat with i from 1 to count of theList if (theItem is (LI(i, theList))) then set end of indexList to i -- Appends number to end of list. end repeat else if class of theList is text then -- Then theItem is also text. set {theLimit, x, i} to {count of theList, 1, 1} set theItem to (theItem as text) repeat while theLimit > (itemLength - 1) if theItem is (characters i thru (i + itemLength - 1) of theList as text) then set end of indexList to i end if set i to (i + 1) set theLimit to (theLimit - 1) end repeat end if if indexList is {} then return false --Else: return indexListend getIndexon incrementAlphabet(theChars) set charList to every text item of theChars set theOrder to every text item of getAlphabet("lower") repeat with i from 1 to count of charList if LI(-i, charList) is "z" then setLI(-i, charList, "a") if i = (count of charList) then set charList to "a" & (charList as text) end if else set thePos to getIndex(LI(-i, charList), theOrder) setLI(-i, charList, LI(thePos + 1, theOrder)) exit repeat end if end repeat return charList as textend incrementAlphabeton dialogWithTextInput(defaultTxt, theMessage, theButtons) set lastButton to count of theButtons return display dialog theMessage with title "BatchRenamer" default answer ¬ defaultTxt buttons theButtons default button lastButtonend dialogWithTextInputon dialogWithButtons(theMessage, theButtons) set lastButton to count of theButtons return display dialog theMessage with title "BatchRenamer" buttons theButtons ¬ default button lastButtonend dialogWithButtonson listChoice(theList, thePrompt, okButton) set theResult to (choose from list theList with title ¬ "BatchRenamer" with prompt thePrompt OK button name okButton) if theResult is false then error "User Canceled" return theResult as text -- converts from a list.end listChoiceon displayAlertDialog(theMessage) return display dialog theMessage with title "BatchRenamer" with icon caution ¬ buttons {"OK"} default button "OK"end displayAlertDialog--itemPath must be a colon-delimited path string.--Example: "Macintosh HD:Users:Username:Desktop:Filename.txt"on getNameFromPath(itemPath) set theParts to explode(itemPath, ":") if theParts ends with "" then set theParts to getWithoutTail(theParts, 1) return LI(-1, theParts) as textend getNameFromPathon getNewName_OA(additionals) set newName to (additionals's curN & additionals's additionalTxt) set additionals's curN to incrementAlphabet(additionals's curN) return newNameend getNewName_OAon getNewName_OAACN(additionals, oldName) if additionals's attachBeginOrEnd is "Beginning" then set newName to (additionals's curN) & (additionals's additionalTxt) & oldName else -- Then place the number at end of name, with the additionalTxt coming just before it. set newName to oldName & (additionals's additionalTxt) & (additionals's curN) end if set additionals's curN to incrementAlphabet(additionals's curN) return newNameend getNewName_OAACNon getNewName_NS(additionals) set newName to (additionals's curN & additionals's additionalTxt) set additionals's curN to incrementNum(additionals's curN) return newNameend getNewName_NSon getNewName_NSACN(additionals, oldName) if additionals's attachBeginOrEnd is "Beginning" then set newName to (additionals's curN) & (additionals's additionalTxt) & oldName else set newName to (oldName & (additionals's additionalTxt) & (additionals's curN)) end if set additionals's curN to incrementNum(additionals's curN) return newNameend getNewName_NSACNon getNewName_CE(additionals, oldName) return (oldName & "." & additionals's newExt)end getNewName_CEon getNewName_FR(additionals, oldName) if additionals's caseBool is "Yes" then considering case return replace(additionals's searchStr, additionals's replaceStr, oldName) end considering else return replace(additionals's searchStr, additionals's replaceStr, oldName) end ifend getNewName_FRon getNewName_IRC(additionals, oldName) script private if additionals's listChoice is "Remove" then set oldNameLength to count of oldName if (additionals's startPosition > oldNameLength) or ¬ ((additionals's removeNum) > oldNameLength) then return oldName set endPosition to ((additionals's startPosition) + (additionals's removeNum)) - 1 if additionals's fromWhichEnd is |RIGHT| then ¬ set oldName to reverse of every text item of oldName as text --If the startPosition is not at beginning of name, save first part of name: if additionals's startPosition > 1 then set firstPart to getHead((additionals's startPosition) - 1, oldName) else set firstPart to "" end if --If (endPosition + 1) exceeds the number of chars in oldName, this means --the user wanted to trim off the end of oldName: if (endPosition + 1) > oldNameLength then set newName to firstPart else set newName to (firstPart & (getTail(oldNameLength - endPosition, oldName))) end if if additionals's fromWhichEnd is |RIGHT| then ¬ set newName to reverse of every text item of newName as text return newName else if additionals's listChoice is "Insert" then set x to (additionals's startPosition) if additionals's fromWhichEnd is |LEFT| then if x > (count of oldName) then set {x, additionals's fromWhichEnd} to {-1, |RIGHT|} if x > 1 then return (getSubstr(1, (x - 1), oldName) & ¬ additionals's insertTxt & getSubstr(x, -1, oldName)) else return (additionals's insertTxt & getSubstr(x, -1, oldName)) end if else if additionals's fromWhichEnd is |RIGHT| then if (x > 0) then set x to -(additionals's startPosition) if x < -1 then return getSubstr(1, x, oldName) & (additionals's insertTxt) & ¬ getSubstr((x + 1), -1, oldName) else return (oldName & additionals's insertTxt) end if end if end if end script run privateend getNewName_IRCon getNewName_ChangeCase(additionals, oldName) script private if additionals's upperOrLower = "Upper" then return getUppercase(oldName) return getLowercase(oldName) on getUppercase(str) return getCharsTranslated(str, getAlphabet("lower"), getAlphabet("upper")) end getUppercase on getLowercase(str) return getCharsTranslated(str, getAlphabet("upper"), getAlphabet("lower")) end getLowercase end script run privateend getNewName_ChangeCaseon getNewName_Capitalize(oldName) return getWithEachWordCapitalized(oldName)end getNewName_Capitalizeon getSubstr(startPos, endPos, str) return (characters startPos thru endPos of str) as textend getSubstron getWithEachWordCapitalized(str) script private set wrds to getWords(str) repeat with i from 1 to (count of wrds) set item i of wrds to getCapitalizedWord(item i of wrds) set str to (replace(item i of wrds, item i of wrds, str)) end repeat return str on getWords(str) set separators to getWordSeparators(str) if (count of separators) = 0 then return {str} set wrds to explode(str, separators) return getWithoutEmptyStrings(wrds) end getWords -- Returns list of substrings found in 'str' that don't have any alphabetical characters on getWordSeparators(str) set {separators, separator} to {{}, ""} repeat with i from 1 to (count of str) if not inAlphabet(character i of str) then set separator to separator & (character i of str) else if separator ≠ "" then set end of separators to separator set separator to "" end if end repeat if separator ≠ "" then set end of separators to separator return separators end getWordSeparators on getCapitalizedWord(wd) set capital to ¬ getTranslated(getHead(1, wd), getAlphabet("lower"), getAlphabet("upper")) if wd's length = 1 then return capital return (capital & getSubstr(2, -1, wd)) end getCapitalizedWord on getWithoutEmptyStrings(strs) set actualWrds to {} repeat with i from 1 to count of strs if item i of strs ≠ "" then set end of actualWrds to item i of strs end repeat return actualWrds end getWithoutEmptyStrings on inAlphabet(char) return (char is in (every text item of getAlphabet("lower"))) end inAlphabet end script run privateend getWithEachWordCapitalizedon getCharsTranslated(str, fromAlphabet, toAlphabet) set translation to "" repeat with char in str set translation to (translation & getTranslated(char, fromAlphabet, toAlphabet)) end repeat return translationend getCharsTranslatedon getTranslated(char, fromAlphabet, toAlphabet) script private set i to offset of char in fromAlphabet if notFound(i) then return char as string return (character i of toAlphabet) as string on notFound(num) return num = 0 end notFound end script run privateend getTranslatedon getAlphabet(theCase) if theCase = "lower" then return "abcdefghijklmnopqrstuvwxyz" return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"end getAlphabet-- lst can be list or stringon getHead(numItems, lst) if class of lst is text then return getSubstr(1, numItems, lst) return LI({1, numItems}, lst)end getHead-- lst can be list or stringon getTail(numItems, lst) if class of lst is text then return getSubstr(-numItems, -1, lst) return LI({-numItems, -1}, lst)end getTailon getWithoutTail(lst, numItems) set lastIndex to (lst's length) - numItems if lastIndex < 1 then return {} return LI({1, lastIndex}, lst)end getWithoutTailon getEachAsString(paths) repeat with i from 1 to (count of paths) setLI(i, paths, LI(i, paths) as text) end repeat return pathsend getEachAsString