mirror of
https://github.com/apple/swift-argument-parser.git
synced 2026-06-06 20:18:23 +00:00
This simplifies declaring alternate names by allowing just
a string representation of an option or flag name. The string
is parsed to split on spaces, and then validates that each
name has either a one or two dash prefix and is ASCII only.
Existing style:
@Flag(name: [.customLong("hex-output"), .customShort("x")])
var hexadecimalOutput = false
New style:
@Flag(name: "--hex-output -x")
var hexadecimalOutput = false
280 lines
8.9 KiB
Swift
280 lines
8.9 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift Argument Parser open source project
|
|
//
|
|
// Copyright (c) 2020 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
extension StringProtocol where SubSequence == Substring {
|
|
func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String {
|
|
let columns = columns - wrappingIndent
|
|
guard columns > 0 else {
|
|
// Skip wrapping logic if the number of columns is less than 1 in release
|
|
// builds and assert in debug builds.
|
|
assertionFailure(
|
|
"`columns - wrappingIndent` should be always be greater than 0.")
|
|
return ""
|
|
}
|
|
|
|
var result: [Substring] = []
|
|
|
|
var currentIndex = startIndex
|
|
|
|
while true {
|
|
let nextChunk = self[currentIndex...].prefix(columns)
|
|
if let lastLineBreak = nextChunk.lastIndex(of: "\n") {
|
|
result.append(
|
|
contentsOf: self[currentIndex..<lastLineBreak].split(
|
|
separator: "\n", omittingEmptySubsequences: false))
|
|
currentIndex = index(after: lastLineBreak)
|
|
} else if nextChunk.endIndex == self.endIndex {
|
|
result.append(self[currentIndex...])
|
|
break
|
|
} else if let lastSpace = nextChunk.lastIndex(of: " ") {
|
|
result.append(self[currentIndex..<lastSpace])
|
|
currentIndex = index(after: lastSpace)
|
|
} else if let nextSpace = self[currentIndex...].firstIndex(of: " ") {
|
|
result.append(self[currentIndex..<nextSpace])
|
|
currentIndex = index(after: nextSpace)
|
|
} else {
|
|
result.append(self[currentIndex...])
|
|
break
|
|
}
|
|
}
|
|
|
|
return
|
|
result
|
|
.map {
|
|
$0.isEmpty ? $0 : String(repeating: " ", count: wrappingIndent) + $0
|
|
}
|
|
.joined(separator: "\n")
|
|
}
|
|
|
|
/// Returns this string prefixed using a camel-case style.
|
|
///
|
|
/// Example:
|
|
///
|
|
/// "hello".addingIntercappedPrefix("my")
|
|
/// // myHello
|
|
func addingIntercappedPrefix(_ prefix: String) -> String {
|
|
guard let firstChar = first else { return prefix }
|
|
return "\(prefix)\(firstChar.uppercased())\(self.dropFirst())"
|
|
}
|
|
|
|
/// Returns this string prefixed using kebab-, snake-, or camel-case style
|
|
/// depending on what can be detected from the string.
|
|
///
|
|
/// Examples:
|
|
///
|
|
/// "hello".addingPrefixWithAutodetectedStyle("my")
|
|
/// // my-hello
|
|
/// "hello_there".addingPrefixWithAutodetectedStyle("my")
|
|
/// // my_hello_there
|
|
/// "hello-there".addingPrefixWithAutodetectedStyle("my")
|
|
/// // my-hello-there
|
|
/// "helloThere".addingPrefixWithAutodetectedStyle("my")
|
|
/// // myHelloThere
|
|
func addingPrefixWithAutodetectedStyle(_ prefix: String) -> String {
|
|
if contains("-") {
|
|
return "\(prefix)-\(self)"
|
|
} else if contains("_") {
|
|
return "\(prefix)_\(self)"
|
|
} else if first?.isLowercase == true && contains(where: { $0.isUppercase })
|
|
{
|
|
return addingIntercappedPrefix(prefix)
|
|
} else {
|
|
return "\(prefix)-\(self)"
|
|
}
|
|
}
|
|
|
|
/// Returns a new string with the camel-case-based words of this string
|
|
/// split by the specified separator.
|
|
///
|
|
/// Examples:
|
|
///
|
|
/// "myProperty".convertedToSnakeCase()
|
|
/// // my_property
|
|
/// "myURLProperty".convertedToSnakeCase()
|
|
/// // my_url_property
|
|
/// "myURLProperty".convertedToSnakeCase(separator: "-")
|
|
/// // my-url-property
|
|
func convertedToSnakeCase(separator: Character = "_") -> String {
|
|
guard !isEmpty else { return "" }
|
|
var result = ""
|
|
// Whether we should append a separator when we see a uppercase character.
|
|
var separateOnUppercase = true
|
|
for index in indices {
|
|
let nextIndex = self.index(after: index)
|
|
let character = self[index]
|
|
if character.isUppercase {
|
|
if separateOnUppercase && !result.isEmpty {
|
|
// Append the separator.
|
|
result += "\(separator)"
|
|
}
|
|
// If the next character is uppercase and the next-next character is lowercase, like "L" in "URLSession", we should separate words.
|
|
separateOnUppercase =
|
|
nextIndex < endIndex && self[nextIndex].isUppercase
|
|
&& self.index(after: nextIndex) < endIndex
|
|
&& self[self.index(after: nextIndex)].isLowercase
|
|
} else {
|
|
// If the character is `separator`, we do not want to append another separator when we see the next uppercase character.
|
|
separateOnUppercase = character != separator
|
|
}
|
|
// Append the lowercased character.
|
|
result += character.lowercased()
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// Returns the edit distance between this string and the provided target string.
|
|
///
|
|
/// Uses the Levenshtein distance algorithm internally.
|
|
///
|
|
/// See: https://en.wikipedia.org/wiki/Levenshtein_distance
|
|
///
|
|
/// Examples:
|
|
///
|
|
/// "kitten".editDistance(to: "sitting")
|
|
/// // 3
|
|
/// "bar".editDistance(to: "baz")
|
|
/// // 1
|
|
func editDistance(to target: String) -> Int {
|
|
let rows = self.count
|
|
let columns = target.count
|
|
|
|
if rows <= 0 || columns <= 0 {
|
|
return Swift.max(rows, columns)
|
|
}
|
|
|
|
// Trim common prefix and suffix
|
|
var selfStartTrim = self.startIndex
|
|
var targetStartTrim = target.startIndex
|
|
while selfStartTrim < self.endIndex && targetStartTrim < target.endIndex
|
|
&& self[selfStartTrim] == target[targetStartTrim]
|
|
{
|
|
self.formIndex(after: &selfStartTrim)
|
|
target.formIndex(after: &targetStartTrim)
|
|
}
|
|
|
|
var selfEndTrim = self.endIndex
|
|
var targetEndTrim = target.endIndex
|
|
|
|
while selfEndTrim > selfStartTrim && targetEndTrim > targetStartTrim {
|
|
let selfIdx = self.index(before: selfEndTrim)
|
|
let targetIdx = target.index(before: targetEndTrim)
|
|
|
|
guard self[selfIdx] == target[targetIdx] else {
|
|
break
|
|
}
|
|
|
|
selfEndTrim = selfIdx
|
|
targetEndTrim = targetIdx
|
|
}
|
|
|
|
// Equal strings
|
|
guard
|
|
!(selfStartTrim == self.endIndex && targetStartTrim == target.endIndex)
|
|
else {
|
|
return 0
|
|
}
|
|
|
|
// After trimming common prefix and suffix, self is empty.
|
|
guard selfStartTrim < selfEndTrim else {
|
|
return target.distance(
|
|
from: targetStartTrim,
|
|
to: targetEndTrim)
|
|
}
|
|
|
|
// After trimming common prefix and suffix, target is empty.
|
|
guard targetStartTrim < targetEndTrim else {
|
|
return distance(
|
|
from: selfStartTrim,
|
|
to: selfEndTrim)
|
|
}
|
|
|
|
let newSelf = self[selfStartTrim..<selfEndTrim]
|
|
let newTarget = target[targetStartTrim..<targetEndTrim]
|
|
|
|
let m = newSelf.count
|
|
let n = newTarget.count
|
|
|
|
// Initialize the levenshtein matrix with only two rows
|
|
// current and previous.
|
|
var previousRow = [Int](repeating: 0, count: n + 1)
|
|
var currentRow = [Int](0...n)
|
|
|
|
var sourceIdx = newSelf.startIndex
|
|
for i in 1...m {
|
|
swap(&previousRow, ¤tRow)
|
|
currentRow[0] = i
|
|
|
|
var targetIdx = newTarget.startIndex
|
|
for j in 1...n {
|
|
// If characteres are equal for the levenshtein algorithm the
|
|
// minimum will always be the substitution cost, so we can fast
|
|
// path here in order to avoid min calls.
|
|
if newSelf[sourceIdx] == newTarget[targetIdx] {
|
|
currentRow[j] = previousRow[j - 1]
|
|
} else {
|
|
let deletion = previousRow[j]
|
|
let insertion = currentRow[j - 1]
|
|
let substitution = previousRow[j - 1]
|
|
currentRow[j] =
|
|
Swift.min(deletion, Swift.min(insertion, substitution)) + 1
|
|
}
|
|
// j += 1
|
|
newTarget.formIndex(after: &targetIdx)
|
|
}
|
|
// i += 1
|
|
newSelf.formIndex(after: &sourceIdx)
|
|
}
|
|
return currentRow[n]
|
|
}
|
|
|
|
func indentingEachLine(by n: Int) -> String {
|
|
let lines = self.split(separator: "\n", omittingEmptySubsequences: false)
|
|
let spacer = String(repeating: " ", count: n)
|
|
return lines.map {
|
|
$0.isEmpty ? $0 : spacer + $0
|
|
}.joined(separator: "\n")
|
|
}
|
|
|
|
func hangingIndentingEachLine(by n: Int) -> String {
|
|
let lines = self.split(
|
|
separator: "\n",
|
|
maxSplits: 1,
|
|
omittingEmptySubsequences: false)
|
|
guard lines.count == 2 else { return lines.joined(separator: "") }
|
|
return "\(lines[0])\n\(lines[1].indentingEachLine(by: n))"
|
|
}
|
|
|
|
var nonEmpty: Self? {
|
|
isEmpty ? nil : self
|
|
}
|
|
}
|
|
|
|
extension Character {
|
|
/// Returns a Boolean value indicating whether this character is valid for the
|
|
/// command-line name of an option or flag.
|
|
///
|
|
/// Only ASCII letters, numbers, dashes, and the underscore are valid name
|
|
/// characters.
|
|
var isValidForName: Bool {
|
|
guard isASCII, let firstScalar = unicodeScalars.first else { return false }
|
|
switch firstScalar.value {
|
|
case 0x41...0x5A, // uppercase
|
|
0x61...0x7A, // lowercase
|
|
0x30...0x39, // numbers
|
|
0x5F, // underscore
|
|
0x2D: // dash
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|