Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Last active February 28, 2019 22:04
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriseidhof/5829efcef67f4e8750254ffdd33050c5 to your computer and use it in GitHub Desktop.
Save chriseidhof/5829efcef67f4e8750254ffdd33050c5 to your computer and use it in GitHub Desktop.
//
// main.swift
// OptionParser
//
// Created by Chris Eidhof on 28.02.19.
// Copyright © 2019 objc.io. All rights reserved.
//
import Foundation
print("Hello, World!")
typealias Path = String
enum Mode: Equatable {
case run(RunType, args: [String])
case eject(Path, force: Bool)
case edit(Path)
case help
enum RunType: Equatable {
case stdin
case file(Path)
}
}
struct ParseError: Error {
var message: String
init(_ message: String) {
self.message = message
}
}
struct Parse<Result> {
let description: String
let _run: (inout Substring) throws -> Result
init(_ desc: String, _ run: @escaping (inout Substring) throws -> Result) {
self.description = desc
self._run = run
}
static func constant(description: String = "", _ x: Result) -> Parse<Result> {
return Parse(description, { _ in x })
}
func run(_ input: String) throws -> Result {
var x = input[...]
let result = try _run(&x)
guard x.isEmpty else { throw ParseError("Non-empty remainder: \(x)") }
return result
}
}
extension Substring {
mutating func removePrefix<S>(_ prefix: S) -> Bool where S: StringProtocol {
guard hasPrefix(prefix) else { return false }
removeFirst(prefix.count)
return true
}
}
extension Parse where Result == () {
static func flag(long: String, short: String) -> Parse<()> {
return Parse("\(long) or \(short)") { str in
if str.removePrefix(long) || str.removePrefix(short) {
str = str.drop(while: { $0 == " "})
return ()
} else {
throw ParseError("Expected \(long) or \(short)")
}
}
}
}
extension Parse {
func map<B>(_ f: @escaping (Result) -> B) -> Parse<B> {
return Parse<B>(description) { x in
return f(try self._run(&x))
}
}
func then<B>(_ f: Parse<B>, combine: (String, String) -> String = { "\($0) \($1)" }) -> Parse<(Result, B)> {
return Parse<(Result,B)>(combine(description, f.description)) { x in
let a = try self._run(&x)
let b = try f._run(&x)
return (a, b)
}
}
/// tries `self`, but if it fails, this tries `other`
func or(_ other: Parse<Result>, combine: (String, String) -> String = { "\($0) or \($1)" }) -> Parse<Result> {
return Parse(combine(description, other.description)) { x in
do {
return try self._run(&x)
} catch {
return try other._run(&x)
}
}
}
var optional: Parse<Result?> {
return self.map { $0 }.or(.constant(nil), combine: { l, r in "[\(l)]" })
}
}
extension Parse where Result == String {
static let string = Parse<String>("<string>") { inp in
inp = inp.drop(while: { $0 == " " })
let x = inp.firstIndex(of: " ") ?? inp.endIndex // doesn't take quoted strings into account!
let result = inp[..<x]
inp.removeFirst(result.count)
return String(result)
}
}
extension Parse where Result == Bool {
static func optionalFlag(long: String, short: String) -> Parse<Bool> {
return Parse<()>.flag(long: long, short: short).map { _ in true }.or(.constant(false))
}
}
func oneOf<R>(_ options: [Parse<R>]) -> Parse<R> {
let desc = options.map { $0.description }.joined(separator: "\n")
return Parse<R>(desc) { result in
for o in options {
if let x = try? o._run(&result) { return x }
}
throw ParseError("Couldn't parse")
}
}
let parser: Parse<Mode> =
oneOf([
Parse.flag(long: "--help", short: "-h").map { _ in .help },
Parse.flag(long: "--eject", short: "-e").then(Parse.optionalFlag(long: "--force", short: "-f")).then(.string).map { input in
let ((_, force), path) = input
return Mode.eject(path, force: force)
}
])
let expected: [(String, Mode?)] = [
("--help", .help),
("--eject foo", Mode.eject("foo", force: false)),
("--eject --force foo", Mode.eject("foo", force: true))
]
for e in expected {
assert(try! parser.run(e.0) == e.1)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment