mirror of
https://github.com/apple/swift.git
synced 2026-02-27 18:26:24 +01:00
174 lines
5.2 KiB
Swift
174 lines
5.2 KiB
Swift
//===--- CommandArgumentParser.swift --------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2024 - 2026 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
public struct CommandArgumentParser {
|
|
private var input: ByteScanner
|
|
public var flagSpec: Command.FlagSpec?
|
|
public var stopAtShellOperator: Bool
|
|
|
|
public init(
|
|
input: ByteScanner, flagSpec: Command.FlagSpec?, stopAtShellOperator: Bool
|
|
) {
|
|
self.input = input
|
|
self.flagSpec = flagSpec
|
|
self.stopAtShellOperator = stopAtShellOperator
|
|
}
|
|
|
|
public static func parse(
|
|
_ string: String, flagSpec: Command.FlagSpec?, stopAtShellOperator: Bool
|
|
) throws -> [Command.Argument] {
|
|
try string.scanningUTF8 { input in
|
|
var parser = Self(
|
|
input: input, flagSpec: flagSpec,
|
|
stopAtShellOperator: stopAtShellOperator
|
|
)
|
|
return try parser.consumeArguments()
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate extension ByteScanner.Consumer {
|
|
/// Consumes a character, unescaping if needed.
|
|
mutating func consumeUnescaped() -> Bool {
|
|
if peek == "\\" {
|
|
skip()
|
|
}
|
|
return eat()
|
|
}
|
|
|
|
mutating func consumeStringLiteral() throws {
|
|
assert(peek == "\"")
|
|
skip()
|
|
repeat {
|
|
if peek == "\"" {
|
|
skip()
|
|
return
|
|
}
|
|
} while consumeUnescaped()
|
|
throw CommandArgumentParserError.unterminatedStringLiteral
|
|
}
|
|
}
|
|
|
|
extension CommandArgumentParser {
|
|
public mutating func consumeArguments() throws -> [Command.Argument] {
|
|
var args = [Command.Argument]()
|
|
while let arg = try consumeArgument() {
|
|
args.append(arg)
|
|
}
|
|
return args
|
|
}
|
|
|
|
public mutating func consumeElement() throws -> ByteScanner.Bytes? {
|
|
// Eat any leading whitespace.
|
|
input.skip(while: \.isSpaceOrTab)
|
|
|
|
// If we're now at the end of the input, nothing can be parsed.
|
|
guard input.hasInput else { return nil }
|
|
|
|
// Consume the element, stopping at the first space or shell operator.
|
|
let start = input.cursor
|
|
let elt = try input.consume(using: { consumer in
|
|
guard let char = consumer.peek else { return false }
|
|
if stopAtShellOperator {
|
|
switch char {
|
|
case "<", ">", "(", ")", "|", "&", ";":
|
|
return false
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
switch char {
|
|
case \.isSpaceOrTab:
|
|
return false
|
|
case "\"":
|
|
try consumer.consumeStringLiteral()
|
|
return true
|
|
default:
|
|
return consumer.consumeUnescaped()
|
|
}
|
|
})
|
|
// Note that we may have an empty element while still moving the cursor
|
|
// for e.g '-I ""', which is an option with an empty value.
|
|
return start != input.cursor ? elt : nil
|
|
}
|
|
|
|
private mutating func tryConsumeOption(
|
|
_ option: ByteScanner, for flagSpec: Command.FlagSpec.Element
|
|
) throws -> Command.Argument? {
|
|
var option = option
|
|
let flag = flagSpec.flag
|
|
guard option.tryEat(utf8: flag.name.rawValue) else {
|
|
return nil
|
|
}
|
|
func makeOption(
|
|
spacing: Command.OptionSpacing, _ value: String
|
|
) -> Command.Argument {
|
|
.option(flag, spacing: spacing, value: value)
|
|
}
|
|
let spacing = flagSpec.spacing
|
|
do {
|
|
var option = option
|
|
if spacing.contains(.equals), option.tryEat("="), option.hasInput {
|
|
return makeOption(spacing: .equals, String(utf8: option.remaining))
|
|
}
|
|
}
|
|
if spacing.contains(.unspaced), option.hasInput {
|
|
return makeOption(spacing: .unspaced, String(utf8: option.remaining))
|
|
}
|
|
if spacing.contains(.spaced), !option.hasInput,
|
|
let value = try consumeElement() {
|
|
return makeOption(spacing: .spaced, String(utf8: value))
|
|
}
|
|
return option.empty ? .flag(flag) : nil
|
|
}
|
|
|
|
public mutating func consumeOption(
|
|
_ option: ByteScanner, dash: Command.Flag.Dash
|
|
) throws -> Command.Argument? {
|
|
// NOTE: If we ever expand the list of flags, we'll likely want to use a
|
|
// trie or something here.
|
|
guard let flagSpec else { return nil }
|
|
for spec in flagSpec.flags where spec.flag.dash == dash {
|
|
if let option = try tryConsumeOption(option, for: spec) {
|
|
return option
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public mutating func consumeArgument() throws -> Command.Argument? {
|
|
guard let element = try consumeElement() else { return nil }
|
|
return try element.withUnsafeBytes { bytes in
|
|
var option = ByteScanner(bytes)
|
|
var numDashes = 0
|
|
if option.tryEat("-") { numDashes += 1 }
|
|
if option.tryEat("-") { numDashes += 1 }
|
|
guard let dash = Command.Flag.Dash(numDashes: numDashes),
|
|
let result = try consumeOption(option, dash: dash) else {
|
|
return .value(String(utf8: option.whole))
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate enum CommandArgumentParserError: Error, CustomStringConvertible {
|
|
case unterminatedStringLiteral
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .unterminatedStringLiteral:
|
|
return "unterminated string literal in command line"
|
|
}
|
|
}
|
|
}
|