From a8c26c03e6a7af080e5ce0063b4ad45ed21f0091 Mon Sep 17 00:00:00 2001 From: integrii Date: Thu, 26 May 2022 17:35:58 -0700 Subject: [PATCH 1/3] new trailingArguments example --- examples/trailingArguments/.gitignore | 1 + examples/trailingArguments/main.go | 27 +++++++++++++++++++++++++++ helpValues.go | 2 +- parser_test.go | 2 +- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 examples/trailingArguments/.gitignore create mode 100644 examples/trailingArguments/main.go diff --git a/examples/trailingArguments/.gitignore b/examples/trailingArguments/.gitignore new file mode 100644 index 0000000..d33d363 --- /dev/null +++ b/examples/trailingArguments/.gitignore @@ -0,0 +1 @@ +trailingArguments diff --git a/examples/trailingArguments/main.go b/examples/trailingArguments/main.go new file mode 100644 index 0000000..2e6df48 --- /dev/null +++ b/examples/trailingArguments/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + + "github.com/integrii/flaggy" +) + +func main() { + + // Declare variables and their defaults + var someString = "" + var someInt = 3 + var someBool bool + + // add a global bool flag for fun + flaggy.Bool(&someBool, "y", "yes", "A sample boolean flag") + flaggy.String(&someString, "s", "string", "A sample string flag") + flaggy.Int(&someInt, "i", "int", "A sample int flag") + + flaggy.ShowHelpOnUnexpectedDisable() + + // Parse the subcommand and all flags + flaggy.Parse() + + fmt.Println(flaggy.TrailingArguments) +} diff --git a/helpValues.go b/helpValues.go index df2f679..d05148d 100644 --- a/helpValues.go +++ b/helpValues.go @@ -218,7 +218,7 @@ func (h *Help) parseFlagsToHelpFlags(flags []*Flag, maxLength int) { _, isBool := f.AssignmentVar.(*bool) if isBool { b := f.AssignmentVar.(*bool) - if *b == false { + if !*b { defaultValue = "" } } diff --git a/parser_test.go b/parser_test.go index 326d830..ddc2650 100644 --- a/parser_test.go +++ b/parser_test.go @@ -45,7 +45,7 @@ func TestFindArgsNotInParsedValues(t *testing.T) { // ensure regular values are not skipped parsedValues = []parsedValue{ - parsedValue{ + { Key: "flaggy", Value: "testing", }, From 74b6ee18a5c8e32a9d066637ff84e5aa701cb60d Mon Sep 17 00:00:00 2001 From: integrii Date: Fri, 27 May 2022 22:08:13 -0700 Subject: [PATCH 2/3] new tests for trailing arguments with positionals. fixed trailing arguments being incorrectly considered as positionals --- examples/trailingArguments/main.go | 11 +++++ main.go => flaggy.go | 6 ++- flaggy_test.go | 35 ++++++++++++++-- helpValues_blackbox_test.go | 1 + subCommand.go | 66 +++++++++++++++++------------- 5 files changed, 86 insertions(+), 33 deletions(-) rename main.go => flaggy.go (98%) diff --git a/examples/trailingArguments/main.go b/examples/trailingArguments/main.go index 2e6df48..418e696 100644 --- a/examples/trailingArguments/main.go +++ b/examples/trailingArguments/main.go @@ -12,16 +12,27 @@ func main() { var someString = "" var someInt = 3 var someBool bool + var positionalValue string // add a global bool flag for fun flaggy.Bool(&someBool, "y", "yes", "A sample boolean flag") flaggy.String(&someString, "s", "string", "A sample string flag") flaggy.Int(&someInt, "i", "int", "A sample int flag") + // this positional value will be parsed specifically before all trailing + // arguments are parsed + flaggy.AddPositionalValue(&positionalValue, "testPositional", 1, false, "a test positional") + + flaggy.DebugMode = false flaggy.ShowHelpOnUnexpectedDisable() // Parse the subcommand and all flags flaggy.Parse() + // here you will see all arguments passsed after the first positional 'testPositional' string is parsed fmt.Println(flaggy.TrailingArguments) + // Input: + // ./trailingArguments one two three + // Output: + // [two three] } diff --git a/main.go b/flaggy.go similarity index 98% rename from main.go rename to flaggy.go index b242cb6..e850823 100644 --- a/main.go +++ b/flaggy.go @@ -57,7 +57,8 @@ func ResetParser() { } } -// Parse parses flags as requested in the default package parser +// Parse parses flags as requested in the default package parser. All trailing arguments +// that result from parsing are placed in the global TrailingArguments variable. func Parse() { err := DefaultParser.Parse() TrailingArguments = DefaultParser.TrailingArguments @@ -67,7 +68,8 @@ func Parse() { } // ParseArgs parses the passed args as if they were the arguments to the -// running binary. Targets the default main parser for the package. +// running binary. Targets the default main parser for the package. All trailing +// arguments are set in the global TrailingArguments variable. func ParseArgs(args []string) { err := DefaultParser.ParseArgs(args) TrailingArguments = DefaultParser.TrailingArguments diff --git a/flaggy_test.go b/flaggy_test.go index 1de99fd..d9c4501 100644 --- a/flaggy_test.go +++ b/flaggy_test.go @@ -7,10 +7,11 @@ import ( "github.com/integrii/flaggy" ) -// TestTrailingArguments tests trailing argument parsing -func TestTrailingArguments(t *testing.T) { +// TestTrailingArgumentsDashes tests trailing argument parsing when --- is used +func TestTrailingArgumentsDashes(t *testing.T) { + flaggy.ResetParser() - args := []string{"./flaggy.text", "--", "one", "two"} + args := []string{"./flaggy.test", "--", "one", "two"} os.Args = args flaggy.Parse() if len(flaggy.TrailingArguments) != 2 { @@ -24,7 +25,35 @@ func TestTrailingArguments(t *testing.T) { if flaggy.TrailingArguments[1] != "two" { t.Fatal("incorrect argument parsed. Got", flaggy.TrailingArguments[1], "but expected two") } +} + +// TestTrailingArgumentsNoDashes tests trailing argument parsing without using --- +func TestTrailingArgumentsNoDashes(t *testing.T) { + + flaggy.ResetParser() + var positionalValue string + args := []string{"./flaggy.test", "positional", "one", "two"} + os.Args = args + flaggy.ShowHelpOnUnexpectedDisable() + flaggy.AddPositionalValue(&positionalValue, "testPositional", 1, false, "a test positional") + + flaggy.Parse() + if len(flaggy.TrailingArguments) != 2 { + t.Fatal("incorrect argument count parsed. Got", len(flaggy.TrailingArguments), "but expected", 2) + } + + if flaggy.TrailingArguments[0] != "one" { + t.Fatal("incorrect argument parsed. Got", flaggy.TrailingArguments[0], "but expected one") + } + + if flaggy.TrailingArguments[1] != "two" { + t.Fatal("incorrect argument parsed. Got", flaggy.TrailingArguments[1], "but expected two") + } + + if positionalValue != "positional" { + t.Fatal("expected positional value was not found set to the string 'positional'") + } } // TestComplexNesting tests various levels of nested subcommands and diff --git a/helpValues_blackbox_test.go b/helpValues_blackbox_test.go index d4b9572..800b497 100644 --- a/helpValues_blackbox_test.go +++ b/helpValues_blackbox_test.go @@ -20,6 +20,7 @@ func TestHelpWithMissingSCName(t *testing.T) { } }() flaggy.ResetParser() + flaggy.PanicInsteadOfExit = true sc := flaggy.NewSubcommand("") sc.ShortName = "sn" flaggy.AttachSubcommand(sc, 1) diff --git a/subCommand.go b/subCommand.go index 7f99e3e..908ab7a 100644 --- a/subCommand.go +++ b/subCommand.go @@ -115,7 +115,7 @@ func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, argType := determineArgType(a) // strip flags from arg - // debugPrint("Parsing flag named", a, "of type", argType) + debugPrint("Parsing flag named", a, "of type", argType) // depending on the flag type, parse the key and value out, then apply it switch argType { @@ -301,39 +301,49 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { // if there aren't any positional flags but there are subcommands that // were not used, display a useful message with subcommand options. - if !foundPositional && p.ShowHelpOnUnexpected { - debugPrint("No positional at position", relativeDepth) - var foundSubcommandAtDepth bool - for _, cmd := range sc.Subcommands { - if cmd.Position == relativeDepth { - foundSubcommandAtDepth = true - } - } - - // if there is a subcommand here but it was not specified, display them all - // as a suggestion to the user before exiting. - if foundSubcommandAtDepth { - // determine which name to use in upcoming help output - fmt.Fprintln(os.Stderr, sc.Name+":", "No subcommand or positional value found at position", strconv.Itoa(relativeDepth)+".") - var output string + if !foundPositional { + if p.ShowHelpOnUnexpected { + debugPrint("No positional at position", relativeDepth) + var foundSubcommandAtDepth bool for _, cmd := range sc.Subcommands { - if cmd.Hidden { - continue + if cmd.Position == relativeDepth { + foundSubcommandAtDepth = true } - output = output + " " + cmd.Name } - // if there are available subcommands, let the user know - if len(output) > 0 { - output = strings.TrimLeft(output, " ") - fmt.Println("Available subcommands:", output) + + // if there is a subcommand here but it was not specified, display them all + // as a suggestion to the user before exiting. + if foundSubcommandAtDepth { + // determine which name to use in upcoming help output + fmt.Fprintln(os.Stderr, sc.Name+":", "No subcommand or positional value found at position", strconv.Itoa(relativeDepth)+".") + var output string + for _, cmd := range sc.Subcommands { + if cmd.Hidden { + continue + } + output = output + " " + cmd.Name + } + // if there are available subcommands, let the user know + if len(output) > 0 { + output = strings.TrimLeft(output, " ") + fmt.Println("Available subcommands:", output) + } + exitOrPanic(2) } + + // if there were not any flags or subcommands at this position at all, then + // throw an error (display Help if necessary) + p.ShowHelpWithMessage("Unexpected argument: " + v) exitOrPanic(2) - } + } else { + // if no positional value was registered at this position, but the parser is not + // configured to show help when any unexpected command is found, add this positional + // to the list of trailing arguments. This allows for any number of unspecified + // values to be added at the end of the arguments list without using the --- + // trailing arguments separator. + p.TrailingArguments = append(p.TrailingArguments, v) - // if there were not any flags or subcommands at this position at all, then - // throw an error (display Help if necessary) - p.ShowHelpWithMessage("Unexpected argument: " + v) - exitOrPanic(2) + } } } From 0be8b6c658106d712caae1f2b492ca2035a38822 Mon Sep 17 00:00:00 2001 From: integrii Date: Fri, 27 May 2022 22:24:59 -0700 Subject: [PATCH 3/3] merge master changes --- flaggy_test.go | 6 +++ go.mod | 2 + go.sum | 4 ++ helpValues.go | 3 -- helpValues_blackbox_test.go | 82 +++++++++++++++++++++++++++++++++++-- parser.go | 5 +++ subCommand.go | 8 ++-- subcommand_test.go | 53 +++++++++++++++++------- 8 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 go.sum diff --git a/flaggy_test.go b/flaggy_test.go index d9c4501..5bee0c9 100644 --- a/flaggy_test.go +++ b/flaggy_test.go @@ -137,6 +137,9 @@ func TestComplexNesting(t *testing.T) { t.Log("testE", testE) t.FailNow() } + if subcommandName := flaggy.DefaultParser.TrailingSubcommand().Name; subcommandName != "scD" { + t.Fatal("Used subcommand was incorrect:", subcommandName) + } } @@ -206,5 +209,8 @@ func TestParsePositionalsA(t *testing.T) { if parser.TrailingArguments[1] != "trailingB" { t.Fatal("Trailing argumentB was incorrect:", parser.TrailingArguments[1]) } + if subcommandName := parser.TrailingSubcommand().Name; subcommandName != "subcommand" { + t.Fatal("Used subcommand was incorrect:", subcommandName) + } } diff --git a/go.mod b/go.mod index 5f87729..23ad7c5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/integrii/flaggy go 1.12 + +require github.com/google/go-cmp v0.5.6 // for tests only diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..03e1a9c --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/helpValues.go b/helpValues.go index d05148d..08e885c 100644 --- a/helpValues.go +++ b/helpValues.go @@ -52,7 +52,6 @@ type HelpFlag struct { // parser. The parser is required in order to detect default flag settings // for help and version output. func (h *Help) ExtractValues(p *Parser, message string) { - // accept message string for output h.Message = message @@ -187,13 +186,11 @@ func (h *Help) ExtractValues(p *Parser, message string) { } h.UsageString = usageString - } // parseFlagsToHelpFlags parses the specified slice of flags into // help flags on the the calling help command func (h *Help) parseFlagsToHelpFlags(flags []*Flag, maxLength int) { - for _, f := range flags { if f.Hidden { continue diff --git a/helpValues_blackbox_test.go b/helpValues_blackbox_test.go index 800b497..35f8eef 100644 --- a/helpValues_blackbox_test.go +++ b/helpValues_blackbox_test.go @@ -1,22 +1,59 @@ package flaggy_test import ( + "os" + "strings" "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/integrii/flaggy" ) func TestMinimalHelpOutput(t *testing.T) { p := flaggy.NewParser("TestMinimalHelpOutput") + + rd, wr, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: error: %s", err) + } + savedStderr := os.Stderr + os.Stderr = wr + + defer func() { + os.Stderr = savedStderr + }() + p.ShowHelp() + + buf := make([]byte, 1024) + n, err := rd.Read(buf) + if err != nil { + t.Fatalf("read: error: %s", err) + } + got := strings.Split(string(buf[:n]), "\n") + want := []string{ + "", + "", + " Flags: ", + " --version Displays the program version string.", + " -h --help Displays help with available flag, subcommand, and positional value parameters.", + "", + "", + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("help mismatch (-want +got):\n%s", diff) + } } func TestHelpWithMissingSCName(t *testing.T) { defer func() { r := recover() - if r == nil { - t.Fatal("Expected panic with subcommand avilability at position, but did not get one") + gotMsg := r.(string) + wantMsg := "Panic instead of exit with code: 2" + if gotMsg != wantMsg { + t.Fatalf("error: got: %s; want: %s", gotMsg, wantMsg) } }() flaggy.ResetParser() @@ -62,6 +99,45 @@ func TestHelpOutput(t *testing.T) { p.Duration(&durationFlag, "d", "durationFlag", "This is a test duration flag that does some untimely stuff.") p.AdditionalHelpPrepend = "This is a prepend for help" p.AdditionalHelpAppend = "This is an append for help" - p.ParseArgs([]string{"subcommandA", "subcommandB", "hiddenPositional1"}) + + rd, wr, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: error: %s", err) + } + savedStderr := os.Stderr + os.Stderr = wr + + defer func() { + os.Stderr = savedStderr + }() + + if err := p.ParseArgs([]string{"subcommandA", "subcommandB", "hiddenPositional1"}); err != nil { + t.Fatalf("got: %s; want: no error", err) + } p.ShowHelpWithMessage("This is a help message on exit") + + buf := make([]byte, 1024) + n, err := rd.Read(buf) + if err != nil { + t.Fatalf("read: error: %s", err) + } + got := strings.Split(string(buf[:n]), "\n") + want := []string{ + "subcommandB - Subcommand B is a command that does other stuff", + "", + " Flags: ", + " --version Displays the program version string.", + " -h --help Displays help with available flag, subcommand, and positional value parameters.", + " -s --stringFlag This is a test string flag that does some stringy string stuff. (default: defaultStringHere)", + " -i --intFlg This is a test int flag that does some interesting int stuff. (default: 0)", + " -b --boolFlag This is a test bool flag that does some booly bool stuff.", + " -d --durationFlag This is a test duration flag that does some untimely stuff. (default: 0s)", + "", + "This is a help message on exit", + "", + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("help mismatch (-want +got):\n%s", diff) + } } diff --git a/parser.go b/parser.go index 495e7c2..08a23bf 100644 --- a/parser.go +++ b/parser.go @@ -26,6 +26,11 @@ type Parser struct { subcommandContext *Subcommand // points to the most specific subcommand being used } +// TrailingSubcommand returns the last and most specific subcommand invoked. +func (p *Parser) TrailingSubcommand() *Subcommand { + return p.subcommandContext +} + // NewParser creates a new ArgumentParser ready to parse inputs func NewParser(name string) *Parser { // this can not be done inline because of struct embedding diff --git a/subCommand.go b/subCommand.go index 908ab7a..c82c081 100644 --- a/subCommand.go +++ b/subCommand.go @@ -666,14 +666,14 @@ func (sc *Subcommand) AddPositionalValue(assignmentVar *string, name string, rel // ensure no other positionals are at this depth for _, other := range sc.PositionalFlags { if relativePosition == other.Position { - log.Panicln("Unable to add positional value because one already exists at position: " + strconv.Itoa(relativePosition)) + log.Panicln("Unable to add positional value " + name + " because " + other.Name + " already exists at position: " + strconv.Itoa(relativePosition)) } } // ensure no subcommands at this depth for _, other := range sc.Subcommands { if relativePosition == other.Position { - log.Panicln("Unable to add positional value a subcommand already exists at position: " + strconv.Itoa(relativePosition)) + log.Panicln("Unable to add positional value " + name + "because a subcommand, " + other.Name + ", already exists at position: " + strconv.Itoa(relativePosition)) } } @@ -699,7 +699,9 @@ func (sc *Subcommand) SetValueForKey(key string, value string) (bool, error) { // debugPrint("Evaluating string flag", f.ShortName, "==", key, "||", f.LongName, "==", key) if f.ShortName == key || f.LongName == key { // debugPrint("Setting string value for", key, "to", value) - f.identifyAndAssignValue(value) + if err := f.identifyAndAssignValue(value); err != nil { + return false, err + } return true, nil } } diff --git a/subcommand_test.go b/subcommand_test.go index cbeafff..d47c3ee 100644 --- a/subcommand_test.go +++ b/subcommand_test.go @@ -21,7 +21,6 @@ func TestSCNameExists(t *testing.T) { scB := flaggy.NewSubcommand("test") flaggy.AttachSubcommand(scA, 1) flaggy.AttachSubcommand(scB, 1) - } func TestFlagExists(t *testing.T) { @@ -37,7 +36,6 @@ func TestFlagExists(t *testing.T) { if e == false { t.Fatal("Flag does not exist on a subcommand that should") } - } // TestExitOnUnknownFlag tests that when an unknown flag is supplied and the @@ -157,7 +155,9 @@ func TestTypoSubcommand(t *testing.T) { newSCB := flaggy.NewSubcommand("TestTypoSubcommandB") p.AttachSubcommand(newSCA, 1) p.AttachSubcommand(newSCB, 1) - p.ParseArgs(args) + if err := p.ParseArgs(args); err != nil { + t.Fatalf("got: %s; want: no error", err) + } } // TestIgnoreUnexpected tests what happens when an invalid subcommand is passed but should be ignored @@ -167,7 +167,9 @@ func TestIgnoreUnexpected(t *testing.T) { args := []string{"unexpectedArg"} newSCA := flaggy.NewSubcommand("TestTypoSubcommandA") p.AttachSubcommand(newSCA, 1) - p.ParseArgs(args) + if err := p.ParseArgs(args); err != nil { + t.Fatalf("got: %s; want: no error", err) + } } // TestSubcommandHelp tests displaying of help on unspecified commands @@ -181,7 +183,9 @@ func TestSubcommandHelp(t *testing.T) { p := flaggy.NewParser("TestSubcommandHelp") p.ShowHelpOnUnexpected = true args := []string{"unexpectedArg"} - p.ParseArgs(args) + if err := p.ParseArgs(args); err != nil { + t.Fatalf("got: %s; want: no error", err) + } } func TestHelpWithHFlagA(t *testing.T) { @@ -194,7 +198,9 @@ func TestHelpWithHFlagA(t *testing.T) { p := flaggy.NewParser("TestHelpWithHFlag") p.ShowHelpWithHFlag = true args := []string{"-h"} - p.ParseArgs(args) + if err := p.ParseArgs(args); err != nil { + t.Fatalf("got: %s; want: no error", err) + } } func TestHelpWithHFlagB(t *testing.T) { @@ -207,7 +213,9 @@ func TestHelpWithHFlagB(t *testing.T) { p := flaggy.NewParser("TestHelpWithHFlag") p.ShowHelpWithHFlag = true args := []string{"--help"} - p.ParseArgs(args) + if err := p.ParseArgs(args); err != nil { + t.Fatalf("got: %s; want: no error", err) + } } func TestVersionWithVFlagB(t *testing.T) { @@ -221,7 +229,9 @@ func TestVersionWithVFlagB(t *testing.T) { p.ShowVersionWithVersionFlag = true p.Version = "TestVersionWithVFlagB 0.0.0a" args := []string{"--version"} - p.ParseArgs(args) + if err := p.ParseArgs(args); err != nil { + t.Fatalf("got: %s; want: no error", err) + } } // TestSubcommandParse tests paring of a single subcommand @@ -243,7 +253,9 @@ func TestSubcommandParse(t *testing.T) { // override os args and parse them os.Args = []string{"binaryName", "testSubcommand", "testPositional"} - p.Parse() + if err := p.Parse(); err != nil { + t.Fatalf("got: %s; want: no error", err) + } // ensure subcommand and positional used if !newSC.Used { @@ -255,7 +267,6 @@ func TestSubcommandParse(t *testing.T) { } func TestBadSubcommand(t *testing.T) { - // create the argument parser p := flaggy.NewParser("TestBadSubcommand") @@ -265,11 +276,12 @@ func TestBadSubcommand(t *testing.T) { // test what happens if you add a bad subcommand os.Args = []string{"test"} - p.Parse() + if err := p.Parse(); err != nil { + t.Fatalf("got: %s; want: no error", err) + } } func TestBadPositional(t *testing.T) { - // create the argument parser p := flaggy.NewParser("TestBadPositional") @@ -310,7 +322,6 @@ func debugOff() { // BenchmarkSubcommandParse benchmarks the creation and parsing of // a basic subcommand func BenchmarkSubcommandParse(b *testing.B) { - // catch errors that may occur defer func(b *testing.B) { err := recover() @@ -342,7 +353,6 @@ func BenchmarkSubcommandParse(b *testing.B) { b.Fatal("Error parsing args: " + err.Error()) } } - } // TestSCInputParsing tests all flag types on subcommands @@ -804,3 +814,18 @@ func TestNestedSCBoolFlag(t *testing.T) { t.Fatal("Error parsing args: " + err.Error()) } } + +func TestParseErrorsAreReportedRegression(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatal("Expected crash on invalid syntax") + } + }() + + flaggy.ResetParser() + intFlag := 42 + flaggy.Int(&intFlag, "i", "int", "dummy") + os.Args = []string{"prog", "--int", "abc"} + flaggy.Parse() +}