Challenge - How to get the parent path of a list of POSIX paths

Hi!

I’m wondering if anyone knows how to solve the following:

I’ve a list of POSIX paths (variable: listOfFiles)
Example:

No I want to find out what parent path all these paths have in common!
(In this case “this/is/” would be the answer)


EqualParentPath({"this/is/a/test/path", "this/is/another/test/path", "this/is/a/taste/path", "this/is/a/test/file", "this/is/not/really/a/test/path"})
--Result: {"this/is/",8}

on EqualParentPath(listOfFiles)
	--script stuff comes here
	return {theEqualPath, lastEqualChar}
end EqualParentPath

Model: iBook G4
AppleScript: 1.10.3
Browser: Safari 412.5
Operating System: Mac OS X (10.4)

I have a commonParent handler that deals with AppleScript paths that I’m sure you could modify to suit your needs.

on commonParent(theseFiles)
	if theseFiles is not {} then
		set startupDisk to characters 1 thru -2 of (path to startup disk as text) as text
		set fList to {}
		repeat with thisFile in theseFiles
			set end of fList to textToList(thisFile as text, ":")
		end repeat
		
		set lastCommonParent to {} -- a list to get all common parent path components
		set shouldRecordParent to true
		try
			set baseCompare to item 1 of fList
			repeat with i from 1 to count of baseCompare
				set thisParentItem to item i of baseCompare
				repeat with j from 2 to count of fList
					set subCompareItem to item i of item j of fList
					if subCompareItem is not equal to thisParentItem then
						set shouldRecordParent to false
					end if
				end repeat
				if shouldRecordParent then set end of lastCommonParent to thisParentItem
			end repeat
			
			if startupDisk is in lastCommonParent and (count of lastCommonParent) is 1 then
				-- the common parent is the startup disk
				-- disallow
				log "EXCEPTION@commonParent() : Common parent is the startup disk."
				return "Common parent is the startup disk."
			else
				set commonParentPath to listToText(lastCommonParent, ":") & ":" as alias
			end if
		on error e
			return e
		end try
	else
		return missing value
	end if
end commonParent

on textToList(thisText, delim)
	set {tid, my text item delimiters} to {my text item delimiters, delim}
	try
		set textList to every text item of thisText
		set my text item delimiters to tid
	on error
		set my text item delimiters to tid
	end try
	return textList
end textToList

on listToText(thisList, delim)
	set {tid, my text item delimiters} to {my text item delimiters, delim}
	try
		set textList to every item of thisList as text
		set my text item delimiters to tid
	on error
		set my text item delimiters to tid
		return e
	end try
	return textList
end listToText

Great! Thanx!
What a quick answer - 12 minutes!

I just had to replace the “:” tith “/” and removed the “as alias” stuff

I figured it would be easy enough to modify. Glad it worked for you.

You can likely utilize this command to help reduce some of the above code.

I read the man page for dirname to see if I could figure how best to trim the code but I couldn’t come up with anything - though I admit I’m not that clever. What I did notice was my sloppy return values, which led to spotting another major fix, which trimmed the main handler nicely. Here is the corrected main handler:

on commonParent(theseFiles)
	set fList to {}
	repeat with thisFile in theseFiles
		set end of fList to textToList(thisFile as text, "/")
	end repeat
	
	set lastCommonParent to {} -- a list to get all common parent path components
	set shouldRecordParent to true
	try
		set baseCompare to item 1 of fList
		repeat with i from 1 to count of baseCompare
			set thisParentItem to item i of baseCompare
			repeat with j from 2 to count of fList
				set subCompareItem to item i of item j of fList
				if subCompareItem is not equal to thisParentItem then set shouldRecordParent to false
			end repeat
			if shouldRecordParent then set end of lastCommonParent to thisParentItem
		end repeat
	end try
	return listToText(lastCommonParent, "/") & "/"
end commonParent

I didn’t read throught the whole script - but I just wanted to elaborate on what Mikey_san was saying…

dirname and basename are 2 unix utilities that I use a ton - and they are much easier than the Applescript way of doing it. eg:

 dirname /this/is/the/path/i_am_th_file.txt
--returns:
/this/is/the/path

 basename /this/is/the/path/i_am_th_file.txt
--returns
i_am_th_file.txt
 

just thought you might find that interesting…

Even though I’m still not seeing how I could use dirname in a more efficient algorithm, this is helping me a lot. It would be nice to know if I’m just overlooking something obvious, which has been known to happen.

Anyway, I made a few more tiny modifications to eliminate the listToText handler and the unecessary first loop, and accommodate either standard paths or POSIX paths.

on commonParent(theseFiles)
	set delim to ":"
	if item 1 of theseFiles as text does not contain ":" then set delim to "/"
	set lastCommonParent to {} -- a list to get all common parent path components
	set shouldRecordParent to true
	try
		set baseCompare to textToList(item 1 of theseFiles as text, delim)
		repeat with i from 1 to count of baseCompare
			set thisParentItem to item i of baseCompare
			repeat with j from 2 to count of theseFiles
				if item i of textToList(item j of theseFiles as text, delim) is not equal to thisParentItem then set shouldRecordParent to false
			end repeat
			if shouldRecordParent then set end of lastCommonParent to thisParentItem & delim
		end repeat
	end try
	if delim is "/" then return lastCommonParent as text
	return lastCommonParent as text as alias
end commonParent

on textToList(thisText, delim)
	set {tid, my text item delimiters} to {my text item delimiters, delim}
	try
		set textList to every text item of thisText
		set my text item delimiters to tid
	on error
		set my text item delimiters to tid
	end try
	return textList
end textToList

Joseph, I now wrote one by myself.
(I just didn’t know how to go about doing this when I posted my question)

I wrote my own routine which is about 3x faster than your last one:

on commonParent(fileList)
	set delim to ":"
	if item 1 of fileList contains "/" then set delim to "/"
	set {tid, my text item delimiters} to {my text item delimiters, delim}
	set PathToCompare to text items of (item 1 of fileList as text)
	set compareUntil to (count PathToCompare)
	repeat with i from 2 to count fileList
		set comparsionPath to text items of (item i of fileList as text)
		repeat with j from 1 to compareUntil
			if (item j of PathToCompare) = (item j of comparsionPath) then
			else
				--this line:
				set compareUntil to j - 1
				--and this line prevent the handler from wasting extra time n very long paths when difference was found somewhere at the beginning
				exit repeat
			end if
		end repeat
	end repeat
	try
		set common to (items 1 thru compareUntil of PathToCompare) as text
		set my text item delimiters to tid
	on error
		set my text item delimiters to tid
	end try
	return common
end commonParent

I have some things in mind that could still improve it - I’ll post a optimized version when I’ve checked it.

-Vincent

Hi. If you’re interested, here’s another approach that doesn’t require nested repeats:

on EqualParentPath(listOfFiles)
	set astid to AppleScript's text item delimiters
	
	-- It's assumed here that the paths are POSIX paths.
	-- Combine the paths into a single text, each path beginning with a 'return' marker.
	set AppleScript's text item delimiters to return
	set listtext to (return as Unicode text) & listOfFiles
	
	-- Use the first return-marked path as a test source.
	set path1 to (return as Unicode text) & item 1 of listOfFiles
	-- Count the paths.
	set pathCount to (count listOfFiles)
	
	-- Set the TIDs to progressively longer sections of the first path.
	-- If there are more text items in the combined text than there are paths, all the paths contain the TIDs value.
	-- Otherwise, at least one of the paths does not match. The previous TIDs value is then what we want.
	set theEqualPath to "" -- In case there are no matches at all.
	set AppleScript's text item delimiters to "/"
	repeat with i from 1 to (count path1's text items)
		set AppleScript's text item delimiters to "/"
		set AppleScript's text item delimiters to (text 1 thru text item i of path1) & "/"
		if (count listtext's text items) > pathCount then
			set theEqualPath to AppleScript's text item delimiters
		else
			exit repeat
		end if
	end repeat
	set AppleScript's text item delimiters to astid
	
	-- Lose the leading return from the result
	if ((count theEqualPath) > 0) then set theEqualPath to text 2 thru -1 of theEqualPath
	
	return {theEqualPath, (count theEqualPath)}
end EqualParentPath

EqualParentPath({"this/is/a/test/path", "this/is/another/test/path", "this/is/a/taste/path", "this/is/a/test/file", "this/is/not/really/a/test/path"})
--Result: {"this/is/",8}

Nigel, I think we’ve (or better “you’ve”) found the best way to do it!

I chacked them all with fila paths like this one:

your method is the only smart one! The other methods do too much checking than needed if the paths differ somewhere near the beginning.
And it is the fastest one! (I replaced the “as Unicode text” with “as text” to have a fair comparison)
It’s even 3x faster than mine! And therefore 10x faster than the one before.

Thank you all(!) for these great code snippets! It’s you who make Macscripter such a great place!

Yeah, thanks Nigel.

One problem for me, Nigel’s handler doesn’t handle a list of Finder aliases :(. But Vincent’s does. Getting the index of the last common component and the exit repeat is just what I was looking for for that handler. Thanks.

Two notes about your script, Vincent:

if item 1 of fileList contains "/" then set delim to "/"

Mac file names can contain “/” but can’t contain “:” which is why I test the other way.

set common to (items 1 thru compareUntil of PathToCompare) as text

For a standard path, this would also need the trailing “:”. I know POSIX paths don’t require the trailing “/” but having it doesn’t effect the reference either so the line should have the " & delim" at the end to be safe.

Anyway, any advice on how to accommodate aliases with your method, Nigel?

Hi, Joseph. I think the version below is good for aliases, file specifications, Mac OS paths, and POSIX paths, but not for Finder or System Events references. It assumes that all the items in the list are of the same type. (You may want to develop the input check at the top to ensure that they are.) It returns a POSIX path if the list contains POSIX paths, and a Mac OS path otherwise.

You noted that Mac OS file names can contain “/”, but the POSIX paths of those same files will contain “:” instead! So I’ve used the expedient of trying to coerce one of the paths to file specification. If it works, the action is for Mac OS paths; otherwise it’s assumed they’re POSIX paths.

on EqualParentPath(listOfFiles)
	-- This input check needs to be more fully developed.
	if not ((class of listOfFiles is list) and ((count listOfFiles) > 0) and (class of item 1 of listOfFiles is in {alias, file specification, string, Unicode text})) then
		error
	end if
	
	set astid to AppleScript's text item delimiters
	
	-- It's assumed here that the list items are all the same type and are
	-- POSIX paths, HFS paths, aliases, or file specifications.
	-- Coerce the list to a single text, each item beginning with a 'return' marker.
	set AppleScript's text item delimiters to return
	set listText to (return as Unicode text) & listOfFiles
	
	-- Use the first return-marked item (path) as a test source.
	set path1 to text 1 thru text item 2 of listText
	-- If it can be coerced to file specification, its a Mac OS path. Otherwise assume it's a POSIX path.
	try
		(text item 2 of path1) as file specification
		set pathDelim to ":"
	on error
		set pathDelim to "/"
	end try
	-- Count the paths.
	set pathCount to (count listOfFiles)
	
	-- Set the TIDs to progressively longer sections of the first path.
	-- If there are more text items in the combined text than there are paths, all the paths contain the TIDs value.
	-- Otherwise, at least one of the paths does not match. The previous TIDs value is then what we want.
	set theEqualPath to "" -- In case there are no matches at all.
	set AppleScript's text item delimiters to pathDelim
	repeat with i from 1 to (count path1's text items)
		set AppleScript's text item delimiters to pathDelim
		set AppleScript's text item delimiters to (text 1 thru text item i of path1) & pathDelim
		if ((count listText's text items) > pathCount) then
			set theEqualPath to AppleScript's text item delimiters
		else
			exit repeat
		end if
	end repeat
	set AppleScript's text item delimiters to astid
	
	-- Lose the leading return from the result
	if ((count theEqualPath) > 0) then set theEqualPath to text 2 thru -1 of theEqualPath
	
	return {theEqualPath, (count theEqualPath)}
end EqualParentPath

You could check whether there are more “/” than “:” → probably POSIX
and vice versa. → probably alias
:D:lol:

I’ve never really understood why there is need for an optional alias path format:|
It only bring problems with it to have several formats!

Good grief. I did not know that.

As far as the handler, I need it to work with Finder refs, too, it seems, so I think I’m sticking with Vincent’s variation. Thanks, though.