UsageGenerator.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. //===----------------------------------------------------------------------===//
  2. //
  3. // This source file is part of the Swift Argument Parser open source project
  4. //
  5. // Copyright (c) 2020 Apple Inc. and the Swift project authors
  6. // Licensed under Apache License v2.0 with Runtime Library Exception
  7. //
  8. // See https://swift.org/LICENSE.txt for license information
  9. //
  10. //===----------------------------------------------------------------------===//
  11. struct UsageGenerator {
  12. var toolName: String
  13. var definition: ArgumentSet
  14. }
  15. extension UsageGenerator {
  16. init(definition: ArgumentSet) {
  17. let toolName =
  18. CommandLine._staticArguments[0]
  19. .split(separator: "/").last.map(String.init) ?? "<command>"
  20. self.init(toolName: toolName, definition: definition)
  21. }
  22. init(
  23. toolName: String, parsable: ParsableArguments,
  24. visibility: ArgumentVisibility, parent: InputKey?
  25. ) {
  26. self.init(
  27. toolName: toolName,
  28. definition: ArgumentSet(
  29. type(of: parsable), visibility: visibility, parent: parent))
  30. }
  31. init(toolName: String, definition: [ArgumentSet]) {
  32. self.init(toolName: toolName, definition: ArgumentSet(sets: definition))
  33. }
  34. }
  35. extension UsageGenerator {
  36. /// The tool synopsis.
  37. ///
  38. /// In `roff`.
  39. var synopsis: String {
  40. var options = Array(definition)
  41. switch options.count {
  42. case 0:
  43. return toolName
  44. case let x where x > 12:
  45. // When we have too many options, keep required and positional arguments,
  46. // but discard the rest.
  47. options = options.filter {
  48. $0.isPositional || !$0.help.options.contains(.isOptional)
  49. }
  50. // If there are between 1 and 12 options left, print them, otherwise print
  51. // a simplified usage string.
  52. if !options.isEmpty, options.count <= 12 {
  53. let synopsis =
  54. options
  55. .map { $0.synopsis }
  56. .joined(separator: " ")
  57. return "\(toolName) [<options>] \(synopsis)"
  58. }
  59. return "\(toolName) <options>"
  60. default:
  61. let synopsis =
  62. options
  63. .map { $0.synopsis }
  64. .joined(separator: " ")
  65. return "\(toolName) \(synopsis)"
  66. }
  67. }
  68. }
  69. extension ArgumentDefinition {
  70. var synopsisForHelp: String {
  71. switch kind {
  72. case .named:
  73. let joinedSynopsisString = names
  74. .partitioned
  75. .map { $0.synopsisString }
  76. .joined(separator: ", ")
  77. switch update {
  78. case .unary:
  79. return "\(joinedSynopsisString) <\(valueName)>"
  80. case .nullary:
  81. return joinedSynopsisString
  82. }
  83. case .positional:
  84. return "<\(valueName)>"
  85. case .default:
  86. return ""
  87. }
  88. }
  89. var unadornedSynopsis: String {
  90. switch kind {
  91. case .named:
  92. guard let name = names.preferredName else {
  93. fatalError("preferredName cannot be nil for named arguments")
  94. }
  95. switch update {
  96. case .unary:
  97. return "\(name.synopsisString) <\(valueName)>"
  98. case .nullary:
  99. return name.synopsisString
  100. }
  101. case .positional:
  102. return "<\(valueName)>"
  103. case .default:
  104. return ""
  105. }
  106. }
  107. var synopsis: String {
  108. var synopsis = unadornedSynopsis
  109. if help.options.contains(.isRepeating) {
  110. synopsis += " ..."
  111. }
  112. if help.options.contains(.isOptional) {
  113. synopsis = "[\(synopsis)]"
  114. }
  115. if parsingStrategy == .postTerminator {
  116. synopsis = "-- \(synopsis)"
  117. }
  118. return synopsis
  119. }
  120. }
  121. extension ArgumentSet {
  122. /// Will generate a descriptive help message if possible.
  123. ///
  124. /// If no descriptive help message can be generated, `nil` will be returned.
  125. ///
  126. /// - Parameter error: the parse error that occurred.
  127. /// - Returns: An error description.
  128. func errorDescription(error: Swift.Error) -> String? {
  129. switch error {
  130. case let parserError as ParserError:
  131. return ErrorMessageGenerator(arguments: self, error: parserError)
  132. .makeErrorMessage()
  133. case let commandError as CommandError:
  134. return ErrorMessageGenerator(
  135. arguments: self, error: commandError.parserError
  136. )
  137. .makeErrorMessage()
  138. default:
  139. return nil
  140. }
  141. }
  142. func helpDescription(error: Swift.Error) -> String? {
  143. switch error {
  144. case let parserError as ParserError:
  145. return ErrorMessageGenerator(arguments: self, error: parserError)
  146. .makeHelpMessage()
  147. case let commandError as CommandError:
  148. return ErrorMessageGenerator(
  149. arguments: self, error: commandError.parserError
  150. )
  151. .makeHelpMessage()
  152. default:
  153. return nil
  154. }
  155. }
  156. }
  157. struct ErrorMessageGenerator {
  158. var arguments: ArgumentSet
  159. var error: ParserError
  160. }
  161. extension ErrorMessageGenerator {
  162. func makeErrorMessage() -> String? {
  163. switch error {
  164. case .helpRequested, .versionRequested, .completionScriptRequested,
  165. .completionScriptCustomResponse, .dumpHelpRequested:
  166. return nil
  167. case .unsupportedShell(let shell?):
  168. return unsupportedShell(shell)
  169. case .unsupportedShell:
  170. return unsupportedAutodetectedShell
  171. case .notImplemented:
  172. return notImplementedMessage
  173. case .invalidState:
  174. return invalidState
  175. case .unknownOption(let o, let n):
  176. return unknownOptionMessage(origin: o, name: n)
  177. case .missingValueForOption(let o, let n):
  178. return missingValueForOptionMessage(origin: o, name: n)
  179. case .missingValueOrUnknownCompositeOption(
  180. let o, let shortName, let compositeName):
  181. return missingValueOrUnknownCompositeOptionMessage(
  182. origin: o, shortName: shortName, compositeName: compositeName)
  183. case .unexpectedValueForOption(let o, let n, let v):
  184. return unexpectedValueForOptionMessage(origin: o, name: n, value: v)
  185. case .unexpectedExtraValues(let v):
  186. return unexpectedExtraValuesMessage(values: v)
  187. case .duplicateExclusiveValues(
  188. let previous, let duplicate, originalInput: let arguments):
  189. return duplicateExclusiveValues(
  190. previous: previous, duplicate: duplicate, arguments: arguments)
  191. case .noValue(forKey: let k):
  192. return noValueMessage(key: k)
  193. case .unableToParseValue(
  194. let o, let n, let v, forKey: let k, originalError: let e):
  195. return unableToParseValueMessage(
  196. origin: o, name: n, value: v, key: k, error: e)
  197. case .invalidOption(let str):
  198. return "Invalid option: \(str)"
  199. case .nonAlphanumericShortOption(let c):
  200. return "Invalid option: -\(c)"
  201. case .missingSubcommand:
  202. return "Missing required subcommand."
  203. case .userValidationError(let error):
  204. return error.describe()
  205. case .noArguments(let error):
  206. switch error {
  207. case let error as ParserError:
  208. return ErrorMessageGenerator(arguments: self.arguments, error: error)
  209. .makeErrorMessage()
  210. default:
  211. return error.describe()
  212. }
  213. case .notParentCommand(let parent):
  214. return "Command '\(parent)' is not a parent of the current command."
  215. }
  216. }
  217. func makeHelpMessage() -> String? {
  218. switch error {
  219. case .unableToParseValue(
  220. let o, let n, let v, forKey: let k, originalError: let e):
  221. return unableToParseHelpMessage(
  222. origin: o, name: n, value: v, key: k, error: e)
  223. case .missingValueForOption(_, let n):
  224. return missingValueForOptionHelpMessage(name: n)
  225. case .noValue(let k):
  226. return noValueHelpMessage(key: k)
  227. default:
  228. return nil
  229. }
  230. }
  231. }
  232. extension ErrorMessageGenerator {
  233. func arguments(for key: InputKey) -> [ArgumentDefinition] {
  234. arguments
  235. .filter { $0.help.keys.contains(key) }
  236. }
  237. func help(for key: InputKey) -> ArgumentDefinition.Help? {
  238. arguments
  239. .first { $0.help.keys.contains(key) }
  240. .map { $0.help }
  241. }
  242. func valueName(for name: Name) -> String? {
  243. arguments
  244. .first { $0.names.contains(name) }
  245. .map { $0.valueName }
  246. }
  247. }
  248. extension ErrorMessageGenerator {
  249. var notImplementedMessage: String {
  250. "Internal error. Parsing command-line arguments hit unimplemented code path."
  251. }
  252. var invalidState: String {
  253. "Internal error. Invalid state while parsing command-line arguments."
  254. }
  255. var unsupportedAutodetectedShell: String {
  256. """
  257. Can't autodetect a supported shell.
  258. Please use --generate-completion-script=<shell> with one of:
  259. \(CompletionShell.allCases.map { $0.rawValue }.joined(separator: " "))
  260. """
  261. }
  262. func unsupportedShell(_ shell: String) -> String {
  263. """
  264. Can't generate completion scripts for '\(shell)'.
  265. Please use --generate-completion-script=<shell> with one of:
  266. \(CompletionShell.allCases.map { $0.rawValue }.joined(separator: " "))
  267. """
  268. }
  269. func unknownOptionMessage(origin: InputOrigin.Element, name: Name) -> String {
  270. if case .short = name {
  271. return "Unknown option '\(name.synopsisString)'"
  272. }
  273. // An empirically derived magic number
  274. let kSimilarityFloor = 4
  275. let notShort: (Name) -> Bool = { (name: Name) in
  276. switch name {
  277. case .short: return false
  278. case .long: return true
  279. case .longWithSingleDash: return true
  280. }
  281. }
  282. let suggestion =
  283. arguments
  284. .flatMap({ $0.names })
  285. .filter({
  286. $0.synopsisString.editDistance(to: name.synopsisString)
  287. < kSimilarityFloor
  288. }) // only include close enough suggestion
  289. .filter(notShort) // exclude short option suggestions
  290. .min(by: { lhs, rhs in // find the suggestion closest to the argument
  291. lhs.synopsisString.editDistance(to: name.synopsisString)
  292. < rhs.synopsisString.editDistance(to: name.synopsisString)
  293. })
  294. if let suggestion = suggestion {
  295. return
  296. "Unknown option '\(name.synopsisString)'. Did you mean '\(suggestion.synopsisString)'?"
  297. }
  298. return "Unknown option '\(name.synopsisString)'"
  299. }
  300. func missingValueForOptionMessage(origin: InputOrigin, name: Name) -> String {
  301. if let valueName = valueName(for: name) {
  302. return "Missing value for '\(name.synopsisString) <\(valueName)>'"
  303. } else {
  304. return "Missing value for '\(name.synopsisString)'"
  305. }
  306. }
  307. func missingValueOrUnknownCompositeOptionMessage(
  308. origin: InputOrigin,
  309. shortName: Name,
  310. compositeName: Name
  311. ) -> String {
  312. let unknownOptionMessage = unknownOptionMessage(
  313. origin: origin.firstElement,
  314. name: compositeName)
  315. let missingValueMessage = missingValueForOptionMessage(
  316. origin: origin,
  317. name: shortName)
  318. return """
  319. \(unknownOptionMessage)
  320. or: \(missingValueMessage) in '\(compositeName.synopsisString)'
  321. """
  322. }
  323. func unexpectedValueForOptionMessage(
  324. origin: InputOrigin.Element, name: Name, value: String
  325. ) -> String? {
  326. "The option '\(name.synopsisString)' does not take any value, but '\(value)' was specified."
  327. }
  328. func unexpectedExtraValuesMessage(values: [(InputOrigin, String)]) -> String?
  329. {
  330. switch values.count {
  331. case 0:
  332. return nil
  333. case 1:
  334. // swift-format-ignore: NeverForceUnwrap
  335. // We know that `values` is not empty.
  336. return "Unexpected argument '\(values.first!.1)'"
  337. default:
  338. let v = values.map { $0.1 }.joined(separator: "', '")
  339. return "\(values.count) unexpected arguments: '\(v)'"
  340. }
  341. }
  342. func duplicateExclusiveValues(
  343. previous: InputOrigin, duplicate: InputOrigin, arguments: [String]
  344. ) -> String? {
  345. func elementString(_ origin: InputOrigin, _ arguments: [String]) -> String?
  346. {
  347. guard case .argumentIndex(let split) = origin.elements.first else {
  348. return nil
  349. }
  350. var argument = "\'\(arguments[split.inputIndex.rawValue])\'"
  351. if case .sub(let offsetIndex) = split.subIndex {
  352. let stringIndex = argument.index(
  353. argument.startIndex, offsetBy: offsetIndex + 2)
  354. argument = "\'\(argument[stringIndex])\' in \(argument)"
  355. }
  356. return "flag \(argument)"
  357. }
  358. // Note that the RHS of these coalescing operators cannot be reached at this time.
  359. let dupeString =
  360. elementString(duplicate, arguments) ?? "position \(duplicate)"
  361. let origString =
  362. elementString(previous, arguments) ?? "position \(previous)"
  363. //TODO: review this message once environment values are supported.
  364. return
  365. "Value to be set with \(dupeString) had already been set with \(origString)"
  366. }
  367. func noValueMessage(key: InputKey) -> String? {
  368. let args = arguments(for: key)
  369. let possibilities: [String] = args.compactMap {
  370. $0.help.visibility.base == .default
  371. ? $0.nonOptional.synopsis
  372. : nil
  373. }
  374. switch possibilities.count {
  375. case 0:
  376. return
  377. "No value set for non-argument var \(key). Replace with a static variable, or let constant."
  378. case 1:
  379. // swift-format-ignore: NeverForceUnwrap
  380. // We know that `possibilities` is not empty.
  381. return "Missing expected argument '\(possibilities.first!)'"
  382. default:
  383. let p = possibilities.joined(separator: "', '")
  384. return "Missing one of: '\(p)'"
  385. }
  386. }
  387. func unableToParseHelpMessage(
  388. origin: InputOrigin, name: Name?, value: String, key: InputKey,
  389. error: Error?
  390. ) -> String {
  391. guard let abstract = help(for: key)?.abstract else { return "" }
  392. let valueName = arguments(for: key).first?.valueName
  393. switch (name, valueName) {
  394. case (let n?, let v?):
  395. return "\(n.synopsisString) <\(v)> \(abstract)"
  396. case (_, let v?):
  397. return "<\(v)> \(abstract)"
  398. case (_, _):
  399. return ""
  400. }
  401. }
  402. func missingValueForOptionHelpMessage(name: Name) -> String {
  403. guard let arg = arguments.first(where: { $0.names.contains(name) }) else {
  404. return ""
  405. }
  406. let help = arg.help.abstract
  407. return "\(name.synopsisString) <\(arg.valueName)> \(help)"
  408. }
  409. func noValueHelpMessage(key: InputKey) -> String {
  410. guard let abstract = help(for: key)?.abstract else { return "" }
  411. guard let arg = arguments(for: key).first else { return "" }
  412. if let synopsisString = arg.names.first?.synopsisString {
  413. return "\(synopsisString) <\(arg.valueName)> \(abstract)"
  414. }
  415. return "<\(arg.valueName)> \(abstract)"
  416. }
  417. func unableToParseValueMessage(
  418. origin: InputOrigin, name: Name?, value: String, key: InputKey,
  419. error: Error?
  420. ) -> String {
  421. let argumentValue = arguments(for: key).first
  422. let valueName = argumentValue?.valueName
  423. // We want to make the "best effort" in producing a custom error message.
  424. // We favor `LocalizedError.errorDescription` and fall back to
  425. // `CustomStringConvertible`. To opt in, return your custom error message
  426. // as the `description` property of `CustomStringConvertible`.
  427. let customErrorMessage: String
  428. switch error {
  429. case .some(let error):
  430. customErrorMessage = ": " + error.describe()
  431. case .none:
  432. customErrorMessage = argumentValue?.formattedValueList ?? ""
  433. }
  434. switch (name, valueName) {
  435. case (let n?, let v?):
  436. return
  437. "The value '\(value)' is invalid for '\(n.synopsisString) <\(v)>'\(customErrorMessage)"
  438. case (_, let v?):
  439. return "The value '\(value)' is invalid for '<\(v)>'\(customErrorMessage)"
  440. case (let n?, _):
  441. return
  442. "The value '\(value)' is invalid for '\(n.synopsisString)'\(customErrorMessage)"
  443. case (nil, nil):
  444. return "The value '\(value)' is invalid.\(customErrorMessage)"
  445. }
  446. }
  447. }
  448. extension ArgumentDefinition {
  449. fileprivate var formattedValueList: String {
  450. if help.allValueStrings.isEmpty {
  451. return ""
  452. }
  453. if help.allValueStrings.count < 6 {
  454. let quotedValues = help.allValueStrings.map { "'\($0)'" }
  455. let validList: String
  456. if quotedValues.count <= 2 {
  457. validList = quotedValues.joined(separator: " and ")
  458. } else {
  459. // swift-format-ignore: NeverForceUnwrap
  460. // We know that `quotedValues` is not empty.
  461. validList =
  462. quotedValues.dropLast().joined(separator: ", ")
  463. + " or \(quotedValues.last!)"
  464. }
  465. return ". Please provide one of \(validList)."
  466. } else {
  467. let bulletValueList = help.allValueStrings.map { " - \($0)" }.joined(
  468. separator: "\n")
  469. return ". Please provide one of the following:\n\(bulletValueList)"
  470. }
  471. }
  472. }