Organizing multiple sub folders into parent folders based on name?

Hi,

My goal is to be be able to automatically sort multiple folders into newly created parent folders based on their naming convention.

I want to be able to run a script on the master folder which will then look at the contents of the master folder and sort the contents by name.

The current naming convention is as follows:

###_XXXX_Type

Where

is a 3-digit episode number

XXXX is a 2 to 4 digit slate number (Can contain letters as well)
Type is the type of contents the folder contains.

Example:
205_V121_Hdri
205_V121_Witness
205_V121_Ref

I would like the script to look at the folder names, recognize that 205_V121_xxxx are all related and then sort those three folders into one parent folder called 205_V121. I would also like it if there was a solo folder with no other folders that had the same name if that could still be sorted into a new parent folder following the same convention.

Example:
202_76_Ref
202_76A_Ref

This would get sorted into two separate folders called 202_76 and 202_76A (They wouldn’t be renamed, they would get put into a new parent folder)

Here is an image of a folder that I am organizing currently:

Here are the results I am looking for:

Note that there is a folder that does not follow the ###_XXXX_Type naming convention (Junkyard_General_Hdris) so it would be ignored.

Thank you so much for your help!
Cheers,
-Rob

Model: Macbook Pro Retina
AppleScript: Version 2.9 (191)
Browser: Chrome Version 55.0.2883.95 (64-bit)
Operating System: Mac OS X (10.10)

If I understood correctly this old fashioned script may fit your needs.

--set theMainFolder to (path to desktop as text) & "dossier 100 copie:" as alias
set theMainFolder to choose folder # returns an alias with an ending colon

tell application "System Events"
	set theNames to name of folders of theMainFolder
	repeat with aname in theNames
		set splitted to my decoupe(aname, "_")
		if (count splitted) > 2 and (count item 1 of splitted) = 3 and (character 1 of aname is in "0123456789") and (character 2 of aname is in "0123456789") and (character 3 of aname is in "0123456789") then # EDITED
			set prefix to my recolle(items 1 thru -2 of splitted, "_")
			set target to (theMainFolder as text) & prefix
			if not (exists folder target) then
				make new folder at end of theMainFolder with properties {name:prefix}
			end if
			my moveOrigInTarget((theMainFolder as text) & aname, target)
		end if
	end repeat
end tell

#=====

on moveOrigInTarget(orig, target)
	do shell script "mv " & quoted form of POSIX path of orig & space & quoted form of POSIX path of target
end moveOrigInTarget

#=====

on decoupe(t, d)
	local oTIDs, l
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set l to text items of t
	set AppleScript's text item delimiters to oTIDs
	return l
end decoupe

#=====

on recolle(l, d)
	local oTIDs, t
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set t to l as text
	set AppleScript's text item delimiters to oTIDs
	return t
end recolle

#=====

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) mardi 7 mars 2017 17:14:48

Hi Yvan,

Thanks so much again for your help.

My only issue I am having with this old script is that it does not ignore folders that do not follow the naming convention.

If there’s a simple fix, that would be incredible!

I’m trying to learn scripting some myself, so I’ve gone through and changed a few names of things so I can better understand what they do (or what I think they do).

I think I’ve cracked the problem of skipping over folders that don’t start with 3 digits. (Can be seen in the on organize(theMainFolder)) I’ve added a try in there that should test if the first set of characters before the “_” is a number, and if it’s not a number it will skip the folder.

If you could take a look to see if my solution makes sense and won’t bite me in the ass later, that would be awesome!

on run
	set theMainFolder to choose folder
	my organize(theMainFolder)
end run

on open sel
	#set theMainFolder to choose folder # returns an alias with an ending colon
	set theMainFolder to item 1 of sel
	my organize(theMainFolder)
end open

on organize(theMainFolder)
	tell application "System Events"
		set folderNames to name of folders of theMainFolder
		repeat with allNames in folderNames
			set parsedName to my parse(allNames, "_")
			if (count parsedName) > 2 then
				set prefix to my rejoin(items 1 thru -2 of paarsedName, "_")
				set episode to item 1 of parsedName
				try
					set episode to episode as number
					set target to (theMainFolder as text) & prefix
					if not (exists folder target) then
						make new folder at end of theMainFolder with properties {name:prefix}
					end if
					my moveOrigInTarget((theMainFolder as text) & allNames, target)
				end try
			end if
		end repeat
	end tell
end organize
#=====

on moveOrigInTarget(orig, target)
	do shell script "mv " & quoted form of POSIX path of orig & space & quoted form of POSIX path of target
end moveOrigInTarget

#=====

on parse(t, d)
	local oTIDs, l
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set l to text items of t
	set AppleScript's text item delimiters to oTIDs
	return l
end parse

#=====

on rejoin(l, d)
	local oTIDs, t
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set t to l as text
	set AppleScript's text item delimiters to oTIDs
	return t
	
end rejoin

#=====

I edited my script.
Now it checks that the first string before an underscore is 3 characters long and contain only digits.
I don’t know how to filter the second component except checking that it contain (maybe start with) two digits.

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) mercredi 8 mars 2017 12:11:05

Here is an enhanced version of your script.

on run
	set theMainFolder to choose folder
	my organize(theMainFolder)
end run

on open sel
	#set theMainFolder to choose folder # returns an alias with an ending colon
	set theMainFolder to item 1 of sel
	tell application "System Events"
		set theClass to (class of item (theMainFolder as text)) as text
		if theClass is not "«class cfol»" then error "Must drop a folder"
	end tell
	my organize(theMainFolder)
end open

on organize(theMainFolder)
	tell application "System Events"
		set folderNames to name of folders of theMainFolder
		repeat with allNames in folderNames
			set parsedName to my parse(allNames, "_")
			if (count parsedName) > 2 then
				set episode to item 1 of parsedName
				if (count episode) = 3 then
					set prefix to my rejoin(items 1 thru -2 of parsedName, "_") # was a typo (paarsedName)
					try
						set episode to episode as number
						set target to (theMainFolder as text) & prefix
						if not (exists folder target) then
							make new folder at end of theMainFolder with properties {name:prefix}
						end if
						my moveOrigInTarget((theMainFolder as text) & allNames, target)
					end try
				end if
			end if
		end repeat
	end tell
end organize
#=====

on moveOrigInTarget(orig, target)
	do shell script "mv " & quoted form of POSIX path of orig & space & quoted form of POSIX path of target
end moveOrigInTarget

#=====

on parse(t, d)
	local oTIDs, l
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set l to text items of t
	set AppleScript's text item delimiters to oTIDs
	return l
end parse

#=====

on rejoin(l, d)
	local oTIDs, t
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set t to l as text
	set AppleScript's text item delimiters to oTIDs
	return t
	
end rejoin

#=====

I will change the code moving the folders because as the move doesn’t change the storage volume, the Finder may be faster than shell script.
I will also build a version using ASObjC to move the folders.

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) mercredi 8 mars 2017 12:41:18

Here is the version using the Finder.

on run
	my organize(choose folder)
end run

on open sel
	#set theMainFolder to choose folder # returns an alias with an ending colon
	set theMainFolder to item 1 of sel
	tell application "Finder"
		set theClass to (class of item (theMainFolder as text)) as text
		if theClass is not "«class cfol»" then error "Must drop a folder"
	end tell
	my organize(theMainFolder)
end open

on organize(theMainFolder)
	
	tell application "Finder"
		set folderNames to name of folders of theMainFolder
		set theMainFolder to (theMainFolder as text)
		repeat with allNames in folderNames
			set parsedName to my parse(allNames, "_")
			if (count parsedName) > 2 then
				set episode to item 1 of parsedName
				if (count episode) = 3 then
					set prefix to my rejoin(items 1 thru -2 of parsedName, "_") # was a typo (paarsedName)
					try
						episode as number # no need to store the result in a variable
						set theTarget to theMainFolder & prefix # CAUTION, can't use target which is the name of a Finder's property
						if not (exists folder theTarget) then
							make new folder at folder theMainFolder with properties {name:prefix}
						end if
						move folder (theMainFolder & allNames) to folder theTarget
					end try
				end if
			end if
		end repeat
	end tell
end organize

#=====

on parse(t, d)
	local oTIDs, l
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set l to text items of t
	set AppleScript's text item delimiters to oTIDs
	return l
end parse

#=====

on rejoin(l, d)
	local oTIDs, t
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, d}
	set t to l as text
	set AppleScript's text item delimiters to oTIDs
	return t
	
end rejoin

#=====

As some of you already know, I hate the Finder.
It’s not this script which will change my advice.
The first script behaves silently.
This alternate version issue some noise which I can’t explain.

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) mercredi 8 mars 2017 14:05:21

Last and best one from my point of view, a version using ASObjC

use AppleScript version "2.4" # requires at least Yosemite
use scripting additions
use framework "Foundation"



on run
	my organize(choose folder)
end run

on open sel
	set theMainFolder to item 1 of sel
	if (my fileTypeOf:(POSIX path of theMainFolder)) ≠ "folder" then error "Must drop a folder"
	my organize(theMainFolder)
end open

on organize(theMainFolder)
	
	set folderNames to my listFolderNamesInFolder:(POSIX path of theMainFolder)
	
	repeat with allNames in folderNames
		set parsedName to (my parse:allNames usingString:"_")
		if (count parsedName) > 2 then
			set episode to item 1 of parsedName
			if (count episode) = 3 then
				set prefix to (my rejoin:(items 1 thru -2 of parsedName) usingString:"_")
				try
					set episode to episode as number
					set target to (theMainFolder as text) & prefix
					# No need to create the folder here, the handler will do the job
					(my movePath:(POSIX path of ((theMainFolder as text) & allNames)) toFolder:(POSIX path of target))
				end try
			end if
		end if
	end repeat
end organize

#=====#=====#=====#=====#=====#=====

# Six ASObjC handlers borrowed to Shane STANLEY

#=====#=====#=====#=====#=====#=====

on fileTypeOf:POSIXPath
	local |⌘|, theSourceURL, theResult, isDirectory, theError, isPackage
	set |⌘| to current application
	set theSourceURL to |⌘|'s class "NSURL"'s fileURLWithPath:POSIXPath
	set {theResult, isDirectory, theError} to theSourceURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsDirectoryKey) |error|:(reference)
	if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
	if isDirectory as boolean then
		set {theResult, isPackage, theError} to theSourceURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsPackageKey) |error|:(reference)
		if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
		if (isPackage as boolean) then
			return "package"
		else
			return "folder"
		end if
	end if
	return "file"
end fileTypeOf:

#=====#=====#=====#=====#=====#=====

on listFolderNamesInFolder:POSIXSourceFolder
	local |⌘|, fileManager, aURL, directoryContents, tempArray, theResult, isDirectory, theError, isPackage, itsName
	set |⌘| to current application
	set fileManager to |⌘|'s NSFileManager's defaultManager()
	set aURL to |⌘|'s |NSURL|'s fileURLWithPath:POSIXSourceFolder
	set directoryContents to fileManager's contentsOfDirectoryAtURL:aURL includingPropertiesForKeys:{} options:0 |error|:(missing value)
	set tempArray to |⌘|'s NSMutableArray's arrayWithCapacity:(directoryContents's |count|()) -- array to hold names
	repeat with aURL in directoryContents
		set itsName to aURL's |lastPathComponent|()
		if itsName as text does not start with "." then
			set {theResult, isDirectory, theError} to (aURL's getResourceValue:(reference) forKey:(current application's NSURLIsDirectoryKey) |error|:(reference))
			if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
			if isDirectory as boolean then
				set {theResult, isPackage, theError} to (aURL's getResourceValue:(reference) forKey:(current application's NSURLIsPackageKey) |error|:(reference))
				if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
				if not (isPackage as boolean) then (tempArray's addObject:(itsName))
			end if
		end if
	end repeat
	return tempArray as list
end listFolderNamesInFolder:

#=====#=====#=====#=====#=====#=====

on parse:sourceString usingString:d1
	set sourceString to current application's NSString's stringWithString:sourceString
	return (sourceString's componentsSeparatedByString:d1) as list
end parse:usingString:

#=====#=====#=====#=====#=====#=====

on rejoin:theList usingString:d1
	set anArray to current application's NSArray's arrayWithArray:theList
	return (anArray's componentsJoinedByString:d1) as text
end rejoin:usingString:

#=====#=====#=====#=====#=====#=====

on movePath:posixSource toFolder:posixDestination
	local |⌘|, theSourceURL, destURL, shortURL, origName, theFileManager, theResult, theError, destName
	
	set |⌘| to current application
	set theSourceURL to |⌘|'s |NSURL|'s fileURLWithPath:posixSource
	set destURL to |⌘|'s |NSURL|'s fileURLWithPath:posixDestination
	set theFileManager to |⌘|'s NSFileManager's |defaultManager|()
	set {theResult, theError} to theFileManager's createDirectoryAtURL:destURL withIntermediateDirectories:true attributes:(missing value) |error|:(reference)
	--if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
	# maintenant, move cheminPosixDuFichierSource item
	set destName to theSourceURL's |lastPathComponent|()
	set destURL to destURL's URLByAppendingPathComponent:destName
	my moveFromURL:theSourceURL toURL:destURL withReplacing:true
	return destURL's |path| as text
	
end movePath:toFolder:

#=====

-- This handler is called by other handlers, and is not meant to called directly
on moveFromURL:sourceURL toURL:destinationURL withReplacing:replaceFlag
	set |⌘| to current application
	set theFileManager to |⌘|'s NSFileManager's |defaultManager|()
	set {theResult, theError} to (theFileManager's moveItemAtURL:sourceURL toURL:destinationURL |error|:(reference))
	if not theResult as boolean then
		if replaceFlag and (theError's code() = |⌘|'s NSFileWriteFileExistsError) then -- it already exists, so try replacing
			-- replace existing file with temp file atomically, then delete temp directory
			set {theResult, theError} to theFileManager's replaceItemAtURL:destinationURL withItemAtURL:sourceURL backupItemName:(missing value) options:(|⌘|'s NSFileManagerItemReplacementUsingNewMetadataOnly) resultingItemURL:(missing value) |error|:(reference)
			-- if replacement failed, return error
			if not theResult as boolean then error (theError's |localizedDescription|() as text)
		else -- replaceFlag is false or an error other than file already exists, so return error
			error (theError's |localizedDescription|() as text)
		end if
	end if
end moveFromURL:toURL:withReplacing:

#=====#=====#=====#=====#=====#=====

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) mercredi 8 mars 2017 16:00:56

Is there a reason for all coercions? From what I see you could make it more efficient by writing the organize handler in ASObjC as well so the code require less coercions from «class ocid» to strings and vice versa.

I’m not at ease with strings manipulation using ASObjC so I choose to stay with what I am able to do.

Of course you are free to rewrite the code if you want.

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) mercredi 8 mars 2017 16:38:15

Here is a redesigned version of the code using ASObjC.

use AppleScript version "2.4" # requires at least Yosemite
use scripting additions
use framework "Foundation"



on run
	my organize(choose folder)
end run

on open sel
	set theMainFolder to item 1 of sel
	if (my fileTypeOf:theMainFolder) ≠ "folder" then error "Must drop a folder"
	my organize(theMainFolder)
end open

on organize(theMainFolder)
	local mainURL, |⌘|, fileManager, directoryContents, theRegEx, rootURL, itsName, theResult, isDirectory, theError
	local isPackage, theFinds, theRange, |length|, destName, theSourceURL, theDestURL, theFinalURL
	
	set mainURL to my makeURLFromFileOrPath:theMainFolder
	set |⌘| to current application
	set fileManager to |⌘|'s NSFileManager's defaultManager()
	set directoryContents to fileManager's contentsOfDirectoryAtURL:mainURL includingPropertiesForKeys:{} options:0 |error|:(missing value)
	# define the regex once
	set theRegEx to (|⌘|'s NSRegularExpression's regularExpressionWithPattern:"^[0-9][0-9][0-9][_][^ ]*[_]" options:0 |error|:(missing value))
	repeat with rootURL in directoryContents
		set itsName to rootURL's |lastPathComponent|()
		if (itsName as text) does not start with "." then
			set {theResult, isDirectory, theError} to (rootURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsDirectoryKey) |error|:(reference))
			if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
			if isDirectory as boolean then
				set {theResult, isPackage, theError} to (rootURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsPackageKey) |error|:(reference))
				if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
				if not (isPackage as boolean) then
					# try to extract : 3 digits, an underscore, some characters ≠ space, an underline starting from the beginning of the string itsName
					set theFinds to (theRegEx's matchesInString:itsName options:0 range:{location:0, |length|:itsName's |length|()})
					if (count of theFinds) > 0 then
						# the name matches our requirements
						set theRange to (item 1 of theFinds)'s range()
						set |length| of theRange to (|length| of theRange) - 1 # must drop the ending underscore
						set destName to (itsName's substringWithRange:theRange)
						set theSourceURL to (mainURL's URLByAppendingPathComponent:itsName)
						set theDestURL to (mainURL's URLByAppendingPathComponent:destName)
						# creates the folder if it doesn't exist, do nothing if it exists
						set {theResult, theError} to (fileManager's createDirectoryAtURL:theDestURL withIntermediateDirectories:true attributes:(missing value) |error|:(reference))
						if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
						set theFinalURL to (theDestURL's URLByAppendingPathComponent:itsName)
						(my moveFromURL:theSourceURL toURL:theFinalURL withReplacing:true)
					end if
				end if
			end if
		end if
	end repeat
end organize

#=====#=====#=====#=====#=====#=====

# Some ASObjC handlers borrowed to Shane STANLEY

#=====#=====#=====#=====#=====#=====

on fileTypeOf:aFileOrPath # use makeURLFromFileOrPath
	local |⌘|, theSourceURL, theResult, isDirectory, theError, isPackage
	set |⌘| to current application
	set theSourceURL to my makeURLFromFileOrPath:aFileOrPath
	set {theResult, isDirectory, theError} to theSourceURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsDirectoryKey) |error|:(reference)
	if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
	if isDirectory as boolean then
		set {theResult, isPackage, theError} to theSourceURL's getResourceValue:(reference) forKey:(|⌘|'s NSURLIsPackageKey) |error|:(reference)
		if not (theResult as boolean) then error (theError's |localizedDescription|() as text)
		if (isPackage as boolean) then
			return "package"
		else
			return "folder"
		end if
	end if
	return "file"
end fileTypeOf:

#=====

-- This handler is called by other handlers, and is not meant to called directly
on makeURLFromFileOrPath:theFileOrPathInput
	-- make it into a Cocoa object for easier comparison
	set theFileOrPath to item 1 of (current application's NSArray's arrayWithObject:theFileOrPathInput)
	if (theFileOrPath's isKindOfClass:(current application's NSString)) as boolean then
		if (theFileOrPath's hasPrefix:"/") as boolean then -- full POSIX path
			return current application's class "NSURL"'s fileURLWithPath:theFileOrPath
		else if (theFileOrPath's hasPrefix:"~") as boolean then -- POSIX path needing ~ expansion
			return current application's class "NSURL"'s fileURLWithPath:(theFileOrPath's |stringByExpandingTildeInPath|())
		else -- must be HFS path
			return current application's class "NSURL"'s fileURLWithPath:(POSIX path of theFileOrPathInput)
		end if
	else if (theFileOrPath's isKindOfClass:(current application's class "NSURL")) as boolean then -- happens with files and aliases in 10.11
		return theFileOrPath
	else -- must be a file or alias
		return current application's class "NSURL"'s fileURLWithPath:(POSIX path of theFileOrPathInput)
	end if
end makeURLFromFileOrPath:

#=====#=====#=====#=====#=====#=====

-- This handler is called by other handlers, and is not meant to called directly
on moveFromURL:sourceURL toURL:destinationURL withReplacing:replaceFlag
	set |⌘| to current application
	set theFileManager to |⌘|'s NSFileManager's |defaultManager|()
	set {theResult, theError} to (theFileManager's moveItemAtURL:sourceURL toURL:destinationURL |error|:(reference))
	if not theResult as boolean then
		if replaceFlag and (theError's code() = |⌘|'s NSFileWriteFileExistsError) then -- it already exists, so try replacing
			-- replace existing file with temp file atomically, then delete temp directory
			set {theResult, theError} to theFileManager's replaceItemAtURL:destinationURL withItemAtURL:sourceURL backupItemName:(missing value) options:(|⌘|'s NSFileManagerItemReplacementUsingNewMetadataOnly) resultingItemURL:(missing value) |error|:(reference)
			-- if replacement failed, return error
			if not theResult as boolean then error (theError's |localizedDescription|() as text)
		else -- replaceFlag is false or an error other than file already exists, so return error
			error (theError's |localizedDescription|() as text)
		end if
	end if
end moveFromURL:toURL:withReplacing:

#=====#=====#=====#=====#=====#=====

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) jeudi 9 mars 2017 15:40:59

Thanks for all your help with this Yvan.

I think I understand the basic difference between using System Events vs Finder.

But, can you help me understand the difference/benefits of using ASObjC over Applescript and why do you feel ASObjC is the best way to handle this?

I’m just genuinely curious.

Thanks!

First point. I know that some users disagree but from my point of view, ASObjC instructions are plain AppleScript.
Two months ago, I worked upon a script forced to use GUI scripting.
As it triggered the print dialog I was facing the fact that the documents to treat were allowed to contain the slash character in their name - bad practice but not illegal one.
Due to that I was facing problems due to the fact that in the print dialog some fields use the Posix spelling : slash embedded in a name are replaced by colons
but some other fields use the Hfs spelling
and some documents windows were using the slash as is while some others used the colon.

Tired to scrap my bare hairs I decided to replace every pieces of code using documents names/paths and all became simple.

It’s true that code usingASObjC requires more typing but its many times faster than other ones.
In several cases I got a factor 40.
When we may concatenate several commands in a single one, do Shell script may be faster but :

  • I’m not at ease with this task
  • as a French user, really often I have non ASCII chars in file names and they may fool shell scripting
  • in many folders I have packages and shell doesn’t make the difference between them and standard folders.

Now you know some reasons which pushed me on the ASObjC road.

In the script studied in this thread, look at the way we extract what you named prefix (which I named destName).
As I answered to DJ Bazzie Wazzie, I am not at ease with Regex but when I decided that there was no valid reason to miss this feature, I scrapped a bit my head and built the pattern required to extract the wanted string in a single call. No need to extraneous checks.

Last not least, I guess that ASObjC will be more and more useful when the new file system introduced in 10.12.4 and 5 will be officially distributed.

Yvan KOENIG running Sierra 10.12.3 in French (VALLAURIS, France) jeudi 9 mars 2017 16:27:59