macOS File Finder Swift Script

This Swift script allows users to search for files of a specific type in a chosen directory on macOS. It interactively prompts for a directory (from common options or a custom path) and a file extension, then recursively locates matching files and displays their names along with human-readable file sizes.

Click to view script…
#!/usr/bin/env swift

import Foundation

// MARK: - Helper: Format File Size in Human-Readable Format
func humanReadableSize(_ size: UInt64) -> String {
    let formatter = ByteCountFormatter()
    formatter.countStyle = .file
    // Allow units from bytes up to TB
    formatter.allowedUnits = [.useKB, .useMB, .useGB, .useTB]
    return formatter.string(fromByteCount: Int64(size))
}

// MARK: - Helper: Prompt for User Input
func prompt(_ message: String) -> String {
    print(message, terminator: " ")
    return readLine() ?? ""
}

// MARK: - Prepare Candidate Directories
let fileManager = FileManager.default
var candidateDirectories: [String: URL] = [:]

if let desktopURL = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first {
    candidateDirectories["Desktop"] = desktopURL
}
if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
    candidateDirectories["Documents"] = documentsURL
}
if let downloadsURL = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first {
    candidateDirectories["Downloads"] = downloadsURL
}
// Provide an option for a custom directory.
candidateDirectories["Other"] = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)

// MARK: - Display Directory Options
print("Select a directory to search in:")
let directoryKeys = Array(candidateDirectories.keys).sorted() // sorted for consistent ordering
for (index, key) in directoryKeys.enumerated() {
    if key == "Other" {
        print("\(index + 1): Enter a custom directory path")
    } else if let url = candidateDirectories[key] {
        print("\(index + 1): \(key) -> \(url.path)")
    }
}

guard let dirChoiceStr = readLine(), let choiceNum = Int(dirChoiceStr),
      choiceNum > 0, choiceNum <= directoryKeys.count else {
    print("Invalid directory choice. Exiting.")
    exit(1)
}

let chosenKey = directoryKeys[choiceNum - 1]
var searchDirectory: URL

if chosenKey == "Other" {
    let customPath = prompt("Enter full directory path:")
    searchDirectory = URL(fileURLWithPath: customPath, isDirectory: true)
} else {
    searchDirectory = candidateDirectories[chosenKey]!
}

// Verify the directory exists
var isDir: ObjCBool = false
guard fileManager.fileExists(atPath: searchDirectory.path, isDirectory: &isDir), isDir.boolValue else {
    print("The selected path does not exist or is not a directory. Exiting.")
    exit(1)
}

// MARK: - Get File Extension to Search For
let fileExtensionInput = prompt("Enter file extension to search for (e.g., txt, pdf, jpg):")
let fileExtension = fileExtensionInput.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()

if fileExtension.isEmpty {
    print("No file extension provided. Exiting.")
    exit(1)
}

// Validate file extension (alphanumeric only)
let regex = try! NSRegularExpression(pattern: "^[a-zA-Z0-9]+$")
let range = NSRange(location: 0, length: fileExtension.utf16.count)
if regex.firstMatch(in: fileExtension, options: [], range: range) == nil {
    print("Invalid file extension provided. Exiting.")
    exit(1)
}

// MARK: - Search for Files
print("\nSearching for .\(fileExtension) files in \(searchDirectory.path)...")

guard let enumerator = fileManager.enumerator(at: searchDirectory,
                                                includingPropertiesForKeys: [.fileSizeKey],
                                                options: [.skipsHiddenFiles],
                                                errorHandler: { (url, error) -> Bool in
                                                    print("Error accessing \(url.path): \(error.localizedDescription)")
                                                    return true
                                                }) else {
    print("Failed to create directory enumerator. Exiting.")
    exit(1)
}

var foundFiles: [(url: URL, size: UInt64)] = []

while let fileURL = enumerator.nextObject() as? URL {
    if fileURL.pathExtension.lowercased() == fileExtension {
        do {
            let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
            if let fileSize = resourceValues.fileSize {
                foundFiles.append((fileURL, UInt64(fileSize)))
            }
        } catch {
            print("Error getting size for \(fileURL.path): \(error.localizedDescription)")
        }
    }
}

// MARK: - Display Results
if foundFiles.isEmpty {
    print("No files with .\(fileExtension) extension were found in \(searchDirectory.path).")
} else {
    print("\nFound \(foundFiles.count) file(s):")
    for (file, size) in foundFiles {
        print("\(file.lastPathComponent) - \(humanReadableSize(size))")
    }
}