Author: Geoff

  • Scheduled URL Launcher AppleScript

    This AppleScript prompts the user to input multiple URLs and offers a choice to open them immediately or after a countdown timer. For immediate launches, it confirms the URLs before opening, while for countdown mode it notifies the user that the countdown has started and then provides a final confirmation after the URLs have been opened.

    Click to view script…
    -- Create a large default text field by including multiple line breaks
    set multiLineDefault to return & return & return & return
    
    -- Prompt the user for a list of URLs (one URL per line)
    set urlInput to text returned of (display dialog "Enter list of URLs (one per line):" default answer multiLineDefault)
    
    -- Split the input into a list of lines
    set urlList to paragraphs of urlInput
    
    -- Ask the user if they want to open the URLs now or after a countdown
    set schedulingOption to button returned of (display dialog "Choose when to open the URLs:" buttons {"Now", "Countdown"} default button "Now")
    
    if schedulingOption is "Now" then
    	-- Build a confirmation message listing the URLs to be opened immediately
    	set confirmDialogText to "The following URLs will be opened now:" & return & return
    	repeat with aURL in urlList
    		set trimmedURL to trimWhitespace(aURL)
    		if trimmedURL is not "" then
    			-- Prepend protocol if missing
    			if (trimmedURL does not start with "http://") and (trimmedURL does not start with "https://") then
    				set trimmedURL to "https://" & trimmedURL
    			end if
    			set confirmDialogText to confirmDialogText & trimmedURL & return
    		end if
    	end repeat
    	
    	-- Show confirmation dialog before launching
    	set confirmResponse to button returned of (display dialog confirmDialogText buttons {"Cancel", "Open Now"} default button "Open Now")
    	if confirmResponse is "Cancel" then return
    	
    	-- Open URLs immediately
    	repeat with aURL in urlList
    		set trimmedURL to trimWhitespace(aURL)
    		if trimmedURL is not "" then
    			if (trimmedURL does not start with "http://") and (trimmedURL does not start with "https://") then
    				set trimmedURL to "https://" & trimmedURL
    			end if
    			open location trimmedURL
    		end if
    	end repeat
    	
    else if schedulingOption is "Countdown" then
    	-- Prompt for countdown seconds
    	set countdownInput to text returned of (display dialog "Enter countdown time in seconds:" default answer "10")
    	try
    		set countdownSeconds to countdownInput as integer
    	on error
    		display dialog "Invalid number. Exiting." buttons {"OK"} default button "OK"
    		return
    	end try
    	
    	-- Notify the user that the countdown is starting
    	display dialog "Countdown started for " & countdownSeconds & " seconds. The URLs will open automatically afterwards." buttons {"OK"} default button "OK"
    	
    	-- Delay for the specified number of seconds
    	delay countdownSeconds
    	
    	-- Open URLs after the delay
    	repeat with aURL in urlList
    		set trimmedURL to trimWhitespace(aURL)
    		if trimmedURL is not "" then
    			if (trimmedURL does not start with "http://") and (trimmedURL does not start with "https://") then
    				set trimmedURL to "https://" & trimmedURL
    			end if
    			open location trimmedURL
    		end if
    	end repeat
    	
    	-- Build a final confirmation message listing the URLs that were opened
    	set finalConfirmDialogText to "The following URLs have been opened:" & return & return
    	repeat with aURL in urlList
    		set trimmedURL to trimWhitespace(aURL)
    		if trimmedURL is not "" then
    			if (trimmedURL does not start with "http://") and (trimmedURL does not start with "https://") then
    				set trimmedURL to "https://" & trimmedURL
    			end if
    			set finalConfirmDialogText to finalConfirmDialogText & trimmedURL & return
    		end if
    	end repeat
    	
    	-- Show the final confirmation dialog
    	display dialog finalConfirmDialogText buttons {"OK"} default button "OK"
    end if
    
    --------------------------------------------------------------------------------
    -- Helper Handler: trimWhitespace
    -- Removes leading and trailing whitespace from a string
    --------------------------------------------------------------------------------
    on trimWhitespace(theText)
    	set AppleScript's text item delimiters to {" ", tab, return, linefeed}
    	set textItems to text items of theText
    	set AppleScript's text item delimiters to space
    	set trimmed to textItems as text
    	set AppleScript's text item delimiters to ""
    	return trimmed
    end trimWhitespace
  • Advanced Password Strength Checker Python Script

    This script assesses the security of any given password by checking its length, character diversity, and commonality, then provides a strength score along with detailed improvement suggestions if needed. It also explains why a password is strong when it meets all the criteria.

    Click to view script…
    #!/usr/bin/env python3
    import re
    import getpass
    
    def evaluate_password_strength(password):
        """
        Evaluate the password strength based on five criteria:
        - At least 8 characters long
        - Contains an uppercase letter
        - Contains a lowercase letter
        - Contains a digit
        - Contains a special character
        Additionally, the function checks for commonly used (and easily guessable) passwords.
        Returns a tuple containing the strength rating, score, and suggestions for improvement.
        """
        score = 0
        suggestions = []
    
        # Check for common (easily guessable) passwords
        common_passwords = {
            "password", "12345", "123456", "qwerty", "abc123",
            "111111", "123123", "password1", "letmein", "admin"
        }
        if password.lower() in common_passwords:
            suggestions.append("Your password is among the most commonly used and easily guessable passwords. Please choose a more unique password.")
            # Override the score for common passwords
            score = 0
            # Early return the evaluation since it's a significant security risk.
            return "Weak", score, suggestions
    
        # Check for minimum length
        if len(password) >= 8:
            score += 1
        else:
            suggestions.append("Make sure your password is at least 8 characters long.")
    
        # Check for uppercase letters
        if re.search(r'[A-Z]', password):
            score += 1
        else:
            suggestions.append("Include at least one uppercase letter.")
    
        # Check for lowercase letters
        if re.search(r'[a-z]', password):
            score += 1
        else:
            suggestions.append("Include at least one lowercase letter.")
    
        # Check for digits
        if re.search(r'\d', password):
            score += 1
        else:
            suggestions.append("Include at least one digit.")
    
        # Check for special characters
        if re.search(r'[\W_]', password):
            score += 1
        else:
            suggestions.append("Include at least one special character.")
    
        # Determine strength rating based on the score
        if score <= 2:
            strength = "Weak"
        elif score in [3, 4]:
            strength = "Moderate"
        else:
            strength = "Strong"
    
        return strength, score, suggestions
    
    def main():
        print("Password Strength Checker")
        # Using getpass to hide input for privacy; replace with input() if needed.
        password = getpass.getpass("Enter a password to evaluate: ")
        
        strength, score, suggestions = evaluate_password_strength(password)
        
        print(f"\nPassword Strength: {strength}")
        print(f"Score: {score} out of 5")
        
        if score == 5:
            print("\nExplanation: This password is strong because it meets all the required criteria: it is at least 8 characters long, includes both uppercase and lowercase letters, contains at least one digit, and has at least one special character.")
        elif suggestions:
            print("\nSuggestions for improvement:")
            for suggestion in suggestions:
                print(f" - {suggestion}")
    
    if __name__ == '__main__':
        main()
  • Universal Age & Birth Date Calculator Python Script

    This script calculates an age in years, months, and days from a given date of birth or determines an approximate birth date based on an exact age input.

    The script utilizes the python-dateutil library, which extends Python’s standard datetime module by offering the relativedelta class for sophisticated date arithmetic; install it using pip3 install python-dateutil if it’s not already available.

    Click to view script…
    #!/usr/bin/env python3
    from datetime import datetime
    try:
        from dateutil.relativedelta import relativedelta
    except ImportError:
        print("This script requires the 'python-dateutil' module. Install it via 'pip3 install python-dateutil'")
        exit(1)
    
    def calculate_age(birth_date, current_date):
        """Calculate the difference between two dates as (years, months, days) using relativedelta."""
        delta = relativedelta(current_date, birth_date)
        return delta.years, delta.months, delta.days
    
    def main():
        print("Select an option:")
        print("1. Calculate an age from a date of birth")
        print("2. Calculate a birth date from an exact age")
        choice = input("Enter 1 or 2: ").strip()
        
        today = datetime.today()
        
        if choice == "1":
            dob_str = input("Enter a date of birth (YYYY-MM-DD): ").strip()
            try:
                birth_date = datetime.strptime(dob_str, "%Y-%m-%d")
            except ValueError:
                print("Invalid date format. Please use YYYY-MM-DD.")
                exit(1)
            
            years, months, days = calculate_age(birth_date, today)
            print(f"Calculated age: {years} years, {months} months, and {days} days.")
        
        elif choice == "2":
            print("Enter an exact age as three numbers separated by spaces.")
            print("For example, if the age is 25 years, 4 months, and 15 days, type: 25 4 15")
            age_input = input("Exact age (years months days): ").strip()
            parts = age_input.split()
            if len(parts) != 3:
                print("Invalid input. Please enter three integers separated by spaces.")
                exit(1)
            try:
                years = int(parts[0])
                months = int(parts[1])
                days = int(parts[2])
            except ValueError:
                print("Invalid input. Please ensure you enter numbers for years, months, and days.")
                exit(1)
            
            birth_date = today - relativedelta(years=years, months=months, days=days)
            print("Calculated birth date is approximately:", birth_date.strftime("%Y-%m-%d"))
        
        else:
            print("Invalid option selected.")
    
    if __name__ == '__main__':
        main()
  • Unique Entry Filter Python Script

    This script prompts the user to enter a comma-separated list of strings, then processes the input by trimming whitespace and converting entries to lowercase for case-insensitive duplicate checking. It finally outputs a comma-separated list of unique entries, preserving the order of their first appearance.

    Click to view script…
    #!/usr/bin/env python3
    
    def remove_duplicates(input_string):
        # Split the input string by commas and remove any extra spaces
        items = [item.strip() for item in input_string.split(',')]
        unique_items = []
        seen = set()
    
        for item in items:
            # Convert to lowercase for case-insensitive comparison
            if item.lower() not in seen:
                seen.add(item.lower())
                unique_items.append(item)
        return unique_items
    
    if __name__ == '__main__':
        user_input = input("Enter a comma-separated list of strings: ")
        unique = remove_duplicates(user_input)
        print("Unique entries:", ", ".join(unique))
  • AES File Encryption & Decryption Utility Python Script

    A Python utility that allows users to securely encrypt or decrypt files using AES with a choice between 128‑bit and 256‑bit keys. It lists files from the current directory, prompts for password confirmation during encryption, and handles decryption seamlessly by deriving keys with PBKDF2 and using CBC mode.

    This script relies on the PyCryptodome library to handle AES encryption and decryption operations. Be sure to install it via pip install pycryptodome before running the script.

    Click to view script…
    #!/usr/bin/env python3
    import os
    import sys
    import getpass
    from Crypto.Cipher import AES
    from Crypto.Random import get_random_bytes
    from Crypto.Protocol.KDF import PBKDF2
    from Crypto.Util.Padding import pad, unpad
    
    HEADER_MAGIC = b"ENCR"  # 4 bytes header to identify our encrypted files
    SALT_SIZE = 16          # Salt size in bytes
    IV_SIZE = 16            # AES block size
    PBKDF2_ITERATIONS = 100000
    
    def list_files():
        files = [f for f in os.listdir('.') if os.path.isfile(f)]
        if not files:
            print("No files found in the current directory.")
            sys.exit(1)
        print("Files in the current directory:")
        for i, f in enumerate(files, start=1):
            print(f"  {i}. {f}")
        return files
    
    def choose_file(files):
        try:
            choice = int(input("Select a file number: "))
            if choice < 1 or choice > len(files):
                raise ValueError
        except ValueError:
            print("Invalid selection.")
            sys.exit(1)
        return files[choice - 1]
    
    def get_password(confirm=False):
        pwd = getpass.getpass("Enter password: ")
        if confirm:
            pwd_confirm = getpass.getpass("Confirm password: ")
            if pwd != pwd_confirm:
                print("Passwords do not match.")
                sys.exit(1)
        return pwd.encode()  # work with bytes
    
    def encrypt_file(filepath):
        print("\n--- Encryption ---")
        # Choose key size
        key_size_choice = input("Choose key size (enter 128 or 256): ").strip()
        if key_size_choice not in ("128", "256"):
            print("Invalid key size selection.")
            sys.exit(1)
        key_len = 16 if key_size_choice == "128" else 32
    
        password = get_password(confirm=True)
    
        # Generate a random salt and IV
        salt = get_random_bytes(SALT_SIZE)
        iv = get_random_bytes(IV_SIZE)
    
        # Derive the AES key from the password and salt
        key = PBKDF2(password, salt, dkLen=key_len, count=PBKDF2_ITERATIONS)
    
        # Read file data
        try:
            with open(filepath, "rb") as f:
                data = f.read()
        except IOError as e:
            print(f"Error reading file: {e}")
            sys.exit(1)
    
        # Pad and encrypt the data using CBC mode
        cipher = AES.new(key, AES.MODE_CBC, iv)
        ciphertext = cipher.encrypt(pad(data, AES.block_size))
    
        # Create an output file format:
        # [4 bytes header][1 byte key length indicator][16 bytes salt][16 bytes IV][ciphertext]
        # We store key length as one byte: 16 (for 128-bit) or 32 (for 256-bit)
        header = HEADER_MAGIC + bytes([key_len]) + salt + iv
        out_data = header + ciphertext
    
        # Determine output file name
        out_filepath = filepath + ".enc"
        try:
            with open(out_filepath, "wb") as f:
                f.write(out_data)
        except IOError as e:
            print(f"Error writing encrypted file: {e}")
            sys.exit(1)
    
        print(f"Encryption successful. Encrypted file: {out_filepath}")
    
    def decrypt_file(filepath):
        print("\n--- Decryption ---")
        # Read the encrypted file and parse header information
        try:
            with open(filepath, "rb") as f:
                file_data = f.read()
        except IOError as e:
            print(f"Error reading file: {e}")
            sys.exit(1)
    
        # Verify header length
        header_length = len(HEADER_MAGIC) + 1 + SALT_SIZE + IV_SIZE
        if len(file_data) < header_length:
            print("File is too short to be a valid encrypted file.")
            sys.exit(1)
    
        header = file_data[:len(HEADER_MAGIC)]
        if header != HEADER_MAGIC:
            print("Invalid file format (missing header).")
            sys.exit(1)
    
        key_len = file_data[len(HEADER_MAGIC)]
        if key_len not in (16, 32):
            print("Invalid key length in file header.")
            sys.exit(1)
    
        salt = file_data[len(HEADER_MAGIC)+1 : len(HEADER_MAGIC)+1+SALT_SIZE]
        iv_start = len(HEADER_MAGIC)+1+SALT_SIZE
        iv = file_data[iv_start : iv_start+IV_SIZE]
        ciphertext = file_data[iv_start+IV_SIZE:]
    
        password = get_password(confirm=False)
        key = PBKDF2(password, salt, dkLen=key_len, count=PBKDF2_ITERATIONS)
    
        # Decrypt and unpad the data
        cipher = AES.new(key, AES.MODE_CBC, iv)
        try:
            plaintext_padded = cipher.decrypt(ciphertext)
            plaintext = unpad(plaintext_padded, AES.block_size)
        except (ValueError, KeyError):
            print("Decryption failed. Incorrect password or corrupted file.")
            sys.exit(1)
    
        # Determine output file name
        if filepath.endswith(".enc"):
            out_filepath = filepath[:-4]  # remove the .enc extension
        else:
            out_filepath = filepath + ".dec"
    
        try:
            with open(out_filepath, "wb") as f:
                f.write(plaintext)
        except IOError as e:
            print(f"Error writing decrypted file: {e}")
            sys.exit(1)
    
        print(f"Decryption successful. Decrypted file: {out_filepath}")
    
    def main():
        print("AES Encryption/Decryption Utility")
        mode = input("Do you want to (E)ncrypt or (D)ecrypt? ").strip().lower()
        if mode not in ("e", "d"):
            print("Invalid mode selection.")
            sys.exit(1)
    
        files = list_files()
        selected_file = choose_file(files)
    
        if mode == "e":
            encrypt_file(selected_file)
        else:
            decrypt_file(selected_file)
    
    if __name__ == "__main__":
        main()
    
  • Interactive Image Converter Swift Script

    This Swift script guides users through selecting an input image from common directories using numbered lists and then converting it to a desired format (PNG, JPEG, or TIFF) while also letting them choose an output directory and filename. It simplifies the image conversion process on macOS by providing an interactive, user-friendly interface.

    Click to view script…
    #!/usr/bin/env swift
    import Foundation
    import AppKit
    
    // MARK: - Helper Functions
    
    /// Prompts the user for input with a message.
    func prompt(_ message: String) -> String {
        print(message, terminator: " ")
        return readLine() ?? ""
    }
    
    /// Presents a numbered list of directory options to the user and returns the selected directory URL.
    func selectDirectory(options: [(name: String, url: URL)], promptMessage: String) -> URL {
        for (index, option) in options.enumerated() {
            print("\(index + 1): \(option.name) -> \(option.url.path)")
        }
        let choiceStr = prompt(promptMessage)
        if let choiceNum = Int(choiceStr), choiceNum > 0, choiceNum <= options.count {
            let selected = options[choiceNum - 1]
            if selected.name.hasPrefix("Other") {
                let customPath = prompt("Enter custom directory path:")
                return URL(fileURLWithPath: customPath, isDirectory: true)
            } else {
                return selected.url
            }
        } else {
            let manualPath = prompt("Invalid selection. Enter directory path manually:")
            return URL(fileURLWithPath: manualPath, isDirectory: true)
        }
    }
    
    /// Presents a numbered list of files for the user to choose from.
    func selectFile(from files: [URL], in directory: URL) -> URL {
        for (index, file) in files.enumerated() {
            print("\(index + 1): \(file.lastPathComponent)")
        }
        let choiceStr = prompt("Enter the number of your choice:")
        if let choiceNum = Int(choiceStr), choiceNum > 0, choiceNum <= files.count {
            return files[choiceNum - 1]
        } else {
            let manualFile = prompt("Invalid selection. Enter the file name (in \(directory.path)):")
            return directory.appendingPathComponent(manualFile)
        }
    }
    
    /// Converts a file size (in bytes) to a human-readable string.
    func humanReadableSize(_ size: UInt64) -> String {
        let formatter = ByteCountFormatter()
        formatter.countStyle = .file
        formatter.allowedUnits = [.useKB, .useMB, .useGB, .useTB]
        return formatter.string(fromByteCount: Int64(size))
    }
    
    // MARK: - Candidate Directories Setup
    
    let fileManager = FileManager.default
    var candidateDirs: [(name: String, url: URL)] = []
    
    if let desktop = fileManager.urls(for: .desktopDirectory, in: .userDomainMask).first {
        candidateDirs.append(("Desktop", desktop))
    }
    if let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
        candidateDirs.append(("Documents", documents))
    }
    if let downloads = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first {
        candidateDirs.append(("Downloads", downloads))
    }
    candidateDirs.append(("Other (enter path manually)", URL(fileURLWithPath: fileManager.currentDirectoryPath)))
    
    // MARK: - Input Directory and File Selection
    
    print("Select the input directory:")
    let inputDirectory = selectDirectory(options: candidateDirs, promptMessage: "Enter the number of your choice:")
    
    // Verify the input directory exists.
    var isDir: ObjCBool = false
    guard fileManager.fileExists(atPath: inputDirectory.path, isDirectory: &isDir), isDir.boolValue else {
        print("Directory \(inputDirectory.path) does not exist or is not a directory.")
        exit(1)
    }
    
    // Define supported image extensions.
    let validImageExtensions: Set<String> = ["png", "jpg", "jpeg", "tiff", "gif"]
    
    // Get and filter files in the chosen directory.
    guard let filesInDirectory = try? fileManager.contentsOfDirectory(at: inputDirectory,
                                                                       includingPropertiesForKeys: nil,
                                                                       options: [.skipsHiddenFiles]) else {
        print("Unable to read contents of directory \(inputDirectory.path)")
        exit(1)
    }
    let imageFiles = filesInDirectory.filter { validImageExtensions.contains($0.pathExtension.lowercased()) }
    
    if imageFiles.isEmpty {
        print("No image files found in \(inputDirectory.path)")
        exit(1)
    }
    
    print("\nSelect the image file to convert:")
    let inputFileURL = selectFile(from: imageFiles, in: inputDirectory)
    
    // Optionally, display file size.
    if let attrs = try? fileManager.attributesOfItem(atPath: inputFileURL.path),
       let fileSize = attrs[.size] as? UInt64 {
        print("Selected file: \(inputFileURL.lastPathComponent) (\(humanReadableSize(fileSize)))")
    }
    
    // MARK: - Output Format Selection
    
    print("\nSelect the desired output image format:")
    print("1: PNG")
    print("2: JPEG")
    print("3: TIFF")
    let formatChoiceStr = prompt("Enter the number of your choice:")
    var outputFileType: NSBitmapImageRep.FileType
    var outputExtension: String
    
    switch formatChoiceStr {
    case "1":
        outputFileType = .png
        outputExtension = "png"
    case "2":
        outputFileType = .jpeg
        outputExtension = "jpg"
    case "3":
        outputFileType = .tiff
        outputExtension = "tiff"
    default:
        print("Invalid selection.")
        exit(1)
    }
    
    // MARK: - Output Directory and File Name Selection
    
    print("\nSelect the output directory:")
    let outputDirectory = selectDirectory(options: candidateDirs, promptMessage: "Enter the number of your choice:")
    
    isDir = false
    guard fileManager.fileExists(atPath: outputDirectory.path, isDirectory: &isDir), isDir.boolValue else {
        print("Output directory \(outputDirectory.path) does not exist or is not a directory.")
        exit(1)
    }
    
    let defaultOutputName = inputFileURL.deletingPathExtension().lastPathComponent + "_converted." + outputExtension
    let outputFileName = prompt("Enter output file name (default: \(defaultOutputName)):")
    let finalOutputFileName = outputFileName.isEmpty ? defaultOutputName : outputFileName
    let outputFileURL = outputDirectory.appendingPathComponent(finalOutputFileName)
    
    // MARK: - Image Conversion
    
    print("\nConverting image...")
    
    guard let inputImage = NSImage(contentsOf: inputFileURL) else {
        print("Failed to load image from \(inputFileURL.path)")
        exit(1)
    }
    guard let tiffData = inputImage.tiffRepresentation else {
        print("Failed to get TIFF representation of the image.")
        exit(1)
    }
    guard let bitmapRep = NSBitmapImageRep(data: tiffData) else {
        print("Failed to create bitmap representation.")
        exit(1)
    }
    guard let outputImageData = bitmapRep.representation(using: outputFileType, properties: [:]) else {
        print("Failed to convert image to selected format.")
        exit(1)
    }
    
    do {
        try outputImageData.write(to: outputFileURL)
        print("Image successfully converted and saved to \(outputFileURL.path)")
    } catch {
        print("Error saving converted image: \(error)")
        exit(1)
    }
  • 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))")
        }
    }
  • Animated Algorithm Playground Swift Script

    This Swift script visually demonstrates the bubble sort algorithm by animating each step in the terminal as it sorts a randomly generated array of numbers from unsorted to sorted order.

    Click to view script…
    #!/usr/bin/env swift
    
    import Foundation
    
    // Function to clear the terminal screen using ANSI escape codes.
    func clearScreen() {
        print("\u{001B}[2J", terminator: "")
        print("\u{001B}[H", terminator: "")
    }
    
    // Function to display the array as bars with their values.
    func displayArray(_ arr: [Int]) {
        for value in arr {
            let bar = String(repeating: "▇", count: value)
            print("\(bar) (\(value))")
        }
    }
    
    // Generate an array of 20 random integers between 1 and 20.
    var numbers = (0..<20).map { _ in Int.random(in: 1...20) }
    
    // Show the initial unsorted array.
    clearScreen()
    print("Initial Array:")
    displayArray(numbers)
    print("\nPress Enter to start the animated bubble sort...")
    _ = readLine()
    
    // Bubble sort with animation.
    let n = numbers.count
    
    for i in 0..<n {
        var didSwap = false
        for j in 0..<n - i - 1 {
            if numbers[j] > numbers[j + 1] {
                numbers.swapAt(j, j + 1)
                didSwap = true
    
                // Clear screen and show the current state of the array.
                clearScreen()
                print("Bubble Sort Animation - Pass \(i + 1), Swap at index \(j) and \(j + 1):")
                displayArray(numbers)
                usleep(200000) // Pause for 0.2 seconds
            }
        }
        // If no swaps occurred, the array is sorted.
        if !didSwap {
            break
        }
    }
    
    // Final sorted array.
    clearScreen()
    print("Sorted Array:")
    displayArray(numbers)
    print("\nSorting complete!")
  • Hello Swift Script

    This simple Swift script prints a greeting message (“Hello, Swift scripting!”) to the console when executed.

    Click to view script…
    #!/usr/bin/env swift
    print("Hello, Swift scripting!")

  • macOS App Version Finder Bash Script

    This interactive script scans your /Applications and ~/Applications directories to list installed apps, allowing you to select one and view its version. It supports multiple searches and includes a built-in quit option for easy use.

    Click to view script…
    #!/bin/bash
    # Interactive App Version Finder for macOS with multi-search capability
    
    # Define directories to search for applications.
    APP_DIRS=("/Applications" "$HOME/Applications")
    
    # Initialize an array to store found apps.
    apps=()
    
    # Search for .app directories (non-recursive) in defined directories.
    for dir in "${APP_DIRS[@]}"; do
      if [ -d "$dir" ]; then
        while IFS= read -r -d $'\0' app; do
          apps+=("$app")
        done < <(find "$dir" -maxdepth 1 -type d -name "*.app" -print0)
      fi
    done
    
    # Check if any apps were found.
    if [ ${#apps[@]} -eq 0 ]; then
      echo "No applications found in ${APP_DIRS[*]}."
      exit 1
    fi
    
    # Main interactive loop.
    while true; do
      echo ""
      echo "Available Applications:"
      for i in "${!apps[@]}"; do
        echo "[$i] $(basename "${apps[$i]}")"
      done
      echo "[q] Quit"
      
      read -p "Enter the number of the app to check its version (or 'q' to quit): " input
    
      # Check for the quit option.
      if [[ "$input" =~ ^[Qq]$ ]]; then
        echo "Exiting."
        exit 0
      fi
    
      # Validate input is a number and within range.
      if ! [[ "$input" =~ ^[0-9]+$ ]] || [ "$input" -ge "${#apps[@]}" ]; then
        echo "Invalid selection. Please try again."
        continue
      fi
    
      APP_PATH="${apps[$input]}"
      APP_NAME=$(basename "$APP_PATH" .app)
    
      # Retrieve the version information from the app's Info.plist.
      VERSION=$(defaults read "$APP_PATH/Contents/Info" CFBundleShortVersionString 2>/dev/null)
    
      if [ -z "$VERSION" ]; then
        echo "Version information not found for $APP_NAME."
      else
        echo "$APP_NAME version: $VERSION"
      fi
    
      echo ""
      # Ask if the user wants to perform another search.
      read -p "Do you want to search for another app? (y/n): " answer
      if [[ ! "$answer" =~ ^[Yy]$ ]]; then
        echo "Exiting."
        exit 0
      fi
    done