This is a library supporting the development of command-line tools in the programming language Swift on macOS. It also compiles on iOS and Linux. The library provides the following functionality:
- Management of command-line arguments,
- Usage of escape sequences on terminals, e.g. for formatting output on ANSI terminals, and
- Reading strings on terminals using a lineread-inspired implementation based on the library Linenoise-Swift, but supporting unicode input, multiple lines, and styled text.
- Reading strings on terminals in a secure way hiding user input, e.g. for entering passwords, API keys, etc.
| Table of contents | |
|---|---|
|
1. Command-line arguments 1.1 Basics 1.2 Programmatic API 1.3 Declarative API 2. Styled output 2.1 Text style and colors 2.2 Styled string formatting |
3. Reading strings 3.1 Readline 3.2 Secure readline 4. Requirements 5. Copyright |
CommandLineKit handles command-line arguments with the following protocol:
- A new Flags object gets created either for the system-provided command-line arguments or for a custom sequence of arguments.
- For every flag, a Flag
object is being created and registered in the
Flagsobject. - Once all flag objects are declared and registered, the command-line gets parsed. After parsing is complete, the flag objects can be used to access the extracted options and arguments.
CommandLineKit defines different types of Flag subclasses for handling options (i.e. flags without parameters) and arguments (i.e. flags with parameters). Arguments are either singleton arguments (i.e. they have exactly one value) or they are repeated arguments (i.e. they have many values). Arguments are parameterized with a type which defines how to parse values. The framework natively supports int, double, string, and enum types, which means that in practice, just using the built-in flag classes are almost always sufficient. Nevertheless, the framework is extensible and supports arbitrary argument types.
A flag is identified by a short name character and a long name string. At least one of the two needs to be
defined. For instance, the "help" option could be defined by the short name "h" and the long name "help".
On the command-line, a user could either use -h or --help to refer to this option; i.e. short names are
prefixed with a single dash, long names are prefixed with a double dash.
An argument is a parameterized flag. The parameters follow directly the flag identifier (typically separated by
a space). For instance, an integer argument with long name "size" could be defined as: --size 64. If the
argument is repeated, then multiple parameters may follow the flag identifier, as in this
example: --size 2 4 8 16. The sequence is terminated by either the end of the command-line arguments,
another flag, or the terminator "---". All command-line arguments following the terminator are not being parsed
and are returned in the parameters field of the Flags object.
Here is an example
from the LispKit project. It uses factory methods (like flags.string,
flags.int, flags.option, flags.strings, etc.) provided by the
Flags
class to create and register individual flags.
// Create a new flags object for the system-provided command-line arguments
varflags=Flags()
// Define the various flags
letfilePaths= flags.strings("f","filepath",
description:"Adds file path in which programs are searched for.")letlibPaths= flags.strings("l","libpath",
description:"Adds file path in which libraries are searched for.")letheapSize= flags.int("x","heapsize",
description:"Initial capacity of the heap", value:1000)letimportLibs= flags.strings("i","import",
description:"Imports library automatically after startup.")letprelude= flags.string("p","prelude",
description:"Path to prelude file which gets executed after "+"loading all provided libraries.")letprompt= flags.string("r","prompt",
description:"String used as prompt in REPL.", value:"> ")letquiet= flags.option("q","quiet",
description:"In quiet mode, optional messages are not printed.")lethelp= flags.option("h","help",
description:"Show description of usage and options of this tools.")
// Parse the command-line arguments and return error message if parsing fails
iflet failure = flags.parsingFailure(){print(failure)exit(1)}The framework supports printing the supported options via the Flags.usageDescription function. For the
command-line flags as defined above, this function returns the following usage description:
usage: LispKitRepl [<option> ...] [---] [<program> <arg> ...]
options:
-f, --filepath <value> ...
Adds file path in which programs are searched for.
-l, --libpath <value> ...
Adds file path in which libraries are searched for.
-h, --heapsize <value>
Initial capacity of the heap
-i, --import <value> ...
Imports library automatically after startup.
-p, --prelude <value>
Path to prelude file which gets executed after loading all provided libraries.
-r, --prompt <value>
String used as prompt in REPL.
-q, --quiet
In quiet mode, optional messages are not printed.
-h, --help
Show description of usage and options of this tools.
Command-line tools can inspect whether a flag was set via the Flag.wasSet field. For flags with
parameters, the parameters are stored in the Flag.value field. The type of this field is dependent on the
flag type. For repeated flags, an array is used.
Here is an example how the flags defined by the code snippet above could be used:
// If help flag was provided, print usage description and exit tool
if help.wasSet {print(flags.usageDescription(usageName:TextStyle.bold.properties.apply(to:"usage:"),
synopsis:"[<option> ...] [---] [<program> <arg> ...]",
usageStyle:TextProperties.none,
optionsName:TextStyle.bold.properties.apply(to:"options:"),
flagStyle:TextStyle.italic.properties),
terminator:"")exit(0)}...
// Define how optional messages and errors are printed
func printOpt(_ message:String){if !quiet.wasSet {print(message)}}...
// Set heap size (assuming 1234 is the default if the flag is not set)
virtualMachine.setHeapSize(heapSize.value ??1234)...
// Register all file paths
forpathin filePaths.value {
virtualMachine.fileHandler.register(path)}...
// Load prelude file if it was provided via flag `prelude`
iflet file = prelude.value {
virtualMachine.load(file)}The code below illustrates how to combine the Command protocol with property wrappers
declaring the various command-line flags. The whole lifecycle of a command-line tool that
is declared like this will be managed automatically. After flags are being parsed, either
methods run() or fail(with:) are being called (depending on whether flag parsing
succeeds or fails).
@mainstructLispKitRepl:Command{@CommandArguments(short:"f", description:"Adds file path in which programs are searched for.")varfilePath:[String]@CommandArguments(short:"l", description:"Adds file path in which libraries are searched for.")varlibPaths:[String]@CommandArgument(short:"x", description:"Initial capacity of the heap")varheapSize:Int=1234...@CommandOption(short:"h", description:"Show description of usage and options of this tools.")varhelp:Bool@CommandParameters // Inject the unparsed parameters
varparams:[String]@CommandFlags // Inject the flags object
varflags:Flagsmutatingfunc fail(with reason:String)throws{print(reason)exit(1)}mutatingfunc run()throws{
// If help flag was provided, print usage description and exit tool
if help {print(flags.usageDescription(usageName:TextStyle.bold.properties.apply(to:"usage:"),
synopsis:"[<option> ...] [---] [<program> <arg> ...]",
usageStyle:TextProperties.none,
optionsName:TextStyle.bold.properties.apply(to:"options:"),
flagStyle:TextStyle.italic.properties),
terminator:"")exit(0)}...
// Define how optional messages and errors are printed
func printOpt(_ message:String){if !quiet {print(message)}}...
// Set heap size
virtualMachine.setHeapSize(heapSize)...
// Register all file paths
forpathin filePaths {
virtualMachine.fileHandler.register(path)}...
// Load prelude file if it was provided via flag `prelude`
iflet file = prelude {
virtualMachine.load(file)}}}CommandLineKit provides a
TextProperties
structure for bundling a text color, a background color, and a text style in a single object. Text properties can be
merged with the with(:) methods and applied to a string with the apply(to:) method.
Individual enumerations for TextColor, BackgroundColor, and TextStyle define the individual properties.
Using TextProperties
and its apply(to:) method can be used to inject ANSI escape sequences into strings so that they appear formatted on
ANSI terminals. But using this approach directly makes it really difficult to format output, e.g. to center or right-align
content. For this purpose, enum AnsiText is provided. It bundles strings with TextProperties-based styling definitions.
AnsiText can be initialized directly from strings and properties can be injected via string interpolation.
AnsiText represents styled text as a tree structure with three cases:
plain(String): Unstyled textsegmented([AnsiText]): Multiple concatenated text segmentsannotated(TextProperties, AnsiText): Text with styling applied
enumAnsiText:...{case plain(String)case segmented([AnsiText])indirectcase annotated(TextProperties,AnsiText)...structNormalized:...{varsegments:[(TextProperties,String)]init(segments:[(TextProperties,String)]){...}init(_ string:String="", properties:TextProperties=.empty){...}init(repeating:String, count:Int, properties:TextProperties=.empty){...}...}...
// Normalized representation
varnormalized:Normalized{...}
// Returns the number of characters (ignoring formatting)
varcount:Int{...}
// Returns the width of the output in ANSI terminals
// (factoring in multi-place unicode characters)
varterminalDisplayWidth:Int{...}}Here are some basic usage examples:
// Create text using string literals
lettext:AnsiText="Hello, World!"
// Apply styling via string interpolation
letstyled:AnsiText="Error: \("File not found", properties:.init(.red,nil,.bold))"
// Compose complex styled text
letmessage:AnsiText=.segmented([.annotated(.init(.green),"Success: "),.plain("Operation completed in "),.annotated(.init(.blue,nil,.bold),"1.2s")])While AnsiText provides a convenient tree-based representation, AnsiText.Normalized offers a flattened, optimized form that merges adjacent segments with identical properties. This makes it more efficient for rendering and text manipulation operations:
lettext:AnsiText="Hello \("World", properties:.init(.red))"letnormalized= text.normalized
// normalized segments: [(.empty, "Hello "), (.red, "World")]
// Normalized provides direct access to segments
for(properties, string)in normalized.segments {print("\(string) with \(properties)")}
// Normalized is a collection and bi-directional sequence
// providing the same access to characters as strings (but with
// text properties injected):
for(properties, ch)in normalized {print("`\(ch)` with \(properties)")}
// Get plain text or encoded output
print(normalized.description) // "Hello World"
print(normalized.encodedString) // "Hello \u{001B}[31mWorld\u{001B}[0m"Key differences between AnsiText and AnsiText.Normalized:
- Structure:
AnsiTextis a hierarchical tree;Normalizedis a flat array of segments - Optimization:
Normalizedmerges adjacent segments with the same properties - Performance:
Normalizedis more efficient for rendering and manipulation - Usage: Use
AnsiTextfor construction; convert toNormalizedfor processing
Arrays of AnsiText, AnsiText?, AnsiText.Normalized, and AnsiText.Normalized? values support
powerful formatting functions for aligning and wrapping text:
justified(maxWidth:align:alignWidth,padCharacter:fill:) interprets the array as an array of lines each
represented by one AnsiText or AnsiText.Normalized value and aligns individual lines to a
specified width. It is using the character count by default to do the alignment. If
alignWidth is set to true, the alignment is done by using terminalDisplayWidth which factors
in that some unicode characters (e.g. emojis) require multiple places when output in ANSI terminals.
letlines:[AnsiText]=["Short line","A \("longer red", properties:.red) line",.annotated(.italic,"And \("short", properties:.underline) again")]
// Normalize the lines first
letnormalizedLines= lines.map{ $0.normalized }
// Left-align with padding
letleft= normalizedLines.justified(maxWidth:20, align:.left)
// Center-align with custom padding
letcentered= normalizedLines.justified(maxWidth:20, align:.center, padCharacter:".", fill:TextProperties(.grey))
// Right-align factoring in the display width
letright= normalizedLines.justified(maxWidth:20, align:.right, alignWidth:true)joined(separator:maxWidth:align:alignWidth:padCharacter:fill) interprets the array as an array
of words each represented by an AnsiText or AnsiText.Normalized value and combines words with
word wrapping and alignment:
// Styled text
lettext:AnsiText="The quick \("brown fox", properties:.bold) jumps over the lazy dog"
// Tokenize styled text
letwords= text.normalized.tokenize()
// Word-wrap to 15 characters with right alignment
letwrapped= words.joined(separator:"", maxWidth:15, align:.right)
// Join the lines and include the ANSI control sequences
letall= wrapped.joined(separator:"\n").encodedString
print(all)
// Output (centered in 15-char field):
// "The quick brown"
// " fox jumps over"
// " the lazy dog"These formatting functions enable sophisticated terminal output, such as creating aligned tables with styled cells, wrapped paragraphs, and justified text blocks while preserving all ANSI styling information.
CommandLineKit includes a significantly improved version of the "readline" API originally defined by the library Linenoise-Swift. It supports unicode text, multi-line text entry, and styled text. It supports all the existing features such as advanced keyboard support, history, text completion, and hints.
The following code illustrates the usage of the LineReader API:
iflet ln =LineReader(){
ln.setCompletionCallback{ currentBuffer inletcompletions=["Hello!","Hello Google","Scheme is awesome!"]return completions.filter{ $0.hasPrefix(currentBuffer)}}
ln.setHintsCallback{ currentBuffer inlethints=["Foo","Lorem Ipsum","Scheme is awesome!"]letfiltered= hints.filter{ $0.hasPrefix(currentBuffer)}iflet hint = filtered.first {lethintText=String(hint.dropFirst(currentBuffer.count))return(hintText,TextColor.grey.properties)}else{returnnil}}print("Type 'exit' to quit")vardone=falsewhile !done {do{letoutput=try ln.readLine(prompt:"> ",
maxCount:200,
strippingNewline:true,
promptProperties:TextProperties(.green,nil,.bold),
readProperties:TextProperties(.blue,nil),
parenProperties:TextProperties(.red,nil,.bold))print("Entered: \(output)")
ln.addHistory(output)if output =="exit"{break}}catchLineReaderError.CTRLC {print("\nCaptured CTRL+C. Quitting.")
done =true}catch{print(error)}}}The Terminal.readLineSecure(prompt:maxLength:allowEmpty:replacementChar) method provides a secure way to read sensitive input from the terminal, such as passwords, API keys, or other confidential data. Unlike regular input, this function conceals user input by replacing each character with a replacement character (by default •).
Key features:
- Unicode support: Correctly handles multi-byte UTF-8 characters and wide characters (e.g., emojis, CJK characters)
- Cursor movement: Supports left/right arrow keys and Ctrl+A/Ctrl+E for navigation
- Editing capabilities: Allows backspace/delete to remove characters and Ctrl+U to clear the entire line
- Length constraints: Optional maximum length enforcement with visual/audible feedback
- Empty input control: Can require non-empty input when needed
- Styled prompts: Supports
AnsiTextfor styled prompts
Here is an example showcasing its usage:
// Simple password prompt
letpassword=tryTerminal.readLineSecure(prompt:"Password: ")print("You entered: \(password)")
// With styled prompt
letstyledPrompt:AnsiText="\("Enter API Key:", properties:.init(.yellow,nil,.bold))"letapiKey=tryTerminal.readLineSecure(prompt: styledPrompt)
// Require non-empty input with maximum length
letpin=tryTerminal.readLineSecure(prompt:"PIN (4 digits): ",
maxLength:4,
allowEmpty:false)readLineSecure throws an LineReaderError.CTRLC exception if CTRL-C is pressed (similar to the readLine API),
allowing your application to gracefully handle cancellation of sensitive input operations.
Author: Matthias Zenger (matthias@objecthub.com)
Copyright © 2018-2025 Google LLC.
Copyright © 2026 Matthias Zenger
Please note: This is not an official Google product.