If you use Swift to create small command-line utilities, you’ve probably gotten tired of recompiling and moving the binary into your $PATH after every change.
For simple scripts, constantly recompiling tends to break the iteration flow quite a bit.
A common way to avoid compiling is to execute the script directly using the Swift interpreter, for example, for a greeting utility:
import Foundation
let name = UserDefaults.standard.string(forKey: "name") ?? "unknown"
print("Hello \(name)!")
You can run it directly without a build step:
swift ~/greeting/main.swift -name "Crisfe"
Or create some kind of alias/manual wrapper:
# ~/.zshrc or ~/.bashrc
greeting() {
swift ~/greeting/main.swift -name $1
}
Although this works, it has some limitations:
It adds weight to
.zshrcIt introduces aliases that need to be maintained manually (and potentially manually parsing argument flags)
It doesn’t scale very well if you end up with multiple utilities
Since it’s just a shell alias, it won’t be available to processes launched outside the shell (for example, from another programming language)
Using npm as a lightweight package manager
A more convenient alternative is to treat the script as a binary.
npm can be used to install
Swift scripts (actually scripts written in any programming language) as global commands.
This approach has several advantages:
no need to recompile or create aliases
no need to move binaries manually
and we get a globally available command in the shell
For our greeting utility, we only need a simple structure:
greeting/
├── package.json
└── main.swift
Where package.json contains:
{
"name": "greeting-swift",
"type": "module",
"bin": {
"greeting": "./main.swift"
}
}
The bin key tells npm to expose that file as an executable called greeting.
The name key defines the package name, which is useful if we later want to uninstall the utility.
Shebangs
The script needs a valid shebang so the system knows how to execute it:
#!/usr/bin/env swift
import Foundation
let name = UserDefaults.standard.string(forKey: "name") ?? "unknown"
print("Hello \(name)!")
And execution permissions:
chmod +x main.swift
Installing globally
From the project folder:
npm install -g .
npm creates a global symlink pointing to the script.
Now we can run it like any other command:
greeting -name Crisfe
Output:
Hello Crisfe!
And uninstall it with:
npm uninstall -g greeting-swift
Conclusion & Tradeoffs
This approach shines for utilities that run occasionally and change often — edit the file, and the update is instantly available system-wide with no recompilation needed.
The main tradeoff is startup time. Interpreting Swift on every run can take 2–5 seconds, which is fine for occasional use but painful for commands invoked hundreds of times a day or from automated scripts. In those cases, compiling a release binary is the right call.
That said, the two approaches aren't mutually exclusive: use the interpreted script during development, compile to a binary once the tool is stable.
Original draft written in spanish.