Create Folders with Subfolders based on list

I’d like to take sitemaps that my clients approve and turn them into folders and subfolders. The idea is that I upload the main folder to Dropbox and my clients can place the relevant assets in the “page” folder. Here’s an example:

(1) Home
(2) About Us
(a) Our Story
(b) Our Services
(3) Growth
(a) Our Methods
(b) Individual Coaching
(c) Groups and Workshops
(4) Blog
(5) Resources
(a) Recommendations
(b) Book Lists
(c) Free Tools
(6) Store

Basically, I’d like the folder structure to match whatever list I have in plaintext without having to manually create folders. I just can’t wrap my head around the AS loop that is needed for this or how to select nested items.

I’ve created a rough script to get you started. I haven’t tested it thoroughly, so report/debug any errors you encounter and I’ll see if I can recreate them.

Here is the sample text file that I created for the purposes of my testing, which I named folders.dat:[format]Folder 1
Folder 2
Folder 2A
Folder 2A(i)
Folder 2A(ii)
Folder 2B
Folder 3
Folder 4
Folder 4A
Folder 4B
Folder 4B(i)
Folder 4B(i)a
Folder 4C
Folder 4C(i)
Folder 5
Folder 5A
Folder 5B
Folder 5C[/format]The leading whitespaces are tabs, where one additional tabstop difference between two consecutive lines represents a child folder located inside the folder on the line above it. I would suggest that, for now, you adhere to this strictly, as I have not gone as far as to build in safeguards to cater for malformed or unpredictable degrees of indentation, e.g. a folder indented two tabstops ahead of the one before it does not make sense.

-----------------------------------------------------------------------------------------------------------------------------
--HANDLERS & SCRIPT OBJECTS:
--ascend
--	Takes a path and returns an HFS path to the containing folder +N levels
--	above
to ascend from dir by N : 1
				local dir, N
				
				if dir starts with "~/" then set dir to the ¬
								contents of [system attribute "HOME", ¬
								text 2 thru -1 of dir] as text as ¬
								POSIX file as text
				
				if N = 0 then return dir
				set dir to POSIX path of ([dir, ":"] as text) ¬
								as POSIX file as text
				
				ascend from dir by N - 1
end ascend

--mapItems
--	Applies a +function (handler) to every item in a list, +L, modifying the
--	original list and returning the result as the return value as well
to mapItems from L as list given handler:function
				local L, function
				
				script
								property list : L
				end script
				
				tell (a reference to the result's list)
								repeat with i from 1 to its length
												set x to (a reference to its item i)
												set x's contents to function's ¬
																fn(x's contents, i, it)
								end repeat
				end tell
				
				L
end mapItems

--folderTree
--	Reads a +textfile that contains a tab-indented map of a folder hierarchy
--	and creates the directory structure in the +root folder
on folderTree from textfile at root
				local textfile, root
				
				tell application "System Events" to set textfile ¬
								to the POSIX path of the file named textfile
				
				--:[untab]
				--	Eliminates leading tabstops and saves the count against folder name
				script untab
								on fn(x)
												script
																to untab(x, |ξ|)
																				if {} = x's words then return false
																				if x's first character ≠ tab ¬
																								then return {|ξ|, x}
																				
																				untab(x's text 2 thru -1, 1 + |ξ|)
																end untab
												end script
												
												result's untab(x, 0)
								end fn
				end script
				
				--:[branch]
				--	Replaces folder names with paths to map intended folder locations
				script branch
								property sys : application "System Events"
								property dir0 : path of sys's item root
								
								on fn(x, i, L)
												if i = 1 then return {0, contents of ¬
																[dir0, x's item 2, ":"] as text}
												set {N, dirN} to x
												set {M, dirM} to L's item (i - 1)
												if N = 0 then
																[dir0, dirN, ":"]
												else if N ≠ 0 then
																if N > M then
																				[dirM, dirN, ":"]
																else if N = M then
																				[ascend from dirM, dirN, ":"]
																else if N < M then
																				set A to M - N + 1
																				[ascend from dirM by A, dirN, ":"]
																end if
												end if
												set dirN to the result as text
												{N, dirN}
								end fn
				end script
				
				--:[restOf]
				--	Discard tab counts and return a flattened list of folder paths
				script restOf
								on fn(x)
												x's end
								end fn
				end script
				
				--:[makedir]
				--	Given a folder path, create the folder
				script makedir
								on fn(x)
												set text item delimiters to ":"
												
												set dir to x's text items 1 thru -3 as text
												set fldrname to x's text item -2
												
												tell application "System Events"
																make new folder at folder dir ¬
																				with properties ¬
																				{name:fldrname}
												end tell
								end fn
				end script
				
				read the textfile using delimiter linefeed
				
				mapItems from the result given handler:untab
				mapItems from the result's lists given handler:branch
				mapItems from the result given handler:restOf
				mapItems from the result given handler:makedir
end folderTree
-----------------------------------------------------------------------------------------------------------------------------
--IMPLICIT RUN HANDLER:
folderTree from "~/Desktop/folders.dat" at "~/Example"
-----------------------------------------------------------------------------------------------------------------------------

It’s by no means the most efficient script and I would, at some point, like to merge the four separate calls to mapItems into a single call. However, it literally went straight from my brain out into the editor, so it’s evident how linearly I approached the problem and how unpolished the methodology remains.

A single handler call at the bottom of the script is where the process initiates. All of my handlers in this script use labelled parameters. folderTree takes two: the from parameter is where you specify the text file from which to read the tab-indented folder map; the at parameter is where you specify the location at which this new folder hierarchy is to be created. Abbreviated posix paths are fine; and HFS paths and alias objects are probably also fine.

[format]AppleScript: 2.7
Operating System: macOS 10.13[/format]

Here’s a non-recursive method (as far as the script’s concerned) which builds a Unix hierarchy template from the input text, appends it to the path to the main folder, and uses the result in a shell script with a single mkdir command to create the hierarchy:

-- The unit of indent (here a tab) MUST be the same for all the lines and mustn't occur in the folder names themselves.
set plainText to "(1) Home
(2) About Us
	(a) Our Story
	(b) Our Services
(3) Growth
	(a) Our Methods
	(b) Individual Coaching
	(c) Groups and Workshops
(4) Blog
(5) Resources
	(a) Recommendations
	(b) Book Lists
		(i) A book
	(c) Free Tools
		(i) Tool 1
		(ii) Tool 2
(6) Store"

set indentUnit to tab

set mainFolderPath to POSIX path of (choose folder with prompt "Choose the folder in which you want the hierarchy to appear …")

set fullTemplate to (quoted form of mainFolderPath) & buildHierarchyTemplate(plainText, indentUnit)
set shellCommand to "mkdir -p " & fullTemplate
do shell script shellCommand

(* Build a Unix folder hierarchy template from the given indented text. *)
on buildHierarchyTemplate(theText, indentUnit)
	set theLines to theText's paragraphs
	set previousIndentLevel to 0
	set output to {}
	
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to indentUnit
	repeat with i from 1 to (count theLines)
		set thisLine to item i of theLines
		set thisIndentLevel to (count thisLine's text items)
		set quotedName to quoted form of text item -1 of thisLine
		if (thisIndentLevel = previousIndentLevel) then
			-- Same indent as in the previous line. Append to this name to the current folder group.
			set end of output to "," & quotedName
		else if (thisIndentLevel > previousIndentLevel) then
			-- Greater indent than in the previous line. Start a new subfolder group with this name.
			set end of output to "/{" & quotedName
			set previousIndentLevel to thisIndentLevel
		else
			-- Lesser indent than in the previous line. Add closing braces for all subfolder groups up to the current level before appending the name.
			-- If the previous name started a new group, lose the brace from that edit. The shell script would count braces round a single name as part of the name.
			set previousEdit to end of output
			if (previousEdit begins with "/{") then
				set item -1 of output to "/" & text 3 thru -1 of previousEdit
				set previousIndentLevel to previousIndentLevel - 1
			end if
			-- Close any other pending subfolder groups up to the current level.
			repeat while (previousIndentLevel > thisIndentLevel)
				set end of output to "}"
				set previousIndentLevel to previousIndentLevel - 1
			end repeat
			-- Append the current name.
			set end of output to "," & quotedName
		end if
	end repeat
	-- At the end, close all outstanding subfolder groups.
	repeat while (previousIndentLevel > 0)
		set end of output to "}"
		set previousIndentLevel to previousIndentLevel - 1
	end repeat
	
	-- Coerce the output list to a single text and return the result
	set AppleScript's text item delimiters to ""
	set output to output as text
	set AppleScript's text item delimiters to astid
	
	return output
end buildHierarchyTemplate