Group selected Finder items by common prefix

Hi, I just finished this script which groups selected Finder items by common prefix.

Two questions:

  • I’ve used checkResourceIsReachableAndReturnError: to test whether a directory exists but am not sure that’s the right one. Should I use fileExistsAtPath:isDirectory: instead?

  • I’m also not sure about creating the directory with createDirectoryAtURL. Can I run into permission trouble or is this automatically handled even if I pass no attributes?

First time I’m doing something like this so it’s potentially harmful … but it seems to work :slight_smile:

-- Group selected Finder items

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property theGenericFolderName : "Neuer Ordner mit Objekten"

tell application "Finder"
	try
		set theSelection to selection as alias list
		if theSelection = {} then error "Keine Auswahl vorhanden"
		
	on error error_message number error_number
		if the error_number is not -128 then display alert "Finder" message error_message as warning
		return
	end try
end tell

set theURLs to current application's NSArray's arrayWithArray:theSelection

set theNames to current application's NSMutableArray's arrayWithArray:{}
repeat with thisURL in theURLs
	(theNames's addObject:(thisURL's lastPathComponent()))
end repeat

set theNames_Count to theNames's |count|()

if theNames_Count > 2 then
	set theNames_sortedByLength to (theNames's sortedArrayUsingDescriptors:{current application's NSSortDescriptor's sortDescriptorWithKey:"length" ascending:true selector:"compare:"})
	set thePrefix to (theNames_sortedByLength's objectAtIndex:0)'s commonPrefixWithString:(theNames_sortedByLength's objectAtIndex:1) options:(current application's NSCaseInsensitiveSearch)
	if (thePrefix's isEqualToString:"") then
		set theCommonPrefix to (current application's NSString's stringWithString:theGenericFolderName)
	else
		set theCommonPrefix to my getCommonPrefix(thePrefix, theNames_sortedByLength, theNames_Count)
	end if
	
else if theNames_Count = 2 then
	set theCommonPrefix to (theNames's objectAtIndex:0)'s commonPrefixWithString:(theNames's objectAtIndex:1) options:(current application's NSCaseInsensitiveSearch)
	if (theCommonPrefix's isEqualToString:"") then set theCommonPrefix to (current application's NSString's stringWithString:theGenericFolderName)
	
else if theNames_Count = 1 then
	set theCommonPrefix to (((current application's |NSURL|'s fileURLWithPath:(POSIX path of item 1 of theSelection))'s URLByDeletingPathExtension())'s pathComponents())'s lastObject()
end if

if (theCommonPrefix's isEqualToString:theGenericFolderName) = false then
	set theCharacterSet to current application's NSMutableCharacterSet's alloc()'s init()
	theCharacterSet's formUnionWithCharacterSet:(current application's NSCharacterSet's punctuationCharacterSet())
	theCharacterSet's formUnionWithCharacterSet:(current application's NSCharacterSet's whitespaceAndNewlineCharacterSet())
	set theCommonPrefix to theCommonPrefix's stringByTrimmingCharactersInSet:theCharacterSet
end if

set theFolderURL to (current application's |NSURL|'s fileURLWithPath:(POSIX path of item 1 of theSelection))'s URLByDeletingLastPathComponent()
set theNewFolderURL to theFolderURL's URLByAppendingPathComponent:theCommonPrefix isDirectory:true

set createdDirectory to current application's NSFileManager's defaultManager's createDirectoryAtURL:theNewFolderURL withIntermediateDirectories:false attributes:(missing value) |error|:(missing value)
if createdDirectory = false then
	set i to 2
	repeat
		set theNewFolderName to (current application's NSString's stringWithString:((theCommonPrefix as string) & space & i))
		set theNewFolderURL to (theFolderURL's URLByAppendingPathComponent:theNewFolderName isDirectory:true)
		set existsURL to (theNewFolderURL's checkResourceIsReachableAndReturnError:(missing value))
		if existsURL = false then
			exit repeat
		else
			set i to i + 1
		end if
	end repeat
	set {createdDirectory, theError} to current application's NSFileManager's defaultManager's createDirectoryAtURL:theNewFolderURL withIntermediateDirectories:false attributes:(missing value) |error|:(reference)
	if createdDirectory = false then
		display alert "Error" message ((theError's localizedDescription()) as string) buttons {"Ok"} default button {"Ok"} as critical
		return
	end if
end if

set theNewFolderPath to (theNewFolderURL's |path|()) as string

tell application "System Events"
	try
		move theSelection to folder theNewFolderPath
		
	on error error_message number error_number
		if the error_number is not -128 then display alert "System Events" message error_message as warning
		return
	end try
end tell

on getCommonPrefix(theCommonPrefix, theNames, theNames_Count)
	try
		repeat with i from 2 to (theNames_Count - 1)
			if (((theNames's objectAtIndex:i))'s hasPrefix:theCommonPrefix) = false then
				set theCommonPrefix_Length to theCommonPrefix's |length|()
				if theCommonPrefix_Length > 1 then
					set theCommonPrefix_shortened to (theCommonPrefix's substringToIndex:(theCommonPrefix_Length - 1))
					set theCommonPrefix to my getCommonPrefix(theCommonPrefix_shortened, theNames, theNames_Count)
				else
					set theCommonPrefix to (current application's NSString's stringWithString:theGenericFolderName)
					exit repeat
				end if
			end if
		end repeat
		return theCommonPrefix
	on error error_message number error_number
		activate
		display alert "Error: Handler \"getCommonPrefix\"" message error_message as warning
		error number -128
	end try
end getCommonPrefix

You can use either, although the general preference these days is to favor NSURL-based methods.

Passing no attributes gives the same result as creating a folder in the Finder, inheriting any permissions.

Thanks! I’ve added a check whether all selected items are in one folder, if not ask where to create the new one. However choose folder run via Finder is painfully slow and using System Events doesn’t help much. Tried the script in Everyday AppleScriptObjC’s chapter “Richer Interfaces” but it doesn’t seem to work anymore. Is it still possible to use ASObjC dialogs? Are they faster than choose folder?

-- Group selected Finder items

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property theGenericFolderName : "Neuer Ordner mit Objekten"

tell application "Finder"
	try
		set theSelection to selection as alias list
		if theSelection = {} then error "Keine Auswahl vorhanden"
		
	on error error_message number error_number
		if the error_number is not -128 then display alert "Finder" message error_message as warning
		return
	end try
end tell

set theURLs to current application's NSArray's arrayWithArray:theSelection
set theURLs_Count to theURLs's |count|()

if theURLs_Count ≥ 2 then
	
	-- Check whether items are all in one folder
	set theURLs_sortedByPath to (theURLs's sortedArrayUsingDescriptors:{current application's NSSortDescriptor's sortDescriptorWithKey:"path" ascending:true selector:"compare:"})
	set theFirstFolderURL to theURLs_sortedByPath's firstObject()'s URLByDeletingLastPathComponent()
	set theLastFolderURL to theURLs_sortedByPath's lastObject()'s URLByDeletingLastPathComponent()
	if theFirstFolderURL's isEqualTo:theLastFolderURL then
		set theFolderURL to theFirstFolderURL
	else
		set theFirstFolderURLComponents to theFirstFolderURL's pathComponents()
		set theLastFolderURLComponents to theLastFolderURL's pathComponents()
		set theCommonFolderURLComponentsArray to current application's NSMutableArray's arrayWithArray:{}
		repeat with i from 1 to ((theFirstFolderURLComponents's |count|()) - 1)
			if ((theFirstFolderURLComponents's objectAtIndex:i)'s isEqualTo:(theLastFolderURLComponents's objectAtIndex:i)) then
				(theCommonFolderURLComponentsArray's addObject:(theFirstFolderURLComponents's objectAtIndex:i))
			else
				exit repeat
			end if
		end repeat
		if theCommonFolderURLComponentsArray's |count|() > 0 then
			set theCommonFolderPath to (current application's |NSURL|'s fileURLWithPathComponents:theCommonFolderURLComponentsArray)'s |path|()
		else
			set theCommonFolderPath to current application's NSHomeDirectory()
		end if
		tell application "System Events"
			try
				activate
				set theFolder to choose folder default location (POSIX file (theCommonFolderPath as string) as alias)
				activate application "Finder"
			on error
				activate application "Finder"
				return
			end try
		end tell
		set theFolderURL to current application's |NSURL|'s fileURLWithPath:(POSIX path of theFolder)
	end if
	
	-- Get names
	set theNames to current application's NSMutableArray's arrayWithArray:{}
	repeat with thisURL in theURLs
		(theNames's addObject:(thisURL's lastPathComponent()))
	end repeat
	
	-- Get common prefix
	if theURLs_Count > 2 then
		set theNames_sortedByLength to (theNames's sortedArrayUsingDescriptors:{current application's NSSortDescriptor's sortDescriptorWithKey:"length" ascending:true selector:"compare:"})
		set thePrefix to (theNames_sortedByLength's objectAtIndex:0)'s commonPrefixWithString:(theNames_sortedByLength's objectAtIndex:1) options:(current application's NSCaseInsensitiveSearch)
		if (thePrefix's isEqualToString:"") then
			set theCommonPrefix to (current application's NSString's stringWithString:theGenericFolderName)
		else
			set theCommonPrefix to my getCommonPrefix(thePrefix, theNames_sortedByLength, theURLs_Count)
		end if
	else if theURLs_Count = 2 then
		set theCommonPrefix to (theNames's objectAtIndex:0)'s commonPrefixWithString:(theNames's objectAtIndex:1) options:(current application's NSCaseInsensitiveSearch)
		if (theCommonPrefix's isEqualToString:"") then
			set theCommonPrefix to (current application's NSString's stringWithString:theGenericFolderName)
		end if
	end if
	
else if theURLs_Count = 1 then
	set theFolderURL to (theURLs's objectAtIndex:0)'s URLByDeletingLastPathComponent()
	set theCommonPrefix to (theURLs's objectAtIndex:0)'s URLByDeletingPathExtension()'s pathComponents()'s lastObject()
end if

-- Trim folder name
if (theCommonPrefix's isEqualToString:theGenericFolderName) = false then
	set theCharacterSet to current application's NSMutableCharacterSet's alloc()'s init()
	theCharacterSet's formUnionWithCharacterSet:(current application's NSCharacterSet's punctuationCharacterSet())
	theCharacterSet's formUnionWithCharacterSet:(current application's NSCharacterSet's whitespaceAndNewlineCharacterSet())
	set theCommonPrefix to theCommonPrefix's stringByTrimmingCharactersInSet:theCharacterSet
end if

-- Create folder URL
set theNewFolderURL to theFolderURL's URLByAppendingPathComponent:theCommonPrefix isDirectory:true

-- Create folder
set createdDirectory to current application's NSFileManager's defaultManager's createDirectoryAtURL:theNewFolderURL withIntermediateDirectories:false attributes:(missing value) |error|:(missing value)
if createdDirectory = false then
	set i to 2
	repeat
		set theNewFolderName to (current application's NSString's stringWithString:((theCommonPrefix as string) & space & i))
		set theNewFolderURL to (theFolderURL's URLByAppendingPathComponent:theNewFolderName isDirectory:true)
		set existsURL to (theNewFolderURL's checkResourceIsReachableAndReturnError:(missing value))
		if existsURL = false then
			exit repeat
		else
			set i to i + 1
		end if
	end repeat
	set {createdDirectory, theError} to current application's NSFileManager's defaultManager's createDirectoryAtURL:theNewFolderURL withIntermediateDirectories:false attributes:(missing value) |error|:(reference)
	if createdDirectory = false then
		display alert "Error" message ((theError's localizedDescription()) as string) buttons {"Ok"} default button {"Ok"} as critical
		return
	end if
end if

set theNewFolderPath to (theNewFolderURL's |path|()) as string

-- Move items
tell application "System Events"
	try
		move theSelection to folder theNewFolderPath
		
	on error error_message number error_number
		if the error_number is not -128 then display alert "System Events" message error_message as warning
		return
	end try
end tell

on getCommonPrefix(theCommonPrefix, theNames, theURLs_Count)
	try
		repeat with i from 2 to (theURLs_Count - 1)
			if (((theNames's objectAtIndex:i))'s hasPrefix:theCommonPrefix) = false then
				set theCommonPrefix_Length to theCommonPrefix's |length|()
				if theCommonPrefix_Length > 1 then
					set theCommonPrefix_shortened to (theCommonPrefix's substringToIndex:(theCommonPrefix_Length - 1))
					set theCommonPrefix to my getCommonPrefix(theCommonPrefix_shortened, theNames, theURLs_Count)
				else
					set theCommonPrefix to (current application's NSString's stringWithString:theGenericFolderName)
					exit repeat
				end if
			end if
		end repeat
		return theCommonPrefix
	on error error_message number error_number
		activate
		display alert "Error: Handler \"getCommonPrefix\"" message error_message as warning
		error number -128
	end try
end getCommonPrefix