Thanks to everyone for discussing this. I’d long been meaning to cook up something similar for myself. When I saw jj’s slightly faster ” and very much shorter ” script, I decided not to post my own. But I believe mine may be immune to the bundle hierarchy problem and it doesn’t omit scripts that are in the root folder of the Scripts folder, so here it is. It handles script/text files saved by any of the three main script editors and assumes that any applications in the hierarchy are AppleScript applets or droplets. The saved text files have “.txt” tagged onto the original files’ full names and their owner is set to BBEdit, as per NovaScotian’s original idea. Works in Jaguar and Tiger. Needs Jon’s Commands for the script-to-text coercions.
Its great length (sorry!) is due to the fact that the code that sorts out the scripts ” ‘main()’ ” is tagged onto a hierarchy duplicating script that I wrote earlier in the year, which in turn uses a previously existing vanilla sort handler. jj’s idea is much simpler.
(Edited 7th October 2005 to show a later version of the script.)
on run
considering case
scripts2text()
end considering
end run
-- The main handler.
on scripts2text()
-- Create an empty copy of subject folder hierarchy in the user's Documents folder.
try
if (keys pressed) contains {"Command"} then -- 'keys pressed' requires Jon's Commands.
set rootPath to (choose folder with prompt "Select a folder or hierarchy containing scripts...") as Unicode text
else -- Default to user domain Scripts folder.
set rootPath to (path to scripts folder as Unicode text)
end if
on error number -1708
showMessage("This script requires the" & return & ""Jon's Commands" OSAX.", stop, 10)
end try
set destinationPath to (path to At Ease documents folder as Unicode text)
set newRootPath to duplicateHierarchy(rootPath, destinationPath)
-- Get a list of all the script files (ie. files saved by SE, Smile, or SD), applets, and droplets, and script bundles in the Scripts folder hierarchy.
set likelyCreators to {"ToyS", "VIZF", "asDB", "aplt", "dplt", missing value}
tell application "Finder"
try
set scriptFiles to (files of entire contents of folder rootPath whose creator type is in likelyCreators) as alias list
on error
set scriptFiles to (first file of entire contents of folder rootPath whose creator type is in likelyCreators) as alias as list
end try
end tell
if scriptFiles is {} then showMessage("No script files were identified in this folder.", stop, 10)
-- Prepare the TIDs for changing the root sections of the file paths.
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to rootPath
-- Some applications may launch when scripts that target them are opened. Disks may be mounted.
-- Note the apps that are open now and count the current disks.
tell application "System Events" to set previousAppNames to name of application processes
set diskCount to (count (list disks))
-- Process the returned files.
set scriptsDone to 0
repeat with thisFile in scriptFiles
try
-- If this is an editable script, get the source code.
set txt to (load script thisFile) as text -- The coercion requires Jon's commands.
on error m number n -- -1752, -1700, -43
if (n is -1752) then
-- Otherwise, if it's a text file, get the text.
set txt to (read thisFile as text from 1)
else if (n is -1700) then
-- If it's a run-only script, write an explanatory note.
set txt to "The original of this file may be a run-only script."
else
-- Otherwise set a zero-length string to bypass the following 'if'.
set txt to ""
end if
end try
if ((count txt) > 0) then
-- Get the path of this file.
set thisFilePath to thisFile as Unicode text
-- Zap the trailing colon if it's a package. (The Finder didn't return folders or disks.)
if (thisFilePath ends with ":") then set thisFilePath to text 1 thru -2 of thisFilePath
-- Cut the root of the path and check the rest for slash characters. (Replace with bars.)
set hierarchyPath to (text from text item 2 to -1 of thisFilePath)
if hierarchyPath contains "/" then
set AppleScript's text item delimiters to "/"
set hierarchyPath to hierarchyPath's text items
set AppleScript's text item delimiters to "|"
set hierarchyPath to hierarchyPath as Unicode text
set AppleScript's text item delimiters to rootPath
end if
-- Assemble the path to the target text file.
set savePath to newRootPath & hierarchyPath & ".txt"
-- Write the text to a file at the new location.
set f to (open for access file savePath with write permission)
try
write txt to f
end try
close access f
-- Every 10 scripts processed, beep to show the script's still working
-- and close any target apps that have been launched.
set scriptsDone to scriptsDone + 1
if (scriptsDone is 10) then
beep
quitOpenedApps(previousAppNames, false)
set scriptsDone to 0
end if
end if
end repeat
set AppleScript's text item delimiters to astid
-- Make BBEdit the owner of the saved text files.
tell application "Finder" to set creator type of files of entire contents of folder newRootPath to "R*ch"
-- Close any remaining launched apps.
quitOpenedApps(previousAppNames, true)
repeat with i from diskCount + 1 to (count (list disks))
tell application "Finder" to eject disk i
end repeat
showMessage("Done!", note, 5)
end scripts2text
-- Display a message with the required icon for the required time. Quit if stop icon.
on showMessage(msg, iconType, maxTime)
beep 2
tell application (path to frontmost application as Unicode text)
display dialog msg buttons {"OK"} default button 1 with icon iconType giving up after maxTime with title "Scripts2Text"
end tell
if (iconType is stop) then error number -128
end showMessage
-- Quit any apps that have been opened during the process, unless Classic has been started too.
on quitOpenedApps(previousAppNames, scriptFinishing)
tell application "System Events" to set ClassicNotRunning to (not ((application process "Classic Support") exists))
if (ClassicNotRunning) or (previousAppNames contains {"Classic Support"}) then
-- If Classic hasn't been started during this script, close any newly opened applications.
tell application "System Events" to set currentAppNames to name of application processes
repeat with thisname in currentAppNames
if (thisname is not in previousAppNames) then
tell application thisname
try
activate -- Necessary in Jaguar.
quit
end try
end tell
end if
end repeat
else
-- If Classic has been started during this script, don't do anything unless 'scriptFinishing' is flagged as true.
if (scriptFinishing) then
-- Otherwise, quit Classic, delaying till it's gone.
tell application "Classic Support" to quit
tell application "System Events"
repeat while ((application process "Classic Support") exists)
delay 0.5
end repeat
end tell
-- When it's gone, quit any other opened apps too.
quitOpenedApps(previousAppNames, scriptFinishing)
end if
end if
end quitOpenedApps
-- The remaining handler
on duplicateHierarchy(rootPath, destinationPath) -- params: HFS paths as Unicode text.
-- Script object for customising CustomQsort.
-- (Straight comparisons. No shadow actions on swaps & shifts.)
script o
on isGreater(a, b)
(a > b)
end isGreater
on isLess(a, b)
(a < b)
end isLess
on swap(a, b)
end swap
property shift : swap
end script
-- Combine the paths of all the folders in the hierarchy into one, delimited string.
-- The path to the root folder will be contained in all the other paths,
-- unless it's the only folder in the hierarchy.
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ":"
set newRootName to rootPath's text item -2
repeat -- until no name clash with any existing folders at this destination
try
(destinationPath & newRootName) as alias
set newRootName to newRootName & " copy"
on error
exit repeat
end try
end repeat
set newRootPath to destinationPath & newRootName & ":"
set AppleScript's text item delimiters to ASCII character 0
try
-- Get the path strings of every subfolder in the existing hierarchy.
tell application "Finder" to set theHierarchy to every folder of entire contents of folder rootPath as Unicode text
set theHierarchy to theHierarchy's text items
-- Sort the paths lexically. This sorts subfolders immediately after their parents.
considering case
CustomQsort(theHierarchy, 1, -1, o)
end considering
-- Parse the hierarchy into a 'mkdir' specification based on the path to the new root folder.
set mkdirString to makeMkdirString(theHierarchy, newRootPath)
on error -- the root folder has no subfolders
set mkdirString to ("mkdir -p " as Unicode text) & quoted form of POSIX path of newRootPath
end try
set AppleScript's text item delimiters to astid
-- Create the new hierarchy.
do shell script mkdirString
return newRootPath
end duplicateHierarchy
-- Parse a sorted list of path strings to folders in a hiercharchy.
-- Use the *name* of each folder to build a new hierarchy specification for 'mkdir'.
on makeMkdirString(sortedList, newRootPath)
script o
-- Assign the list to a script object property for fast, referenced access.
property pathList : sortedList
-- Another list to collect the 'mkdir' specification parts.
property outputComponents : {}
-- Some pre-defined stuff in variables for speed.
property singleQuote : "'" as Unicode text
property slashQuote : "/'" as Unicode text
property slashBraceQuote : "/{'" as Unicode text
property comma : "," as Unicode text
property quoteComma : "'," as Unicode text
property braceComma : "}," as Unicode text
-- A recursive subhandler that analyses how the paths relate to each other
-- and decides how to structure the 'mkdir' string.
on parse(a, b)
-- At each recursion, items a thru b of the list are sibling folders and their descendents.
-- Since each sibling is sorted immediately before its own descendents, item a is an immediate
-- child of the previous level and may or may not have its own descendents and/or siblings.
set basic to item a of my pathList
if (a = b) then
-- This is a childless subfolder with no later siblings. Append "/'<name>'," to the output.
addNameToList(basic, slashQuote, quoteComma)
else
if (item b of my pathList begins with basic) then
-- This folder has descendents but no later siblings. Append "/'<name>'" to output.
addNameToList(basic, slashQuote, singleQuote)
set closingBraceRequired to false
else
-- This folder is the first of siblings. May or may not have children. Append "/{'<name>'" to output.
addNameToList(basic, slashBraceQuote, singleQuote)
set closingBraceRequired to true
end if
set x to a + 1
repeat with i from x to b
-- Loop while the following paths denote descendents of the current sibling.
if (item i of my pathList begins with basic) then
else
if (i > x) then
-- If there are any, recurse to sort out the immediate subfolders and *their* descendents.
parse(x, i - 1)
else
-- Otherwise, just append a comma to the output.
set end of my outputComponents to comma
end if
-- Append the next sibling's name, in quotes, and continue the repeat.
set basic to item i of my pathList
addNameToList(basic, singleQuote, singleQuote)
set x to i + 1
end if
end repeat
-- At the end of the repeat, sort out any descendents of the last sibling.
if (x > b) then
else
parse(x, b)
end if
-- Append a closing brace and a comma to the output if there were siblings at this level.
if (closingBraceRequired) then set end of my outputComponents to braceComma
end if
end parse
-- Another subhandler that parses the folder name from the supplied path string,
-- UNIXifies any instances of "/" or "'", and appends to result to the output.
on addNameToList(str, startQuote, endQuote)
set str to str's text item -2
if (str contains "/") then
set AppleScript's text item delimiters to "/"
set str to str's text items
set AppleScript's text item delimiters to ":"
set str to str as Unicode text
end if
if (str contains "'") then
set AppleScript's text item delimiters to "'"
set str to str's text items
set AppleScript's text item delimiters to "'\\''"
set str to str as Unicode text
set AppleScript's text item delimiters to ":"
end if
set end of my outputComponents to startQuote & str & endQuote
end addNameToList
end script
-- *The main part of makeMkdirString().*
-- Set AppleScript's TIDs to ":" and call the recursive handler in the script object to arrange the output.
set astid to AppleScript's text item delimiters
set AppleScript's text item delimiters to ":"
considering case
tell o to parse(1, count sortedList)
end considering
-- Coerce the resulting list to Unicode text and tidy up the result.
set AppleScript's text item delimiters to ""
set mkdirString to o's outputComponents as Unicode text
set mkdirString to text 1 thru -2 of mkdirString
set AppleScript's text item delimiters to ",}"
set mkdirString to mkdirString's text items
set AppleScript's text item delimiters to "}"
set mkdirString to mkdirString as Unicode text
set AppleScript's text item delimiters to astid
-- Return the constructed hierarchy string, appended to the POSIX path of the new base folder.
return ("mkdir -p " as Unicode text) & text 1 thru -3 of (quoted form of POSIX path of newRootPath) & "'" & mkdirString
end makeMkdirString
-- CustomQsort by Arthur Knapp and Nigel Garvey.
-- Params: a list, the start and end indices of the range to be sorted, a script object containing comparison handlers.
on CustomQsort(theList, l, r, compObj)
script o
property cutoff : 10
property p : theList
on qsrt(l, r)
set i to l
set j to r
set v to my p's item ((l + r) div 2)
repeat while (j > i)
set u to my p's item i
repeat while (compObj's isLess(u, v))
set i to i + 1
set u to my p's item i
end repeat
set w to my p's item j
repeat while (compObj's isGreater(w, v))
set j to j - 1
set w to my p's item j
end repeat
if (i > j) then
else
set my p's item i to w
set my p's item j to u
compObj's swap(i, j)
set i to i + 1
set j to j - 1
end if
end repeat
if (j - l < cutoff) then
else
qsrt(l, j)
end if
if (r - i < cutoff) then
else
qsrt(i, r)
end if
end qsrt
on isrt(l, r)
set x to l
set z to l + cutoff - 1
if (z > r) then set z to r
set v to my p's item x
repeat with y from (x + 1) to z
if (compObj's isLess(my p's item y, v)) then
set x to y
set v to my p's item y
end if
end repeat
tell my p's item l
set my p's item l to v
set my p's item x to it
end tell
compObj's swap(l, x)
set u to my p's item (l + 1)
repeat with i from (l + 2) to r
set v to my p's item i
if (compObj's isLess(v, u)) then
set my p's item i to u
repeat with j from (i - 2) to l by -1
if (compObj's isLess(v, my p's item j)) then
set my p's item (j + 1) to my p's item j
else
set my p's item (j + 1) to v
compObj's shift(j + 1, i)
exit repeat
end if
end repeat
else
set u to v
end if
end repeat
end isrt
end script
set listLen to (count theList)
if (listLen > 1) then -- otherwise the handler will error
-- Translate negative indices
if (l < 0) then set l to listLen + l + 1
if (r < 0) then set r to listLen + r + 1
if (r = l) then
-- No point in sorting just one item
else
-- Transpose transposed indices
if (l > r) then
set temp to l
set l to r
set r to temp
end if
if (r - l < o's cutoff) then
-- Skip the Quicksort if cutoff or less items
else
o's qsrt(l, r)
end if
o's isrt(l, r)
end if
end if
end CustomQsort