Last active
February 28, 2019 22:04
-
-
Save chriseidhof/5829efcef67f4e8750254ffdd33050c5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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