Merge pull request #175 from alexflint/bracketing-positionals

Fix bracketing for non-required positionals in usage
This commit is contained in:
Alex Flint 2022-02-09 18:04:03 -08:00 committed by GitHub
commit f0f44b65d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 96 additions and 20 deletions

View File

@ -154,7 +154,7 @@ func Example_helpText() {
os.Args = split("./example --help")
var args struct {
Input string `arg:"positional"`
Input string `arg:"positional,required"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`
@ -188,7 +188,7 @@ func Example_helpPlaceholder() {
os.Args = split("./example --help")
var args struct {
Input string `arg:"positional" placeholder:"SRC"`
Input string `arg:"positional,required" placeholder:"SRC"`
Output []string `arg:"positional" placeholder:"DST"`
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
@ -259,7 +259,7 @@ func Example_helpTextWhenUsingSubcommand() {
os.Args = split("./example get --help")
type getCmd struct {
Item string `arg:"positional" help:"item to fetch"`
Item string `arg:"positional,required" help:"item to fetch"`
}
type listCmd struct {
@ -389,7 +389,7 @@ func Example_errorText() {
os.Args = split("./example --optimize INVALID")
var args struct {
Input string `arg:"positional"`
Input string `arg:"positional,required"`
Output []string `arg:"positional"`
Verbose bool `arg:"-v" help:"verbosity level"`
Dataset string `help:"dataset to use"`

View File

@ -124,22 +124,32 @@ func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
}
}
// write the positional component of the usage message
// When we parse positionals, we check that:
// 1. required positionals come before non-required positionals
// 2. there is at most one multiple-value positional
// 3. if there is a multiple-value positional then it comes after all other positionals
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
// write the positionals in following form:
// REQUIRED1 REQUIRED2
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
var closeBrackets int
for _, spec := range positionals {
// prefix with a space
fmt.Fprint(w, " ")
if spec.cardinality == multiple {
if !spec.required {
fmt.Fprint(w, "[")
closeBrackets += 1
}
if spec.cardinality == multiple {
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
if !spec.required {
fmt.Fprint(w, "]")
}
} else {
fmt.Fprint(w, spec.placeholder)
}
}
fmt.Fprint(w, strings.Repeat("]", closeBrackets))
// if the program supports subcommands, give a hint to the user about their existence
if len(cmd.subcommands) > 0 {

View File

@ -59,7 +59,7 @@ Options:
`
var args struct {
Input string `arg:"positional"`
Input string `arg:"positional,required"`
Output []string `arg:"positional" help:"list of outputs"`
Name string `help:"name to use"`
Value int `help:"secret value"`
@ -141,10 +141,10 @@ func TestUsageCannotMarshalToString(t *testing.T) {
}
func TestUsageLongPositionalWithHelp_legacyForm(t *testing.T) {
expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
expectedHelp := `
Usage: example VERYLONGPOSITIONALWITHHELP
Usage: example [VERYLONGPOSITIONALWITHHELP]
Positional arguments:
VERYLONGPOSITIONALWITHHELP
@ -170,10 +170,10 @@ Options:
}
func TestUsageLongPositionalWithHelp_newForm(t *testing.T) {
expectedUsage := "Usage: example VERYLONGPOSITIONALWITHHELP"
expectedUsage := "Usage: example [VERYLONGPOSITIONALWITHHELP]"
expectedHelp := `
Usage: example VERYLONGPOSITIONALWITHHELP
Usage: example [VERYLONGPOSITIONALWITHHELP]
Positional arguments:
VERYLONGPOSITIONALWITHHELP
@ -285,8 +285,74 @@ Options:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
func TestUsageForRequiredPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageForMixedPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
Optional1 string `arg:"positional"`
Optional2 string `arg:"positional"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageForRepeatedPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
Repeated []string `arg:"positional,required"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageForMixedAndRepeatedPositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2 [REPEATED [REPEATED ...]]]]\n"
var args struct {
Required1 string `arg:"positional,required"`
Required2 string `arg:"positional,required"`
Optional1 string `arg:"positional"`
Optional2 string `arg:"positional"`
Repeated []string `arg:"positional"`
}
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, usage.String())
}
func TestRequiredMultiplePositionals(t *testing.T) {
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]"
expectedUsage := "Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]\n"
expectedHelp := `
Usage: example REQUIREDMULTIPLE [REQUIREDMULTIPLE ...]
@ -301,7 +367,7 @@ Options:
RequiredMultiple []string `arg:"positional,required" help:"required multiple positional"`
}
p, err := NewParser(Config{}, &args)
p, err := NewParser(Config{Program: "example"}, &args)
require.NoError(t, err)
var help bytes.Buffer
@ -310,7 +376,7 @@ Options:
var usage bytes.Buffer
p.WriteUsage(&usage)
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
assert.Equal(t, expectedUsage, usage.String())
}
func TestUsageWithNestedSubcommands(t *testing.T) {