Skip to Content
Course content

51: Building a Command-Line Tool in Swift

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

Up until now, we've mostly been writing code that lives inside an app or a playground. But one of the most satisfying things you can do with Swift is build a tool that lives in your terminal. I use these all the time for automating boring parts of my workflow—things like renaming batches of files or parsing JSON logs.

For this lesson, let's build a tool called wordly. The goal is simple: it should take a path to a text file as an argument, count the words, and tell us which word appears most frequently. It's a classic utility, but it covers all the bases of CLI development.

Setting up the Executable Package

You can't just run a random .swift file and expect it to behave like a professional tool. We need a proper package structure. Open your terminal and run:

mkdir wordly
cd wordly
swift package init --type executable

I use --type executable because it tells Swift Package Manager (SPM) to create a main.swift file (or a @main entry point). This is the "front door" of your program. Without this, the compiler wouldn't know where the execution starts.

Dealing with Command-Line Arguments

Now, let's look at how we get data into the tool. Swift provides the CommandLine.arguments array. The first element is always the path to the executable itself, so our actual input starts at index 1.

Here is my first pass at the logic. I'm just going to grab the filename and try to read it:

import Foundation

let fileName = CommandLine.arguments[1]
let content = try String(contentsOfFile: fileName)
print("Reading file: \(fileName)")

I ran this immediately, and it crashed. Fatal error: Index out of range. I did exactly what I always do when I'm rushing: I assumed the user would actually provide a filename. If you just run swift run without arguments, the program dies instantly.

We need to be defensive. I'll swap that out for a guard statement to provide a helpful usage message instead of a crash:

import Foundation

guard CommandLine.arguments.count > 1 else {
    print("Usage: wordly [file-path]")
    exit(1) // Exit with an error code
}

let fileName = CommandLine.arguments[1]

Processing the Text

Now that we have the file path, we need to actually analyze the text. I want to ignore punctuation and case sensitivity, otherwise "Apple" and "apple," would be treated as different words. I'll use a Dictionary to keep track of the counts.

do {
    let content = try String(contentsOfFile: fileName, encoding: .utf8)
    
    // Split by whitespace and punctuation
    let words = content.lowercased()
        .components(separatedBy: CharacterSet.alphanumerics.inverted)
        .filter { !$0.isEmpty }

    var counts: [String: Int] = [:]
    for word in words {
        counts[word, default: 0] += 1
    }

    if let mostFrequent = counts.max(by: { $0.value < $1.value }) {
        print("Total words: \(words.count)")
        print("Most frequent word: '\(mostFrequent.key)' (\(mostFrequent.value) occurrences)")
    } else {
        print("The file is empty.")
    }
} catch {
    print("Error: Could not read file at \(fileName)")
    exit(1)
}

I used CharacterSet.alphanumerics.inverted here. It's a handy trick—basically, I'm telling Swift, "Split this string whenever you hit anything that isn't a letter or a number." It's much cleaner than trying to list every possible punctuation mark manually.

Running and Distributing the Tool

To test it, create a quick text file in your folder: echo "Swift is great. Swift is fast." > test.txt. Then run your tool:

swift run wordly test.txt

If you want to use this tool anywhere on your system without typing swift run, you can build it in release mode:

swift build -c release

You'll find the binary in the .build/release/ folder. You can move that binary to /usr/local/bin, and suddenly wordly becomes a first-class citizen in your terminal.




📋 Practical Task

Build the "Line-Finder" Utility

Your task is to build a new command-line tool called linefinder. This tool should take two arguments: a file path and a search term. The tool should read the file and print every line number where the search term appears.

Requirements:

  • Ensure the program prints a "Usage" message and exits gracefully if either the file path or search term is missing.
  • The search should be case-insensitive.
  • The output should follow this format: Line 4: [The content of the line].
  • Handle the error if the file cannot be found or read.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.