Writing a Command Line Tool in Swift
Published: Tuesday, April 29, 2025A concise guide to creating a CLI tool in Swift using the Swift Package Manager.This guide demonstrates how to create a simplified version of the UNIX ls
command using Swift. The ls
command is chosen for its simplicity and familiarity, making it an ideal example for building a command-line tool.
Overview
The ls
command lists files in the current or specified directory, aiding navigation within a file system. Example output:
Desktop Documents Library Music Public Developer Downloads Movies Pictures
Creating the Tool
Initialize a new Swift Package Manager project:
mkdir ls-clone
cd ls-clone
swift package init --name LsClone --type executable
This generates the project structure. Open it in your preferred editor or run open Package.swift
to use Xcode.
Adding Dependencies
The tool uses Swift Argument Parser for handling command-line flags and options. Update Package.swift
:
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "LsClone",
products: [
.executable(name: "lsc", targets: ["lsc"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")
],
targets: [
.executableTarget(
name: "lsc",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
)
]
)
The tool is named lsc
(ls clone), but you may rename it as desired.
Implementation
This guide implements a minimal ls
clone that:
Lists files in the current or specified directory.
Supports a flag to include hidden files.
Steps
Define the main entry point using ParsableCommand
:
import ArgumentParser
import Foundation
@main
struct Lsc: ParsableCommand {
// Configuration and logic here
}
Add a flag and argument for user input. The all
flag supports -a
or --all
due to the .shortAndLong
name parameter and is set to a default value of false. The paths
argument accepts a collection of directory paths:
@Flag(name: .shortAndLong, help: "Display all files, including hidden.") var all = false
@Argument(help: "Directories to list.") var paths: [String] = []
Implement the run
function:
func run() throws {
let fm = FileManager.default
let locations = paths.isEmpty ? [fm.currentDirectoryPath] : paths
for path in locations {
let url = URL(fileURLWithPath: path)
let items = try fm.contentsOfDirectory(
at: url,
includingPropertiesForKeys: nil,
options: all ? [] : [.skipsHiddenFiles]
)
for item in items {
print(item.lastPathComponent)
}
}
}
Complete Code
// Sources/lsc/main.swift
import ArgumentParser
import Foundation
@main
struct Lsc: ParsableCommand {
@Flag(name: .shortAndLong, help: "Display all files, including hidden.") var all = false
@Argument(help: "Directories to list.") var paths: [String] = []
func run() throws {
let fm = FileManager.default
let locations = paths.isEmpty ? [fm.currentDirectoryPath] : paths
for path in locations {
let url = URL(fileURLWithPath: path)
let items = try fm.contentsOfDirectory(
at: url,
includingPropertiesForKeys: nil,
options: all ? [] : [.skipsHiddenFiles]
)
for item in items {
print(item.lastPathComponent)
}
}
}
}
Run the tool with swift run lsc
or swift run lsc -a
.
Example Output:
Tests
Package.resolved
Package.swift
Sources
With -a
Flag:
.build
Tests
Package.resolved
.gitignore
Package.swift
Sources
Build a release version for distribution with swift build -c release
, producing an executable at .build/arm64-apple-macosx/release/lsc
. For cross-platform builds, refer to the Swift Build System or Static Linux SDK.
Testing
It is good practice to include tests to ensure reliability. Below is a sample test suite that verifies the tool’s output:
// Tests/LsCloneTests.swift
import Foundation
import Testing
@testable import lsc
@Suite("LsClone Tests")
struct LscTestSuite {
@Test("List Current Directory")
func testListCurrentDirectory() throws {
let output = try captureOutput {
try Lsc.parse([]).run()
}
#expect(output.contains("Package.swift"))
}
@Test("List Hidden Files")
func testListHiddenFiles() throws {
let output = try captureOutput {
try Lsc.parse(["-a"]).run()
}
#expect(output.contains(".gitignore"))
}
func captureOutput(_ closure: () throws -> Void) throws -> String {
let pipe = Pipe()
let original = dup(STDOUT_FILENO)
defer { dup2(original, STDOUT_FILENO); close(original) }
dup2(pipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
try pipe.fileHandleForWriting.close()
try closure()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(decoding: data, as: UTF8.self)
}
}
Run tests with swift test
.
Example Test Output:
Test Suite 'All tests' started at 2025-04-29 10:38:20.741
Test Suite 'LscTestSuite' started
Test Case 'List Current Directory' passed
Test Case 'List Hidden Files' passed
Test Suite 'All tests' passed
Executed 2 tests, with 0 failures in 0.002 seconds
Conclusion
Swift is an excellent language for writing command line tools in a concise and efficient manner, if you’d like to see a more extensive implementation of ls
in Swift you can see my tool sls
at SwiftList.