Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Print supported values in synopses, when practical #620

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ You can build a program with commands and subcommands by defining multiple comma
% math stats
OVERVIEW: Calculate descriptive statistics.

USAGE: math stats <subcommand>
USAGE: math stats <average|stdev|quantiles>

OPTIONS:
-h, --help Show help information.
Expand Down Expand Up @@ -118,7 +118,7 @@ extension Math.Statistics {
static let configuration = CommandConfiguration(
abstract: "Print the average of the values.")

enum Kind: String, ExpressibleByArgument {
enum Kind: String, ExpressibleByArgument, CaseIterable {
case mean, median, mode
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ struct FruitStore: ParsableCommand {
The help screen includes the list of values in the description of the `<fruit>` argument:

```
USAGE: fruit-store <fruit> [--quantity <quantity>]
USAGE: fruit-store <apple|banana|coconut|dragon-fruit> [--quantity <quantity>]

ARGUMENTS:
<fruit> The fruit to purchase (values: apple, banana,
Expand Down Expand Up @@ -137,7 +137,7 @@ struct FruitStore: ParsableCommand {
The help screen still contains all the possible values.

```
USAGE: fruit-store <fruit> [--quantity <quantity>]
USAGE: fruit-store <apple|banana|coconut|dragon-fruit> [--quantity <quantity>]

ARGUMENTS:
<fruit> The fruit to purchase (values: apple, banana,
Expand Down
10 changes: 8 additions & 2 deletions Sources/ArgumentParser/Usage/HelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,15 @@ internal struct HelpGenerator {
} else {
var usage = UsageGenerator(toolName: toolName, definition: [currentArgSet])
.synopsis
if !currentCommand.configuration.subcommands.isEmpty {
let subcommands = currentCommand.configuration.subcommands.filter { $0.configuration.shouldDisplay }
if !subcommands.isEmpty {
if usage.last != " " { usage += " " }
usage += "<subcommand>"
let joinedSubcommands = subcommands.map { $0._commandName }.joined(separator: "|")
if subcommands.count > 1 && joinedSubcommands.count <= 40 {
usage += "<\(joinedSubcommands)>"
} else {
usage += "<subcommand>"
}
}
self.usage = usage
}
Expand Down
14 changes: 12 additions & 2 deletions Sources/ArgumentParser/Usage/UsageGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,22 @@ extension ArgumentDefinition {

switch update {
case .unary:
return "\(name.synopsisString) <\(valueName)>"
let joinedValues = help.allValueStrings.joined(separator: "|")
if help.allValueStrings.count > 1 && joinedValues.count <= 40 {
return "\(name.synopsisString) <\(joinedValues)>"
} else {
return "\(name.synopsisString) <\(valueName)>"
}
case .nullary:
return name.synopsisString
}
case .positional:
return "<\(valueName)>"
let joinedValues = help.allValueStrings.joined(separator: "|")
if help.allValueStrings.count > 1 && joinedValues.count <= 40 {
return "<\(joinedValues)>"
} else {
return "<\(valueName)>"
}
case .default:
return ""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ extension SubcommandEndToEndTests {
let helpB = Foo.message(for: CleanExit.helpRequest(CommandB.self))

AssertEqualStrings(actual: helpFoo, expected: """
USAGE: foo --name <name> <subcommand>
USAGE: foo --name <name> <a|b>

OPTIONS:
--name <name>
Expand Down
28 changes: 25 additions & 3 deletions Tests/ArgumentParserExampleTests/MathExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class MathExampleTests: XCTestCase {
let helpText = """
OVERVIEW: A utility for performing maths.

USAGE: math <subcommand>
USAGE: math <add|multiply|stats>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise, it seems like this loses the context that add/multiply/stats are subcommands.


OPTIONS:
--version Show the version.
Expand Down Expand Up @@ -73,11 +73,33 @@ final class MathExampleTests: XCTestCase {
try AssertExecuteCommand(command: "math help add --help", expected: helpText)
}

func testMath_StatsHelp() throws {
let helpText = """
OVERVIEW: Calculate descriptive statistics.

USAGE: math stats <average|stdev|quantiles>

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

SUBCOMMANDS:
average 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.
"""
try AssertExecuteCommand(command: "math stats -h", expected: helpText)
try AssertExecuteCommand(command: "math stats --help", expected: helpText)
try AssertExecuteCommand(command: "math help stats", expected: helpText)
}

func testMath_StatsMeanHelp() throws {
let helpText = """
OVERVIEW: Print the average of the values.

USAGE: math stats average [--kind <kind>] [<values> ...]
USAGE: math stats average [--kind <mean|median|mode>] [<values> ...]

ARGUMENTS:
<values> A group of floating-point values to operate on.
Expand Down Expand Up @@ -128,7 +150,7 @@ final class MathExampleTests: XCTestCase {
command: "math stats average --kind mode",
expected: """
Error: Please provide at least one value to calculate the mode.
Usage: math stats average [--kind <kind>] [<values> ...]
Usage: math stats average [--kind <mean|median|mode>] [<values> ...]
See 'math stats average --help' for more information.
""",
exitCode: .validationFailure)
Expand Down
6 changes: 3 additions & 3 deletions Tests/ArgumentParserPackageManagerTests/HelpTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ extension HelpTests {
XCTAssertEqual(
getErrorText(Package.self, ["help"]).trimmingLines(),
"""
USAGE: package <subcommand>
USAGE: package <clean|config|describe|generate-xcodeproj>

OPTIONS:
-h, --help Show help information.
Expand All @@ -69,7 +69,7 @@ extension HelpTests {
XCTAssertEqual(
Package.message(for: CleanExit.helpRequest()).trimmingLines(),
"""
USAGE: package <subcommand>
USAGE: package <clean|config|describe|generate-xcodeproj>

OPTIONS:
-h, --help Show help information.
Expand Down Expand Up @@ -97,7 +97,7 @@ extension HelpTests {
XCTAssertEqual(
getErrorText(Package.self, ["help", "config"], screenWidth: 80).trimmingLines(),
"""
USAGE: package config <subcommand>
USAGE: package config <get-mirror|set-mirror|unset-mirror>

OPTIONS:
-h, --help Show help information.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------*- swift -*-===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2024 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 XCTest
import ArgumentParserTestHelpers
@testable import ArgumentParser

extension HelpGenerationTests {
enum Fruit: String, ExpressibleByArgument, CaseIterable {
case apple, banana, coconut, dragonFruit = "dragon-fruit", elderberry, fig, grape, honeydew
}

enum Action: String, ExpressibleByArgument, CaseIterable {
case purchase, sample, refund = "return"
}

enum Count: Int, ExpressibleByArgument, CaseIterable {
case zero, one, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve, thirteen, fourteen, fifteen, sixteen, seventeen, eighteen, nineteen, twenty
}

enum Ripeness: String, ExpressibleByArgument, CaseIterable {
case under, perfect, over
}

struct FruitStore: ParsableArguments {
@Argument(help: "The transaction type")
var action: Action = .purchase

@Argument(help: "The fruit to purchase")
var fruit: Fruit

@Option(help: "The number of fruit to purchase")
var quantity: Count = .one

@Option(help: "The desired ripeness of fruit")
var ripeness: Ripeness = .perfect
}

func testFruitStoreHelp() {
AssertHelp(.default, for: FruitStore.self, equals: """
USAGE: fruit_store [<purchase|sample|return>] <fruit> [--quantity <quantity>] [--ripeness <under|perfect|over>]

ARGUMENTS:
<action> The transaction type (values: purchase, sample,
return; default: purchase)
<fruit> The fruit to purchase (values: apple, banana,
coconut, dragon-fruit, elderberry, fig, grape,
honeydew)
Comment on lines +49 to +56
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like replacing <action> with its values makes it harder to see the correspondence between the usage string and the help descriptions. Are there existing tools that include values in the usage string like this? What would you think of including the argument name – something like:

USAGE: fruit_store [<action: purchase|sample|return>] <fruit> ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm certain that I've used a cli before that had this feature, but over the past two weeks I could not remember what it was.

It did have the issue you describe though, and I like your solution! Another possibility is that ArgumentParser only expand values when showing a short help (such as gets printed after a ValidationError) and leaving the behaviour as-is when printing the complete --help output.

Copy link
Member Author

@numist numist Mar 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh the tool I'm thinking of is something I use frequently at work. Instead of an expanded --help, it explains multiple subcommands at once and then refers to a man page. To paraphrase:

> foo bar --help
Usage:
foo alice      --baz <baz_id> [--[no-]qux] [--[no-]thbpt] [--fix]
foo bar        --baz <baz_id> [--[no-]qux] [--blarpy <blarpy>] [--minimal | --details] [--[no-]box] [--[no-]link] [--eh|--bee|--see] (--regex|--exact|--contains|--suffix) <search_term>
…

foo sub-commands and options are documented in the manual page (`man foo`).

I was thinking ArgumentParser's top level help for commands with subcommands is a bit spartan, I wonder if this tool is on to something…


OPTIONS:
--quantity <quantity> The number of fruit to purchase (values: 0, 1, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 20; default: 1)
--ripeness <ripeness> The desired ripeness of fruit (values: under,
perfect, over; default: perfect)
-h, --help Show help information.

""")
}
}
2 changes: 1 addition & 1 deletion Tests/ArgumentParserUnitTests/HelpGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ extension HelpGenerationTests {

func testHelpWithDefaultValues() {
AssertHelp(.default, for: D.self, equals: """
USAGE: d [<occupation>] [--name <name>] [--age <age>] [--logging <logging>] [--lucky <numbers> ...] [--optional] [--required] [--degree <degree>] [--directory <directory>] [--manual <manual>] [--unspecial <unspecial>] [--special <special>]
USAGE: d [<occupation>] [--name <name>] [--age <age>] [--logging <logging>] [--lucky <numbers> ...] [--optional] [--required] [--degree <degree>] [--directory <directory>] [--manual <manual>] [--unspecial <0|1>] [--special <Apple|Banana>]

ARGUMENTS:
<occupation> Your occupation. (default: --)
Expand Down