Skip to Content
Course content

158: Building a Command-Line Package with Swift Argument Parser

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're walking into a high-end deli. You don't just walk up to the counter and scream "SANDWICH!" at the staff. You follow a specific protocol: you pick a base sandwich (the command), you specify the bread (an option), and maybe you ask for it to be toasted (a flag). The person taking your order is essentially a parser; they take your spoken, unstructured request and translate it into a precise ticket that the kitchen staff can actually execute without asking you twenty follow-up questions.

That's exactly what swift-argument-parser does for your code. In the old days (and in some languages still), you'd have to manually sift through an array of strings called CommandLine.arguments, writing messy if-else blocks to figure out if the user typed --verbose or -v. It was brittle and a nightmare to maintain. This library turns that process into a declarative structure where your code defines the "menu," and the library handles the translation.

Defining Your Tool's Menu

Let's say we're building a tool called LogScraper. Its job is to sift through a massive server log file and find specific error messages. To do this, we need a few things: a path to the file, a keyword to search for, and a way to toggle "detailed" mode.

import ArgumentParser

struct LogScraper: ParsableCommand {
    static var configuration = CommandConfiguration(
        commandName: "logscraper",
        abstract: "A utility for extracting specific errors from server logs."
    )

    @Option(name: .shortAndLong, help: "The keyword to search for in the logs.")
    var keyword: String

    @Option(name: .customShort("f"), help: "The path to the log file.")
    var file: String

    @Flag(name: .shortAndLong, help: "Show the line number for every match.")
    var verbose = false

    func run() throws {
        print("Searching for '\(keyword)' in \(file)...")
        if verbose {
            print("Verbose mode enabled: Including line numbers.")
        }
        // Logic to actually read the file and filter lines goes here
    }
}

Notice how I used @Option and @Flag. In our deli analogy, the keyword and file are the "bread" and "meat"—they require specific values. The verbose variable is the "toasted" request—it's either there or it isn't. One thing I love about this approach is that the library automatically generates a --help command for you. You don't have to write a single line of documentation code; the help strings you provide in the property wrappers are used to build a professional-looking CLI manual.

Adding Subcommands for More Power

Sometimes a single command isn't enough. Maybe LogScraper needs to do more than just search; maybe it needs to "clean" the logs or "summarize" them. This is where subcommands come in. Instead of making one giant command with fifty options, we create a hierarchy.

I usually recommend creating a main "entry point" command that doesn't do much itself but acts as a container for other commands. It looks like this:

struct LogTool: ParsableCommand {
    static var configuration = CommandConfiguration(
        commandName: "logtool",
        abstract: "The ultimate log management suite.",
        subcommands: [Scrape.self, Clean.self]
    )
}

struct Scrape: ParsableCommand {
    // All the logic from our previous LogScraper example goes here
    func run() throws { /* ... */ }
}

struct Clean: ParsableCommand {
    @Option(help: "The age of logs to delete in days.")
    var days: Int

    func run() throws {
        print("Cleaning logs older than \(days) days...")
    }
}

Now, the user interacts with your tool like this: logtool scrape --keyword "404" or logtool clean --days 30. By separating these into different structs, you keep your logic isolated. The "Scrape" logic doesn't need to know anything about how "Clean" works, which makes your codebase much easier to test as it grows.

Handling the Execution Flow

You might be wondering where the actual execution starts. In a Swift Package, you'll typically have a main.swift file or use the @main attribute on your top-level command. When you call LogTool.main(), the library takes over. It reads the CommandLine.arguments, validates that the user provided the required options, converts the strings into the correct types (like turning "30" into an Int for our days option), and finally calls the run() method of the matched command.

If the user forgets a required argument or types a flag that doesn't exist, the library catches that before run() is ever called, prints a helpful error message, and exits with a non-zero status code. This saves you from writing a mountain of validation logic.




📋 Practical Task

Build a "Project File Organizer" CLI

Your task is to create a command-line tool using swift-argument-parser that helps a developer organize their project folder. The tool should be named organizer and support the following requirements:

  • Main Command: The tool should have a root command called organizer.
  • Subcommand: Create a subcommand called sort.
  • The Sort Logic: The sort command needs:
    • A required --directory (or -d) option to specify which folder to organize.
    • An optional --extension (or -e) option to only move files of a certain type (e.g., .swift or .png).
    • A --dry-run flag that, when present, prints what would happen without actually moving any files.

Implement the run() method for the sort command so that it prints a summary of the action. For example: "Dry run: Would move all .swift files in /Users/dev/project to /Users/dev/project/swift_files".

Rating
0 0

There are no comments for now.

to be the first to leave a comment.