SwiftShell
A library for creating command-line applications and running shell commands in Swift.
Features
- [x] run commands, and handle the output.
- [x] run commands asynchronously, and be notified when output is available.
- [x] access the context your application is running in, like environment variables, standard input, standard output, standard error, the current directory and the command line arguments.
- [x] create new such contexts you can run commands in.
- [x] handle errors.
- [x] read and write files.
See also
- API Documentation.
- A description of the project on skilled.io.
Example
Print line numbers
Launched with e.g. cat long.txt | print_linenumbers.swift
or print_linenumbers.swift long.txt
this will print the line number at the beginning of each line.
Others
- Test the latest commit (using make and/or Swift).
- Run a shell command in the middle of a method chain.
- Move files to the trash.
Context
All commands (a.k.a. processes) you run in SwiftShell need context: environment variables, the current working directory, standard input, standard output and standard error (standard streams).
You can create a copy of your application's context: let context = CustomContext(main)
, or create a new empty one: let context = CustomContext()
. Everything is mutable, so you can set e.g. the current directory or redirect standard error to a file.
Main context
The global variable main
is the Context for the application itself. In addition to the properties mentioned above it also has these:
public var encoding: String.Encoding
The default encoding used when opening files or creating new streams.public let tempdirectory: String
A temporary directory you can use for temporary stuff.public let arguments: [String]
The arguments used when launching the application.public let path: String
The path to the application.
main.stdout
is for normal output, like Swift's print
function. main.stderror
is for error output, and main.stdin
is the standard input to your application, provided by something like somecommand | yourapplication
in the terminal.
Commands can't change the context they run in (or anything else internally in your application) so e.g. main.run("cd", "somedirectory")
will achieve nothing. Use main.currentdirectory = "somedirectory"
instead, this changes the current working directory for the entire application.
Example
Prepare a context similar to a new macOS user account's environment in the terminal (from kareman/testcommit):
Streams
The protocols ReadableStream and WritableStream in Context
above can read and write text from/to commands, files or the application's own standard streams. They both have an .encoding
property they use when encoding/decoding text.
You can use let (input,output) = streams()
to create a new pair of streams. What you write to input
you can read from output
.
WritableStream
When writing to a WritableStream you normally use .print
which works exactly like Swift's built-in print function:
If you want to be taken literally, use .write
instead. It doesn't add a newline and writes exactly and only what you write:
You can close the stream, so anyone who tries to read from the other end won't have to wait around forever:
ReadableStream
When reading from a ReadableStream you can read everything at once:
This will read everything and wait for the stream to be closed if it isn't already.
You can also read it asynchronously, that is read whatever is in there now and continue without waiting for it to be closed:
.readSome()
returns String?
- if there is anything there it returns it, if the stream is closed it returns nil, and if there is nothing there and the stream is still open it will wait until either there is more content or the stream is closed.
Another way to read asynchronously is to use the lines
method which creates a lazy sequence of Strings, one for each line in the stream:
Or instead of stopping and waiting for any output you can be notified whenever there is something in the stream:
Data
In addition to text, streams can also handle raw Data:
Commands
All Contexts (CustomContext
and main
) implement CommandRunning
, which means they can run commands using themselves as the Context. ReadableStream and String can also run commands, they use main
as the Context and themselves as .stdin
. As a shortcut you can just use run(...)
instead of main.run(...)
There are 4 different ways of running a command:
Run
The simplest is to just run the command, wait until it's finished and return the results:
If you don't provide the full path to the executable, then SwiftShell will try to find it in any of the directories in the PATH
environment variable.
run
returns the following information:
For example:
Print output
This runs a command like in the terminal, where any output goes to the Context's (main
in this case) .stdout
and .stderror
respectively. If the executable could not be found, was inaccessible or not executable, or the command returned with an exit code other than zero, then runAndPrint
will throw a CommandError
.
The name may seem a bit cumbersome, but it explains exactly what it does. SwiftShell never prints anything without explicitly being told to.
Asynchronous
runAsync
launches a command and continues before it's finished. It returns AsyncCommand
which contains this:
You can process standard output and standard error, and optionally wait until it's finished and handle any errors.
If you read all of command.stderror or command.stdout it will automatically wait for the command to close its streams (and presumably finish running). You can still call finish()
to check for errors.
runAsyncAndPrint
does the same as runAsync
, but prints any output directly and it's return type PrintedAsyncCommand
doesn't have the .stdout
and .stderror
properties.
Parameters
The run
* functions above take 2 different types of parameters:
(_ executable: String, _ args: Any ...)
If the path to the executable is without any /
, SwiftShell will try to find the full path using the which
shell command, which searches the directories in the PATH
environment variable in order.
The array of arguments can contain any type, since everything is convertible to strings in Swift. If it contains any arrays it will be flattened so only the elements will be used, not the arrays themselves.
(bash bashcommand: String)
These are the commands you normally use in the Terminal. You can use pipes and redirection and all that good stuff:
Note that you can achieve the same thing in pure SwiftShell, though nowhere near as succinctly:
Errors
If the command provided to runAsync
could not be launched for any reason the program will print the error to standard error and exit, as is usual in scripts. The runAsync("cmd").finish()
method throws an error if the exit code of the command is anything but 0:
The runAndPrint
command can also throw this error, in addition to this one if the command could not be launched:
Instead of dealing with the values from these errors you can just print them:
... or if they are sufficiently serious you can print them to standard error and exit:
When at the top code level you don't need to catch any errors, but you still have to use try
.
Setup
Stand-alone project
If you put Misc/swiftshell-init somewhere in your $PATH you can create a new project with swiftshell-init <name>
. This creates a new folder, initialises a Swift Package Manager executable folder structure, downloads the latest version of SwiftShell, creates an Xcode project and opens it. After running swift build
you can find the compiled executable at .build/debug/<name>
.
Script file using Marathon
First add SwiftShell to Marathon:
Then run your Swift scripts with marathon run <name>.swift
. Or add #!/usr/bin/env marathon run
to the top of every script file and run them with ./<name>.swift
.
Swift Package Manager
Add .package(url: "https://github.com/kareman/SwiftShell", from: "5.1.0")
to your Package.swift:
and run swift build
.
Carthage
Add github "kareman/SwiftShell" >= 5.1
to your Cartfile, then run carthage update
and add the resulting framework to the "Embedded Binaries" section of the application. See Carthage's README for further instructions.
CocoaPods
Add SwiftShell
to your Podfile
.
Then run pod install
to install it.
License
Released under the MIT License (MIT)