The library that can help you create a command line applications. This library is inspired by Swiftline, Path.swift and ShellOut.
- Path struct for handling files and directories.
- String styles which helps styling the strings before print them to the terminal.
- Prompt functions for process input and output in the terminal.
- Functions for run an external commands and read its standard output.
- Read and write environment variables.
- Parse command line arguments.
Path is a simple way for accessing, reading and writing files and directories.
Crate a Path instance:
// Path are always absolute
let path = try Path("/Users/hejki/tools/README") // absolute path from root /
let pathFromURL = try Path(url: URL(fileURLWithPath: "/Users/hejki/tools/README"))
try Path("~/Downloads") // path relative to current user home
try Path("~tom/Downloads") // path relative to another user home
try Path("Package.swift") // path relative to current working directory
Path(".stool/config.yaml", relativeTo: .home) // relative path to another path
Shortcut paths for system directories:
Path.root // File system root
Path.home // Current user's home
Path.current // Current working directory
Path.temporary // Path to temporary directory
Path components and path chaining:
// Paths can be joined with appending function or operator
let readme = Path.home.appending("tools") "README.md"
readme.url // URL representation
readme.path // Absolute path string
readme.pathComponents // ["Users", "hejki", "tools", "README.md"]
readme.extension // "md"
readme.basename // "README.md"
readme.basenameWithoutExtension // "README"
readme.parent // Path("/Users/hejki/tools")
readme.path(relativeTo: Path.home "Downloads") // "../tools/README.md"
Iterate over the directory content:
// Sequence with a shallow search of the specified directory, without hidden files
for path in Path.current.children {
print(path)
}
// Use recursive to deep search and/or includingHidden for hidden files
Path.current.recursive.includingHidden.forEach { print($0) }
Access to file and directory attributes:
readme.exist // `true` if this path represents an actual filesystem entry
readme.type // The type of filesystem entry. Can be .file, .directory, .symlink and .pipe
let attributes = readme.attributes
attributes.creationDate // item's creation date
attributes.modificationDate // item's last modify date
attributes.extensionHidden // item's extension is hidden
attributes.userName // item's owner user name
attributes.groupName // item's owner group name
attributes.permissions // item's permissions
attributes.size // item's size in bytes (read-only)
Create, copy, move and delete filesystem items:
let downloads = Path("Downloads/stool", relativeTo: .home)
let documents = try Path.home.createDirectory("Projects") // Creates a directory
try downloads.touch() // Creates an empty file, or updates its modification time
.copy(to: documents, overwrite: true) // Copy that file to documents directory
.rename(to: "stool.todo") // Rename that file
try downloads.delete(useTrash: false) // Delete original file, or move to trash
Read and write filesystem item's content:
// Read content functions
let string = String(contentsOf: readme) // File content as String
let data = Data(contentsOf: readme) // File content as Data
// Write functions on Data and String
try "Hi!".write(to: path, append: false, atomically: false, encoding: .utf8)
try Data().write(to: path, append: false, atomically: false)
// Write functions on Path
try path.write(text: "README", append: false, encoding: .utf8)
try path.write(data: Data(), append: false)
String styles helps styling the strings before printing them to the terminal. You can change the text color, the text background color and the text style. String styles works in string interpolation and for implementations of StringProtocol
.
Change style of string part using string interpolation extension:
print("Result is \(result.exitCode, styled: .fgRed)")
print("Result is \(result.exitCode, styled: .fgRed, .italic)") // multiple styles at once
The types that conforming StringProtocol
can use styles directly:
print("Init...".styled(.bgMagenta, .bold, .fg(r: 12, g: 42, b: 0)))
The string style interpolation can be globaly disabled by setting CLI.enableStringStyles
to false
, the interpolation is enabled by default.
Functions for print strings to standard output/error or for read input from standard input.
CLI.print("Print text to console without \n at end.")
CLI.println("Print text to console with terminating newline.")
CLI.print(error: "Print error to console without \n at end.")
CLI.println(error: "Print error to console with terminating newline.")
// read user input
let fileName = CLI.read()
Handler for this functions can be changed by setting CLI.prompt
variable. This can be handle for tests, for example:
class TestPromptHandler: PromptHandler {
func print(_ string: String) {
XCTAssertEqual("test print", string)
}
...
}
CLI.prompt = TestPromptHandler()
Ask presents the user with a prompt and waits for the user input.
let toolName = CLI.ask("Enter tool name: ")
Types that confirms ExpressibleByStringArgument can be returned from ask.
let timeout = CLI.ask("Enter timeout: ", type: Int.self)
// If user enters something that cannot be converted to Int, a new prompt is displayed,
// this prompt will keep displaying until the user enters an Int:
// $ Enter timeout: No
// $ Please enter a valid Int.
// > 2.3
// $ Please enter a valid Int.
// > 2
Prompt can be customized througt ask options.
// to specify default value which is used if the user only press Enter key
CLI.ask("Output path [/tmp]?\n ", options: .default("/tmp"))
// use .confirm() if you require value confirmation
CLI.ask("Remove file? ", type: Bool.self, options: .confirm())
.confirm(message: "Use this value?\n ") // to specify custom message
.confirm(block: { "Use \($0) value? " }) // to specify custom message with entered value
// add some .validator() to validate an entered value
let positive = AskOption<Int>.validator("Value must be positive.") { $0 > 0 }
let maxVal = AskOption<Int>.validator("Max value is 100.") { $0 <= 100 }
CLI.ask("Requested value: ", options: positive, maxVal)
// you can use some predefined validators
.notEmptyValidator() // for Strings
.rangeValidator(0...5) // for Comparable instances
// options can be combined together
let i: Int = CLI.ask("Value: ", options: .default(3), .confirm(), positive)
Choose is used to prompt the user to select an item between several possible items.
let user = CLI.choose("Select user: ", choices: ["hejki", "guest"])
// This will print:
// $ 1. hejki
// $ 2. guest
// $ Select user:
The user must choose one item from the list by entering its number. If the user enters a wrong input, a prompt will keep showing until the user makes a correct choice.
Choices can be supplied with dictionary, to display a different value than you later get as a result.
let difficulty = CLI.choose("Select difficulty: ", choices: [
"Easy": 0, "Hard": 1, "Extreme": 10
])
Run provides a quick way to run an external command and read its standard output.
let files = try CLI.run("ls -al")
print(files)
// Complex example with pipes different commands together
try CLI.run("ps aux | grep php | awk '{print $2}' | xargs kill")
In case of error, run
will automatically read stderr
and format it into a typed Swift error:
do {
try CLI.run("swift", "build")
} catch let error as CLI.CommandExecutionError {
print(error.terminationStatus) // Prints termination status code
print(error.stderr) // Prints error output
print(error.stdout) // Prints standard output
}
Each command can be run with one of available executors. The executor defines how to run command.
.default
executor is dedicated to non-interactive, short running tasks. This executor runs the command and consumes all outputs. Command standard output will be returned after task execution. This executor is default forCLI.run
functions..dummy
executor that only prints command toCLI.println
. You can specify returned stdout string, stderr and exitCode..interactive
executor runs command with redirected standard/error outputs to system outputs. This executor can handle user's inputs from system standard input. The command output will not be recorded.
For more complex executions use Command
type directly:
let command = CLI.Command(
["swift", "build"],
executor: .interactive,
workingDirectory: Path.home "/Projects/CommandLineAPI",
environment: ["PATH": "/usr/bin/"]
)
let result = try command.execute()
Commands are executed within context of some shell. If you want change the default zsh
shell for command execution, see the documentation of variable CLI.processBuilder
for more informations.
Read and write the environment variables passed to the script:
// Array with all envirnoment keys
CLI.env.keys
// Get environment variable
CLI.env["PATH"]
// Set environment variable
CLI.env["PATH"] = "~/bin"
Returns the arguments passed to the script.
// For example when calling `stool init -s default -q -- tool`
// CLI.args contains following results
CLI.args.all == ["stool", "init", "-s", "default", "-q", "--", "tool"]
CLI.args.command == "stool"
CLI.args.flags == ["s": "default", "q": ""]
CLI.args.parameters == ["init", "tool"]
To install CommandLineAPI for use in a Swift Package Manager powered tool, add CommandLineAPI as a dependency to your Package.swift
file. For more information, please see the Swift Package Manager documentation.
.package(url: "https://github.com/Hejki/CommandLineAPI", from: "0.3.0")
- Path.swift by Max Howell
- Pathos by Daniel Duan
- PathKit by Kyle Fuller
- Files by John Sundell
- Utility by Apple
Feel free to open an issue, or find me @hejki on Twitter.