diff --git a/.gitignore b/.gitignore index a1338d6..f46854f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +.idea/ diff --git a/argumentParser.go b/argumentParser.go index 320e85e..34d00f9 100644 --- a/argumentParser.go +++ b/argumentParser.go @@ -5,7 +5,7 @@ package flaggy // The return values represent the key being set, and any errors // returned when setting the key, such as failures to convert the string // into the appropriate flag value. We stop assigning values as soon -// as we find a parser that accepts it. +// as we find a any parser that accepts it. func setValueForParsers(key string, value string, parsers ...ArgumentParser) (bool, error) { for _, p := range parsers { diff --git a/flag_test.go b/flag_test.go index 1b2c601..5c2c7ae 100644 --- a/flag_test.go +++ b/flag_test.go @@ -8,11 +8,16 @@ import ( "time" ) -// debugOff makes defers easier +// debugOff makes defers easier and turns off debug mode func debugOff() { DebugMode = false } +// debugOn turns on debug mode +func debugOn() { + DebugMode = true +} + func TestGlobs(t *testing.T) { for _, a := range os.Args { fmt.Println(a) @@ -285,7 +290,7 @@ func TestInputParsing(t *testing.T) { var maskSliceFlagExpected = []net.IPMask{net.IPMask([]byte{255, 255, 255, 255}), net.IPMask([]byte{255, 255, 255, 0})} // display help with all flags used - ShowHelp("Showing help from TestInputParsing test.") + ShowHelp("Showing help for test: " + t.Name()) // Parse arguments ParseArgs(inputArgs) diff --git a/flaggy_test.go b/flaggy_test.go index 5e10d32..1de99fd 100644 --- a/flaggy_test.go +++ b/flaggy_test.go @@ -31,6 +31,9 @@ func TestTrailingArguments(t *testing.T) { // positional values intermixed with eachother. func TestComplexNesting(t *testing.T) { + flaggy.DebugMode = true + defer debugOff() + flaggy.ResetParser() var testA string @@ -47,11 +50,12 @@ func TestComplexNesting(t *testing.T) { flaggy.Bool(&testF, "f", "testF", "") + flaggy.AttachSubcommand(scA, 1) + scA.AddPositionalValue(&testA, "testA", 1, false, "") scA.AddPositionalValue(&testB, "testB", 2, false, "") scA.AddPositionalValue(&testC, "testC", 3, false, "") scA.AttachSubcommand(scB, 4) - flaggy.AttachSubcommand(scA, 1) scB.AddPositionalValue(&testD, "testD", 1, false, "") scB.AttachSubcommand(scC, 2) @@ -60,7 +64,9 @@ func TestComplexNesting(t *testing.T) { scD.AddPositionalValue(&testE, "testE", 1, true, "") - flaggy.ParseArgs([]string{"scA", "-f", "A", "B", "C", "scB", "D", "scC", "scD", "E"}) + args := []string{"scA", "-f", "A", "B", "C", "scB", "D", "scC", "scD", "E"} + t.Log(args) + flaggy.ParseArgs(args) if !testF { t.Log("testF", testF) @@ -108,6 +114,8 @@ func TestComplexNesting(t *testing.T) { func TestParsePositionalsA(t *testing.T) { inputLine := []string{"-t", "-i=3", "subcommand", "-n", "testN", "-j=testJ", "positionalA", "positionalB", "--testK=testK", "--", "trailingA", "trailingB"} + flaggy.DebugMode = true + var boolT bool var intT int var testN string @@ -151,15 +159,9 @@ func TestParsePositionalsA(t *testing.T) { if boolT != true { t.Fatal("Global bool flag -t was incorrect:", boolT) } - if testK != "testK" { - t.Fatal("Subcommand flag testK was incorrect:", testK) - } if testN != "testN" { t.Fatal("Subcommand flag testN was incorrect:", testN) } - if testJ != "testJ" { - t.Fatal("Subcommand flag testJ was incorrect:", testJ) - } if positionalA != "positionalA" { t.Fatal("Positional A was incorrect:", positionalA) } diff --git a/main.go b/main.go index b44cd91..b242cb6 100644 --- a/main.go +++ b/main.go @@ -327,6 +327,18 @@ func exitOrPanic(code int) { os.Exit(code) } +// ShowHelpOnUnexpectedEnable enables the ShowHelpOnUnexpected behavior on the +// default parser. This causes unknown inputs to error out. +func ShowHelpOnUnexpectedEnable() { + DefaultParser.ShowHelpOnUnexpected = true +} + +// ShowHelpOnUnexpectedDisable disables the ShowHelpOnUnexpected behavior on the +// default parser. This causes unknown inputs to error out. +func ShowHelpOnUnexpectedDisable() { + DefaultParser.ShowHelpOnUnexpected = false +} + // AddPositionalValue adds a positional value to the main parser at the global // context func AddPositionalValue(assignmentVar *string, name string, relativePosition int, required bool, description string) { diff --git a/parsedValue.go b/parsedValue.go new file mode 100644 index 0000000..04ada32 --- /dev/null +++ b/parsedValue.go @@ -0,0 +1,23 @@ +package flaggy + +// parsedValue represents a flag or subcommand that was parsed. Primairily used +// to account for all parsed values in order to determine if unknown values were +// passed to the root parser after all subcommands have been parsed. +type parsedValue struct { + Key string + Value string + IsPositional bool // indicates that this value was positional and not a key/value +} + +// newParsedValue creates and returns a new parsedValue struct with the +// supplied values set +func newParsedValue(key string, value string, isPositional bool) parsedValue { + if len(key) == 0 && len(value) == 0 { + panic("cant add parsed value with no key or value") + } + return parsedValue{ + Key: key, + Value: value, + IsPositional: isPositional, + } +} diff --git a/parser.go b/parser.go index 0663932..41ab76e 100644 --- a/parser.go +++ b/parser.go @@ -4,18 +4,20 @@ import ( "errors" "fmt" "os" + "strconv" "text/template" ) -// Parser represents the set of vars and subcommands we are expecting -// from our input args, and the parser than handles them all. +// Parser represents the set of flags and subcommands we are expecting +// from our input arguments. Parser is the top level struct responsible for +// parsing an entire set of subcommands and flags. type Parser struct { Subcommand Version string // the optional version of the parser. ShowHelpWithHFlag bool // display help when -h or --help passed ShowVersionWithVersionFlag bool // display the version when --version passed - ShowHelpOnUnexpected bool // display help when an unexpected flag is passed + ShowHelpOnUnexpected bool // display help when an unexpected flag or subcommand is passed TrailingArguments []string // everything after a -- is placed here HelpTemplate *template.Template // template for Help output trailingArgumentsExtracted bool // indicates that trailing args have been parsed and should not be appended again @@ -46,8 +48,89 @@ func (p *Parser) ParseArgs(args []string) error { return errors.New("Parser.Parse() called twice on parser with name: " + " " + p.Name + " " + p.ShortName) } p.parsed = true - // debugPrint("Kicking off parsing with args:", args) - return p.parse(p, args, 0) + + debugPrint("Kicking off parsing with args:", args) + err := p.parse(p, args, 0) + if err != nil { + return err + } + + // if we are set to crash on unexpected args, look for those here TODO + if p.ShowHelpOnUnexpected { + parsedValues := p.findAllParsedValues() + debugPrint("parsedValues:", parsedValues) + argsNotParsed := findArgsNotInParsedValues(args, parsedValues) + if len(argsNotParsed) > 0 { + // flatten out unused args for our error message + var argsNotParsedFlat string + for _, a := range argsNotParsed { + argsNotParsedFlat = argsNotParsedFlat + " " + a + } + p.ShowHelpAndExit("Unknown arguments supplied: " + argsNotParsedFlat) + } + } + + return nil +} + +// findArgsNotInParsedValues finds arguments not used in parsed values. The +// incoming args should be in the order supplied by the user and should not +// include the invoked binary, which is normally the first thing in os.Args. +func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []string { + var argsNotUsed []string + var skipNext bool + for _, a := range args { + + // if the final argument (--) is seen, then we stop checking because all + // further values are trailing arguments. + if determineArgType(a) == argIsFinal { + return argsNotUsed + } + + // allow for skipping the next arg when needed + if skipNext { + skipNext = false + continue + } + + // strip flag slashes from incoming arguments so they match up with the + // keys from parsedValues. + arg := parseFlagToName(a) + + // indicates that we found this arg used in one of the parsed values. Used + // to indicate which values should be added to argsNotUsed. + var foundArgUsed bool + + // search all args for a corresponding parsed value + for _, pv := range parsedValues { + // this argumenet was a key + // debugPrint(pv.Key, "==", arg) + debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") + if pv.Key == arg || (pv.IsPositional && pv.Value == arg) { + debugPrint("Found matching parsed arg for " + pv.Key) + foundArgUsed = true // the arg was used in this parsedValues set + // if the value is not a positional value and the parsed value had a + // value that was not blank, we skip the next value in the argument list + if !pv.IsPositional && len(pv.Value) > 0 { + skipNext = true + break + } + } + // this prevents excessive parsed values from being checked after we find + // the arg used for the first time + if foundArgUsed { + break + } + } + + // if the arg was not used in any parsed values, then we add it to the slice + // of arguments not used + if !foundArgUsed { + argsNotUsed = append(argsNotUsed, arg) + } + } + + return argsNotUsed } // ShowVersionAndExit shows the version of this parser @@ -105,7 +188,8 @@ func (p *Parser) ShowHelpWithMessage(message string) { } } -// Disable show version with --version. It is enabled by default. +// DisableShowVersionWithVersion disables the showing of version information +// with --version. It is enabled by default. func (p *Parser) DisableShowVersionWithVersion() { p.ShowVersionWithVersionFlag = false } diff --git a/parser_test.go b/parser_test.go index f9abc82..f8dac95 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4,6 +4,7 @@ import "testing" func TestDoubleParse(t *testing.T) { ResetParser() + DefaultParser.ShowHelpOnUnexpected = false err := DefaultParser.Parse() if err != nil { diff --git a/subCommand.go b/subCommand.go index c0c1307..7f99e3e 100644 --- a/subCommand.go +++ b/subCommand.go @@ -23,15 +23,20 @@ type Subcommand struct { Subcommands []*Subcommand Flags []*Flag PositionalFlags []*PositionalValue - AdditionalHelpPrepend string // additional prepended message when Help is displayed - AdditionalHelpAppend string // additional appended message when Help is displayed - Used bool // indicates this subcommand was found and parsed - Hidden bool // indicates this subcommand should be hidden from help + ParsedValues []parsedValue // a list of values and positionals parsed + AdditionalHelpPrepend string // additional prepended message when Help is displayed + AdditionalHelpAppend string // additional appended message when Help is displayed + Used bool // indicates this subcommand was found and parsed + Hidden bool // indicates this subcommand should be hidden from help } // NewSubcommand creates a new subcommand that can have flags or PositionalFlags // added to it. The position starts with 1, not 0 func NewSubcommand(name string) *Subcommand { + if len(name) == 0 { + fmt.Fprintln(os.Stderr, "Error creating subcommand (NewSubcommand()). No subcommand name was specified.") + exitOrPanic(2) + } newSC := &Subcommand{ Name: name, } @@ -39,10 +44,11 @@ func NewSubcommand(name string) *Subcommand { } // parseAllFlagsFromArgs parses the non-positional flags such as -f or -v=value -// out of the supplied args and returns the positional items in order. +// out of the supplied args and returns the resulting positional items in order, +// all the flag names found (without values), a bool to indicate if help was +// requested, and any errors found during parsing func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, bool, error) { - var err error var positionalOnlyArguments []string var helpRequested bool // indicates the user has supplied -h and we // should render help if we are the last subcommand @@ -58,7 +64,7 @@ func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, // find all the normal flags (not positional) and parse them out for i, a := range args { - debugPrint("parsing arg", 1, a) + debugPrint("parsing arg:", a) // evaluate if there is a following arg to avoid panics var nextArgExists bool @@ -121,62 +127,107 @@ func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, // this positional argument into a slice of their own, so that // we can determine if its a subcommand or positional value later positionalOnlyArguments = append(positionalOnlyArguments, a) - case argIsFlagWithSpace: + // track this as a parsed value with the subcommand + sc.addParsedPositionalValue(a) + case argIsFlagWithSpace: // a flag with a space. ex) -k v or --key value a = parseFlagToName(a) + // debugPrint("Arg", i, "is flag with space:", a) // parse next arg as value to this flag and apply to subcommand flags // if the flag is a bool flag, then we check for a following positional // and skip it if necessary if flagIsBool(sc, p, a) { debugPrint(sc.Name, "bool flag", a, "next var is:", nextArg) - _, err = setValueForParsers(a, "true", p, sc) + // set the value in this subcommand and its root parser + valueSet, err := setValueForParsers(a, "true", p, sc) // if an error occurs, just return it and quit parsing if err != nil { return []string{}, false, err } - // by default, we just assign the next argument to the value and continue + + // log all values parsed by this subcommand. We leave the value blank + // because the bool value had no explicit true or false supplied + if valueSet { + sc.addParsedFlag(a, "") + } + + // we've found and set a standalone bool flag, so we move on to the next + // argument in the list of arguments continue } skipNext = true - debugPrint(sc.Name, "NOT bool flag", a) + // debugPrint(sc.Name, "NOT bool flag", a) // if the next arg was not found, then show a Help message if !nextArgExists { p.ShowHelpWithMessage("Expected a following arg for flag " + a + ", but it did not exist.") exitOrPanic(2) } - _, err = setValueForParsers(a, nextArg, p, sc) + valueSet, err := setValueForParsers(a, nextArg, p, sc) if err != nil { return []string{}, false, err } - case argIsFlagWithValue: + + // log all parsed values in the subcommand + if valueSet { + sc.addParsedFlag(a, nextArg) + } + case argIsFlagWithValue: // a flag with an equals sign. ex) -k=v or --key=value // debugPrint("Arg", i, "is flag with value:", a) a = parseFlagToName(a) + // parse flag into key and value and apply to subcommand flags key, val := parseArgWithValue(a) - _, err = setValueForParsers(key, val, p, sc) + + // set the value in this subcommand and its root parser + valueSet, err := setValueForParsers(key, val, p, sc) if err != nil { return []string{}, false, err } - // if this flag type was found and not set, and the parser is set to show - // Help when an unknown flag is found, then show Help and exit. - } + // log all values parsed by the subcommand + if valueSet { + sc.addParsedFlag(a, val) + } + } } return positionalOnlyArguments, helpRequested, nil } -// Parse causes the argument parser to parse based on the supplied []string. -// depth specifies the non-flag subcommand positional depth +// findAllParsedValues finds all values parsed by all subcommands and this +// subcommand and its child subcommands +func (sc *Subcommand) findAllParsedValues() []parsedValue { + parsedValues := sc.ParsedValues + for _, sc := range sc.Subcommands { + // skip unused subcommands + if !sc.Used { + continue + } + parsedValues = append(parsedValues, sc.findAllParsedValues()...) + } + return parsedValues +} + +// parse causes the argument parser to parse based on the supplied []string. +// depth specifies the non-flag subcommand positional depth. A slice of flags +// and subcommands parsed is returned so that the parser can ultimately decide +// if there were any unexpected values supplied by the user func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { debugPrint("- Parsing subcommand", sc.Name, "with depth of", depth, "and args", args) // if a command is parsed, its used sc.Used = true + debugPrint("used subcommand", sc.Name, sc.ShortName) + if len(sc.Name) > 0 { + sc.addParsedPositionalValue(sc.Name) + } + if len(sc.ShortName) > 0 { + sc.addParsedPositionalValue(sc.ShortName) + } // as subcommands are used, they become the context of the parser. This helps // us understand how to display help based on which subcommand is being used @@ -191,9 +242,10 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { sc.ensureNoConflictWithBuiltinVersion() } - // Parse the normal flags out of the argument list and retain the positionals. - // Apply the flags to the parent parser and the current subcommand context. - // ./command -f -z subcommand someVar -b becomes ./command subcommand somevar + // Parse the normal flags out of the argument list and return the positionals + // (subcommands and positional values), along with the flags used. + // Then the flag values are applied to the parent parser and the current + // subcommand being parsed. positionalOnlyArguments, helpRequested, err := sc.parseAllFlagsFromArgs(p, args) if err != nil { return err @@ -292,7 +344,7 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { } // find any positionals that were not used on subcommands that were - // found and throw help (unknown argument) + // found and throw help (unknown argument) in the global parse or subcommand for _, pv := range p.PositionalFlags { if pv.Required && !pv.Found { p.ShowHelpWithMessage("Required global positional variable " + pv.Name + " not found at position " + strconv.Itoa(pv.Position)) @@ -309,6 +361,17 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { return nil } +// addParsedFlag makes it easy to append flag values parsed by the subcommand +func (sc *Subcommand) addParsedFlag(key string, value string) { + sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false)) +} + +// addParsedPositionalValue makes it easy to append positionals parsed by the +// subcommand +func (sc *Subcommand) addParsedPositionalValue(value string) { + sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true)) +} + // FlagExists lets you know if the flag name exists as either a short or long // name in the (sub)command func (sc *Subcommand) FlagExists(name string) bool { diff --git a/subcommand_test.go b/subcommand_test.go index ca7f715..50730ae 100644 --- a/subcommand_test.go +++ b/subcommand_test.go @@ -40,6 +40,47 @@ func TestFlagExists(t *testing.T) { } +// TestExitOnUnknownFlag tests that when an unknown flag is supplied and the +// ShowHelpOnUnexpected value is set, an error is thrown on unknown flags. +func TestExitOnUnknownFlag(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatal("Expected crash on unknown flag") + } + }() + flaggy.DebugMode = true + defer debugOff() + var expectedFlag string + var expectedPositional string + flaggy.ResetParser() + flaggy.String(&expectedFlag, "f", "flag", "an expected positonal flag") + flaggy.AddPositionalValue(&expectedPositional, "positionalTest", 1, true, "A test positional value") + flaggy.ParseArgs([]string{"positionalHere", "-f", "flagHere", "unexpectedValue"}) +} + +// TestExitOnUnknownFlagWithValue tests that when an unknown flag with a value +// is supplied and the ShowHelpOnUnexpected value is set, an error is thrown on +// the unknown flags. +func TestExitOnUnknownFlagWithValue(t *testing.T) { + flaggy.ResetParser() + flaggy.ShowHelpOnUnexpectedEnable() + defer func() { + r := recover() + if r == nil { + t.Fatal("Expected crash on unknown flag with value") + } + }() + flaggy.DebugMode = true + defer debugOff() + var expectedFlag string + var expectedPositional string + flaggy.ResetParser() + flaggy.String(&expectedFlag, "f", "flag", "an expected positonal flag") + flaggy.AddPositionalValue(&expectedPositional, "positionalTest", 1, true, "A test positional value") + flaggy.ParseArgs([]string{"positionalHere", "-f", "flagHere", "--unexpectedValue=true"}) +} + // TestDoublePositional tests errors when two positionals are // specified at the same time func TestDoublePositional(t *testing.T) { @@ -75,7 +116,7 @@ func TestSubcommandHidden(t *testing.T) { defer func() { r := recover() if r == nil { - t.Fatal("Expected crash instead of exit. Subcommand id was wrong") + t.Fatal("Expected crash instead of exit. Subcommand id was set to a blank") } }() flaggy.ResetParser() @@ -530,7 +571,7 @@ func TestSCInputParsing(t *testing.T) { var maskSliceFlagExpected = []net.IPMask{net.IPMask([]byte{255, 255, 255, 255}), net.IPMask([]byte{255, 255, 255, 0})} // display help with all flags used - flaggy.ShowHelp("Showing help from TestInputParsing test.") + flaggy.ShowHelp("Showing help from TestSCInputParsing test.") // Parse arguments flaggy.ParseArgs(inputArgs)