Get all subfolders in a target folder

As far as I am aware, it is not possible with a shortcut to recursively get all subfolders (including empty subfolders) in a target folder. A possible alternative is to call a second shortcut that is recursive, but I can’t seem to get this to work. Thanks for any help.

This is the calling shortcut.

Call Get Folders.shortcut (21.9 KB)

This is the called shortcut. I’m not sure if the recursion stops prematurely, or if the TheSubfolders list is reset with each recursion.

Get Folders.shortcut (22.2 KB)

In this case the help of the shell for example

set rootFolder to quoted form of POSIX path of (choose folder)
do shell script "/usr/bin/find " & rootFolder & " -type d"

might be more efficient.

1 Like

Thanks Stefan. That approach works well and will be useful in many instances (see screenshot below). However, the Find command returns POSIX paths, which cannot be used in many shortcut actions. I don’t believe there is a shortcut equivalent of AppleScript’s POSIX file.

Please try this AppleScript, it converts the POSIX paths to the universal class furl

set rootFolder to quoted form of POSIX path of (choose folder)
set allFolders to paragraphs of (do shell script "/usr/bin/find " & rootFolder & " -type d")
set theResult to {}
repeat with aFolder in allFolders
	set end of theResult to aFolder as «class furl»
end repeat
return theResult

1 Like

Thanks Stefan. That does work, and, FWIW, Finder can also be used. In both cases, folder objects that can be used directly in a shortcut action are returned.

I know, but it’s horribly slow.

Happened to be browsing here and this script caught my attention. Any chance to add to this script the ability to print a text file to the desktop?

Sure, this script creates a plain text file on desktop with name FoldersOf plus the name of the chosen folder

set rootFolder to (choose folder)
tell application "System Events" to set folderName to name of rootFolder
set destinationFile to quoted form of (POSIX path of (path to desktop) & "FoldersOf" & folderName & ".txt")
do shell script "/usr/bin/find " & quoted form of POSIX path of rootFolder & " -type d > " & destinationFile

Thank you, works perfectly, and the text file will prove very useful.

I decided to see if Google AI might have a solution. After creating the two shortcuts, it occurred to me that they were surprisingly similar to the shortcuts I included in post 1. The Google AI shortcuts didn’t work (even after correcting a few obvious errors).

I think the answer may lie in returning as input a list of the subfolders when the second shortcut calls itself. I’ll keep at it.

BTW, if including a screenshot of the Google AI suggestion violates something, please let me know.

I’m making progress. I found that the recursion was working and that the subfolders were found. The issue is the procedure by which the subfolders are made into a list and returned to the calling shortcut.

To confirm this, I rewrote the recursive shortcut to append the POSIX paths of the subfolders to a text file, which worked well. To test these shortcuts, the target folder and the folder that will contain the text file need to be set, and the name of the called shortcut may also need to be set.

If this was an AppleScript, I would make the recursive shortcut into a handler, and add the subfolders to a list, which I would make global. It’s the global part that can’t be done with a shortcut.

Anyways, at least I understand the issue.

Save Folders One.shortcut (22.1 KB)

Save Folders Two.shortcut (22.6 KB)

This is a revision of my post on 2025-08-24 that stated folder recursion, but used the wrong FileManager function to achieve that goal. It now does folder recursion returning an array of URLs and optionally (by uncommenting), a list of HFS paths for those URLs.

The entire Swift code that goes into the Run Shell Script:

import Foundation
import OSAKit

/* subDirectories URL extension
   Leo Dabus at:
   https://stackoverflow.com/questions/34388582/get-subdirectories-using-swift
*/

extension URL {
    func subDirectories() -> [URL] {
        // @available(macOS 10.11, iOS 9.0, *)
        guard hasDirectoryPath else { return [] }
        let foo = FileManager.default.enumerator(at: self,
                                                 includingPropertiesForKeys: nil,
                                                 options: [.skipsPackageDescendants, .skipsHiddenFiles],
                                                 errorHandler: nil)
        return (foo!.allObjects).compactMap { $0 as? URL }.filter { $0.hasDirectoryPath }
    }
}

var args: [URL]
var subDirs: [URL]

args = CommandLine.arguments.dropFirst().map { URL(fileURLWithPath: $0).standardizedFileURL }

// args.first contains the folder URL selected in the Finder
subDirs = args.first!.subDirectories()

let tildePath = (args.first!.path as NSString).abbreviatingWithTildeInPath
print("Subfolders of:  \(tildePath)")
print(String(repeating: "_", count: 14), terminator: "\n")

// as folder names
subDirs.forEach {
   print(($0.path as NSString).abbreviatingWithTildeInPath)
}

// as optional HFS subfolder paths
/*
print("\n")
print("HFS subfolders of:  \(tildePath)")
print(String(repeating: "_", count: 18), terminator: "\n")
subDirs.forEach { print(($0.path as NSString).value(forKey: "_osa_hfsPath")!) }
*/

The Test folder hierarchy:

Screenshot 2025-08-24 at 11.47.01 AM

and the result of the Shortcut (with HFS output suppressed):

Screenshot 2025-08-24 at 12.16.18 PM

and what the HFS output would appear as if enabled:

Screenshot 2025-08-24 at 12.19.00 PM

The Shortcut itself:

Clearly, one is not going to open this in Script Editor… :nerd_face:

I finally gave up on the goal set in post 1 and thought I’d finish up with a few comments and working shortcut examples. I incorporated Stefan’s suggestion in post 4 in a shortcut, and I tested it against my Finder shortcut. My observations are:

  • Both shortcuts returned shortcut folders that could be used in other shortcut actions.

  • With a folder that contained 26 subfolders, Stefan’s suggestion in a shortcut was faster at 290 milliseconds as compared with 350 milliseconds for my Finder shortcut.

  • Tested on a folder that contained packages, Stefan’s suggestion in a shortcut returned folders in the packages, and my Finder shortcut did not.

  • Stefan’s suggestion in a shortcut returned hidden folders. My Finder shortcut did not return hidden folders unless the Finder was set to show hidden folders.

The following shortcuts input a shortcut folder and output a list of shortcut folders.

Get Folders Shell.shortcut (22.3 KB)

Get Folders Finder.shortcut (22.1 KB)

BTW, I was able to get ASObjC code to run in a shortcut, but the result was slow.

Here is a Quick Action Shortcut that gets only the sorted subfolder hierarchy of a Finder selected folder. It uses a Run AppleScript and ASOC to quickly produce three display dialogs of the folder hierarchy:

  • POSIX paths

  • URL paths

  • HFS paths

Tested on macOS 15.6.1 with Script Debugger 8.0.10 and Script Editor 2.11.

Run AppleScript content:

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

property ca : current application

on run {input, parameters}
	
	set mutPath to ca's NSMutableArray's new()
	set mutURL to ca's NSMutableArray's new()
	set mutHFS to ca's NSMutableArray's new()
	
	set ascend to ca's NSSortDescriptor's sortDescriptorWithKey:"" ascending:true
	set theFolder to ca's NSURL's fileURLWithPath:(POSIX path of input)
	
	mutPath's addObjectsFromArray:(ca's NSArray's arrayWithArray:(my recursiveFolderEnumerate(theFolder)))
	mutPath's sortUsingDescriptors:{ascend}
	
	my convert_array(mutPath, mutURL, "fileURL")
	my convert_array(mutPath, mutHFS, "_osa_hfsPath")
	
	display dialog (mutPath's componentsJoinedByString:return) as text
	display dialog (mutURL's componentsJoinedByString:return) as text
	display dialog (mutHFS's componentsJoinedByString:return) as text
	
	return input
end run

on recursiveFolderEnumerate(afolder)
	set fm to ca's NSFileManager's defaultManager()
	-- presumption is that folders do not have extensions
	set pred to ca's NSPredicate's predicateWithFormat:"SELF.pathExtension == '' "
	set theKeys to {ca's kCFURLIsDirectoryKey}
	set theOptions to (ca's NSDirectoryEnumerationSkipsPackageDescendants as integer) + (ca's NSDirectoryEnumerationSkipsHiddenFiles as integer)
	set enumerator to (((fm's enumeratorAtURL:afolder includingPropertiesForKeys:theKeys options:theOptions errorHandler:(reference))'s allObjects()'s ¬
		valueForKey:"path")'s filteredArrayUsingPredicate:pred) as list
	return enumerator
end recursiveFolderEnumerate

on convert_array(src_ary, dest_ary, keyStr)
	try
		(dest_ary's setArray:(src_ary's allObjects()'s valueForKeyPath:keyStr))
	on error
		-- On macOS 15.6.1, Apple's Script Editor 2.11 accepts fileURL and Script Debugger 8.0.10 throws a KVC error 
		if keyStr = "fileURL" then
			repeat with apath in src_ary
				(dest_ary's addObject:(ca's NSURL's fileURLWithPath:apath))
			end repeat
		end if
	end try
	return
end convert_array

And the Shortcut:

Another option that might be of use on occasion is to get folders or files by way of an AppleScript script library. This has several advantages but can be slow, and it breaks if the size of the source folder is very large.

The script library is called as follows and returns Shortcuts folder or file objects.

Get Folders with Script Library.shortcut (22.0 KB)

This is the script library, which must be placed in the user’s ~/Library/Script Libraries/ folder. This folder will need to be created if it doesn’t exist.

use framework "Foundation"
use scripting additions

--This is an AppleScript script library that must be called from an AppleScript or Shortcuts app shortcut
--The sourceFolder parameter is a path

--Get folders but not packages
on getFolders(sourceFolder)
	set folderContents to getFolderContents(sourceFolder)
	
	set folderKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set theFolders to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true
	
	repeat with anItem in folderContents
		set {theResult, aFolder} to (anItem's getResourceValue:(reference) forKey:folderKey |error|:(missing value))
		if aFolder is booleanTrue then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
			if aPackage is not booleanTrue then (theFolders's addObject:anItem)
		end if
	end repeat
	
	return theFolders as list
end getFolders

--Get files but not alias files
on getFiles(sourceFolder)
	set folderContents to getFolderContents(sourceFolder)
	
	set fileKey to current application's NSURLIsRegularFileKey
	set aliasKey to current application's NSURLIsAliasFileKey
	set theFiles to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true
	
	repeat with anItem in folderContents
		set {theResult, aFile} to (anItem's getResourceValue:(reference) forKey:fileKey |error|:(missing value))
		if aFile is booleanTrue then
			set {theResult, anAlias} to (anItem's getResourceValue:(reference) forKey:aliasKey |error|:(missing value))
			if anAlias is not booleanTrue then (theFiles's addObject:anItem)
		end if
	end repeat
	
	return theFiles as list
end getFiles

--Get alias files but not symbolic links
on getAliasFiles(sourceFolder)
	set folderContents to getFolderContents(sourceFolder)
	
	set aliasKey to current application's NSURLIsAliasFileKey
	set symbolicLinkKey to current application's NSURLIsSymbolicLinkKey
	set theAliasFiles to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true
	
	repeat with anItem in folderContents
		set {theResult, anAliasFile} to (anItem's getResourceValue:(reference) forKey:aliasKey |error|:(missing value))
		if anAliasFile is booleanTrue then
			set {theResult, aSymbolicLink} to (anItem's getResourceValue:(reference) forKey:symbolicLinkKey |error|:(missing value))
			if aSymbolicLink is not booleanTrue then (theAliasFiles's addObject:anItem)
		end if
	end repeat
	
	return theAliasFiles as list
end getAliasFiles

--Get packages
on getPackages(sourceFolder)
	set folderContents to getFolderContents(sourceFolder)
	
	set packageKey to current application's NSURLIsPackageKey
	set thePackages to current application's NSMutableArray's new()
	set booleanTrue to current application's NSNumber's numberWithBool:true
	
	repeat with anItem in folderContents
		set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
		if aPackage is booleanTrue then (thePackages's addObject:anItem)
	end repeat
	
	return thePackages as list
end getPackages

--Gets folder contents for use in the above handlers
on getFolderContents(sourceFolder)
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set fileManager to current application's NSFileManager's defaultManager()
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() --hidden files and package contents are skipped
end getFolderContents

BTW, as noted above, the script library approach has several advantages over a shortcut that uses a Get Contents of Folder action with the recursive option enabled. The script library approach:

  • will return folders, alias files, or packages if desired; and

  • does not return package contents.

Just to quantify, I tested the script library shortcut and a shortcut that uses a Get Folder Contents action. Both of these were set to return all files in my smallish Home folder with the Library folder hidden. The results were:

  • The script library shortcut took 2.98 seconds and the Get Folder Contents shortcut took 2.30 seconds.

  • The script library shortcut returned 853 files which included no files in packages and the Get Folder Contents shortcut returned 1,165 files which included a large number of files in packages.

A significant amount of the time taken by the script library shortcut is attributable to the time it takes to coerce AppleScript folder and file objects to Shortcuts folder and file objects. I added an option in the script library to return paths, which reduced the timing result from 2.98 seconds to 0.81 second when getting all files in my Home folder with the Library folder hidden. Running the shortcut by way of Spotlight reduces both of these timing results by about 650 milliseconds.

Folder Contents.shortcut (22.1 KB)

use framework "Foundation"
use scripting additions

--This script library recursively gets items of a specified type from the contents of a folder
--The first handler parameter is the path of the source folder
--The second handler parameter is a boolean that returns paths if true and folders and files if false

--Get folders skip packages
on getFolders(sourceFolder, pathOption)
	set folderContents to getFolderContents(sourceFolder)
	
	set folderKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set booleanTrue to current application's NSNumber's numberWithBool:true
	set theFolders to current application's NSMutableArray's new()
	
	repeat with anItem in folderContents
		set {theResult, aFolder} to (anItem's getResourceValue:(reference) forKey:folderKey |error|:(missing value))
		if aFolder is booleanTrue then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
			if aPackage is not booleanTrue then (theFolders's addObject:anItem)
		end if
	end repeat
	
	if pathOption is true then
		return (theFolders's valueForKey:"path") as list
	else
		return theFolders as list
	end if
end getFolders

--Get files skip alias files and symbolic links
on getFiles(sourceFolder, pathOption)
	set folderContents to getFolderContents(sourceFolder)
	
	set fileKey to current application's NSURLIsRegularFileKey
	set aliasKey to current application's NSURLIsAliasFileKey
	set booleanTrue to current application's NSNumber's numberWithBool:true
	set theFiles to current application's NSMutableArray's new()
	
	repeat with anItem in folderContents
		set {theResult, aFile} to (anItem's getResourceValue:(reference) forKey:fileKey |error|:(missing value))
		if aFile is booleanTrue then
			set {theResult, anAlias} to (anItem's getResourceValue:(reference) forKey:aliasKey |error|:(missing value))
			if anAlias is not booleanTrue then (theFiles's addObject:anItem)
		end if
	end repeat
	
	if pathOption is true then
		return (theFiles's valueForKey:"path") as list
	else
		return theFiles as list
	end if
end getFiles

--Get alias files skip symbolic links
on getAliasFiles(sourceFolder, pathOption)
	set folderContents to getFolderContents(sourceFolder)
	
	set aliasKey to current application's NSURLIsAliasFileKey
	set symbolicLinkKey to current application's NSURLIsSymbolicLinkKey
	set booleanTrue to current application's NSNumber's numberWithBool:true
	set theAliasFiles to current application's NSMutableArray's new()
	
	repeat with anItem in folderContents
		set {theResult, anAliasFile} to (anItem's getResourceValue:(reference) forKey:aliasKey |error|:(missing value))
		if anAliasFile is booleanTrue then
			set {theResult, aSymbolicLink} to (anItem's getResourceValue:(reference) forKey:symbolicLinkKey |error|:(missing value))
			if aSymbolicLink is not booleanTrue then (theAliasFiles's addObject:anItem)
		end if
	end repeat
	
	if pathOption is true then
		return (theAliasFiles's valueForKey:"path") as list
	else
		return theAliasFiles as list
	end if
end getAliasFiles

--Get packages
on getPackages(sourceFolder, pathOption)
	set folderContents to getFolderContents(sourceFolder)
	
	set packageKey to current application's NSURLIsPackageKey
	set booleanTrue to current application's NSNumber's numberWithBool:true
	set thePackages to current application's NSMutableArray's new()
	
	repeat with anItem in folderContents
		set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
		if aPackage is booleanTrue then (thePackages's addObject:anItem)
	end repeat
	
	if pathOption is true then
		return (thePackages's valueForKey:"path") as list
	else
		return thePackages as list
	end if
end getPackages

--Get folder contents for use in above handlers
on getFolderContents(sourceFolder)
	set sourceFolder to current application's |NSURL|'s fileURLWithPath:sourceFolder
	set fileManager to current application's NSFileManager's defaultManager()
	set folderContents to (fileManager's enumeratorAtURL:sourceFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() --hidden files and package contents are skipped
	return folderContents
end getFolderContents

My Home folder has stabilized after a reinstall, and I thought I would rerun the script-library timing tests. The details of the timing tests are:

  • All files in my Home folder (about 814) were gotten by way of the Folder Contents script library.
  • The Library folder in my Home folder was hidden.
  • The script library was called by an AppleScript.
  • The calling AppleScript was first set to return paths and then folders and files.
  • The vehicles that ran the calling AppleScript were Script Geek, a shortcut by way of Spotlight, and a shortcut by way of the Shortcuts Editor.
  • The folders and files returned by the calling AppleScript when run by Script Geek were AppleScript folders and files.
  • The folders and files returned by the calling AppleScript when run by the Shortcuts Editor and by Spotlight were Shortcuts folders and files.
  • My computer is an M2 Mac mini running macOS 26.5.

The results in seconds were:

Test Vehicle Return Path Return Folder/File
Script Geek 0.06 0.08
Spotlight 0.17 2.04
Shortcuts Editor 0.81 2.70

The reason for the relatively high timing results of 2.04 and 2.70 were the time it took the Run AppleScript action to coerce AppleScript folder and files to Shortcuts folders and files.

The reason the Spotlight result is about 0.65 second faster than the Shortcuts Editor result reflects the time it takes to load (or more accurately not load) both the Foundation framework and the underlying code for the Run AppleScript action. This speed advantage is not present the first time the shortcut is run by way of Spotlight (e.g. after a reboot).

I also ran the same timing tests using the Get Contents of Folder action, and those results are shown below. Because this action gets package contents, the number of files returned was 1,113.

Test Vehicle Return Path Return Folder/File
Spotlight 2.19 1.75
Shortcuts Editor 2.26 1.82

A shortcut using the Get Contents of Folder action has a timing result of about 0.03 second when run on a folder containing 5 files and no folders or packages. In this use scenario, the Get Contents of Folder action seems the obvious choice.

Just to set a baseline, I attempted to get all files in my Home folder by way of Finder in an AppleScript, but the script failed with a time-out error message.

The following is my script-library test shortcut.

The Shortcuts app does not have a direct equivalent to AppleScript’s POSIX file class, which gets a folder or file object from a POSIX path. However, the Get File from Folder action is close, and the following is an example of how it can be used for this purpose.

Path to File.shortcut (22.9 KB)

I attempted to use this approach to create a list of Shortcuts file objects from the path of every file in my smallish Home folder (Library folder hidden), and the timing result was 1 minute and 3 seconds. However, with a folder that contained 10 files, the timing result was 70 milliseconds.

That is weird, because even Automator uses POSIX behind the scenes. What happens if you just run the paths through a shell script that only returns its input?

In that case, the original path is returned.

The issue (at least for me) is that many Shortcuts actions require a start folder, and it has to be a folder not a path. I suspect this is more of an issue for people used to writing AppleScripts than it is for other users.