Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Introduction to Command Line Swift

Avatar for bok bok
May 08, 2018

Introduction to Command Line Swift

A quick look at building command line apps in Swift including using the Swift Package Manager in unconventional ways.

Avatar for bok

bok

May 08, 2018
Tweet

Other Decks in Technology

Transcript

  1. Included • Simple CLI Tool in 100% Swift • Swift

    Package Manager • Accepts command line arguments • Supports subcommands
 (i.e. git pull) • Processes standard input • Executes other programs
  2. Target MyMac:~ bok$ json OVERVIEW: json: Magic Demo Project. The

    audience is impressed. USAGE: json [options] <command> [options] OPTIONS: --version Prints the current version of json
 --help Display available options SUBCOMMANDS: minify Minifies prints the provided JSON string. Typically from stdin.
 pretty Pretty prints the provided JSON string. Typically from stdin.
 render Renders the specified mustache template file using the JSON provided on stdin.
  3. Structure Location Purpose Package.swift Manages project dependencies and targets. Sources

    Where your code lives. One directory per target / module. Tests Where your tests live (yes, you still have to write tests). .build Where swift keeps its dependencies and derived data. README.md / .gitignore Usual boilerplate stuff.
  4. json Core Formatter Renderer Not Testable! Testable ✅ Command Line

    Parsing Subcommand Setup All actual subcommand code Architecture
  5. Add Core • Each target / module gets its own

    directory. • Inside each module, create .swift files as normal. • Remember to mark things public!
  6. New main.swift • Init App • Run app • (optional)

    Handle exceptions Don’t forget this is all untestable 
 so keep it lightweight.
  7. Let’s go! • swift build
 
 Downloads dependencies and builds

    your code. Defaults to Debug config, binary lives in .build/x86_64-apple- macosx10.10/debug/<name> • swift run
 
 Downloads dependencies, builds and runs your code.
 You can also run it as swift run <name> [arguments]
  8. Testing Time • Don’t forget to create folders when adding

    Targets. • XCTest works the same - so no excuse!
  9. Makefiles • A makefile is a file (by default named

    "Makefile") containing a set of directives used with by make build automation tool to generate a target/goal.
 (thanks Wikipedia) • POSIX Standard build automation tool • Great for saving all those xcodebuild / swift build command line arguments. • Also great for installation.
  10. So use Xcode $ swift package generate-xcodeproj --help
 
 OVERVIEW:

    Generates an Xcode project OPTIONS:
 --enable-code-coverage
 Enable code coverage in the generated project --output
 Path where the Xcode project should be generated --xcconfig-overrides
 Path to xcconfig file
  11. But not all of it • Don’t mess with build

    settings • Don’t mess with Targets • Don’t mess with Schemes • Just… don’t mess with things • Do everything in Package.swift and re-generate your project
  12. So, about print() Standard print() is fine and all, but

    we’re supposed to print errors on stderr #
  13. So, about print() Declaration func print<Target>( _ items: Any..., separator:

    String = default, terminator: String = default, to output: inout Target ) where Target : TextOutputStream Parameters items Zero or more items to print. separator A string to print between each item. The default is a single space (" "). terminator The string to print after all items have been printed. The default is a newline ("\n"). output An output stream to receive the text representation of each item.
  14. TextOutputStream public protocol TextOutputStream { /// Appends the given string

    to /// the stream. public mutating func write ( _ string: String ) } /// Yep, that’s all.
  15. Wrap FileHandle /// A wrapper for stdout struct StandardOutputStream: TextOutputStream

    { func write(_ string: String) { guard let data = string.data(using: .utf8) else { return } FileHandle.standardOutput.write(data) } } /// A wrapper for stderr struct StandardErrorOutputStream: TextOutputStream { func write(_ string: String) { guard let data = string.data(using: .utf8) else { return } FileHandle.standardError.write(data) } }
  16. Command Line Parsing // From https://github.com/apple/swift-package- manager/blob/master/Package.swift import PackageDescription let

    package = Package( name: "SwiftPM", products: [ // The `libSwiftPM` set of interfaces to // programatically work with Swift packages. // // NOTE: This API is *unstable* and may change at // any time. .library( name: "SwiftPM", type: .dynamic, targets: [ "clibc", "SPMLibc", "POSIX", "Basic", "Utility", "SourceControl", "PackageDescription", "PackageDescription4", "PackageModel", "PackageLoading", "PackageGraph", "Build", "Xcodeproj", "Workspace" ] ), // Collection of general purpose utilities. // // NOTE: This product consists of *unsupported*, // *unstable* API. These APIs are implementation // details of the package manager. Depend on it // at your own risk. .library( name: "Utility", targets: [ "clibc", "SPMLibc", "POSIX", "Basic", "Utility", ] ), ] )
  17. ArgumentParser Declaration public init ( commandName: String? = nil, usage:

    String, overview: String, seeAlso: String? = nil ) Parameters commandName If provided, this will be substituted in “usage” line of the generated usage text. Otherwise, first command line argument will be used. usage The “usage” line of the generated usage text. overview The “overview” line of the generated usage text. seeAlso The “see also” line of generated usage text.
  18. ArgumentParser parse() Parses the provided array and return the result.

    Declaration public func parse ( _ arguments: [String] = [] ) throws -> Result printUsage() Prints usage text for this parser on the provided stream. Declaration public func printUsage ( on stream: OutputByteStream )
  19. --version ArgumentParser.add<T>() Adds an option to the parser. Declaration public

    func add<T: ArgumentKind>( option: String, shortName: String? = nil, kind: T.Type, usage: String? = nil, completion: ShellCompletion? = nil ) -> OptionArgument<T> OptionArgument.get<T>() Get an option argument's value from the results. Since the options are optional, their result may or may not be present. Declaration public func get<T>( _ argument: OptionArgument<T> ) -> T?
  20. Tip • Unified error API for each module • Wrap

    any errors you throw! • Much better context if a user copy/ pastes you command output. • (Also useful for iOS apps)
  21. Subcommands ArgumentParser.Result Get the subparser which was chosen for the

    given parser. Declaration public func subparser ( _ parser: ArgumentParser ) -> String?
  22. ¯\_(π)_/¯ /* Sorts dictionary keys for output using [NSLocale systemLocale].

    Keys are compared using NSNumericSearch. The specific sorting method used is subject to change. */ @available(OSX 10.13, *) public static var sortedKeys: JSONSerialization.WritingOptions { get }
  23. Process • Provided in SwiftPM’s Basic • Executes command and

    captures results • Sadly no support for passing stdin to the executed process.
  24. TemporaryFile /// This class is basically a /// wrapper over

    posix’s /// mkstemps() function to create /// disposable files. /// /// The file is deleted as soon as /// the object of this class is /// deallocated. /// public final class TemporaryFile
  25. Process Declaration public init ( arguments: [String], environment: [String: String]

    = env, redirectOutput: Bool = true, verbose: Bool = Process.verbose ) Parameters arguments The arguments for the subprocess. environment The environment to pass to subprocess. By default the current process environment will be inherited. redirectOutput Redirect and store stdout/stderr output (of subprocess) in the process result, instead of printing on the standard streams. Default value is true. verbose If true, launch() will print the arguments of the subprocess before launching it.
  26. Results /// In Process: /// Launch the subprocess. public func

    launch() throws /// Blocks the calling process /// until the subprocess finishes /// execution. @discardableResult public func waitUntilExit() throws -> ProcessResult /// Process result data which is available after process termination. public struct ProcessResult { public let exitStatus: ExitStatus public enum ExitStatus { case terminated(code: Int32) case signalled(signal: Int32) } /// The output bytes of the process. Available /// only if the process was asked to redirect /// its output. public let output: Result<[Int8], AnyError> /// The output bytes of the process. Available /// only if the process was asked to redirect /// its output. public let stderrOutput: Result<[Int8], AnyError> }
  27. One Last Thing • There is no Run Loop! •

    All your nice async code will not be executed. • Further reading: •(NS)RunLoop • SwiftPM’s await() • SwiftPM’s Condition
 (wraps NSCondition)