Files
T
Danny Canter 2b962934d1 Add aliases support for sub commands (#627)
This adds support for aliases for subcommands via a new parameter to
CommandConfigurations constructors. The aliases are passed as an array
of strings, where the default is just an empty array that signifies there
are no aliases. The aliases are supported regardless of if a different
commandName is chosen or not. This also updates how subcommands show up
in the help text. Any aliases are now displayed to the right of the original
command.

In addition to the functionality itself, this change:

1. Updates some of the EndToEnd parsing tests to make sure they function
while using aliases.
2. Sprinkles mentions where I saw fit in the documentation.
3. Updates the Math example to have aliases for `math stats average`
(`math stats avg`), and `math multiply` (`math mul`).

`math`'s help text now looks like the below:

```
~ math --help
OVERVIEW: A utility for performing maths.

USAGE: math <subcommand>

OPTIONS:
  --version               Show the version.
  -h, --help              Show help information.

SUBCOMMANDS:
  add (default)           Print the sum of the values.
  multiply, mul           Print the product of the values.
  stats                   Calculate descriptive statistics.

  See 'math help <subcommand>' for detailed help.

~ math stats --help
OVERVIEW: Calculate descriptive statistics.

USAGE: math stats <subcommand>

OPTIONS:
  --version               Show the version.
  -h, --help              Show help information.

SUBCOMMANDS:
  average, avg            Print the average of the values.
  stdev                   Print the standard deviation of the values.
  quantiles               Print the quantiles of the values (TBD).

  See 'math help stats <subcommand>' for detailed help.
```

and use of the aliases:

```
~ math mul 10 10
100

~ math stats avg 10 20
15.0
```

This change does NOT add any updates to the shell completion logic for
this feature.

Fixes #248
2024-04-30 18:20:03 -05:00

248 lines
8.0 KiB
Swift

//===----------------------------------------------------------*- 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
//
//===----------------------------------------------------------------------===//
import ArgumentParser
@main
struct Math: ParsableCommand {
// Customize your command's help and subcommands by implementing the
// `configuration` property.
static let configuration = CommandConfiguration(
// Optional abstracts and discussions are used for help output.
abstract: "A utility for performing maths.",
// Commands can define a version for automatic '--version' support.
version: "1.0.0",
// Pass an array to `subcommands` to set up a nested tree of subcommands.
// With language support for type-level introspection, this could be
// provided by automatically finding nested `ParsableCommand` types.
subcommands: [Add.self, Multiply.self, Statistics.self],
// A default subcommand, when provided, is automatically selected if a
// subcommand is not given on the command line.
defaultSubcommand: Add.self)
}
struct Options: ParsableArguments {
@Flag(name: [.customLong("hex-output"), .customShort("x")],
help: "Use hexadecimal notation for the result.")
var hexadecimalOutput = false
@Argument(
help: "A group of integers to operate on.")
var values: [Int] = []
}
extension Math {
static func format(_ result: Int, usingHex: Bool) -> String {
usingHex ? String(result, radix: 16)
: String(result)
}
struct Add: ParsableCommand {
static let configuration =
CommandConfiguration(abstract: "Print the sum of the values.")
// The `@OptionGroup` attribute includes the flags, options, and
// arguments defined by another `ParsableArguments` type.
@OptionGroup var options: Options
mutating func run() {
let result = options.values.reduce(0, +)
print(format(result, usingHex: options.hexadecimalOutput))
}
}
struct Multiply: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Print the product of the values.",
aliases: ["mul"])
@OptionGroup var options: Options
mutating func run() {
let result = options.values.reduce(1, *)
print(format(result, usingHex: options.hexadecimalOutput))
}
}
}
// In practice, these nested types could be broken out into different files.
extension Math {
struct Statistics: ParsableCommand {
static let configuration = CommandConfiguration(
// Command names are automatically generated from the type name
// by default; you can specify an override here.
commandName: "stats",
abstract: "Calculate descriptive statistics.",
subcommands: [Average.self, StandardDeviation.self, Quantiles.self])
}
}
extension Math.Statistics {
struct Average: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Print the average of the values.",
version: "1.5.0-alpha",
aliases: ["avg"])
enum Kind: String, ExpressibleByArgument, CaseIterable {
case mean, median, mode
}
@Option(help: "The kind of average to provide.")
var kind: Kind = .mean
@Argument(help: "A group of floating-point values to operate on.")
var values: [Double] = []
func validate() throws {
if (kind == .median || kind == .mode) && values.isEmpty {
throw ValidationError("Please provide at least one value to calculate the \(kind).")
}
}
func calculateMean() -> Double {
guard !values.isEmpty else {
return 0
}
let sum = values.reduce(0, +)
return sum / Double(values.count)
}
func calculateMedian() -> Double {
guard !values.isEmpty else {
return 0
}
let sorted = values.sorted()
let mid = sorted.count / 2
if sorted.count.isMultiple(of: 2) {
return (sorted[mid - 1] + sorted[mid]) / 2
} else {
return sorted[mid]
}
}
func calculateMode() -> [Double] {
guard !values.isEmpty else {
return []
}
let grouped = Dictionary(grouping: values, by: { $0 })
let highestFrequency = grouped.lazy.map { $0.value.count }.max()!
return grouped.filter { _, v in v.count == highestFrequency }
.map { k, _ in k }
}
mutating func run() {
switch kind {
case .mean:
print(calculateMean())
case .median:
print(calculateMedian())
case .mode:
let result = calculateMode()
.map(String.init(describing:))
.joined(separator: " ")
print(result)
}
}
}
struct StandardDeviation: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "stdev",
abstract: "Print the standard deviation of the values.")
@Argument(help: "A group of floating-point values to operate on.")
var values: [Double] = []
mutating func run() {
if values.isEmpty {
print(0.0)
} else {
let sum = values.reduce(0, +)
let mean = sum / Double(values.count)
let squaredErrors = values
.map { $0 - mean }
.map { $0 * $0 }
let variance = squaredErrors.reduce(0, +) / Double(values.count)
let result = variance.squareRoot()
print(result)
}
}
}
struct Quantiles: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Print the quantiles of the values (TBD).")
@Argument(completion: .list(["alphabet", "alligator", "branch", "braggart"]))
var oneOfFour: String?
@Argument(completion: .custom { _ in ["alabaster", "breakfast", "crunch", "crash"] })
var customArg: String?
@Argument(help: "A group of floating-point values to operate on.")
var values: [Double] = []
// These args and the validation method are for testing exit codes:
@Flag(help: .hidden)
var testSuccessExitCode = false
@Flag(help: .hidden)
var testFailureExitCode = false
@Flag(help: .hidden)
var testValidationExitCode = false
@Option(help: .hidden)
var testCustomExitCode: Int32?
// These args are for testing custom completion scripts:
@Option(completion: .file(extensions: ["txt", "md"]))
var file: String?
@Option(completion: .directory)
var directory: String?
@Option(completion: .shellCommand("head -100 /usr/share/dict/words | tail -50"))
var shell: String?
@Option(completion: .custom(customCompletion))
var custom: String?
func validate() throws {
if testSuccessExitCode {
throw ExitCode.success
}
if testFailureExitCode {
throw ExitCode.failure
}
if testValidationExitCode {
throw ExitCode.validationFailure
}
if let exitCode = testCustomExitCode {
throw ExitCode(exitCode)
}
}
}
}
func customCompletion(_ s: [String]) -> [String] {
return (s.last ?? "").starts(with: "a")
? ["aardvark", "aaaaalbert"]
: ["hello", "helicopter", "heliotrope"]
}