Swiftpack.co - Package - kareman/SwiftShell

Run shell commands | Parse command line arguments | Handle files and directories


Swift 5 | Swift 4 | Swift 3 | Swift 2

SwiftShell logo

Build Status Platforms Swift Package Manager Carthage compatible Twitter: @nottoobadsw

SwiftShell

A library for creating command-line applications and running shell commands in Swift.

Features

  • ☑ run commands, and handle the output.
  • ☑ run commands asynchronously, and be notified when output is available.
  • ☑ 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.
  • ☑ create new such contexts you can run commands in.
  • ☑ handle errors.
  • ☑ read and write files.

See also

Table of Contents

Example

Print line numbers

#!/usr/bin/env swiftshell

import SwiftShell

do {
	// If there is an argument, try opening it as a file. Otherwise use standard input.
	let input = try main.arguments.first.map {try open($0)} ?? main.stdin

	input.lines().enumerated().forEach { (linenr,line) in 
		print(linenr+1, ":", line) 
	}

	// Add a newline at the end.
	print("")
} catch {
	exit(error)
}

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

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).

public struct CustomContext: Context, CommandRunning {
	public var env: [String: String]
	public var currentdirectory: String
	public var stdin: ReadableStream
	public var stdout: WritableStream
	public var stderror: WritableStream
}

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):

import SwiftShell
import Foundation

extension Dictionary where Key:Hashable {
	public func filterToDictionary <C: Collection> (keys: C) -> [Key:Value]
		where C.Iterator.Element == Key, C.IndexDistance == Int {

		var result = [Key:Value](https://raw.github.com/kareman/SwiftShell/blob/master/minimumCapacity: keys.count)
		for key in keys { result[key] = self[key] }
		return result
	}
}

// Prepare an environment as close to a new OS X user account as possible.
var cleanctx = CustomContext(main)
let cleanenvvars = ["TERM_PROGRAM", "SHELL", "TERM", "TMPDIR", "Apple_PubSub_Socket_Render", "TERM_PROGRAM_VERSION", "TERM_SESSION_ID", "USER", "SSH_AUTH_SOCK", "__CF_USER_TEXT_ENCODING", "XPC_FLAGS", "XPC_SERVICE_NAME", "SHLVL", "HOME", "LOGNAME", "LC_CTYPE", "_"]
cleanctx.env = cleanctx.env.filterToDictionary(keys: cleanenvvars)
cleanctx.env["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

// Create a temporary directory for testing.
cleanctx.currentdirectory = main.tempdirectory

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.

FileSmith's WritableFile and ReadableFile are streams too, and can be used as stdin, stdout and stderror in SwiftShell Contexts, including main.

WritableStream

When writing to a WritableStream you normally use .print which works exactly like Swift's built-in print function:

main.stdout.print("everything is fine")
main.stderror.print("no wait, something went wrong ...")

let writefile = try open(forWriting: path) // WritableStream
writefile.print("1", 2, 3/5, separator: "+", terminator: "=")

If you want to be taken literally, use .write instead. It doesn't add a newline and writes exactly and only what you write:

writefile.write("Read my lips:")

You can close the stream, so anyone who tries to read from the other end won't have to wait around forever:

writefile.close()

ReadableStream

When reading from a ReadableStream you can read everything at once:

let readfile = try open(path) // ReadableStream
let contents = readfile.read()

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:

while let text = main.stdin.readSome() {
	// do something with ‘text’...
}

.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:

for line in main.stdin.lines() {
	// ...
}

Or instead of stopping and waiting for any output you can be notified whenever there is something in the stream:

main.stdin.onOutput { stream in
	// ‘stream’ refers to main.stdin
}

Data

In addition to text, streams can also handle raw Data:

let data = Data(...)
writer.write(data: data)
reader.readSomeData()
reader.readData() 

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:

let result1 = run("/usr/bin/executable", "argument1", "argument2")
let result2 = run("executable", "argument1", "argument2")

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:

/// Output from a `run` command.
public final class RunOutput {

	/// The error from running the command, if any.
	let error: CommandError?

	/// Standard output, trimmed for whitespace and newline if it is single-line.
	let stdout: String

	/// Standard error, trimmed for whitespace and newline if it is single-line.
	let stderror: String

	/// The exit code of the command. Anything but 0 means there was an error.
	let exitcode: Int

	/// Checks if the exit code is 0.
	let succeeded: Bool
}

For example:

let date = run("date", "-u").stdout
print("Today's date in UTC is " + date)

Print output

try runAndPrint("executable", "arg") 

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

let command = runAsync("cmd", "-n", 245).onCompletion { command in
	// be notified when the command is finished.
}
command.stdout.onOutput { stdout in 
	// be notified when the command produces output (only on macOS).	
}

// do something with ‘command’ while it is still running.

try command.finish() // wait for it to finish.

runAsync launches a command and continues before it's finished. It returns AsyncCommand which contains this:

    public let stdout: ReadableStream
    public let stderror: ReadableStream

    /// Is the command still running?
    public var isRunning: Bool { get }

    /// Terminates the command by sending the SIGTERM signal.
    public func stop()

    /// Interrupts the command by sending the SIGINT signal.
    public func interrupt()

    /// Temporarily suspends a command. Call resume() to resume a suspended command.
    public func suspend() -> Bool

    /// Resumes a command previously suspended with suspend().
    public func resume() -> Bool

    /// Waits for this command to finish.
    public func finish() throws -> Self

    /// Waits for command to finish, then returns with exit code.
    public func exitcode() -> Int

    /// Waits for the command to finish, then returns why the command terminated.
    /// - returns: `.exited` if the command exited normally, otherwise `.uncaughtSignal`.
    public func terminationReason() -> Process.TerminationReason

    /// Takes a closure to be called when the command has finished.
    public func onCompletion(_ handler: @escaping (AsyncCommand) -> Void) -> Self

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.

try runAndPrint("echo", "We are", 4, "arguments")
// echo "We are" 4 arguments

let array = ["But", "we", "are"]
try runAndPrint("echo", array, array.count + 2, "arguments")
// echo But we are 5 arguments
(bash bashcommand: String)

These are the commands you normally use in the Terminal. You can use pipes and redirection and all that good stuff:

try runAndPrint(bash: "cmd1 arg1 | cmd2 > output.txt")

Note that you can achieve the same thing in pure SwiftShell, though nowhere near as succinctly:

var file = try open(forWriting: "output.txt")
runAsync("cmd1", "arg1").stdout.runAsync("cmd2").stdout.write(to: &file)

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:

let command = runAsync("cmd", "-n", 245)
// ...
do {
	try command.finish()
} catch CommandError.returnedErrorCode(let error) {
	// use error.command or error.errorcode
}

The runAndPrint command can also throw this error, in addition to this one if the command could not be launched:

} catch CommandError.inAccessibleExecutable(let path) {
	// ‘path’ is the full path to the executable
}

Instead of dealing with the values from these errors you can just print them:

} catch {
	print(error)
}

... or if they are sufficiently serious you can print them to standard error and exit:

} catch {
	exit(error)
}

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:

marathon add https://github.com/kareman/SwiftShell.git

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.0.1") to your Package.swift:

// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ProjectName",
    platforms: [.macOS(.v10_13)],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/kareman/SwiftShell", from: "5.0.1")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "ProjectName",
            dependencies: ["SwiftShell"]),
    ]
)

and run swift build.

Carthage

Add github "kareman/SwiftShell" >= 5.0 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.

pod 'SwiftShell', '>= 5.0.0'

Then run pod install to install it.

License

Released under the MIT License (MIT), https://opensource.org/licenses/MIT

Kåre Morstøl, NotTooBad Software

Github

link
Stars: 742

Dependencies

Used By

Total: 0

Releases

- 2019-10-03 13:47:27

  • fix framework build setting from corrupting xcode archive type.
  • support older macOS versions (10.11)

Swift 5 - 2019-03-27 22:32:25

  • Update to Swift 5
  • Set minimum macOS version to 10.13, because of deprecated methods in Foundation.Process.
  • swiftshell-init uses Swift 5 in it's generated Package.swift.
  • The 'exit' functions now only print filename and line number in debug builds.

- 2018-10-03 19:51:27

  • set Swift version to 4.2.
  • fix multi threading issue raised by Xcode's tread sanitiser when using the 'run(...)' commands. (#69)

- 2018-07-31 16:25:58

Remove swiftlint build phase, as it was inactive and led to Carthage build errors on Xcode 10 beta 3.

4.1.0 - 2018-04-18 01:00:39

  • Update to Swift 4.1.
  • Add AsyncCommand.stop, interrupt, suspend and resume. Also for Linux (#59, #60).
  • Add runAsyncAndPrint command (#61).
  • Improve documentation and formatting. Use present tense in documentation. Add missing documentation.

Bug fixes

  • Make main.stdout.encoding default to main.encoding.
  • run: do not read both standard output and standard error if they are the same.

Miscellaneous

  • Mark stream's readData() with @discardableResult
  • Make testIntsLazySplit_NoEmptySlices actually do something.
  • Xcode 9.3: update to recommended project settings.
  • Readme: update with new functionality. Also fix some other stuff.

4.0.2 - 2018-03-04 16:43:33

  • Fix #52: 'run' hangs on large output.
  • Update Swift to version 4.0.3.

- 2018-01-30 02:10:49

  • Remove deprecated uses of .characters, and thereby get rid of all the warnings.
  • Get swiftshell-init to work with new Package.swift layout.
  • Add missing Linux unit test.
  • Fix bug in ReadableStream.onOutput where the callback would crash if the ReadableStream instance had ceased to exist (#45)

Swift 4 - 2018-01-30 02:04:00

Update to Swift 4.

Improve error output - 2017-05-12 19:04:54

  • Improve error output from exit(...) and non-SwiftShell errors.

SwiftShell 3.0 - 2017-04-29 20:06:20

The readme says it all.

Most notable recent changes:

  • the run(...) functions now return RunOutput instead of a String.

  • many things were renamed to follow standard Swift naming guidelines.

  • Streams can read and write raw data.

  • Support for iOS – but only for Streams (running shell commands is not possible on iOS). This way other projects, like FileSmith, can use Streams. Streams should be their own module, but then they couldn't implement CommandRunning. This will be possible in Swift 4 I believe.

Support Swift 3.1 beta - 2017-03-24 17:16:42

- 2016-05-08 20:13:16

  • Have main.tempdirectory use the name of the script/application.

    Fall back to ‘SwiftShell’ if main.path is empty (running in a playground).

  • Allow passing Ints as error codes to "exit".

  • Actually print error messages in release builds.

    Workaround for a compiler bug where passing an NSError to exit in release builds would not print the actual error message.

  • Fix #16 by having WriteableStream use ‘print’ when writing to standard output.

    This way print's buffering when standard output is not a terminal does not rearrange the order in which output is printed when using both 'print' and 'main.stdout.writeln'. See also https://bugs.swift.org/browse/SR-1127 and http://stackoverflow.com/a/13933741/96587 .

  • Add stop func to stop an AsyncShellTask early.

  • Add async callbacks for ReadableStream.onOutput and AsyncShellTask.onCompletion - allows for getting callbacks with data as the AsyncShellTask is running.

  • Add AsyncShellTask.onStringOutput.

Thanks to @kenthinson for implementing the 3 last ones.

Swift 2.2 - 2016-03-22 11:04:43

Silenced all the warnings by replacing __X__ with #x.

2.0.1 - 2016-03-21 00:20:47

  • Fix #15: run("cat","longtext.txt") never finishes for large files.
  • Add AsyncShellTask.exitcode().
  • Make sure the test script finds the SwiftShell launcher script.

SwiftShell 2.0 - 2016-02-24 02:39:42

Pretty much everything has changed since SwiftShell 1. See the readme for more info.

- 2015-12-18 22:33:05

Use $SWIFTSHELL_FRAMEWORK_PATH instead of $DYLD_FRAMEWORK_PATH in the swiftshell launcher script.

Because of this change in El Capitan:

Spawning children processes of processes restricted by System Integrity Protection, such as by launching a helper process in a bundle with NSTask or calling the exec(2) command, resets the Mach special ports of that child process. Any dynamic linker (dyld) environment variables, such as DYLD_LIBRARY_PATH, are purged when launching protected processes.

https://developer.apple.com/library/prerelease/mac/documentation/Security/Conceptual/System_Integrity_Protection_Guide/RuntimeProtections/RuntimeProtections.html

- 2015-12-18 22:17:10

  • Enable installation of framework to ~/Library/Frameworks by running 'xcodebuild install'.
  • Add CollectionType.splitOnce and LazySplitGenerator.
  • Add LazyCollectionType.split .
  • Add ReadableStream.lines() for lazily splitting output into lines.

Swift 2.0 - 2015-09-14 19:51:03

Swift 1.2 - 2015-08-31 17:40:05

SwiftShell for Swift 1.1 (Xcode 6.1.1-6.2) - 2015-02-14 17:23:05

This is the last version of SwiftShell before the syntax changes in Swift 1.2.