├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── argumentParser.go ├── examples ├── .gitignore ├── complex │ └── main.go ├── customParser │ └── main.go ├── customTemplate │ └── main.go ├── positionalValue │ └── main.go ├── simple │ └── main.go ├── sliceFlag │ ├── .gitignore │ └── main.go ├── subcommand │ └── main.go └── trailingArguments │ ├── .gitignore │ └── main.go ├── examples_test.go ├── flag.go ├── flag_test.go ├── flaggy.go ├── flaggy_test.go ├── go.mod ├── go.sum ├── help.go ├── helpValues.go ├── helpValues_blackbox_test.go ├── helpValues_whitebox_test.go ├── logo.png ├── main_test.go ├── parsedValue.go ├── parser.go ├── parser_test.go ├── positionalValue.go ├── subCommand.go └── subcommand_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | .idea/ 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | { 2 | "language": "go", 3 | "os": "linux", 4 | "group": "stable", 5 | "dist": "trusty", 6 | "script": "go get -v && go test -v" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | Sensible and _fast_ command-line flag parsing with excellent support for **subcommands** and **positional values**. Flags can be at any position. Flaggy has no required project or package layout like [Cobra requires](https://github.com/spf13/cobra/issues/641), and **no external dependencies**! 14 | 15 | Check out the [go doc](http://pkg.go.dev/github.com/integrii/flaggy), [examples directory](https://github.com/integrii/flaggy/tree/master/examples), and [examples in this readme](https://github.com/integrii/flaggy#super-simple-example) to get started quickly. You can also read the Flaggy introduction post with helpful examples [on my weblog](https://ericgreer.info/post/a-better-flags-package-for-go/). 16 | 17 | # Installation 18 | 19 | `go get -u github.com/integrii/flaggy` 20 | 21 | # Key Features 22 | 23 | - Very easy to use ([see examples below](https://github.com/integrii/flaggy#super-simple-example)) 24 | - 35 different flag types supported 25 | - Any flag can be at any position 26 | - Pretty and readable help output by default 27 | - Positional subcommands 28 | - Positional parameters 29 | - Suggested subcommands when a subcommand is typo'd 30 | - Nested subcommands 31 | - Both global and subcommand specific flags 32 | - Both global and subcommand specific positional parameters 33 | - [Customizable help templates for both the global command and subcommands](https://github.com/integrii/flaggy/blob/master/examples/customTemplate/main.go) 34 | - Customizable appended/prepended help messages for both the global command and subcommands 35 | - Simple function that displays help followed by a custom message string 36 | - Flags and subcommands may have both a short and long name 37 | - Unlimited trailing arguments after a `--` 38 | - Flags can use a single dash or double dash (`--flag`, `-flag`, `-f`, `--f`) 39 | - Flags can have `=` assignment operators, or use a space (`--flag=value`, `--flag value`) 40 | - Flags support single quote globs with spaces (`--flag 'this is all one value'`) 41 | - Flags of slice types can be passed multiple times (`-f one -f two -f three`) 42 | - Optional but default version output with `--version` 43 | - Optional but default help output with `-h` or `--help` 44 | - Optional but default help output when any invalid or unknown parameter is passed 45 | - It's _fast_. All flag and subcommand parsing takes less than `1ms` in most programs. 46 | 47 | # Example Help Output 48 | 49 | ``` 50 | testCommand - Description goes here. Get more information at http://flaggy. 51 | This is a prepend for help 52 | 53 | Usage: 54 | testCommand [subcommandA|subcommandB|subcommandC] [testPositionalA] [testPositionalB] 55 | 56 | Positional Variables: 57 | testPositionalA Test positional A does some things with a positional value. (Required) 58 | testPositionalB Test positional B does some less than serious things with a positional value. 59 | 60 | Subcommands: 61 | subcommandA (a) Subcommand A is a command that does stuff 62 | subcommandB (b) Subcommand B is a command that does other stuff 63 | subcommandC (c) Subcommand C is a command that does SERIOUS stuff 64 | 65 | Flags: 66 | --version Displays the program version string. 67 | -h --help Displays help with available flag, subcommand, and positional value parameters. 68 | -s --stringFlag This is a test string flag that does some stringy string stuff. 69 | -i --intFlg This is a test int flag that does some interesting int stuff. (default: 5) 70 | -b --boolFlag This is a test bool flag that does some booly bool stuff. (default: true) 71 | -d --durationFlag This is a test duration flag that does some untimely stuff. (default: 1h23s) 72 | 73 | This is an append for help 74 | This is a help add-on message 75 | ``` 76 | 77 | # Super Simple Example 78 | 79 | `./yourApp -f test` 80 | 81 | ```go 82 | // Declare variables and their defaults 83 | var stringFlag = "defaultValue" 84 | 85 | // Add a flag 86 | flaggy.String(&stringFlag, "f", "flag", "A test string flag") 87 | 88 | // Parse the flag 89 | flaggy.Parse() 90 | 91 | // Use the flag 92 | print(stringFlag) 93 | ``` 94 | 95 | 96 | # Example with Subcommand 97 | 98 | `./yourApp subcommandExample -f test` 99 | 100 | ```go 101 | // Declare variables and their defaults 102 | var stringFlag = "defaultValue" 103 | 104 | // Create the subcommand 105 | subcommand := flaggy.NewSubcommand("subcommandExample") 106 | 107 | // Add a flag to the subcommand 108 | subcommand.String(&stringFlag, "f", "flag", "A test string flag") 109 | 110 | // Add the subcommand to the parser at position 1 111 | flaggy.AttachSubcommand(subcommand, 1) 112 | 113 | // Parse the subcommand and all flags 114 | flaggy.Parse() 115 | 116 | // Use the flag 117 | print(stringFlag) 118 | ``` 119 | 120 | # Example with Nested Subcommands, Various Flags and Trailing Arguments 121 | 122 | `./yourApp subcommandExample --flag=5 nestedSubcommand -t test -y -- trailingArg` 123 | 124 | ```go 125 | // Declare variables and their defaults 126 | var stringFlagF = "defaultValueF" 127 | var intFlagT = 3 128 | var boolFlagB bool 129 | 130 | // Create the subcommands 131 | subcommandExample := flaggy.NewSubcommand("subcommandExample") 132 | nestedSubcommand := flaggy.NewSubcommand("nestedSubcommand") 133 | 134 | // Add a flag to both subcommands 135 | subcommandExample.String(&stringFlagF, "t", "testFlag", "A test string flag") 136 | nestedSubcommand.Int(&intFlagT, "f", "flag", "A test int flag") 137 | 138 | // add a global bool flag for fun 139 | flaggy.Bool(&boolFlagB, "y", "yes", "A sample boolean flag") 140 | 141 | // attach the nested subcommand to the parent subcommand at position 1 142 | subcommandExample.AttachSubcommand(nestedSubcommand, 1) 143 | // attach the base subcommand to the parser at position 1 144 | flaggy.AttachSubcommand(subcommandExample, 1) 145 | 146 | // Parse everything, then use the flags and trailing arguments 147 | flaggy.Parse() 148 | print(stringFlagF) 149 | print(intFlagT) 150 | print(boolFlagB) 151 | print(flaggy.TrailingArguments[0]) 152 | ``` 153 | 154 | # Supported Flag Types 155 | 156 | Flaggy has specific flag types for all basic types included in go as well as a slice of any of those types. This includes all of the following types: 157 | 158 | - string and []string 159 | - bool and []bool 160 | - all int types and all []int types 161 | - all float types and all []float types 162 | - all uint types and all []uint types 163 | 164 | Other more specific types can also be used as flag types. They will be automatically parsed using the standard parsing functions included with those types in those packages. This includes: 165 | 166 | - net.IP 167 | - []net.IP 168 | - net.HardwareAddr 169 | - []net.HardwareAddr 170 | - net.IPMask 171 | - []net.IPMask 172 | - time.Duration 173 | - []time.Duration 174 | 175 | # An Example Program 176 | 177 | Best practice when using flaggy includes setting your program's name, description, and version (at build time) as shown in this example program. 178 | 179 | ```go 180 | package main 181 | 182 | import "github.com/integrii/flaggy" 183 | 184 | // Make a variable for the version which will be set at build time. 185 | var version = "unknown" 186 | 187 | // Keep subcommands as globals so you can easily check if they were used later on. 188 | var mySubcommand *flaggy.Subcommand 189 | 190 | // Setup the variables you want your incoming flags to set. 191 | var testVar string 192 | 193 | // If you would like an environment variable as the default for a value, just populate the flag 194 | // with the value of the environment by default. If the flag corresponding to this value is not 195 | // used, then it will not be changed. 196 | var myVar = os.Getenv("MY_VAR") 197 | 198 | 199 | func init() { 200 | // Set your program's name and description. These appear in help output. 201 | flaggy.SetName("Test Program") 202 | flaggy.SetDescription("A little example program") 203 | 204 | // You can disable various things by changing bools on the default parser 205 | // (or your own parser if you have created one). 206 | flaggy.DefaultParser.ShowHelpOnUnexpected = false 207 | 208 | // You can set a help prepend or append on the default parser. 209 | flaggy.DefaultParser.AdditionalHelpPrepend = "http://github.com/integrii/flaggy" 210 | 211 | // Add a flag to the main program (this will be available in all subcommands as well). 212 | flaggy.String(&testVar, "tv", "testVariable", "A variable just for testing things!") 213 | 214 | // Create any subcommands and set their parameters. 215 | mySubcommand = flaggy.NewSubcommand("mySubcommand") 216 | mySubcommand.Description = "My great subcommand!" 217 | 218 | // Add a flag to the subcommand. 219 | mySubcommand.String(&myVar, "mv", "myVariable", "A variable just for me!") 220 | 221 | // Set the version and parse all inputs into variables. 222 | flaggy.SetVersion(version) 223 | flaggy.Parse() 224 | } 225 | 226 | func main(){ 227 | if mySubcommand.Used { 228 | ... 229 | } 230 | } 231 | ``` 232 | 233 | Then, you can use the following build command to set the `version` variable in the above program at build time. 234 | 235 | ```bash 236 | # build your app and set the version string 237 | $ go build -ldflags='-X main.version=1.0.3-a3db3' 238 | $ ./yourApp version 239 | Version: 1.0.3-a3db3 240 | $ ./yourApp --help 241 | Test Program - A little example program 242 | http://github.com/integrii/flaggy 243 | ``` 244 | 245 | # Contributions 246 | 247 | Please feel free to open an issue if you find any bugs or see any features that make sense. Pull requests will be reviewed and accepted if they make sense, but it is always wise to submit a proposal issue before any major changes. 248 | -------------------------------------------------------------------------------- /argumentParser.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | // setValueForParsers sets the value for a specified key in the 4 | // specified parsers (which normally include a Parser and Subcommand). 5 | // The return values represent the key being set, and any errors 6 | // returned when setting the key, such as failures to convert the string 7 | // into the appropriate flag value. We stop assigning values as soon 8 | // as we find a any parser that accepts it. 9 | func setValueForParsers(key string, value string, parsers ...ArgumentParser) (bool, error) { 10 | 11 | for _, p := range parsers { 12 | valueWasSet, err := p.SetValueForKey(key, value) 13 | if err != nil { 14 | return valueWasSet, err 15 | } 16 | if valueWasSet { 17 | return true, nil 18 | } 19 | } 20 | 21 | return false, nil 22 | } 23 | 24 | // ArgumentParser represents a parser or subcommand 25 | type ArgumentParser interface { 26 | SetValueForKey(key string, value string) (bool, error) 27 | } 28 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | complex/complex 2 | customParser/customParser 3 | positionalValue/positionalValue 4 | simple/simple 5 | subcommand/subcommand 6 | -------------------------------------------------------------------------------- /examples/complex/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/integrii/flaggy" 7 | ) 8 | 9 | func main() { 10 | 11 | // Declare variables and their defaults 12 | var stringFlagF = "defaultValueF" 13 | var intFlagT = 3 14 | var boolFlagB bool 15 | 16 | // Create the subcommand 17 | subcommandExample := flaggy.NewSubcommand("subcommandExample") 18 | nestedSubcommand := flaggy.NewSubcommand("nestedSubcommand") 19 | 20 | // Add a flag to the subcommand 21 | subcommandExample.String(&stringFlagF, "t", "testFlag", "A test string flag") 22 | nestedSubcommand.Int(&intFlagT, "f", "flag", "A test int flag") 23 | 24 | // add a global bool flag for fun 25 | flaggy.Bool(&boolFlagB, "y", "yes", "A sample boolean flag") 26 | 27 | // the nested subcommand to the parent subcommand at position 1 28 | subcommandExample.AttachSubcommand(nestedSubcommand, 1) 29 | 30 | // the base subcommand to the parser at position 1 31 | flaggy.AttachSubcommand(subcommandExample, 1) 32 | 33 | // Parse the subcommand and all flags 34 | flaggy.Parse() 35 | 36 | // Use the flags and trailing arguments 37 | fmt.Println(stringFlagF) 38 | fmt.Println(intFlagT) 39 | 40 | // we can check if a subcommand was used easily 41 | if nestedSubcommand.Used { 42 | fmt.Println(boolFlagB) 43 | } 44 | fmt.Println(flaggy.TrailingArguments[0:]) 45 | } 46 | -------------------------------------------------------------------------------- /examples/customParser/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/integrii/flaggy" 7 | ) 8 | 9 | // Declare variables and their defaults 10 | var positionalValue = "defaultString" 11 | var intFlagT = 3 12 | var boolFlagB bool 13 | 14 | func main() { 15 | 16 | // set a description, name, and version for our parser 17 | p := flaggy.NewParser("myAppName") 18 | p.Description = "This parser just shows you how to make a parser." 19 | p.Version = "1.3.5" 20 | // display some before and after text for all help outputs 21 | p.AdditionalHelpPrepend = "I hope you like this program!" 22 | p.AdditionalHelpAppend = "This command has no warranty." 23 | 24 | // add a positional value at position 1 25 | p.AddPositionalValue(&positionalValue, "testPositional", 1, true, "This is a test positional value that is required") 26 | 27 | // create a subcommand at position 2 28 | // you don't have to finish the subcommand before adding it to the parser 29 | subCmd := flaggy.NewSubcommand("subCmd") 30 | subCmd.Description = "Description of subcommand" 31 | p.AttachSubcommand(subCmd, 2) 32 | 33 | // add a flag to the subcomand 34 | subCmd.Int(&intFlagT, "i", "testInt", "This is a test int flag") 35 | 36 | // add a bool flag to the root command 37 | p.Bool(&boolFlagB, "b", "boolTest", "This is a test boolean flag") 38 | 39 | p.Parse() 40 | 41 | fmt.Println(positionalValue, intFlagT, boolFlagB) 42 | 43 | // Imagine the following command line: 44 | // ./customParser positionalHere subCmd -i 33 -b 45 | // It would produce: 46 | // positionalHere 33 true 47 | } 48 | -------------------------------------------------------------------------------- /examples/customTemplate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/integrii/flaggy" 4 | 5 | // The custom help message template. 6 | // For rendering text/template will be used: https://godoc.org/text/template 7 | // Object propperties can be looked up here: https://github.com/integrii/flaggy/blob/master/helpValues.go 8 | const helpTemplate = `{{.CommandName}}{{if .Description}} - {{.Description}}{{end}}{{if .PrependMessage}} 9 | {{.PrependMessage}}{{end}} 10 | {{if .UsageString}} 11 | Usage: 12 | {{.UsageString}}{{end}}{{if .Positionals}} 13 | 14 | Positional Variables: {{range .Positionals}} 15 | {{.Name}} {{.Spacer}}{{if .Description}} {{.Description}}{{end}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{else}}{{if .Required}} (Required){{end}}{{end}}{{end}}{{end}}{{if .Subcommands}} 16 | 17 | Subcommands: {{range .Subcommands}} 18 | {{.LongName}}{{if .ShortName}} ({{.ShortName}}){{end}}{{if .Position}}{{if gt .Position 1}} (position {{.Position}}){{end}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{end}}{{end}} 19 | {{end}}{{if (gt (len .Flags) 0)}} 20 | Flags: {{if .Flags}}{{range .Flags}} 21 | {{if .ShortName}}-{{.ShortName}} {{else}} {{end}}{{if .LongName}}--{{.LongName}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{end}} 22 | {{end}}{{if .AppendMessage}}{{.AppendMessage}} 23 | {{end}}{{if .Message}} 24 | {{.Message}}{{end}} 25 | ` 26 | 27 | func main() { 28 | // Declare variables and their defaults 29 | var stringFlag = "defaultValue" 30 | 31 | // Add a flag 32 | flaggy.String(&stringFlag, "f", "flag", "A test string flag") 33 | 34 | // Set the help template 35 | flaggy.DefaultParser.SetHelpTemplate(helpTemplate) 36 | 37 | // Parse the flag 38 | flaggy.Parse() 39 | } 40 | -------------------------------------------------------------------------------- /examples/positionalValue/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/integrii/flaggy" 4 | 5 | func main() { 6 | // Declare variables and their defaults 7 | var positionalValue = "defaultValue" 8 | 9 | // Add the positional value to the parser at position 1 10 | flaggy.AddPositionalValue(&positionalValue, "test", 1, true, "a test positional value") 11 | 12 | // Parse the positional value 13 | flaggy.Parse() 14 | 15 | // Use the value 16 | print(positionalValue) 17 | } 18 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/integrii/flaggy" 4 | 5 | func main() { 6 | // Declare variables and their defaults 7 | var stringFlag = "defaultValue" 8 | 9 | // Add a flag 10 | flaggy.String(&stringFlag, "f", "flag", "A test string flag") 11 | 12 | // Parse the flag 13 | flaggy.Parse() 14 | 15 | // Use the flag 16 | print(stringFlag) 17 | } 18 | -------------------------------------------------------------------------------- /examples/sliceFlag/.gitignore: -------------------------------------------------------------------------------- 1 | sliceFlag 2 | -------------------------------------------------------------------------------- /examples/sliceFlag/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/integrii/flaggy" 4 | 5 | func main() { 6 | // Declare variables and their defaults 7 | var stringSliceFlag []string 8 | var boolSliceFlag []bool 9 | 10 | // Add a slice flag 11 | flaggy.DefaultParser.AdditionalHelpAppend = "Example: ./sliceFlag -b -b -s one -s two -b=false" 12 | flaggy.StringSlice(&stringSliceFlag, "s", "string", "A test string slice flag") 13 | flaggy.BoolSlice(&boolSliceFlag, "b", "bool", "A test bool slice flag") 14 | 15 | // Parse the flag 16 | flaggy.Parse() 17 | 18 | // output the flag contents 19 | for i := range stringSliceFlag { 20 | println(stringSliceFlag[i]) 21 | } 22 | 23 | for i := range boolSliceFlag { 24 | println(boolSliceFlag[i]) 25 | } 26 | 27 | // ./sliceFlag -b -b -s one -s two -b=false 28 | // output: 29 | // one 30 | // two 31 | // true 32 | // true 33 | // false 34 | } 35 | -------------------------------------------------------------------------------- /examples/subcommand/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/integrii/flaggy" 4 | 5 | // Declare variables and their defaults 6 | var stringFlagA = "defaultValueA" 7 | var stringFlagB = "defaultValueB" 8 | 9 | func main() { 10 | 11 | // Add a flag to the root of flaggy 12 | flaggy.String(&stringFlagA, "a", "flagA", "A test string flag (A)") 13 | 14 | // Create the subcommand 15 | subcommand := flaggy.NewSubcommand("subcommandExample") 16 | 17 | // Add a flag to the subcommand 18 | subcommand.String(&stringFlagB, "b", "flagB", "A test string flag (B)") 19 | 20 | // Add the subcommand to the parser at position 1 21 | flaggy.AttachSubcommand(subcommand, 1) 22 | 23 | // Parse the subcommand and all flags 24 | flaggy.Parse() 25 | 26 | // Use the flags 27 | println("A: " + stringFlagA) 28 | println("B: " + stringFlagB) 29 | } 30 | -------------------------------------------------------------------------------- /examples/trailingArguments/.gitignore: -------------------------------------------------------------------------------- 1 | trailingArguments 2 | -------------------------------------------------------------------------------- /examples/trailingArguments/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/integrii/flaggy" 7 | ) 8 | 9 | func main() { 10 | 11 | // Declare variables and their defaults 12 | var someString = "" 13 | var someInt = 3 14 | var someBool bool 15 | var positionalValue string 16 | 17 | // add a global bool flag for fun 18 | flaggy.Bool(&someBool, "y", "yes", "A sample boolean flag") 19 | flaggy.String(&someString, "s", "string", "A sample string flag") 20 | flaggy.Int(&someInt, "i", "int", "A sample int flag") 21 | 22 | // this positional value will be parsed specifically before all trailing 23 | // arguments are parsed 24 | flaggy.AddPositionalValue(&positionalValue, "testPositional", 1, false, "a test positional") 25 | 26 | flaggy.DebugMode = false 27 | flaggy.ShowHelpOnUnexpectedDisable() 28 | 29 | // Parse the subcommand and all flags 30 | flaggy.Parse() 31 | 32 | // here you will see all arguments passsed after the first positional 'testPositional' string is parsed 33 | fmt.Println(flaggy.TrailingArguments) 34 | // Input: 35 | // ./trailingArguments one two three 36 | // Output: 37 | // [two three] 38 | } 39 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/integrii/flaggy" 8 | ) 9 | 10 | // ExampleSubcommand_AddPositionalValue adds two levels of subcommands with a 11 | // positional value on the second level one 12 | func ExampleSubcommand_AddPositionalValue() { 13 | 14 | // Simulate some input from the CLI. Don't do this in your program. 15 | flaggy.ResetParser() 16 | os.Args = []string{"binaryName", "subcommandA", "subcommandB", "subcommandBPositionalValue"} 17 | 18 | // Imagine the following program usage: 19 | // 20 | // ./binaryName subcommandA subcommandB subcommandBPositional 21 | // 22 | 23 | var subcommandBPositional string 24 | 25 | // create a subcommand 26 | subcommandA := flaggy.NewSubcommand("subcommandA") 27 | // add the subcommand at relative position 1 within the default root parser 28 | flaggy.AttachSubcommand(subcommandA, 1) 29 | 30 | // create a second subcommand 31 | subcommandB := flaggy.NewSubcommand("subcommandB") 32 | // add the second subcommand to the first subcommand as a child at relative 33 | // position 1 34 | subcommandA.AttachSubcommand(subcommandB, 1) 35 | // add a positional to the second subcommand with a relative position of 1 36 | subcommandB.AddPositionalValue(&subcommandBPositional, "subcommandTestPositonalValue", 1, false, "A test positional input variable") 37 | 38 | // Parse the input arguments from the OS (os.Args) using the default parser 39 | flaggy.Parse() 40 | 41 | // see if our flag was set properly 42 | fmt.Println("Positional flag set to", subcommandBPositional) 43 | // Output: Positional flag set to subcommandBPositionalValue 44 | } 45 | 46 | // ExamplePositionalValue shows how to add positional variables at the 47 | // global level. 48 | func ExamplePositionalValue() { 49 | 50 | // Simulate some input from the CLI. Don't do this in your program. 51 | flaggy.ResetParser() 52 | os.Args = []string{"binaryName", "positionalValue"} 53 | 54 | // Imagine the following program usage: 55 | // 56 | // ./binaryName positionalValue 57 | 58 | // add a bool flag at the global level 59 | var stringVar string 60 | flaggy.AddPositionalValue(&stringVar, "positionalVar", 1, false, "A test positional flag") 61 | 62 | // Parse the input arguments from the OS (os.Args) 63 | flaggy.Parse() 64 | 65 | // see if our flag was set properly 66 | if stringVar == "positionalValue" { 67 | fmt.Println("Flag set to", stringVar) 68 | } 69 | // Output: Flag set to positionalValue 70 | } 71 | 72 | // ExampleBoolFlag shows how to global bool flags in your program. 73 | func ExampleBool() { 74 | 75 | // Simulate some input from the CLI. Don't do these two lines in your program. 76 | flaggy.ResetParser() 77 | os.Args = []string{"binaryName", "-f"} 78 | 79 | // Imagine the following program usage: 80 | // 81 | // ./binaryName -f 82 | // or 83 | // ./binaryName --flag=true 84 | 85 | // add a bool flag at the global level 86 | var boolFlag bool 87 | flaggy.Bool(&boolFlag, "f", "flag", "A test bool flag") 88 | 89 | // Parse the input arguments from the OS (os.Args) 90 | flaggy.Parse() 91 | 92 | // see if our flag was set properly 93 | if boolFlag == true { 94 | fmt.Println("Flag set") 95 | } 96 | // Output: Flag set 97 | } 98 | 99 | // ExampleIntFlag shows how to global int flags in your program. 100 | func ExampleInt() { 101 | 102 | // Simulate some input from the CLI. Don't do these two lines in your program. 103 | flaggy.ResetParser() 104 | os.Args = []string{"binaryName", "-f", "5"} 105 | 106 | // Imagine the following program usage: 107 | // 108 | // ./binaryName -f 5 109 | // or 110 | // ./binaryName --flag=5 111 | 112 | // add a int flag at the global level 113 | var intFlag int 114 | flaggy.Int(&intFlag, "f", "flag", "A test int flag") 115 | 116 | // Parse the input arguments from the OS (os.Args) 117 | flaggy.Parse() 118 | 119 | // see if our flag was set properly 120 | if intFlag == 5 { 121 | fmt.Println("Flag set to:", intFlag) 122 | } 123 | // Output: Flag set to: 5 124 | } 125 | 126 | // Example shows how to add string flags in your program. 127 | func Example() { 128 | 129 | // Simulate some input from the CLI. Don't do this in your program. 130 | flaggy.ResetParser() 131 | os.Args = []string{"binaryName", "-f", "flagName"} 132 | 133 | // Imagine the following program usage: 134 | // 135 | // ./binaryName -f flagName 136 | // or 137 | // ./binaryName --flag=flagName 138 | 139 | // add a string flag at the global level 140 | var stringFlag string 141 | flaggy.String(&stringFlag, "f", "flag", "A test string flag") 142 | 143 | // Parse the input arguments from the OS (os.Args) 144 | flaggy.Parse() 145 | 146 | // see if our flag was set properly 147 | if stringFlag == "flagName" { 148 | fmt.Println("Flag set to:", stringFlag) 149 | } 150 | // Output: Flag set to: flagName 151 | } 152 | 153 | // ExampleSubcommand shows usage of subcommands in flaggy. 154 | func ExampleSubcommand() { 155 | 156 | // Do not include the following two lines in your real program, it is for this 157 | // example only: 158 | flaggy.ResetParser() 159 | os.Args = []string{"programName", "-v", "VariableHere", "subcommandName", "subcommandPositional", "--", "trailingVar"} 160 | 161 | // Imagine the input to this program is as follows: 162 | // 163 | // ./programName subcommandName -v VariableHere subcommandPositional -- trailingVar 164 | // or 165 | // ./programName subcommandName subcommandPositional --variable VariableHere -- trailingVar 166 | // or 167 | // ./programName subcommandName --variable=VariableHere subcommandPositional -- trailingVar 168 | // or even 169 | // ./programName subcommandName subcommandPositional -v=VariableHere -- trailingVar 170 | // 171 | 172 | // Create a new subcommand to attach flags and other subcommands to. It must be attached 173 | // to something before being used. 174 | newSC := flaggy.NewSubcommand("subcommandName") 175 | 176 | // Attach a string variable to the subcommand 177 | var subcommandVariable string 178 | newSC.String(&subcommandVariable, "v", "variable", "A test variable.") 179 | 180 | var subcommandPositional string 181 | newSC.AddPositionalValue(&subcommandPositional, "testPositionalVar", 1, false, "A test positional variable to a subcommand.") 182 | 183 | // Attach the subcommand to the parser. This will panic if another 184 | // positional value or subcommand is already present at the depth supplied. 185 | // Later you can check if this command was used with a simple bool (newSC.Used). 186 | flaggy.AttachSubcommand(newSC, 1) 187 | 188 | // Parse the input arguments from the OS (os.Args) 189 | flaggy.Parse() 190 | 191 | // see if the subcommand was found during parsing: 192 | if newSC.Used { 193 | // Do subcommand operations here 194 | fmt.Println("Subcommand used") 195 | 196 | // check the input on your subcommand variable 197 | if subcommandVariable == "VariableHere" { 198 | fmt.Println("Subcommand variable set correctly") 199 | } 200 | 201 | // Print the subcommand positional value 202 | fmt.Println("Subcommand Positional:", subcommandPositional) 203 | 204 | // Print the first trailing argument 205 | fmt.Println("Trailing variable 1:", flaggy.TrailingArguments[0]) 206 | } 207 | // Output: 208 | // Subcommand used 209 | // Subcommand variable set correctly 210 | // Subcommand Positional: subcommandPositional 211 | // Trailing variable 1: trailingVar 212 | } 213 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Flag holds the base methods for all flag types 14 | type Flag struct { 15 | ShortName string 16 | LongName string 17 | Description string 18 | rawValue string // the value as a string before being parsed 19 | Hidden bool // indicates this flag should be hidden from help and suggestions 20 | AssignmentVar interface{} 21 | defaultValue string // the value (as a string), that was set by default before any parsing and assignment 22 | parsed bool // indicates that this flag has already been parsed 23 | } 24 | 25 | // HasName indicates that this flag's short or long name matches the 26 | // supplied name string 27 | func (f *Flag) HasName(name string) bool { 28 | name = strings.TrimSpace(name) 29 | if f.ShortName == name || f.LongName == name { 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | // identifyAndAssignValue identifies the type of the incoming value 36 | // and assigns it to the AssignmentVar pointer's target value. If 37 | // the value is a type that needs parsing, that is performed as well. 38 | func (f *Flag) identifyAndAssignValue(value string) error { 39 | 40 | var err error 41 | 42 | // Only parse this flag default value once. This keeps us from 43 | // overwriting the default value in help output 44 | if !f.parsed { 45 | f.parsed = true 46 | // parse the default value as a string and remember it for help output 47 | f.defaultValue, err = f.returnAssignmentVarValueAsString() 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | 53 | debugPrint("attempting to assign value", value, "to flag", f.LongName) 54 | f.rawValue = value // remember the raw value 55 | 56 | // depending on the type of the assignment variable, we convert the 57 | // incoming string and assign it. We only use pointers to variables 58 | // in flagy. No returning vars by value. 59 | switch f.AssignmentVar.(type) { 60 | case *string: 61 | v, _ := (f.AssignmentVar).(*string) 62 | *v = value 63 | case *[]string: 64 | v := f.AssignmentVar.(*[]string) 65 | splitString := strings.Split(value, ",") 66 | new := append(*v, splitString...) 67 | *v = new 68 | case *bool: 69 | v, err := strconv.ParseBool(value) 70 | if err != nil { 71 | return err 72 | } 73 | a, _ := (f.AssignmentVar).(*bool) 74 | *a = v 75 | case *[]bool: 76 | // parse the incoming bool 77 | b, err := strconv.ParseBool(value) 78 | if err != nil { 79 | return err 80 | } 81 | // cast the assignment var 82 | existing := f.AssignmentVar.(*[]bool) 83 | // deref the assignment var and append to it 84 | v := append(*existing, b) 85 | // pointer the new value and assign it 86 | a, _ := (f.AssignmentVar).(*[]bool) 87 | *a = v 88 | case *time.Duration: 89 | v, err := time.ParseDuration(value) 90 | if err != nil { 91 | return err 92 | } 93 | a, _ := (f.AssignmentVar).(*time.Duration) 94 | *a = v 95 | case *[]time.Duration: 96 | t, err := time.ParseDuration(value) 97 | if err != nil { 98 | return err 99 | } 100 | existing := f.AssignmentVar.(*[]time.Duration) 101 | // deref the assignment var and append to it 102 | v := append(*existing, t) 103 | // pointer the new value and assign it 104 | a, _ := (f.AssignmentVar).(*[]time.Duration) 105 | *a = v 106 | case *float32: 107 | v, err := strconv.ParseFloat(value, 32) 108 | if err != nil { 109 | return err 110 | } 111 | float := float32(v) 112 | a, _ := (f.AssignmentVar).(*float32) 113 | *a = float 114 | case *[]float32: 115 | v, err := strconv.ParseFloat(value, 32) 116 | if err != nil { 117 | return err 118 | } 119 | float := float32(v) 120 | existing := f.AssignmentVar.(*[]float32) 121 | new := append(*existing, float) 122 | *existing = new 123 | case *float64: 124 | v, err := strconv.ParseFloat(value, 64) 125 | if err != nil { 126 | return err 127 | } 128 | a, _ := (f.AssignmentVar).(*float64) 129 | *a = v 130 | case *[]float64: 131 | v, err := strconv.ParseFloat(value, 64) 132 | if err != nil { 133 | return err 134 | } 135 | existing := f.AssignmentVar.(*[]float64) 136 | new := append(*existing, v) 137 | 138 | *existing = new 139 | case *int: 140 | v, err := strconv.Atoi(value) 141 | if err != nil { 142 | return err 143 | } 144 | e := f.AssignmentVar.(*int) 145 | *e = v 146 | case *[]int: 147 | v, err := strconv.Atoi(value) 148 | if err != nil { 149 | return err 150 | } 151 | existing := f.AssignmentVar.(*[]int) 152 | new := append(*existing, v) 153 | *existing = new 154 | case *uint: 155 | v, err := strconv.ParseUint(value, 10, 64) 156 | if err != nil { 157 | return err 158 | } 159 | existing := f.AssignmentVar.(*uint) 160 | *existing = uint(v) 161 | case *[]uint: 162 | v, err := strconv.ParseUint(value, 10, 64) 163 | if err != nil { 164 | return err 165 | } 166 | existing := f.AssignmentVar.(*[]uint) 167 | new := append(*existing, uint(v)) 168 | *existing = new 169 | case *uint64: 170 | v, err := strconv.ParseUint(value, 10, 64) 171 | if err != nil { 172 | return err 173 | } 174 | existing := f.AssignmentVar.(*uint64) 175 | *existing = v 176 | case *[]uint64: 177 | v, err := strconv.ParseUint(value, 10, 64) 178 | if err != nil { 179 | return err 180 | } 181 | existing := f.AssignmentVar.(*[]uint64) 182 | new := append(*existing, v) 183 | *existing = new 184 | case *uint32: 185 | v, err := strconv.ParseUint(value, 10, 32) 186 | if err != nil { 187 | return err 188 | } 189 | existing := f.AssignmentVar.(*uint32) 190 | *existing = uint32(v) 191 | case *[]uint32: 192 | v, err := strconv.ParseUint(value, 10, 32) 193 | if err != nil { 194 | return err 195 | } 196 | existing := f.AssignmentVar.(*[]uint32) 197 | new := append(*existing, uint32(v)) 198 | *existing = new 199 | case *uint16: 200 | v, err := strconv.ParseUint(value, 10, 16) 201 | if err != nil { 202 | return err 203 | } 204 | val := uint16(v) 205 | existing := f.AssignmentVar.(*uint16) 206 | *existing = val 207 | case *[]uint16: 208 | v, err := strconv.ParseUint(value, 10, 16) 209 | if err != nil { 210 | return err 211 | } 212 | existing := f.AssignmentVar.(*[]uint16) 213 | new := append(*existing, uint16(v)) 214 | *existing = new 215 | case *uint8: 216 | v, err := strconv.ParseUint(value, 10, 8) 217 | if err != nil { 218 | return err 219 | } 220 | val := uint8(v) 221 | existing := f.AssignmentVar.(*uint8) 222 | *existing = val 223 | case *[]uint8: 224 | var newSlice []uint8 225 | 226 | v, err := strconv.ParseUint(value, 10, 8) 227 | if err != nil { 228 | return err 229 | } 230 | newV := uint8(v) 231 | existing := f.AssignmentVar.(*[]uint8) 232 | newSlice = append(*existing, newV) 233 | *existing = newSlice 234 | case *int64: 235 | v, err := strconv.ParseInt(value, 10, 64) 236 | if err != nil { 237 | return err 238 | } 239 | existing := f.AssignmentVar.(*int64) 240 | *existing = v 241 | case *[]int64: 242 | v, err := strconv.ParseInt(value, 10, 64) 243 | if err != nil { 244 | return err 245 | } 246 | existingSlice := f.AssignmentVar.(*[]int64) 247 | newSlice := append(*existingSlice, v) 248 | *existingSlice = newSlice 249 | case *int32: 250 | v, err := strconv.ParseInt(value, 10, 32) 251 | if err != nil { 252 | return err 253 | } 254 | converted := int32(v) 255 | existing := f.AssignmentVar.(*int32) 256 | *existing = converted 257 | case *[]int32: 258 | v, err := strconv.ParseInt(value, 10, 32) 259 | if err != nil { 260 | return err 261 | } 262 | existingSlice := f.AssignmentVar.(*[]int32) 263 | newSlice := append(*existingSlice, int32(v)) 264 | *existingSlice = newSlice 265 | case *int16: 266 | v, err := strconv.ParseInt(value, 10, 16) 267 | if err != nil { 268 | return err 269 | } 270 | converted := int16(v) 271 | existing := f.AssignmentVar.(*int16) 272 | *existing = converted 273 | case *[]int16: 274 | v, err := strconv.ParseInt(value, 10, 16) 275 | if err != nil { 276 | return err 277 | } 278 | existingSlice := f.AssignmentVar.(*[]int16) 279 | newSlice := append(*existingSlice, int16(v)) 280 | *existingSlice = newSlice 281 | case *int8: 282 | v, err := strconv.ParseInt(value, 10, 8) 283 | if err != nil { 284 | return err 285 | } 286 | converted := int8(v) 287 | existing := f.AssignmentVar.(*int8) 288 | *existing = converted 289 | case *[]int8: 290 | v, err := strconv.ParseInt(value, 10, 8) 291 | if err != nil { 292 | return err 293 | } 294 | existingSlice := f.AssignmentVar.(*[]int8) 295 | newSlice := append(*existingSlice, int8(v)) 296 | *existingSlice = newSlice 297 | case *net.IP: 298 | v := net.ParseIP(value) 299 | existing := f.AssignmentVar.(*net.IP) 300 | *existing = v 301 | case *[]net.IP: 302 | v := net.ParseIP(value) 303 | existing := f.AssignmentVar.(*[]net.IP) 304 | new := append(*existing, v) 305 | *existing = new 306 | case *net.HardwareAddr: 307 | v, err := net.ParseMAC(value) 308 | if err != nil { 309 | return err 310 | } 311 | existing := f.AssignmentVar.(*net.HardwareAddr) 312 | *existing = v 313 | case *[]net.HardwareAddr: 314 | v, err := net.ParseMAC(value) 315 | if err != nil { 316 | return err 317 | } 318 | existing := f.AssignmentVar.(*[]net.HardwareAddr) 319 | new := append(*existing, v) 320 | *existing = new 321 | case *net.IPMask: 322 | v := net.IPMask(net.ParseIP(value).To4()) 323 | existing := f.AssignmentVar.(*net.IPMask) 324 | *existing = v 325 | case *[]net.IPMask: 326 | v := net.IPMask(net.ParseIP(value).To4()) 327 | existing := f.AssignmentVar.(*[]net.IPMask) 328 | new := append(*existing, v) 329 | *existing = new 330 | default: 331 | return errors.New("Unknown flag assignmentVar supplied in flag " + f.LongName + " " + f.ShortName) 332 | } 333 | 334 | return err 335 | } 336 | 337 | const argIsPositional = "positional" // subcommand or positional value 338 | const argIsFlagWithSpace = "flagWithSpace" // -f path or --file path 339 | const argIsFlagWithValue = "flagWithValue" // -f=path or --file=path 340 | const argIsFinal = "final" // the final argument only '--' 341 | 342 | // determineArgType determines if the specified arg is a flag with space 343 | // separated value, a flag with a connected value, or neither (positional) 344 | func determineArgType(arg string) string { 345 | 346 | // if the arg is --, then its the final arg 347 | if arg == "--" { 348 | return argIsFinal 349 | } 350 | 351 | // if it has the prefix --, then its a long flag 352 | if strings.HasPrefix(arg, "--") { 353 | // if it contains an equals, it is a joined value 354 | if strings.Contains(arg, "=") { 355 | return argIsFlagWithValue 356 | } 357 | return argIsFlagWithSpace 358 | } 359 | 360 | // if it has the prefix -, then its a short flag 361 | if strings.HasPrefix(arg, "-") { 362 | // if it contains an equals, it is a joined value 363 | if strings.Contains(arg, "=") { 364 | return argIsFlagWithValue 365 | } 366 | return argIsFlagWithSpace 367 | } 368 | 369 | return argIsPositional 370 | } 371 | 372 | // parseArgWithValue parses a key=value concatenated argument into a key and 373 | // value 374 | func parseArgWithValue(arg string) (key string, value string) { 375 | 376 | // remove up to two minuses from start of flag 377 | arg = strings.TrimPrefix(arg, "-") 378 | arg = strings.TrimPrefix(arg, "-") 379 | 380 | // debugPrint("parseArgWithValue parsing", arg) 381 | 382 | // break at the equals 383 | args := strings.SplitN(arg, "=", 2) 384 | 385 | // if its a bool arg, with no explicit value, we return a blank 386 | if len(args) == 1 { 387 | return args[0], "" 388 | } 389 | 390 | // if its a key and value pair, we return those 391 | if len(args) == 2 { 392 | // debugPrint("parseArgWithValue parsed", args[0], args[1]) 393 | return args[0], args[1] 394 | } 395 | 396 | fmt.Println("Warning: attempted to parseArgWithValue but did not have correct parameter count.", arg, "->", args) 397 | return "", "" 398 | } 399 | 400 | // parseFlagToName parses a flag with space value down to a key name: 401 | // --path -> path 402 | // -p -> p 403 | func parseFlagToName(arg string) string { 404 | // remove minus from start 405 | arg = strings.TrimLeft(arg, "-") 406 | arg = strings.TrimLeft(arg, "-") 407 | return arg 408 | } 409 | 410 | // collectAllNestedFlags recurses through the command tree to get all 411 | // flags specified on a subcommand and its descending subcommands 412 | func collectAllNestedFlags(sc *Subcommand) []*Flag { 413 | fullList := sc.Flags 414 | for _, sc := range sc.Subcommands { 415 | fullList = append(fullList, sc.Flags...) 416 | fullList = append(fullList, collectAllNestedFlags(sc)...) 417 | } 418 | return fullList 419 | } 420 | 421 | // flagIsBool determines if the flag is a bool within the specified parser 422 | // and subcommand's context 423 | func flagIsBool(sc *Subcommand, p *Parser, key string) bool { 424 | for _, f := range append(collectAllNestedFlags(sc), p.Flags...) { 425 | if f.HasName(key) { 426 | _, isBool := f.AssignmentVar.(*bool) 427 | _, isBoolSlice := f.AssignmentVar.(*[]bool) 428 | if isBool || isBoolSlice { 429 | return true 430 | } 431 | } 432 | } 433 | 434 | // by default, the answer is false 435 | return false 436 | } 437 | 438 | // returnAssignmentVarValueAsString returns the value of the flag's 439 | // assignment variable as a string. This is used to display the 440 | // default value of flags before they are assigned (like when help is output). 441 | func (f *Flag) returnAssignmentVarValueAsString() (string, error) { 442 | 443 | debugPrint("returning current value of assignment var of flag", f.LongName) 444 | 445 | var err error 446 | 447 | // depending on the type of the assignment variable, we convert the 448 | // incoming string and assign it. We only use pointers to variables 449 | // in flagy. No returning vars by value. 450 | switch f.AssignmentVar.(type) { 451 | case *string: 452 | v, _ := (f.AssignmentVar).(*string) 453 | return *v, err 454 | case *[]string: 455 | v := f.AssignmentVar.(*[]string) 456 | return strings.Join(*v, ","), err 457 | case *bool: 458 | a, _ := (f.AssignmentVar).(*bool) 459 | return strconv.FormatBool(*a), err 460 | case *[]bool: 461 | value := f.AssignmentVar.(*[]bool) 462 | var ss []string 463 | for _, b := range *value { 464 | ss = append(ss, strconv.FormatBool(b)) 465 | } 466 | return strings.Join(ss, ","), err 467 | case *time.Duration: 468 | a := f.AssignmentVar.(*time.Duration) 469 | return (*a).String(), err 470 | case *[]time.Duration: 471 | tds := f.AssignmentVar.(*[]time.Duration) 472 | var asSlice []string 473 | for _, td := range *tds { 474 | asSlice = append(asSlice, td.String()) 475 | } 476 | return strings.Join(asSlice, ","), err 477 | case *float32: 478 | a := f.AssignmentVar.(*float32) 479 | return strconv.FormatFloat(float64(*a), 'f', 2, 32), err 480 | case *[]float32: 481 | v := f.AssignmentVar.(*[]float32) 482 | var strSlice []string 483 | for _, f := range *v { 484 | formatted := strconv.FormatFloat(float64(f), 'f', 2, 32) 485 | strSlice = append(strSlice, formatted) 486 | } 487 | return strings.Join(strSlice, ","), err 488 | case *float64: 489 | a := f.AssignmentVar.(*float64) 490 | return strconv.FormatFloat(float64(*a), 'f', 2, 64), err 491 | case *[]float64: 492 | v := f.AssignmentVar.(*[]float64) 493 | var strSlice []string 494 | for _, f := range *v { 495 | formatted := strconv.FormatFloat(float64(f), 'f', 2, 64) 496 | strSlice = append(strSlice, formatted) 497 | } 498 | return strings.Join(strSlice, ","), err 499 | case *int: 500 | a := f.AssignmentVar.(*int) 501 | return strconv.Itoa(*a), err 502 | case *[]int: 503 | val := f.AssignmentVar.(*[]int) 504 | var strSlice []string 505 | for _, i := range *val { 506 | str := strconv.Itoa(i) 507 | strSlice = append(strSlice, str) 508 | } 509 | return strings.Join(strSlice, ","), err 510 | case *uint: 511 | v := f.AssignmentVar.(*uint) 512 | return strconv.FormatUint(uint64(*v), 10), err 513 | case *[]uint: 514 | values := f.AssignmentVar.(*[]uint) 515 | var strVars []string 516 | for _, i := range *values { 517 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 518 | } 519 | return strings.Join(strVars, ","), err 520 | case *uint64: 521 | v := f.AssignmentVar.(*uint64) 522 | return strconv.FormatUint(*v, 10), err 523 | case *[]uint64: 524 | values := f.AssignmentVar.(*[]uint64) 525 | var strVars []string 526 | for _, i := range *values { 527 | strVars = append(strVars, strconv.FormatUint(i, 10)) 528 | } 529 | return strings.Join(strVars, ","), err 530 | case *uint32: 531 | v := f.AssignmentVar.(*uint32) 532 | return strconv.FormatUint(uint64(*v), 10), err 533 | case *[]uint32: 534 | values := f.AssignmentVar.(*[]uint32) 535 | var strVars []string 536 | for _, i := range *values { 537 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 538 | } 539 | return strings.Join(strVars, ","), err 540 | case *uint16: 541 | v := f.AssignmentVar.(*uint16) 542 | return strconv.FormatUint(uint64(*v), 10), err 543 | case *[]uint16: 544 | values := f.AssignmentVar.(*[]uint16) 545 | var strVars []string 546 | for _, i := range *values { 547 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 548 | } 549 | return strings.Join(strVars, ","), err 550 | case *uint8: 551 | v := f.AssignmentVar.(*uint8) 552 | return strconv.FormatUint(uint64(*v), 10), err 553 | case *[]uint8: 554 | values := f.AssignmentVar.(*[]uint8) 555 | var strVars []string 556 | for _, i := range *values { 557 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 558 | } 559 | return strings.Join(strVars, ","), err 560 | case *int64: 561 | v := f.AssignmentVar.(*int64) 562 | return strconv.FormatInt(int64(*v), 10), err 563 | case *[]int64: 564 | values := f.AssignmentVar.(*[]int64) 565 | var strVars []string 566 | for _, i := range *values { 567 | strVars = append(strVars, strconv.FormatInt(i, 10)) 568 | } 569 | return strings.Join(strVars, ","), err 570 | case *int32: 571 | v := f.AssignmentVar.(*int32) 572 | return strconv.FormatInt(int64(*v), 10), err 573 | case *[]int32: 574 | values := f.AssignmentVar.(*[]int32) 575 | var strVars []string 576 | for _, i := range *values { 577 | strVars = append(strVars, strconv.FormatInt(int64(i), 10)) 578 | } 579 | return strings.Join(strVars, ","), err 580 | case *int16: 581 | v := f.AssignmentVar.(*int16) 582 | return strconv.FormatInt(int64(*v), 10), err 583 | case *[]int16: 584 | values := f.AssignmentVar.(*[]int16) 585 | var strVars []string 586 | for _, i := range *values { 587 | strVars = append(strVars, strconv.FormatInt(int64(i), 10)) 588 | } 589 | return strings.Join(strVars, ","), err 590 | case *int8: 591 | v := f.AssignmentVar.(*int8) 592 | return strconv.FormatInt(int64(*v), 10), err 593 | case *[]int8: 594 | values := f.AssignmentVar.(*[]int8) 595 | var strVars []string 596 | for _, i := range *values { 597 | strVars = append(strVars, strconv.FormatInt(int64(i), 10)) 598 | } 599 | return strings.Join(strVars, ","), err 600 | case *net.IP: 601 | val := f.AssignmentVar.(*net.IP) 602 | return val.String(), err 603 | case *[]net.IP: 604 | val := f.AssignmentVar.(*[]net.IP) 605 | var strSlice []string 606 | for _, ip := range *val { 607 | strSlice = append(strSlice, ip.String()) 608 | } 609 | return strings.Join(strSlice, ","), err 610 | case *net.HardwareAddr: 611 | val := f.AssignmentVar.(*net.HardwareAddr) 612 | return val.String(), err 613 | case *[]net.HardwareAddr: 614 | val := f.AssignmentVar.(*[]net.HardwareAddr) 615 | var strSlice []string 616 | for _, mac := range *val { 617 | strSlice = append(strSlice, mac.String()) 618 | } 619 | return strings.Join(strSlice, ","), err 620 | case *net.IPMask: 621 | val := f.AssignmentVar.(*net.IPMask) 622 | return val.String(), err 623 | case *[]net.IPMask: 624 | val := f.AssignmentVar.(*[]net.IPMask) 625 | var strSlice []string 626 | for _, m := range *val { 627 | strSlice = append(strSlice, m.String()) 628 | } 629 | return strings.Join(strSlice, ","), err 630 | default: 631 | return "", errors.New("Unknown flag assignmentVar found in flag " + f.LongName + " " + f.ShortName + ". Type not supported: " + reflect.TypeOf(f.AssignmentVar).String()) 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /flag_test.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | // debugOff makes defers easier and turns off debug mode 12 | func debugOff() { 13 | DebugMode = false 14 | } 15 | 16 | // debugOn turns on debug mode 17 | func debugOn() { 18 | DebugMode = true 19 | } 20 | 21 | func TestGlobs(t *testing.T) { 22 | for _, a := range os.Args { 23 | fmt.Println(a) 24 | } 25 | } 26 | 27 | func TestParseArgWithValue(t *testing.T) { 28 | 29 | testCases := make(map[string][]string) 30 | testCases["-f=test"] = []string{"f", "test"} 31 | testCases["--f=test"] = []string{"f", "test"} 32 | testCases["--flag=test"] = []string{"flag", "test"} 33 | testCases["-flag=test"] = []string{"flag", "test"} 34 | testCases["----flag=--test"] = []string{"--flag", "--test"} 35 | testCases["-b"] = []string{"b", ""} 36 | testCases["--bool"] = []string{"bool", ""} 37 | 38 | for arg, correctValues := range testCases { 39 | key, value := parseArgWithValue(arg) 40 | if key != correctValues[0] { 41 | t.Fatalf("Flag %s parsed key as %s but expected key %s", arg, key, correctValues[0]) 42 | } 43 | if value != correctValues[1] { 44 | t.Fatalf("Flag %s parsed value as %s but expected value %s", arg, value, correctValues[1]) 45 | } 46 | t.Logf("Flag %s parsed key as %s and value as %s correctly", arg, key, value) 47 | } 48 | } 49 | 50 | func TestDetermineArgType(t *testing.T) { 51 | 52 | testCases := make(map[string]string) 53 | testCases["-f"] = argIsFlagWithSpace 54 | testCases["--f"] = argIsFlagWithSpace 55 | testCases["-flag"] = argIsFlagWithSpace 56 | testCases["--flag"] = argIsFlagWithSpace 57 | testCases["positionalArg"] = argIsPositional 58 | testCases["subcommand"] = argIsPositional 59 | testCases["sub--+/\\324command"] = argIsPositional 60 | testCases["--flag=CONTENT"] = argIsFlagWithValue 61 | testCases["-flag=CONTENT"] = argIsFlagWithValue 62 | testCases["-anotherfl-ag=CONTENT"] = argIsFlagWithValue 63 | testCases["--anotherfl-ag=CONTENT"] = argIsFlagWithValue 64 | testCases["1--anotherfl-ag=CONTENT"] = argIsPositional 65 | 66 | for arg, correctArgType := range testCases { 67 | argType := determineArgType(arg) 68 | if argType != correctArgType { 69 | t.Fatalf("Flag %s determined to be type %s but expected type %s", arg, argType, correctArgType) 70 | } else { 71 | t.Logf("Flag %s correctly determined to be type %s", arg, argType) 72 | } 73 | } 74 | } 75 | 76 | // TestInputParsing tests all flag types. 77 | func TestInputParsing(t *testing.T) { 78 | defer debugOff() 79 | DebugMode = true 80 | 81 | ResetParser() 82 | var err error 83 | inputArgs := []string{} 84 | 85 | // Setup input arguments for every input type 86 | 87 | var stringFlag = "defaultVar" 88 | String(&stringFlag, "s", "string", "string flag") 89 | inputArgs = append(inputArgs, "-s", "flaggy") 90 | var stringFlagExpected = "flaggy" 91 | 92 | var stringSliceFlag []string 93 | StringSlice(&stringSliceFlag, "ssf", "stringSlice", "string slice flag") 94 | inputArgs = append(inputArgs, "-ssf", "one", "-ssf", "two") 95 | var stringSliceFlagExpected = []string{"one", "two"} 96 | 97 | var boolFlag bool 98 | Bool(&boolFlag, "bf", "bool", "bool flag") 99 | inputArgs = append(inputArgs, "-bf") 100 | var boolFlagExpected = true 101 | 102 | var boolSliceFlag []bool 103 | BoolSlice(&boolSliceFlag, "bsf", "boolSlice", "bool slice flag") 104 | inputArgs = append(inputArgs, "-bsf", "-bsf") 105 | var boolSliceFlagExpected = []bool{true, true} 106 | 107 | var byteSliceFlag []byte 108 | ByteSlice(&byteSliceFlag, "bysf", "byteSlice", "byte slice flag") 109 | inputArgs = append(inputArgs, "-bysf", "17", "-bysf", "18") 110 | var byteSliceFlagExpected = []uint8{17, 18} 111 | 112 | var durationFlag time.Duration 113 | Duration(&durationFlag, "df", "duration", "duration flag") 114 | inputArgs = append(inputArgs, "-df", "33s") 115 | var durationFlagExpected = time.Second * 33 116 | 117 | var durationSliceFlag []time.Duration 118 | DurationSlice(&durationSliceFlag, "dsf", "durationSlice", "duration slice flag") 119 | inputArgs = append(inputArgs, "-dsf", "33s", "-dsf", "1h") 120 | var durationSliceFlagExpected = []time.Duration{time.Second * 33, time.Hour} 121 | 122 | var float32Flag float32 123 | Float32(&float32Flag, "f32", "float32", "float32 flag") 124 | inputArgs = append(inputArgs, "-f32", "33.343") 125 | var float32FlagExpected float32 = 33.343 126 | 127 | var float32SliceFlag []float32 128 | Float32Slice(&float32SliceFlag, "f32s", "float32Slice", "float32 slice flag") 129 | inputArgs = append(inputArgs, "-f32s", "33.343", "-f32s", "33.222") 130 | var float32SliceFlagExpected = []float32{33.343, 33.222} 131 | 132 | var float64Flag float64 133 | Float64(&float64Flag, "f64", "float64", "float64 flag") 134 | inputArgs = append(inputArgs, "-f64", "33.222343") 135 | var float64FlagExpected = 33.222343 136 | 137 | var float64SliceFlag []float64 138 | Float64Slice(&float64SliceFlag, "f64s", "float64Slice", "float64 slice flag") 139 | inputArgs = append(inputArgs, "-f64s", "64.343", "-f64s", "64.222") 140 | var float64SliceFlagExpected = []float64{64.343, 64.222} 141 | 142 | var intFlag int 143 | Int(&intFlag, "i", "int", "int flag") 144 | inputArgs = append(inputArgs, "-i", "3553") 145 | var intFlagExpected = 3553 146 | 147 | var intSliceFlag []int 148 | IntSlice(&intSliceFlag, "is", "intSlice", "int slice flag") 149 | inputArgs = append(inputArgs, "-is", "6446", "-is", "64") 150 | var intSliceFlagExpected = []int{6446, 64} 151 | 152 | var uintFlag uint 153 | UInt(&uintFlag, "ui", "uint", "uint flag") 154 | inputArgs = append(inputArgs, "-ui", "3553") 155 | var uintFlagExpected uint = 3553 156 | 157 | var uintSliceFlag []uint 158 | UIntSlice(&uintSliceFlag, "uis", "uintSlice", "uint slice flag") 159 | inputArgs = append(inputArgs, "-uis", "6446", "-uis", "64") 160 | var uintSliceFlagExpected = []uint{6446, 64} 161 | 162 | var uint64Flag uint64 163 | UInt64(&uint64Flag, "ui64", "uint64", "uint64 flag") 164 | inputArgs = append(inputArgs, "-ui64", "3553") 165 | var uint64FlagExpected uint64 = 3553 166 | 167 | var uint64SliceFlag []uint64 168 | UInt64Slice(&uint64SliceFlag, "ui64s", "uint64Slice", "uint64 slice flag") 169 | inputArgs = append(inputArgs, "-ui64s", "6446", "-ui64s", "64") 170 | var uint64SliceFlagExpected = []uint64{6446, 64} 171 | 172 | var uint32Flag uint32 173 | UInt32(&uint32Flag, "ui32", "uint32", "uint32 flag") 174 | inputArgs = append(inputArgs, "-ui32", "6446") 175 | var uint32FlagExpected uint32 = 6446 176 | 177 | var uint32SliceFlag []uint32 178 | UInt32Slice(&uint32SliceFlag, "ui32s", "uint32Slice", "uint32 slice flag") 179 | inputArgs = append(inputArgs, "-ui32s", "6446", "-ui32s", "64") 180 | var uint32SliceFlagExpected = []uint32{6446, 64} 181 | 182 | var uint16Flag uint16 183 | UInt16(&uint16Flag, "ui16", "uint16", "uint16 flag") 184 | inputArgs = append(inputArgs, "-ui16", "6446") 185 | var uint16FlagExpected uint16 = 6446 186 | 187 | var uint16SliceFlag []uint16 188 | UInt16Slice(&uint16SliceFlag, "ui16s", "uint16Slice", "uint16 slice flag") 189 | inputArgs = append(inputArgs, "-ui16s", "6446", "-ui16s", "64") 190 | var uint16SliceFlagExpected = []uint16{6446, 64} 191 | 192 | var uint8Flag uint8 193 | UInt8(&uint8Flag, "ui8", "uint8", "uint8 flag") 194 | inputArgs = append(inputArgs, "-ui8", "50") 195 | var uint8FlagExpected uint8 = 50 196 | 197 | var uint8SliceFlag []uint8 198 | UInt8Slice(&uint8SliceFlag, "ui8s", "uint8Slice", "uint8 slice flag") 199 | inputArgs = append(inputArgs, "-ui8s", "3", "-ui8s", "2") 200 | var uint8SliceFlagExpected = []uint8{uint8(3), uint8(2)} 201 | 202 | var int64Flag int64 203 | Int64(&int64Flag, "i64", "i64", "int64 flag") 204 | inputArgs = append(inputArgs, "-i64", "33445566") 205 | var int64FlagExpected int64 = 33445566 206 | 207 | var int64SliceFlag []int64 208 | Int64Slice(&int64SliceFlag, "i64s", "int64Slice", "int64 slice flag") 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | inputArgs = append(inputArgs, "-i64s", "40", "-i64s", "50") 213 | var int64SliceFlagExpected = []int64{40, 50} 214 | 215 | var int32Flag int32 216 | Int32(&int32Flag, "i32", "int32", "int32 flag") 217 | inputArgs = append(inputArgs, "-i32", "445566") 218 | var int32FlagExpected int32 = 445566 219 | 220 | var int32SliceFlag []int32 221 | Int32Slice(&int32SliceFlag, "i32s", "int32Slice", "uint32 slice flag") 222 | inputArgs = append(inputArgs, "-i32s", "40", "-i32s", "50") 223 | var int32SliceFlagExpected = []int32{40, 50} 224 | 225 | var int16Flag int16 226 | Int16(&int16Flag, "i16", "int16", "int16 flag") 227 | if err != nil { 228 | t.Fatal(err) 229 | } 230 | inputArgs = append(inputArgs, "-i16", "5566") 231 | var int16FlagExpected int16 = 5566 232 | 233 | var int16SliceFlag []int16 234 | Int16Slice(&int16SliceFlag, "i16s", "int16Slice", "int16 slice flag") 235 | inputArgs = append(inputArgs, "-i16s", "40", "-i16s", "50") 236 | var int16SliceFlagExpected = []int16{40, 50} 237 | 238 | var int8Flag int8 239 | Int8(&int8Flag, "i8", "int8", "int8 flag") 240 | inputArgs = append(inputArgs, "-i8", "32") 241 | var int8FlagExpected int8 = 32 242 | 243 | var int8SliceFlag []int8 244 | Int8Slice(&int8SliceFlag, "i8s", "int8Slice", "uint8 slice flag") 245 | inputArgs = append(inputArgs, "-i8s", "4", "-i8s", "2") 246 | var int8SliceFlagExpected = []int8{4, 2} 247 | 248 | var ipFlag net.IP 249 | IP(&ipFlag, "ip", "ipFlag", "ip flag") 250 | inputArgs = append(inputArgs, "-ip", "1.1.1.1") 251 | var ipFlagExpected = net.IPv4(1, 1, 1, 1) 252 | 253 | var ipSliceFlag []net.IP 254 | IPSlice(&ipSliceFlag, "ips", "ipFlagSlice", "ip slice flag") 255 | inputArgs = append(inputArgs, "-ips", "1.1.1.1", "-ips", "4.4.4.4") 256 | var ipSliceFlagExpected = []net.IP{net.IPv4(1, 1, 1, 1), net.IPv4(4, 4, 4, 4)} 257 | 258 | var hwFlag net.HardwareAddr 259 | HardwareAddr(&hwFlag, "hw", "hwFlag", "hw flag") 260 | inputArgs = append(inputArgs, "-hw", "32:00:16:46:20:00") 261 | hwFlagExpected, err := net.ParseMAC("32:00:16:46:20:00") 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | 266 | var hwFlagSlice []net.HardwareAddr 267 | HardwareAddrSlice(&hwFlagSlice, "hws", "hwFlagSlice", "hw slice flag") 268 | inputArgs = append(inputArgs, "-hws", "32:00:16:46:20:00", "-hws", "32:00:16:46:20:01") 269 | macA, err := net.ParseMAC("32:00:16:46:20:00") 270 | if err != nil { 271 | t.Fatal(err) 272 | } 273 | macB, err := net.ParseMAC("32:00:16:46:20:01") 274 | if err != nil { 275 | t.Fatal(err) 276 | } 277 | var hwFlagSliceExpected = []net.HardwareAddr{macA, macB} 278 | 279 | var maskFlag net.IPMask 280 | IPMask(&maskFlag, "m", "mFlag", "mask flag") 281 | inputArgs = append(inputArgs, "-m", "255.255.255.255") 282 | var maskFlagExpected = net.IPMask([]byte{255, 255, 255, 255}) 283 | 284 | var maskSliceFlag []net.IPMask 285 | IPMaskSlice(&maskSliceFlag, "ms", "mFlagSlice", "mask slice flag") 286 | if err != nil { 287 | t.Fatal(err) 288 | } 289 | inputArgs = append(inputArgs, "-ms", "255.255.255.255", "-ms", "255.255.255.0") 290 | var maskSliceFlagExpected = []net.IPMask{net.IPMask([]byte{255, 255, 255, 255}), net.IPMask([]byte{255, 255, 255, 0})} 291 | 292 | // display help with all flags used 293 | ShowHelp("Showing help for test: " + t.Name()) 294 | 295 | // Parse arguments 296 | ParseArgs(inputArgs) 297 | 298 | // validate parsed values 299 | if stringFlag != stringFlagExpected { 300 | t.Fatal("string flag incorrect", stringFlag, stringFlagExpected) 301 | } 302 | 303 | for i, f := range stringSliceFlagExpected { 304 | if stringSliceFlag[i] != f { 305 | t.Fatal("stringSlice value incorrect", stringSliceFlag[i], f) 306 | } 307 | } 308 | 309 | if boolFlag != boolFlagExpected { 310 | t.Fatal("bool flag incorrect", boolFlag, boolFlagExpected) 311 | } 312 | 313 | for i, f := range boolSliceFlagExpected { 314 | if boolSliceFlag[i] != f { 315 | t.Fatal("boolSlice value incorrect", boolSliceFlag[i], f) 316 | } 317 | } 318 | 319 | for i, f := range byteSliceFlagExpected { 320 | if byteSliceFlag[i] != f { 321 | t.Fatal("byteSlice value incorrect", boolSliceFlag[i], f) 322 | } 323 | } 324 | 325 | if durationFlag != durationFlagExpected { 326 | t.Fatal("duration flag incorrect", durationFlag, durationFlagExpected) 327 | } 328 | 329 | for i, f := range durationSliceFlagExpected { 330 | if durationSliceFlag[i] != f { 331 | t.Fatal("durationSlice value incorrect", durationSliceFlag[i], f) 332 | } 333 | } 334 | 335 | if float32Flag != float32FlagExpected { 336 | t.Fatal("float32 flag incorrect", float32Flag, float32FlagExpected) 337 | } 338 | 339 | for i, f := range float32SliceFlagExpected { 340 | if float32SliceFlag[i] != f { 341 | t.Fatal("float32Slice value incorrect", float32SliceFlag[i], f) 342 | } 343 | } 344 | 345 | if float64Flag != float64FlagExpected { 346 | t.Fatal("float64 flag incorrect", float64Flag, float64FlagExpected) 347 | } 348 | 349 | for i, f := range float64SliceFlagExpected { 350 | if float64SliceFlag[i] != f { 351 | t.Fatal("float64Slice value incorrect", float64SliceFlag[i], f) 352 | } 353 | } 354 | 355 | if intFlag != intFlagExpected { 356 | t.Fatal("int flag incorrect", intFlag, intFlagExpected) 357 | } 358 | 359 | for i, f := range intSliceFlagExpected { 360 | if intSliceFlag[i] != f { 361 | t.Fatal("intSlice value incorrect", intSliceFlag[i], f) 362 | } 363 | } 364 | 365 | if uintFlag != uintFlagExpected { 366 | t.Fatal("uint flag incorrect", uintFlag, uintFlagExpected) 367 | } 368 | 369 | for i, f := range uintSliceFlagExpected { 370 | if uintSliceFlag[i] != f { 371 | t.Fatal("uintSlice value incorrect", uintSliceFlag[i], f) 372 | } 373 | } 374 | 375 | if uint64Flag != uint64FlagExpected { 376 | t.Fatal("uint64 flag incorrect", uint64Flag, uint64FlagExpected) 377 | } 378 | 379 | for i, f := range uint64SliceFlagExpected { 380 | if uint64SliceFlag[i] != f { 381 | t.Fatal("uint64Slice value incorrect", uint64SliceFlag[i], f) 382 | } 383 | } 384 | 385 | if uint32Flag != uint32FlagExpected { 386 | t.Fatal("uint32 flag incorrect", uint32Flag, uint32FlagExpected) 387 | } 388 | 389 | for i, f := range uint32SliceFlagExpected { 390 | if uint32SliceFlag[i] != f { 391 | t.Fatal("uint32Slice value incorrect", uint32SliceFlag[i], f) 392 | } 393 | } 394 | 395 | if uint16Flag != uint16FlagExpected { 396 | t.Fatal("uint16 flag incorrect", uint16Flag, uint16FlagExpected) 397 | } 398 | 399 | for i, f := range uint16SliceFlagExpected { 400 | if uint16SliceFlag[i] != f { 401 | t.Fatal("uint16Slice value incorrect", uint16SliceFlag[i], f) 402 | } 403 | } 404 | 405 | if uint8Flag != uint8FlagExpected { 406 | t.Fatal("uint8 flag incorrect", uint8Flag, uint8FlagExpected) 407 | } 408 | 409 | for i, f := range uint8SliceFlagExpected { 410 | if uint8SliceFlag[i] != f { 411 | t.Fatal("uint8Slice value", i, "incorrect", uint8SliceFlag[i], f) 412 | } 413 | } 414 | 415 | if int64Flag != int64FlagExpected { 416 | t.Fatal("int64 flag incorrect", int64Flag, int64FlagExpected) 417 | } 418 | 419 | for i, f := range int64SliceFlagExpected { 420 | if int64SliceFlag[i] != f { 421 | t.Fatal("int64Slice value incorrect", int64SliceFlag[i], f) 422 | } 423 | } 424 | 425 | if int32Flag != int32FlagExpected { 426 | t.Fatal("int32 flag incorrect", int32Flag, int32FlagExpected) 427 | } 428 | 429 | for i, f := range int32SliceFlagExpected { 430 | if int32SliceFlag[i] != f { 431 | t.Fatal("int32Slice value incorrect", int32SliceFlag[i], f) 432 | } 433 | } 434 | 435 | if int16Flag != int16FlagExpected { 436 | t.Fatal("int16 flag incorrect", int16Flag, int16FlagExpected) 437 | } 438 | 439 | for i, f := range int16SliceFlagExpected { 440 | if int16SliceFlag[i] != f { 441 | t.Fatal("int16Slice value incorrect", int16SliceFlag[i], f) 442 | } 443 | } 444 | 445 | if int8Flag != int8FlagExpected { 446 | t.Fatal("int8 flag incorrect", int8Flag, int8FlagExpected) 447 | } 448 | 449 | for i, f := range int8SliceFlagExpected { 450 | if int8SliceFlag[i] != f { 451 | t.Fatal("int8Slice value incorrect", int8SliceFlag[i], f) 452 | } 453 | } 454 | 455 | if !ipFlag.Equal(ipFlagExpected) { 456 | t.Fatal("ip flag incorrect", ipFlag, ipFlagExpected) 457 | } 458 | 459 | for i, f := range ipSliceFlagExpected { 460 | if !f.Equal(ipSliceFlag[i]) { 461 | t.Fatal("ipSlice value incorrect", ipSliceFlag[i], f) 462 | } 463 | } 464 | 465 | if hwFlag.String() != hwFlagExpected.String() { 466 | t.Fatal("hw flag incorrect", hwFlag, hwFlagExpected) 467 | } 468 | 469 | for i, f := range hwFlagSliceExpected { 470 | if f.String() != hwFlagSlice[i].String() { 471 | t.Fatal("hw flag slice value incorrect", hwFlagSlice[i].String(), f.String()) 472 | } 473 | } 474 | 475 | if maskFlag.String() != maskFlagExpected.String() { 476 | t.Fatal("mask flag incorrect", maskFlag, maskFlagExpected) 477 | } 478 | 479 | for i, f := range maskSliceFlagExpected { 480 | if f.String() != maskSliceFlag[i].String() { 481 | t.Fatal("mask flag slice value incorrect", maskSliceFlag[i].String(), f.String()) 482 | } 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /flaggy.go: -------------------------------------------------------------------------------- 1 | // Package flaggy is a input flag parsing package that supports recursive 2 | // subcommands, positional values, and any-position flags without 3 | // unnecessary complexeties. 4 | // 5 | // For a getting started tutorial and full feature list, check out the 6 | // readme at https://github.com/integrii/flaggy. 7 | package flaggy // import "github.com/integrii/flaggy" 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "net" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // strings used for builtin help and version flags both short and long 20 | const versionFlagLongName = "version" 21 | const helpFlagLongName = "help" 22 | const helpFlagShortName = "h" 23 | 24 | // defaultVersion is applied to parsers when they are created 25 | const defaultVersion = "0.0.0" 26 | 27 | // DebugMode indicates that debug output should be enabled 28 | var DebugMode bool 29 | 30 | // DefaultHelpTemplate is the help template that will be used 31 | // for newly created subcommands and commands 32 | var DefaultHelpTemplate = defaultHelpTemplate 33 | 34 | // DefaultParser is the default parser that is used with the package-level public 35 | // functions 36 | var DefaultParser *Parser 37 | 38 | // TrailingArguments holds trailing arguments in the main parser after parsing 39 | // has been run. 40 | var TrailingArguments []string 41 | 42 | func init() { 43 | 44 | // set the default help template 45 | // allow usage like flaggy.StringVar by enabling a default Parser 46 | ResetParser() 47 | } 48 | 49 | // ResetParser resets the default parser to a fresh instance. Uses the 50 | // name of the binary executing as the program name by default. 51 | func ResetParser() { 52 | if len(os.Args) > 0 { 53 | chunks := strings.Split(os.Args[0], "/") 54 | DefaultParser = NewParser(chunks[len(chunks)-1]) 55 | } else { 56 | DefaultParser = NewParser("default") 57 | } 58 | } 59 | 60 | // Parse parses flags as requested in the default package parser. All trailing arguments 61 | // that result from parsing are placed in the global TrailingArguments variable. 62 | func Parse() { 63 | err := DefaultParser.Parse() 64 | TrailingArguments = DefaultParser.TrailingArguments 65 | if err != nil { 66 | log.Panicln("Error from argument parser:", err) 67 | } 68 | } 69 | 70 | // ParseArgs parses the passed args as if they were the arguments to the 71 | // running binary. Targets the default main parser for the package. All trailing 72 | // arguments are set in the global TrailingArguments variable. 73 | func ParseArgs(args []string) { 74 | err := DefaultParser.ParseArgs(args) 75 | TrailingArguments = DefaultParser.TrailingArguments 76 | if err != nil { 77 | log.Panicln("Error from argument parser:", err) 78 | } 79 | } 80 | 81 | // String adds a new string flag 82 | func String(assignmentVar *string, shortName string, longName string, description string) { 83 | DefaultParser.add(assignmentVar, shortName, longName, description) 84 | } 85 | 86 | // StringSlice adds a new slice of strings flag 87 | // Specify the flag multiple times to fill the slice 88 | func StringSlice(assignmentVar *[]string, shortName string, longName string, description string) { 89 | DefaultParser.add(assignmentVar, shortName, longName, description) 90 | } 91 | 92 | // Bool adds a new bool flag 93 | func Bool(assignmentVar *bool, shortName string, longName string, description string) { 94 | DefaultParser.add(assignmentVar, shortName, longName, description) 95 | } 96 | 97 | // BoolSlice adds a new slice of bools flag 98 | // Specify the flag multiple times to fill the slice 99 | func BoolSlice(assignmentVar *[]bool, shortName string, longName string, description string) { 100 | DefaultParser.add(assignmentVar, shortName, longName, description) 101 | } 102 | 103 | // ByteSlice adds a new slice of bytes flag 104 | // Specify the flag multiple times to fill the slice. Takes hex as input. 105 | func ByteSlice(assignmentVar *[]byte, shortName string, longName string, description string) { 106 | DefaultParser.add(assignmentVar, shortName, longName, description) 107 | } 108 | 109 | // Duration adds a new time.Duration flag. 110 | // Input format is described in time.ParseDuration(). 111 | // Example values: 1h, 1h50m, 32s 112 | func Duration(assignmentVar *time.Duration, shortName string, longName string, description string) { 113 | DefaultParser.add(assignmentVar, shortName, longName, description) 114 | } 115 | 116 | // DurationSlice adds a new time.Duration flag. 117 | // Input format is described in time.ParseDuration(). 118 | // Example values: 1h, 1h50m, 32s 119 | // Specify the flag multiple times to fill the slice. 120 | func DurationSlice(assignmentVar *[]time.Duration, shortName string, longName string, description string) { 121 | DefaultParser.add(assignmentVar, shortName, longName, description) 122 | } 123 | 124 | // Float32 adds a new float32 flag. 125 | func Float32(assignmentVar *float32, shortName string, longName string, description string) { 126 | DefaultParser.add(assignmentVar, shortName, longName, description) 127 | } 128 | 129 | // Float32Slice adds a new float32 flag. 130 | // Specify the flag multiple times to fill the slice. 131 | func Float32Slice(assignmentVar *[]float32, shortName string, longName string, description string) { 132 | DefaultParser.add(assignmentVar, shortName, longName, description) 133 | } 134 | 135 | // Float64 adds a new float64 flag. 136 | func Float64(assignmentVar *float64, shortName string, longName string, description string) { 137 | DefaultParser.add(assignmentVar, shortName, longName, description) 138 | } 139 | 140 | // Float64Slice adds a new float64 flag. 141 | // Specify the flag multiple times to fill the slice. 142 | func Float64Slice(assignmentVar *[]float64, shortName string, longName string, description string) { 143 | DefaultParser.add(assignmentVar, shortName, longName, description) 144 | } 145 | 146 | // Int adds a new int flag 147 | func Int(assignmentVar *int, shortName string, longName string, description string) { 148 | DefaultParser.add(assignmentVar, shortName, longName, description) 149 | } 150 | 151 | // IntSlice adds a new int slice flag. 152 | // Specify the flag multiple times to fill the slice. 153 | func IntSlice(assignmentVar *[]int, shortName string, longName string, description string) { 154 | DefaultParser.add(assignmentVar, shortName, longName, description) 155 | } 156 | 157 | // UInt adds a new uint flag 158 | func UInt(assignmentVar *uint, shortName string, longName string, description string) { 159 | DefaultParser.add(assignmentVar, shortName, longName, description) 160 | } 161 | 162 | // UIntSlice adds a new uint slice flag. 163 | // Specify the flag multiple times to fill the slice. 164 | func UIntSlice(assignmentVar *[]uint, shortName string, longName string, description string) { 165 | DefaultParser.add(assignmentVar, shortName, longName, description) 166 | } 167 | 168 | // UInt64 adds a new uint64 flag 169 | func UInt64(assignmentVar *uint64, shortName string, longName string, description string) { 170 | DefaultParser.add(assignmentVar, shortName, longName, description) 171 | } 172 | 173 | // UInt64Slice adds a new uint64 slice flag. 174 | // Specify the flag multiple times to fill the slice. 175 | func UInt64Slice(assignmentVar *[]uint64, shortName string, longName string, description string) { 176 | DefaultParser.add(assignmentVar, shortName, longName, description) 177 | } 178 | 179 | // UInt32 adds a new uint32 flag 180 | func UInt32(assignmentVar *uint32, shortName string, longName string, description string) { 181 | DefaultParser.add(assignmentVar, shortName, longName, description) 182 | } 183 | 184 | // UInt32Slice adds a new uint32 slice flag. 185 | // Specify the flag multiple times to fill the slice. 186 | func UInt32Slice(assignmentVar *[]uint32, shortName string, longName string, description string) { 187 | DefaultParser.add(assignmentVar, shortName, longName, description) 188 | } 189 | 190 | // UInt16 adds a new uint16 flag 191 | func UInt16(assignmentVar *uint16, shortName string, longName string, description string) { 192 | DefaultParser.add(assignmentVar, shortName, longName, description) 193 | } 194 | 195 | // UInt16Slice adds a new uint16 slice flag. 196 | // Specify the flag multiple times to fill the slice. 197 | func UInt16Slice(assignmentVar *[]uint16, shortName string, longName string, description string) { 198 | DefaultParser.add(assignmentVar, shortName, longName, description) 199 | } 200 | 201 | // UInt8 adds a new uint8 flag 202 | func UInt8(assignmentVar *uint8, shortName string, longName string, description string) { 203 | DefaultParser.add(assignmentVar, shortName, longName, description) 204 | } 205 | 206 | // UInt8Slice adds a new uint8 slice flag. 207 | // Specify the flag multiple times to fill the slice. 208 | func UInt8Slice(assignmentVar *[]uint8, shortName string, longName string, description string) { 209 | DefaultParser.add(assignmentVar, shortName, longName, description) 210 | } 211 | 212 | // Int64 adds a new int64 flag 213 | func Int64(assignmentVar *int64, shortName string, longName string, description string) { 214 | DefaultParser.add(assignmentVar, shortName, longName, description) 215 | } 216 | 217 | // Int64Slice adds a new int64 slice flag. 218 | // Specify the flag multiple times to fill the slice. 219 | func Int64Slice(assignmentVar *[]int64, shortName string, longName string, description string) { 220 | DefaultParser.add(assignmentVar, shortName, longName, description) 221 | } 222 | 223 | // Int32 adds a new int32 flag 224 | func Int32(assignmentVar *int32, shortName string, longName string, description string) { 225 | DefaultParser.add(assignmentVar, shortName, longName, description) 226 | } 227 | 228 | // Int32Slice adds a new int32 slice flag. 229 | // Specify the flag multiple times to fill the slice. 230 | func Int32Slice(assignmentVar *[]int32, shortName string, longName string, description string) { 231 | DefaultParser.add(assignmentVar, shortName, longName, description) 232 | } 233 | 234 | // Int16 adds a new int16 flag 235 | func Int16(assignmentVar *int16, shortName string, longName string, description string) { 236 | DefaultParser.add(assignmentVar, shortName, longName, description) 237 | } 238 | 239 | // Int16Slice adds a new int16 slice flag. 240 | // Specify the flag multiple times to fill the slice. 241 | func Int16Slice(assignmentVar *[]int16, shortName string, longName string, description string) { 242 | DefaultParser.add(assignmentVar, shortName, longName, description) 243 | } 244 | 245 | // Int8 adds a new int8 flag 246 | func Int8(assignmentVar *int8, shortName string, longName string, description string) { 247 | DefaultParser.add(assignmentVar, shortName, longName, description) 248 | } 249 | 250 | // Int8Slice adds a new int8 slice flag. 251 | // Specify the flag multiple times to fill the slice. 252 | func Int8Slice(assignmentVar *[]int8, shortName string, longName string, description string) { 253 | DefaultParser.add(assignmentVar, shortName, longName, description) 254 | } 255 | 256 | // IP adds a new net.IP flag. 257 | func IP(assignmentVar *net.IP, shortName string, longName string, description string) { 258 | DefaultParser.add(assignmentVar, shortName, longName, description) 259 | } 260 | 261 | // IPSlice adds a new int8 slice flag. 262 | // Specify the flag multiple times to fill the slice. 263 | func IPSlice(assignmentVar *[]net.IP, shortName string, longName string, description string) { 264 | DefaultParser.add(assignmentVar, shortName, longName, description) 265 | } 266 | 267 | // HardwareAddr adds a new net.HardwareAddr flag. 268 | func HardwareAddr(assignmentVar *net.HardwareAddr, shortName string, longName string, description string) { 269 | DefaultParser.add(assignmentVar, shortName, longName, description) 270 | } 271 | 272 | // HardwareAddrSlice adds a new net.HardwareAddr slice flag. 273 | // Specify the flag multiple times to fill the slice. 274 | func HardwareAddrSlice(assignmentVar *[]net.HardwareAddr, shortName string, longName string, description string) { 275 | DefaultParser.add(assignmentVar, shortName, longName, description) 276 | } 277 | 278 | // IPMask adds a new net.IPMask flag. IPv4 Only. 279 | func IPMask(assignmentVar *net.IPMask, shortName string, longName string, description string) { 280 | DefaultParser.add(assignmentVar, shortName, longName, description) 281 | } 282 | 283 | // IPMaskSlice adds a new net.HardwareAddr slice flag. IPv4 only. 284 | // Specify the flag multiple times to fill the slice. 285 | func IPMaskSlice(assignmentVar *[]net.IPMask, shortName string, longName string, description string) { 286 | DefaultParser.add(assignmentVar, shortName, longName, description) 287 | } 288 | 289 | // AttachSubcommand adds a subcommand for parsing 290 | func AttachSubcommand(subcommand *Subcommand, relativePosition int) { 291 | DefaultParser.AttachSubcommand(subcommand, relativePosition) 292 | } 293 | 294 | // ShowHelp shows parser help 295 | func ShowHelp(message string) { 296 | DefaultParser.ShowHelpWithMessage(message) 297 | } 298 | 299 | // SetDescription sets the description of the default package command parser 300 | func SetDescription(description string) { 301 | DefaultParser.Description = description 302 | } 303 | 304 | // SetVersion sets the version of the default package command parser 305 | func SetVersion(version string) { 306 | DefaultParser.Version = version 307 | } 308 | 309 | // SetName sets the name of the default package command parser 310 | func SetName(name string) { 311 | DefaultParser.Name = name 312 | } 313 | 314 | // ShowHelpAndExit shows parser help and exits with status code 2 315 | func ShowHelpAndExit(message string) { 316 | ShowHelp(message) 317 | exitOrPanic(2) 318 | } 319 | 320 | // PanicInsteadOfExit is used when running tests 321 | var PanicInsteadOfExit bool 322 | 323 | // exitOrPanic panics instead of calling os.Exit so that tests can catch 324 | // more failures 325 | func exitOrPanic(code int) { 326 | if PanicInsteadOfExit { 327 | panic("Panic instead of exit with code: " + strconv.Itoa(code)) 328 | } 329 | os.Exit(code) 330 | } 331 | 332 | // ShowHelpOnUnexpectedEnable enables the ShowHelpOnUnexpected behavior on the 333 | // default parser. This causes unknown inputs to error out. 334 | func ShowHelpOnUnexpectedEnable() { 335 | DefaultParser.ShowHelpOnUnexpected = true 336 | } 337 | 338 | // ShowHelpOnUnexpectedDisable disables the ShowHelpOnUnexpected behavior on the 339 | // default parser. This causes unknown inputs to error out. 340 | func ShowHelpOnUnexpectedDisable() { 341 | DefaultParser.ShowHelpOnUnexpected = false 342 | } 343 | 344 | // AddPositionalValue adds a positional value to the main parser at the global 345 | // context 346 | func AddPositionalValue(assignmentVar *string, name string, relativePosition int, required bool, description string) { 347 | DefaultParser.AddPositionalValue(assignmentVar, name, relativePosition, required, description) 348 | } 349 | 350 | // debugPrint prints if debugging is enabled 351 | func debugPrint(i ...interface{}) { 352 | if DebugMode { 353 | fmt.Println(i...) 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /flaggy_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/integrii/flaggy" 8 | ) 9 | 10 | // TestTrailingArgumentsDashes tests trailing argument parsing when --- is used 11 | func TestTrailingArgumentsDashes(t *testing.T) { 12 | 13 | flaggy.ResetParser() 14 | args := []string{"./flaggy.test", "--", "one", "two"} 15 | os.Args = args 16 | flaggy.Parse() 17 | if len(flaggy.TrailingArguments) != 2 { 18 | t.Fatal("incorrect argument count parsed. Got", len(flaggy.TrailingArguments), "but expected", 2) 19 | } 20 | 21 | if flaggy.TrailingArguments[0] != "one" { 22 | t.Fatal("incorrect argument parsed. Got", flaggy.TrailingArguments[0], "but expected one") 23 | } 24 | 25 | if flaggy.TrailingArguments[1] != "two" { 26 | t.Fatal("incorrect argument parsed. Got", flaggy.TrailingArguments[1], "but expected two") 27 | } 28 | } 29 | 30 | // TestTrailingArgumentsNoDashes tests trailing argument parsing without using --- 31 | func TestTrailingArgumentsNoDashes(t *testing.T) { 32 | 33 | flaggy.ResetParser() 34 | var positionalValue string 35 | args := []string{"./flaggy.test", "positional", "one", "two"} 36 | os.Args = args 37 | 38 | flaggy.ShowHelpOnUnexpectedDisable() 39 | flaggy.AddPositionalValue(&positionalValue, "testPositional", 1, false, "a test positional") 40 | 41 | flaggy.Parse() 42 | if len(flaggy.TrailingArguments) != 2 { 43 | t.Fatal("incorrect argument count parsed. Got", len(flaggy.TrailingArguments), "but expected", 2) 44 | } 45 | 46 | if flaggy.TrailingArguments[0] != "one" { 47 | t.Fatal("incorrect argument parsed. Got", flaggy.TrailingArguments[0], "but expected one") 48 | } 49 | 50 | if flaggy.TrailingArguments[1] != "two" { 51 | t.Fatal("incorrect argument parsed. Got", flaggy.TrailingArguments[1], "but expected two") 52 | } 53 | 54 | if positionalValue != "positional" { 55 | t.Fatal("expected positional value was not found set to the string 'positional'") 56 | } 57 | } 58 | 59 | // TestComplexNesting tests various levels of nested subcommands and 60 | // positional values intermixed with eachother. 61 | func TestComplexNesting(t *testing.T) { 62 | 63 | flaggy.DebugMode = true 64 | defer debugOff() 65 | 66 | flaggy.ResetParser() 67 | 68 | var testA string 69 | var testB string 70 | var testC string 71 | var testD string 72 | var testE string 73 | var testF bool 74 | 75 | scA := flaggy.NewSubcommand("scA") 76 | scB := flaggy.NewSubcommand("scB") 77 | scC := flaggy.NewSubcommand("scC") 78 | scD := flaggy.NewSubcommand("scD") 79 | 80 | flaggy.Bool(&testF, "f", "testF", "") 81 | 82 | flaggy.AttachSubcommand(scA, 1) 83 | 84 | scA.AddPositionalValue(&testA, "testA", 1, false, "") 85 | scA.AddPositionalValue(&testB, "testB", 2, false, "") 86 | scA.AddPositionalValue(&testC, "testC", 3, false, "") 87 | scA.AttachSubcommand(scB, 4) 88 | 89 | scB.AddPositionalValue(&testD, "testD", 1, false, "") 90 | scB.AttachSubcommand(scC, 2) 91 | 92 | scC.AttachSubcommand(scD, 1) 93 | 94 | scD.AddPositionalValue(&testE, "testE", 1, true, "") 95 | 96 | args := []string{"scA", "-f", "A", "B", "C", "scB", "D", "scC", "scD", "E"} 97 | t.Log(args) 98 | flaggy.ParseArgs(args) 99 | 100 | if !testF { 101 | t.Log("testF", testF) 102 | t.FailNow() 103 | } 104 | if !scA.Used { 105 | t.Log("sca", scA.Name) 106 | t.FailNow() 107 | } 108 | if !scB.Used { 109 | t.Log("scb", scB.Name) 110 | t.FailNow() 111 | } 112 | if !scC.Used { 113 | t.Log("scc", scC.Name) 114 | t.FailNow() 115 | } 116 | if !scD.Used { 117 | t.Log("scd", scD.Name) 118 | t.FailNow() 119 | } 120 | if testA != "A" { 121 | t.Log("testA", testA) 122 | t.FailNow() 123 | } 124 | if testB != "B" { 125 | t.Log("testB", testB) 126 | t.FailNow() 127 | } 128 | if testC != "C" { 129 | t.Log("testC", testC) 130 | t.FailNow() 131 | } 132 | if testD != "D" { 133 | t.Log("testD", testD) 134 | t.FailNow() 135 | } 136 | if testE != "E" { 137 | t.Log("testE", testE) 138 | t.FailNow() 139 | } 140 | if subcommandName := flaggy.DefaultParser.TrailingSubcommand().Name; subcommandName != "scD" { 141 | t.Fatal("Used subcommand was incorrect:", subcommandName) 142 | } 143 | 144 | } 145 | 146 | func TestParsePositionalsA(t *testing.T) { 147 | inputLine := []string{"-t", "-i=3", "subcommand", "-n", "testN", "-j=testJ", "positionalA", "positionalB", "--testK=testK", "--", "trailingA", "trailingB"} 148 | 149 | flaggy.DebugMode = true 150 | 151 | var boolT bool 152 | var intT int 153 | var testN string 154 | var testJ string 155 | var testK string 156 | var positionalA string 157 | var positionalB string 158 | var err error 159 | 160 | // make a new parser 161 | parser := flaggy.NewParser("testParser") 162 | 163 | // add a bool flag to the parser 164 | parser.Bool(&boolT, "t", "", "test flag for bool arg") 165 | // add an int flag to the parser 166 | parser.Int(&intT, "i", "", "test flag for int arg") 167 | 168 | // create a subcommand 169 | subCommand := flaggy.NewSubcommand("subcommand") 170 | parser.AttachSubcommand(subCommand, 1) 171 | 172 | // add flags to subcommand 173 | subCommand.String(&testN, "n", "testN", "test flag for value with space arg") 174 | subCommand.String(&testJ, "j", "testJ", "test flag for value with equals arg") 175 | subCommand.String(&testK, "k", "testK", "test full length flag with attached arg") 176 | 177 | // add positionals to subcommand 178 | subCommand.AddPositionalValue(&positionalA, "PositionalA", 1, false, "PositionalA test value") 179 | subCommand.AddPositionalValue(&positionalB, "PositionalB", 2, false, "PositionalB test value") 180 | 181 | // parse input 182 | err = parser.ParseArgs(inputLine) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | 187 | // check the results 188 | if intT != 3 { 189 | t.Fatal("Global int flag -i was incorrect:", intT) 190 | } 191 | if boolT != true { 192 | t.Fatal("Global bool flag -t was incorrect:", boolT) 193 | } 194 | if testN != "testN" { 195 | t.Fatal("Subcommand flag testN was incorrect:", testN) 196 | } 197 | if positionalA != "positionalA" { 198 | t.Fatal("Positional A was incorrect:", positionalA) 199 | } 200 | if positionalB != "positionalB" { 201 | t.Fatal("Positional B was incorrect:", positionalB) 202 | } 203 | if len(parser.TrailingArguments) < 2 { 204 | t.Fatal("Incorrect number of trailing arguments. Got", len(parser.TrailingArguments)) 205 | } 206 | if parser.TrailingArguments[0] != "trailingA" { 207 | t.Fatal("Trailing argumentA was incorrect:", parser.TrailingArguments[0]) 208 | } 209 | if parser.TrailingArguments[1] != "trailingB" { 210 | t.Fatal("Trailing argumentB was incorrect:", parser.TrailingArguments[1]) 211 | } 212 | if subcommandName := parser.TrailingSubcommand().Name; subcommandName != "subcommand" { 213 | t.Fatal("Used subcommand was incorrect:", subcommandName) 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/integrii/flaggy 2 | 3 | go 1.12 4 | 5 | require github.com/google/go-cmp v0.5.6 // for tests only 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | // defaultHelpTemplate is the help template used by default 4 | // {{if (or (or (gt (len .StringFlags) 0) (gt (len .IntFlags) 0)) (gt (len .BoolFlags) 0))}} 5 | // {{if (or (gt (len .StringFlags) 0) (gt (len .BoolFlags) 0))}} 6 | const defaultHelpTemplate = `{{.CommandName}}{{if .Description}} - {{.Description}}{{end}}{{if .PrependMessage}} 7 | {{.PrependMessage}}{{end}} 8 | {{if .UsageString}} 9 | Usage: 10 | {{.UsageString}}{{end}}{{if .Positionals}} 11 | 12 | Positional Variables: {{range .Positionals}} 13 | {{.Name}} {{.Spacer}}{{if .Description}} {{.Description}}{{end}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{else}}{{if .Required}} (Required){{end}}{{end}}{{end}}{{end}}{{if .Subcommands}} 14 | 15 | Subcommands: {{range .Subcommands}} 16 | {{.LongName}}{{if .ShortName}} ({{.ShortName}}){{end}}{{if .Position}}{{if gt .Position 1}} (position {{.Position}}){{end}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{end}}{{end}} 17 | {{end}}{{if (gt (len .Flags) 0)}} 18 | Flags: {{if .Flags}}{{range .Flags}} 19 | {{if .ShortName}}-{{.ShortName}} {{else}} {{end}}{{if .LongName}}--{{.LongName}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{end}} 20 | {{end}}{{if .AppendMessage}}{{.AppendMessage}} 21 | {{end}}{{if .Message}} 22 | {{.Message}}{{end}} 23 | ` 24 | -------------------------------------------------------------------------------- /helpValues.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "strings" 7 | "unicode/utf8" 8 | ) 9 | 10 | // Help represents the values needed to render a Help page 11 | type Help struct { 12 | Subcommands []HelpSubcommand 13 | Positionals []HelpPositional 14 | Flags []HelpFlag 15 | UsageString string 16 | CommandName string 17 | PrependMessage string 18 | AppendMessage string 19 | Message string 20 | Description string 21 | } 22 | 23 | // HelpSubcommand is used to template subcommand Help output 24 | type HelpSubcommand struct { 25 | ShortName string 26 | LongName string 27 | Description string 28 | Position int 29 | Spacer string 30 | } 31 | 32 | // HelpPositional is used to template positional Help output 33 | type HelpPositional struct { 34 | Name string 35 | Description string 36 | Required bool 37 | Position int 38 | DefaultValue string 39 | Spacer string 40 | } 41 | 42 | // HelpFlag is used to template string flag Help output 43 | type HelpFlag struct { 44 | ShortName string 45 | LongName string 46 | Description string 47 | DefaultValue string 48 | Spacer string 49 | } 50 | 51 | // ExtractValues extracts Help template values from a subcommand and its parent 52 | // parser. The parser is required in order to detect default flag settings 53 | // for help and version output. 54 | func (h *Help) ExtractValues(p *Parser, message string) { 55 | // accept message string for output 56 | h.Message = message 57 | 58 | // extract Help values from the current subcommand in context 59 | // prependMessage string 60 | h.PrependMessage = p.subcommandContext.AdditionalHelpPrepend 61 | // appendMessage string 62 | h.AppendMessage = p.subcommandContext.AdditionalHelpAppend 63 | // command name 64 | h.CommandName = p.subcommandContext.Name 65 | // description 66 | h.Description = p.subcommandContext.Description 67 | 68 | maxLength := getLongestNameLength(p.subcommandContext.Subcommands, 0) 69 | 70 | // subcommands []HelpSubcommand 71 | for _, cmd := range p.subcommandContext.Subcommands { 72 | if cmd.Hidden { 73 | continue 74 | } 75 | newHelpSubcommand := HelpSubcommand{ 76 | ShortName: cmd.ShortName, 77 | LongName: cmd.Name, 78 | Description: cmd.Description, 79 | Position: cmd.Position, 80 | Spacer: makeSpacer(cmd.Name, maxLength), 81 | } 82 | h.Subcommands = append(h.Subcommands, newHelpSubcommand) 83 | } 84 | 85 | maxLength = getLongestNameLength(p.subcommandContext.PositionalFlags, 0) 86 | 87 | // parse positional flags into help output structs 88 | for _, pos := range p.subcommandContext.PositionalFlags { 89 | if pos.Hidden { 90 | continue 91 | } 92 | newHelpPositional := HelpPositional{ 93 | Name: pos.Name, 94 | Position: pos.Position, 95 | Description: pos.Description, 96 | Required: pos.Required, 97 | DefaultValue: pos.defaultValue, 98 | Spacer: makeSpacer(pos.Name, maxLength), 99 | } 100 | h.Positionals = append(h.Positionals, newHelpPositional) 101 | } 102 | 103 | maxLength = len(versionFlagLongName) 104 | if len(helpFlagLongName) > maxLength { 105 | maxLength = len(helpFlagLongName) 106 | } 107 | maxLength = getLongestNameLength(p.subcommandContext.Flags, maxLength) 108 | maxLength = getLongestNameLength(p.Flags, maxLength) 109 | 110 | // if the built-in version flag is enabled, then add it as a help flag 111 | if p.ShowVersionWithVersionFlag { 112 | defaultVersionFlag := HelpFlag{ 113 | ShortName: "", 114 | LongName: versionFlagLongName, 115 | Description: "Displays the program version string.", 116 | DefaultValue: "", 117 | Spacer: makeSpacer(versionFlagLongName, maxLength), 118 | } 119 | h.Flags = append(h.Flags, defaultVersionFlag) 120 | } 121 | 122 | // if the built-in help flag exists, then add it as a help flag 123 | if p.ShowHelpWithHFlag { 124 | defaultHelpFlag := HelpFlag{ 125 | ShortName: helpFlagShortName, 126 | LongName: helpFlagLongName, 127 | Description: "Displays help with available flag, subcommand, and positional value parameters.", 128 | DefaultValue: "", 129 | Spacer: makeSpacer(helpFlagLongName, maxLength), 130 | } 131 | h.Flags = append(h.Flags, defaultHelpFlag) 132 | } 133 | 134 | // go through every flag in the subcommand and add it to help output 135 | h.parseFlagsToHelpFlags(p.subcommandContext.Flags, maxLength) 136 | 137 | // go through every flag in the parent parser and add it to help output 138 | h.parseFlagsToHelpFlags(p.Flags, maxLength) 139 | 140 | // formulate the usage string 141 | // first, we capture all the command and positional names by position 142 | commandsByPosition := make(map[int]string) 143 | for _, pos := range p.subcommandContext.PositionalFlags { 144 | if pos.Hidden { 145 | continue 146 | } 147 | if len(commandsByPosition[pos.Position]) > 0 { 148 | commandsByPosition[pos.Position] = commandsByPosition[pos.Position] + "|" + pos.Name 149 | } else { 150 | commandsByPosition[pos.Position] = pos.Name 151 | } 152 | } 153 | for _, cmd := range p.subcommandContext.Subcommands { 154 | if cmd.Hidden { 155 | continue 156 | } 157 | if len(commandsByPosition[cmd.Position]) > 0 { 158 | commandsByPosition[cmd.Position] = commandsByPosition[cmd.Position] + "|" + cmd.Name 159 | } else { 160 | commandsByPosition[cmd.Position] = cmd.Name 161 | } 162 | } 163 | 164 | // find the highest position count in the map 165 | var highestPosition int 166 | for i := range commandsByPosition { 167 | if i > highestPosition { 168 | highestPosition = i 169 | } 170 | } 171 | 172 | // only have a usage string if there are positional items 173 | var usageString string 174 | if highestPosition > 0 { 175 | // find each positional value and make our final string 176 | usageString = p.subcommandContext.Name 177 | for i := 1; i <= highestPosition; i++ { 178 | if len(commandsByPosition[i]) > 0 { 179 | usageString = usageString + " [" + commandsByPosition[i] + "]" 180 | } else { 181 | // dont keep listing after the first position without any properties 182 | // it will be impossible to reach anything beyond here anyway 183 | break 184 | } 185 | } 186 | } 187 | 188 | h.UsageString = usageString 189 | } 190 | 191 | // parseFlagsToHelpFlags parses the specified slice of flags into 192 | // help flags on the the calling help command 193 | func (h *Help) parseFlagsToHelpFlags(flags []*Flag, maxLength int) { 194 | for _, f := range flags { 195 | if f.Hidden { 196 | continue 197 | } 198 | 199 | // parse help values out if the flag hasn't been parsed yet 200 | if !f.parsed { 201 | f.parsed = true 202 | // parse the default value as a string and remember it for help output 203 | f.defaultValue, _ = f.returnAssignmentVarValueAsString() 204 | } 205 | 206 | // determine the default value based on the assignment variable 207 | defaultValue := f.defaultValue 208 | 209 | // dont show nils 210 | if defaultValue == "" { 211 | defaultValue = "" 212 | } 213 | 214 | // for bools, dont show a default of false 215 | _, isBool := f.AssignmentVar.(*bool) 216 | if isBool { 217 | b := f.AssignmentVar.(*bool) 218 | if !*b { 219 | defaultValue = "" 220 | } 221 | } 222 | 223 | newHelpFlag := HelpFlag{ 224 | ShortName: f.ShortName, 225 | LongName: f.LongName, 226 | Description: f.Description, 227 | DefaultValue: defaultValue, 228 | Spacer: makeSpacer(f.LongName, maxLength), 229 | } 230 | h.AddFlagToHelp(newHelpFlag) 231 | } 232 | } 233 | 234 | // AddFlagToHelp adds a flag to help output if it does not exist 235 | func (h *Help) AddFlagToHelp(f HelpFlag) { 236 | for _, existingFlag := range h.Flags { 237 | if len(existingFlag.ShortName) > 0 && existingFlag.ShortName == f.ShortName { 238 | return 239 | } 240 | if len(existingFlag.LongName) > 0 && existingFlag.LongName == f.LongName { 241 | return 242 | } 243 | } 244 | h.Flags = append(h.Flags, f) 245 | } 246 | 247 | // getLongestNameLength takes a slice of any supported flag and returns the length of the longest of their names 248 | func getLongestNameLength(slice interface{}, min int) int { 249 | var maxLength = min 250 | 251 | s := reflect.ValueOf(slice) 252 | if s.Kind() != reflect.Slice { 253 | log.Panicf("Paremeter given to getLongestNameLength() is of type %s. Expected slice", s.Kind()) 254 | } 255 | 256 | for i := 0; i < s.Len(); i++ { 257 | option := s.Index(i).Interface() 258 | var name string 259 | switch t := option.(type) { 260 | case *Subcommand: 261 | name = t.Name 262 | case *Flag: 263 | name = t.LongName 264 | case *PositionalValue: 265 | name = t.Name 266 | default: 267 | log.Panicf("Unexpected type %T found in slice passed to getLongestNameLength(). Possible types: *Subcommand, *Flag, *PositionalValue", t) 268 | } 269 | length := len(name) 270 | if length > maxLength { 271 | maxLength = length 272 | } 273 | } 274 | 275 | return maxLength 276 | } 277 | 278 | // makeSpacer creates a string of whitespaces, with a length of the given 279 | // maxLength minus the length of the given name 280 | func makeSpacer(name string, maxLength int) string { 281 | length := maxLength - utf8.RuneCountInString(name) 282 | if length < 0 { 283 | length = 0 284 | } 285 | return strings.Repeat(" ", length) 286 | } 287 | -------------------------------------------------------------------------------- /helpValues_blackbox_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/integrii/flaggy" 11 | ) 12 | 13 | func TestMinimalHelpOutput(t *testing.T) { 14 | p := flaggy.NewParser("TestMinimalHelpOutput") 15 | 16 | rd, wr, err := os.Pipe() 17 | if err != nil { 18 | t.Fatalf("pipe: error: %s", err) 19 | } 20 | savedStderr := os.Stderr 21 | os.Stderr = wr 22 | 23 | defer func() { 24 | os.Stderr = savedStderr 25 | }() 26 | 27 | p.ShowHelp() 28 | 29 | buf := make([]byte, 1024) 30 | n, err := rd.Read(buf) 31 | if err != nil { 32 | t.Fatalf("read: error: %s", err) 33 | } 34 | got := strings.Split(string(buf[:n]), "\n") 35 | want := []string{ 36 | "", 37 | "", 38 | " Flags: ", 39 | " --version Displays the program version string.", 40 | " -h --help Displays help with available flag, subcommand, and positional value parameters.", 41 | "", 42 | "", 43 | } 44 | 45 | if diff := cmp.Diff(want, got); diff != "" { 46 | t.Errorf("help mismatch (-want +got):\n%s", diff) 47 | } 48 | } 49 | 50 | func TestHelpWithMissingSCName(t *testing.T) { 51 | defer func() { 52 | r := recover() 53 | gotMsg := r.(string) 54 | wantMsg := "Panic instead of exit with code: 2" 55 | if gotMsg != wantMsg { 56 | t.Fatalf("error: got: %s; want: %s", gotMsg, wantMsg) 57 | } 58 | }() 59 | flaggy.ResetParser() 60 | flaggy.PanicInsteadOfExit = true 61 | sc := flaggy.NewSubcommand("") 62 | sc.ShortName = "sn" 63 | flaggy.AttachSubcommand(sc, 1) 64 | flaggy.ParseArgs([]string{"x"}) 65 | } 66 | 67 | // TestHelpOutput tests the dislay of help with -h 68 | func TestHelpOutput(t *testing.T) { 69 | flaggy.ResetParser() 70 | // flaggy.DebugMode = true 71 | // defer debugOff() 72 | 73 | p := flaggy.NewParser("testCommand") 74 | p.Description = "Description goes here. Get more information at https://github.com/integrii/flaggy." 75 | scA := flaggy.NewSubcommand("subcommandA") 76 | scA.ShortName = "a" 77 | scA.Description = "Subcommand A is a command that does stuff" 78 | scB := flaggy.NewSubcommand("subcommandB") 79 | scB.ShortName = "b" 80 | scB.Description = "Subcommand B is a command that does other stuff" 81 | scX := flaggy.NewSubcommand("subcommandX") 82 | scX.Description = "This should be hidden." 83 | scX.Hidden = true 84 | 85 | var posA = "defaultPosA" 86 | var posB string 87 | p.AttachSubcommand(scA, 1) 88 | scA.AttachSubcommand(scB, 1) 89 | scA.AddPositionalValue(&posA, "testPositionalA", 2, false, "Test positional A does some things with a positional value.") 90 | scB.AddPositionalValue(&posB, "hiddenPositional", 1, false, "Hidden test positional B does some less than serious things with a positional value.") 91 | scB.PositionalFlags[0].Hidden = true 92 | var stringFlag = "defaultStringHere" 93 | var intFlag int 94 | var boolFlag bool 95 | var durationFlag time.Duration 96 | p.String(&stringFlag, "s", "stringFlag", "This is a test string flag that does some stringy string stuff.") 97 | p.Int(&intFlag, "i", "intFlg", "This is a test int flag that does some interesting int stuff.") 98 | p.Bool(&boolFlag, "b", "boolFlag", "This is a test bool flag that does some booly bool stuff.") 99 | p.Duration(&durationFlag, "d", "durationFlag", "This is a test duration flag that does some untimely stuff.") 100 | p.AdditionalHelpPrepend = "This is a prepend for help" 101 | p.AdditionalHelpAppend = "This is an append for help" 102 | 103 | rd, wr, err := os.Pipe() 104 | if err != nil { 105 | t.Fatalf("pipe: error: %s", err) 106 | } 107 | savedStderr := os.Stderr 108 | os.Stderr = wr 109 | 110 | defer func() { 111 | os.Stderr = savedStderr 112 | }() 113 | 114 | if err := p.ParseArgs([]string{"subcommandA", "subcommandB", "hiddenPositional1"}); err != nil { 115 | t.Fatalf("got: %s; want: no error", err) 116 | } 117 | p.ShowHelpWithMessage("This is a help message on exit") 118 | 119 | buf := make([]byte, 1024) 120 | n, err := rd.Read(buf) 121 | if err != nil { 122 | t.Fatalf("read: error: %s", err) 123 | } 124 | got := strings.Split(string(buf[:n]), "\n") 125 | want := []string{ 126 | "subcommandB - Subcommand B is a command that does other stuff", 127 | "", 128 | " Flags: ", 129 | " --version Displays the program version string.", 130 | " -h --help Displays help with available flag, subcommand, and positional value parameters.", 131 | " -s --stringFlag This is a test string flag that does some stringy string stuff. (default: defaultStringHere)", 132 | " -i --intFlg This is a test int flag that does some interesting int stuff. (default: 0)", 133 | " -b --boolFlag This is a test bool flag that does some booly bool stuff.", 134 | " -d --durationFlag This is a test duration flag that does some untimely stuff. (default: 0s)", 135 | "", 136 | "This is a help message on exit", 137 | "", 138 | } 139 | 140 | if diff := cmp.Diff(want, got); diff != "" { 141 | t.Errorf("help mismatch (-want +got):\n%s", diff) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /helpValues_whitebox_test.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMakeSpacer(t *testing.T) { 8 | if spacer := makeSpacer("short", 20); len(spacer) != 15 { 9 | t.Errorf("spacer length expected to be 15, got %d.", len(spacer)) 10 | } 11 | 12 | if spacer := makeSpacer("very long", 20); len(spacer) != 11 { 13 | t.Errorf("spacer length expected to be 11, got %d.", len(spacer)) 14 | } 15 | 16 | if spacer := makeSpacer("very long", 0); len(spacer) != 0 { 17 | t.Errorf("spacer length expected to be 0, got %d.", len(spacer)) 18 | } 19 | } 20 | 21 | func TestGetLongestNameLength(t *testing.T) { 22 | input := []string{"short", "longer", "very-long"} 23 | var subcommands []*Subcommand 24 | var flags []*Flag 25 | var positionalValues []*PositionalValue 26 | 27 | for _, name := range input { 28 | subcommands = append(subcommands, NewSubcommand(name)) 29 | flags = append(flags, &Flag{LongName: name}) 30 | positionalValues = append(positionalValues, &PositionalValue{Name: name}) 31 | } 32 | 33 | if l := getLongestNameLength(subcommands, 0); l != 9 { 34 | t.Errorf("should have returned 9, got %d.", l) 35 | } 36 | 37 | if l := getLongestNameLength(subcommands, 15); l != 15 { 38 | t.Errorf("should have returned 15, got %d.", l) 39 | } 40 | 41 | if l := getLongestNameLength(flags, 0); l != 9 { 42 | t.Errorf("should have returned 9, got %d.", l) 43 | } 44 | 45 | if l := getLongestNameLength(flags, 15); l != 15 { 46 | t.Errorf("should have returned 15, got %d.", l) 47 | } 48 | 49 | if l := getLongestNameLength(positionalValues, 0); l != 9 { 50 | t.Errorf("should have returned 15, got %d.", l) 51 | } 52 | 53 | if l := getLongestNameLength(positionalValues, 15); l != 15 { 54 | t.Errorf("should have returned 9, got %d.", l) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/integrii/flaggy/7f802351f0f4a4d7b6646af52302245fb0638d78/logo.png -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/integrii/flaggy" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | flaggy.PanicInsteadOfExit = true 12 | // flaggy.DebugMode = true 13 | os.Exit(m.Run()) 14 | } 15 | 16 | func TestSetDescription(t *testing.T) { 17 | desc := "Test Description" 18 | flaggy.SetDescription(desc) 19 | if flaggy.DefaultParser.Description != desc { 20 | t.Fatal("set description does not match") 21 | } 22 | } 23 | 24 | func TestSetVersion(t *testing.T) { 25 | ver := "Test Version" 26 | flaggy.SetVersion(ver) 27 | if flaggy.DefaultParser.Version != ver { 28 | t.Fatal("set version does not match") 29 | } 30 | } 31 | 32 | func TestParserWithNoArgs(t *testing.T) { 33 | os.Args = []string{} 34 | flaggy.ResetParser() 35 | } 36 | 37 | func TestSetName(t *testing.T) { 38 | name := "Test Name" 39 | flaggy.SetName(name) 40 | if flaggy.DefaultParser.Name != name { 41 | t.Fatal("set name does not match") 42 | } 43 | } 44 | 45 | func TestShowHelpAndExit(t *testing.T) { 46 | flaggy.PanicInsteadOfExit = true 47 | defer func() { 48 | r := recover() 49 | if r == nil { 50 | t.Fatal("Expected panic on show help and exit call") 51 | } 52 | }() 53 | flaggy.ShowHelpAndExit("test show help and exit") 54 | } 55 | -------------------------------------------------------------------------------- /parsedValue.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | // parsedValue represents a flag or subcommand that was parsed. Primairily used 4 | // to account for all parsed values in order to determine if unknown values were 5 | // passed to the root parser after all subcommands have been parsed. 6 | type parsedValue struct { 7 | Key string 8 | Value string 9 | IsPositional bool // indicates that this value was positional and not a key/value 10 | } 11 | 12 | // newParsedValue creates and returns a new parsedValue struct with the 13 | // supplied values set 14 | func newParsedValue(key string, value string, isPositional bool) parsedValue { 15 | if len(key) == 0 && len(value) == 0 { 16 | panic("cant add parsed value with no key or value") 17 | } 18 | return parsedValue{ 19 | Key: key, 20 | Value: value, 21 | IsPositional: isPositional, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "strings" 9 | 10 | "text/template" 11 | ) 12 | 13 | // Parser represents the set of flags and subcommands we are expecting 14 | // from our input arguments. Parser is the top level struct responsible for 15 | // parsing an entire set of subcommands and flags. 16 | type Parser struct { 17 | Subcommand 18 | Version string // the optional version of the parser. 19 | ShowHelpWithHFlag bool // display help when -h or --help passed 20 | ShowVersionWithVersionFlag bool // display the version when --version passed 21 | ShowHelpOnUnexpected bool // display help when an unexpected flag or subcommand is passed 22 | TrailingArguments []string // everything after a -- is placed here 23 | HelpTemplate *template.Template // template for Help output 24 | trailingArgumentsExtracted bool // indicates that trailing args have been parsed and should not be appended again 25 | parsed bool // indicates this parser has parsed 26 | subcommandContext *Subcommand // points to the most specific subcommand being used 27 | } 28 | 29 | // TrailingSubcommand returns the last and most specific subcommand invoked. 30 | func (p *Parser) TrailingSubcommand() *Subcommand { 31 | return p.subcommandContext 32 | } 33 | 34 | // NewParser creates a new ArgumentParser ready to parse inputs 35 | func NewParser(name string) *Parser { 36 | // this can not be done inline because of struct embedding 37 | p := &Parser{} 38 | p.Name = name 39 | p.Version = defaultVersion 40 | p.ShowHelpOnUnexpected = true 41 | p.ShowHelpWithHFlag = true 42 | p.ShowVersionWithVersionFlag = true 43 | p.SetHelpTemplate(DefaultHelpTemplate) 44 | p.subcommandContext = &Subcommand{} 45 | return p 46 | } 47 | 48 | // ParseArgs parses as if the passed args were the os.Args, but without the 49 | // binary at the 0 position in the array. An error is returned if there 50 | // is a low level issue converting flags to their proper type. No error 51 | // is returned for invalid arguments or missing require subcommands. 52 | func (p *Parser) ParseArgs(args []string) error { 53 | if p.parsed { 54 | return errors.New("Parser.Parse() called twice on parser with name: " + " " + p.Name + " " + p.ShortName) 55 | } 56 | p.parsed = true 57 | 58 | debugPrint("Kicking off parsing with args:", args) 59 | err := p.parse(p, args, 0) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // if we are set to crash on unexpected args, look for those here TODO 65 | if p.ShowHelpOnUnexpected { 66 | parsedValues := p.findAllParsedValues() 67 | debugPrint("parsedValues:", parsedValues) 68 | argsNotParsed := findArgsNotInParsedValues(args, parsedValues) 69 | if len(argsNotParsed) > 0 { 70 | // flatten out unused args for our error message 71 | var argsNotParsedFlat string 72 | for _, a := range argsNotParsed { 73 | argsNotParsedFlat = argsNotParsedFlat + " " + a 74 | } 75 | p.ShowHelpAndExit("Unknown arguments supplied: " + argsNotParsedFlat) 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // findArgsNotInParsedValues finds arguments not used in parsed values. The 83 | // incoming args should be in the order supplied by the user and should not 84 | // include the invoked binary, which is normally the first thing in os.Args. 85 | func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []string { 86 | // DebugMode = true 87 | // defer func() { 88 | // DebugMode = false 89 | // }() 90 | 91 | var argsNotUsed []string 92 | var skipNext bool 93 | for _, a := range args { 94 | 95 | // if the final argument (--) is seen, then we stop checking because all 96 | // further values are trailing arguments. 97 | if determineArgType(a) == argIsFinal { 98 | return argsNotUsed 99 | } 100 | 101 | // allow for skipping the next arg when needed 102 | if skipNext { 103 | skipNext = false 104 | continue 105 | } 106 | 107 | // strip flag slashes from incoming arguments so they match up with the 108 | // keys from parsedValues. 109 | arg := parseFlagToName(a) 110 | 111 | // skip args that start with 'test.' because they are injected with go test 112 | debugPrint("flagsNotParsed: checking arg for test prefix:", arg) 113 | if strings.HasPrefix(arg, "test.") { 114 | debugPrint("skipping test. prefixed arg has test prefix:", arg) 115 | continue 116 | } 117 | debugPrint("flagsNotParsed: flag is not a test. flag:", arg) 118 | 119 | // indicates that we found this arg used in one of the parsed values. Used 120 | // to indicate which values should be added to argsNotUsed. 121 | var foundArgUsed bool 122 | 123 | // search all args for a corresponding parsed value 124 | for _, pv := range parsedValues { 125 | // this argumenet was a key 126 | // debugPrint(pv.Key, "==", arg) 127 | debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") 128 | if pv.Key == arg || (pv.IsPositional && pv.Value == arg) { 129 | debugPrint("Found matching parsed arg for " + pv.Key) 130 | foundArgUsed = true // the arg was used in this parsedValues set 131 | // if the value is not a positional value and the parsed value had a 132 | // value that was not blank, we skip the next value in the argument list 133 | if !pv.IsPositional && len(pv.Value) > 0 { 134 | skipNext = true 135 | break 136 | } 137 | } 138 | // this prevents excessive parsed values from being checked after we find 139 | // the arg used for the first time 140 | if foundArgUsed { 141 | break 142 | } 143 | } 144 | 145 | // if the arg was not used in any parsed values, then we add it to the slice 146 | // of arguments not used 147 | if !foundArgUsed { 148 | argsNotUsed = append(argsNotUsed, arg) 149 | } 150 | } 151 | 152 | return argsNotUsed 153 | } 154 | 155 | // ShowVersionAndExit shows the version of this parser 156 | func (p *Parser) ShowVersionAndExit() { 157 | fmt.Println("Version:", p.Version) 158 | exitOrPanic(0) 159 | } 160 | 161 | // SetHelpTemplate sets the go template this parser will use when rendering 162 | // Help. 163 | func (p *Parser) SetHelpTemplate(tmpl string) error { 164 | var err error 165 | p.HelpTemplate = template.New(helpFlagLongName) 166 | p.HelpTemplate, err = p.HelpTemplate.Parse(tmpl) 167 | if err != nil { 168 | return err 169 | } 170 | return nil 171 | } 172 | 173 | // Parse calculates all flags and subcommands 174 | func (p *Parser) Parse() error { 175 | return p.ParseArgs(os.Args[1:]) 176 | } 177 | 178 | // ShowHelp shows Help without an error message 179 | func (p *Parser) ShowHelp() { 180 | debugPrint("showing help for", p.subcommandContext.Name) 181 | p.ShowHelpWithMessage("") 182 | } 183 | 184 | // ShowHelpAndExit shows parser help and exits with status code 2 185 | func (p *Parser) ShowHelpAndExit(message string) { 186 | p.ShowHelpWithMessage(message) 187 | exitOrPanic(2) 188 | } 189 | 190 | // ShowHelpWithMessage shows the Help for this parser with an optional string error 191 | // message as a header. The supplied subcommand will be the context of Help 192 | // displayed to the user. 193 | func (p *Parser) ShowHelpWithMessage(message string) { 194 | 195 | // create a new Help values template and extract values into it 196 | help := Help{} 197 | help.ExtractValues(p, message) 198 | err := p.HelpTemplate.Execute(os.Stderr, help) 199 | if err != nil { 200 | fmt.Fprintln(os.Stderr, "Error rendering Help template:", err) 201 | } 202 | } 203 | 204 | // DisableShowVersionWithVersion disables the showing of version information 205 | // with --version. It is enabled by default. 206 | func (p *Parser) DisableShowVersionWithVersion() { 207 | p.ShowVersionWithVersionFlag = false 208 | } 209 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import "testing" 4 | 5 | func TestDoubleParse(t *testing.T) { 6 | ResetParser() 7 | DefaultParser.ShowHelpOnUnexpected = false 8 | 9 | err := DefaultParser.Parse() 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | err = DefaultParser.Parse() 14 | if err == nil { 15 | t.Fatal(err) 16 | } 17 | } 18 | 19 | func TestDisableShowVersionFlag(t *testing.T) { 20 | ResetParser() 21 | 22 | // if this fails the function tested might be useless. 23 | // Review if it's still useful and adjust. 24 | if DefaultParser.ShowVersionWithVersionFlag != true { 25 | t.Fatal("The tested function might not make sense any more.") 26 | } 27 | 28 | DefaultParser.DisableShowVersionWithVersion() 29 | 30 | if DefaultParser.ShowVersionWithVersionFlag != false { 31 | t.Fatal("ShowVersionWithVersionFlag should have been false.") 32 | } 33 | } 34 | 35 | func TestFindArgsNotInParsedValues(t *testing.T) { 36 | t.Parallel() 37 | 38 | // ensure all 'test.' values are skipped 39 | args := []string{"test.timeout=10s", "test.v=true"} 40 | parsedValues := []parsedValue{} 41 | unusedArgs := findArgsNotInParsedValues(args, parsedValues) 42 | if len(unusedArgs) > 0 { 43 | t.Fatal("Found 'test.' args as unused when they should be ignored") 44 | } 45 | 46 | // ensure regular values are not skipped 47 | parsedValues = []parsedValue{ 48 | { 49 | Key: "flaggy", 50 | Value: "testing", 51 | }, 52 | } 53 | args = []string{"flaggy", "testing", "unusedFlag"} 54 | unusedArgs = findArgsNotInParsedValues(args, parsedValues) 55 | t.Log(unusedArgs) 56 | if len(unusedArgs) == 0 { 57 | t.Fatal("Found no args as unused when --flaggy=testing should have been detected") 58 | } 59 | if len(unusedArgs) != 1 { 60 | t.Fatal("Invalid number of unused args found. Expected 1 but found", len(unusedArgs)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /positionalValue.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | // PositionalValue represents a value which is determined by its position 4 | // relative to where a subcommand was detected. 5 | type PositionalValue struct { 6 | Name string // used in documentation only 7 | Description string 8 | AssignmentVar *string // the var that will get this variable 9 | Position int // the position, not including switches, of this variable 10 | Required bool // this subcommand must always be specified 11 | Found bool // was this positional found during parsing? 12 | Hidden bool // indicates this positional value should be hidden from help 13 | defaultValue string // used for help output 14 | } 15 | -------------------------------------------------------------------------------- /subCommand.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Subcommand represents a subcommand which contains a set of child 14 | // subcommands along with a set of flags relevant to it. Parsing 15 | // runs until a subcommand is detected by matching its name and 16 | // position. Once a matching subcommand is found, the next set 17 | // of parsing occurs within that matched subcommand. 18 | type Subcommand struct { 19 | Name string 20 | ShortName string 21 | Description string 22 | Position int // the position of this subcommand, not including flags 23 | Subcommands []*Subcommand 24 | Flags []*Flag 25 | PositionalFlags []*PositionalValue 26 | ParsedValues []parsedValue // a list of values and positionals parsed 27 | AdditionalHelpPrepend string // additional prepended message when Help is displayed 28 | AdditionalHelpAppend string // additional appended message when Help is displayed 29 | Used bool // indicates this subcommand was found and parsed 30 | Hidden bool // indicates this subcommand should be hidden from help 31 | } 32 | 33 | // NewSubcommand creates a new subcommand that can have flags or PositionalFlags 34 | // added to it. The position starts with 1, not 0 35 | func NewSubcommand(name string) *Subcommand { 36 | if len(name) == 0 { 37 | fmt.Fprintln(os.Stderr, "Error creating subcommand (NewSubcommand()). No subcommand name was specified.") 38 | exitOrPanic(2) 39 | } 40 | newSC := &Subcommand{ 41 | Name: name, 42 | } 43 | return newSC 44 | } 45 | 46 | // parseAllFlagsFromArgs parses the non-positional flags such as -f or -v=value 47 | // out of the supplied args and returns the resulting positional items in order, 48 | // all the flag names found (without values), a bool to indicate if help was 49 | // requested, and any errors found during parsing 50 | func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, bool, error) { 51 | 52 | var positionalOnlyArguments []string 53 | var helpRequested bool // indicates the user has supplied -h and we 54 | // should render help if we are the last subcommand 55 | 56 | // indicates we should skip the next argument, like when parsing a flag 57 | // that separates key and value by space 58 | var skipNext bool 59 | 60 | // endArgfound indicates that a -- was found and everything 61 | // remaining should be added to the trailing arguments slices 62 | var endArgFound bool 63 | 64 | // find all the normal flags (not positional) and parse them out 65 | for i, a := range args { 66 | 67 | debugPrint("parsing arg:", a) 68 | 69 | // evaluate if there is a following arg to avoid panics 70 | var nextArgExists bool 71 | var nextArg string 72 | if len(args)-1 >= i+1 { 73 | nextArgExists = true 74 | nextArg = args[i+1] 75 | } 76 | 77 | // if end arg -- has been found, just add everything to TrailingArguments 78 | if endArgFound { 79 | if !p.trailingArgumentsExtracted { 80 | p.TrailingArguments = append(p.TrailingArguments, a) 81 | } 82 | continue 83 | } 84 | 85 | // skip this run if specified 86 | if skipNext { 87 | skipNext = false 88 | debugPrint("skipping flag because it is an arg:", a) 89 | continue 90 | } 91 | 92 | // parse the flag into its name for consideration without dashes 93 | flagName := parseFlagToName(a) 94 | 95 | // if the flag being passed is version or v and the option to display 96 | // version with version flags, then display version 97 | if p.ShowVersionWithVersionFlag { 98 | if flagName == versionFlagLongName { 99 | p.ShowVersionAndExit() 100 | } 101 | } 102 | 103 | // if the show Help on h flag option is set, then show Help when h or Help 104 | // is passed as an option 105 | if p.ShowHelpWithHFlag { 106 | if flagName == helpFlagShortName || flagName == helpFlagLongName { 107 | // Ensure this is the last subcommand passed so we give the correct 108 | // help output 109 | helpRequested = true 110 | continue 111 | } 112 | } 113 | 114 | // determine what kind of flag this is 115 | argType := determineArgType(a) 116 | 117 | // strip flags from arg 118 | debugPrint("Parsing flag named", a, "of type", argType) 119 | 120 | // depending on the flag type, parse the key and value out, then apply it 121 | switch argType { 122 | case argIsFinal: 123 | // debugPrint("Arg", i, "is final:", a) 124 | endArgFound = true 125 | case argIsPositional: 126 | // debugPrint("Arg is positional or subcommand:", a) 127 | // this positional argument into a slice of their own, so that 128 | // we can determine if its a subcommand or positional value later 129 | positionalOnlyArguments = append(positionalOnlyArguments, a) 130 | // track this as a parsed value with the subcommand 131 | sc.addParsedPositionalValue(a) 132 | case argIsFlagWithSpace: // a flag with a space. ex) -k v or --key value 133 | a = parseFlagToName(a) 134 | 135 | // debugPrint("Arg", i, "is flag with space:", a) 136 | // parse next arg as value to this flag and apply to subcommand flags 137 | // if the flag is a bool flag, then we check for a following positional 138 | // and skip it if necessary 139 | if flagIsBool(sc, p, a) { 140 | debugPrint(sc.Name, "bool flag", a, "next var is:", nextArg) 141 | // set the value in this subcommand and its root parser 142 | valueSet, err := setValueForParsers(a, "true", p, sc) 143 | 144 | // if an error occurs, just return it and quit parsing 145 | if err != nil { 146 | return []string{}, false, err 147 | } 148 | 149 | // log all values parsed by this subcommand. We leave the value blank 150 | // because the bool value had no explicit true or false supplied 151 | if valueSet { 152 | sc.addParsedFlag(a, "") 153 | } 154 | 155 | // we've found and set a standalone bool flag, so we move on to the next 156 | // argument in the list of arguments 157 | continue 158 | } 159 | 160 | skipNext = true 161 | // debugPrint(sc.Name, "NOT bool flag", a) 162 | 163 | // if the next arg was not found, then show a Help message 164 | if !nextArgExists { 165 | p.ShowHelpWithMessage("Expected a following arg for flag " + a + ", but it did not exist.") 166 | exitOrPanic(2) 167 | } 168 | valueSet, err := setValueForParsers(a, nextArg, p, sc) 169 | if err != nil { 170 | return []string{}, false, err 171 | } 172 | 173 | // log all parsed values in the subcommand 174 | if valueSet { 175 | sc.addParsedFlag(a, nextArg) 176 | } 177 | case argIsFlagWithValue: // a flag with an equals sign. ex) -k=v or --key=value 178 | // debugPrint("Arg", i, "is flag with value:", a) 179 | a = parseFlagToName(a) 180 | 181 | // parse flag into key and value and apply to subcommand flags 182 | key, val := parseArgWithValue(a) 183 | 184 | // set the value in this subcommand and its root parser 185 | valueSet, err := setValueForParsers(key, val, p, sc) 186 | if err != nil { 187 | return []string{}, false, err 188 | } 189 | 190 | // log all values parsed by the subcommand 191 | if valueSet { 192 | sc.addParsedFlag(a, val) 193 | } 194 | } 195 | } 196 | 197 | return positionalOnlyArguments, helpRequested, nil 198 | } 199 | 200 | // findAllParsedValues finds all values parsed by all subcommands and this 201 | // subcommand and its child subcommands 202 | func (sc *Subcommand) findAllParsedValues() []parsedValue { 203 | parsedValues := sc.ParsedValues 204 | for _, sc := range sc.Subcommands { 205 | // skip unused subcommands 206 | if !sc.Used { 207 | continue 208 | } 209 | parsedValues = append(parsedValues, sc.findAllParsedValues()...) 210 | } 211 | return parsedValues 212 | } 213 | 214 | // parse causes the argument parser to parse based on the supplied []string. 215 | // depth specifies the non-flag subcommand positional depth. A slice of flags 216 | // and subcommands parsed is returned so that the parser can ultimately decide 217 | // if there were any unexpected values supplied by the user 218 | func (sc *Subcommand) parse(p *Parser, args []string, depth int) error { 219 | 220 | debugPrint("- Parsing subcommand", sc.Name, "with depth of", depth, "and args", args) 221 | 222 | // if a command is parsed, its used 223 | sc.Used = true 224 | debugPrint("used subcommand", sc.Name, sc.ShortName) 225 | if len(sc.Name) > 0 { 226 | sc.addParsedPositionalValue(sc.Name) 227 | } 228 | if len(sc.ShortName) > 0 { 229 | sc.addParsedPositionalValue(sc.ShortName) 230 | } 231 | 232 | // as subcommands are used, they become the context of the parser. This helps 233 | // us understand how to display help based on which subcommand is being used 234 | p.subcommandContext = sc 235 | 236 | // ensure that help and version flags are not used if the parser has the 237 | // built-in help and version flags enabled 238 | if p.ShowHelpWithHFlag { 239 | sc.ensureNoConflictWithBuiltinHelp() 240 | } 241 | if p.ShowVersionWithVersionFlag { 242 | sc.ensureNoConflictWithBuiltinVersion() 243 | } 244 | 245 | // Parse the normal flags out of the argument list and return the positionals 246 | // (subcommands and positional values), along with the flags used. 247 | // Then the flag values are applied to the parent parser and the current 248 | // subcommand being parsed. 249 | positionalOnlyArguments, helpRequested, err := sc.parseAllFlagsFromArgs(p, args) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | // indicate that trailing arguments have been extracted, so that they aren't 255 | // appended a second time 256 | p.trailingArgumentsExtracted = true 257 | 258 | // loop over positional values and look for their matching positional 259 | // parameter, or their positional command. If neither are found, then 260 | // we throw an error 261 | var parsedArgCount int 262 | for pos, v := range positionalOnlyArguments { 263 | 264 | // the first relative positional argument will be human natural at position 1 265 | // but offset for the depth of relative commands being parsed for currently. 266 | relativeDepth := pos - depth + 1 267 | // debugPrint("Parsing positional only position", relativeDepth, "with value", v) 268 | 269 | if relativeDepth < 1 { 270 | // debugPrint(sc.Name, "skipped value:", v) 271 | continue 272 | } 273 | parsedArgCount++ 274 | 275 | // determine subcommands and parse them by positional value and name 276 | for _, cmd := range sc.Subcommands { 277 | // debugPrint("Subcommand being compared", relativeDepth, "==", cmd.Position, "and", v, "==", cmd.Name, "==", cmd.ShortName) 278 | if relativeDepth == cmd.Position && (v == cmd.Name || v == cmd.ShortName) { 279 | debugPrint("Decending into positional subcommand", cmd.Name, "at relativeDepth", relativeDepth, "and absolute depth", depth+1) 280 | return cmd.parse(p, args, depth+parsedArgCount) // continue recursive positional parsing 281 | } 282 | } 283 | 284 | // determine positional args and parse them by positional value and name 285 | var foundPositional bool 286 | for _, val := range sc.PositionalFlags { 287 | if relativeDepth == val.Position { 288 | debugPrint("Found a positional value at relativePos:", relativeDepth, "value:", v) 289 | 290 | // set original value for help output 291 | val.defaultValue = *val.AssignmentVar 292 | 293 | // defrerence the struct pointer, then set the pointer property within it 294 | *val.AssignmentVar = v 295 | // debugPrint("set positional to value", *val.AssignmentVar) 296 | foundPositional = true 297 | val.Found = true 298 | break 299 | } 300 | } 301 | 302 | // if there aren't any positional flags but there are subcommands that 303 | // were not used, display a useful message with subcommand options. 304 | if !foundPositional { 305 | if p.ShowHelpOnUnexpected { 306 | debugPrint("No positional at position", relativeDepth) 307 | var foundSubcommandAtDepth bool 308 | for _, cmd := range sc.Subcommands { 309 | if cmd.Position == relativeDepth { 310 | foundSubcommandAtDepth = true 311 | } 312 | } 313 | 314 | // if there is a subcommand here but it was not specified, display them all 315 | // as a suggestion to the user before exiting. 316 | if foundSubcommandAtDepth { 317 | // determine which name to use in upcoming help output 318 | fmt.Fprintln(os.Stderr, sc.Name+":", "No subcommand or positional value found at position", strconv.Itoa(relativeDepth)+".") 319 | var output string 320 | for _, cmd := range sc.Subcommands { 321 | if cmd.Hidden { 322 | continue 323 | } 324 | output = output + " " + cmd.Name 325 | } 326 | // if there are available subcommands, let the user know 327 | if len(output) > 0 { 328 | output = strings.TrimLeft(output, " ") 329 | fmt.Println("Available subcommands:", output) 330 | } 331 | exitOrPanic(2) 332 | } 333 | 334 | // if there were not any flags or subcommands at this position at all, then 335 | // throw an error (display Help if necessary) 336 | p.ShowHelpWithMessage("Unexpected argument: " + v) 337 | exitOrPanic(2) 338 | } else { 339 | // if no positional value was registered at this position, but the parser is not 340 | // configured to show help when any unexpected command is found, add this positional 341 | // to the list of trailing arguments. This allows for any number of unspecified 342 | // values to be added at the end of the arguments list without using the --- 343 | // trailing arguments separator. 344 | p.TrailingArguments = append(p.TrailingArguments, v) 345 | 346 | } 347 | } 348 | } 349 | 350 | // if help was requested and we should show help when h is passed, 351 | if helpRequested && p.ShowHelpWithHFlag { 352 | p.ShowHelp() 353 | exitOrPanic(0) 354 | } 355 | 356 | // find any positionals that were not used on subcommands that were 357 | // found and throw help (unknown argument) in the global parse or subcommand 358 | for _, pv := range p.PositionalFlags { 359 | if pv.Required && !pv.Found { 360 | p.ShowHelpWithMessage("Required global positional variable " + pv.Name + " not found at position " + strconv.Itoa(pv.Position)) 361 | exitOrPanic(2) 362 | } 363 | } 364 | for _, pv := range sc.PositionalFlags { 365 | if pv.Required && !pv.Found { 366 | p.ShowHelpWithMessage("Required positional of subcommand " + sc.Name + " named " + pv.Name + " not found at position " + strconv.Itoa(pv.Position)) 367 | exitOrPanic(2) 368 | } 369 | } 370 | 371 | return nil 372 | } 373 | 374 | // addParsedFlag makes it easy to append flag values parsed by the subcommand 375 | func (sc *Subcommand) addParsedFlag(key string, value string) { 376 | sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false)) 377 | } 378 | 379 | // addParsedPositionalValue makes it easy to append positionals parsed by the 380 | // subcommand 381 | func (sc *Subcommand) addParsedPositionalValue(value string) { 382 | sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true)) 383 | } 384 | 385 | // FlagExists lets you know if the flag name exists as either a short or long 386 | // name in the (sub)command 387 | func (sc *Subcommand) FlagExists(name string) bool { 388 | 389 | for _, f := range sc.Flags { 390 | if f.HasName(name) { 391 | return true 392 | } 393 | } 394 | 395 | return false 396 | } 397 | 398 | // AttachSubcommand adds a possible subcommand to the Parser. 399 | func (sc *Subcommand) AttachSubcommand(newSC *Subcommand, relativePosition int) { 400 | 401 | // assign the depth of the subcommand when its attached 402 | newSC.Position = relativePosition 403 | 404 | // ensure no subcommands at this depth with this name 405 | for _, other := range sc.Subcommands { 406 | if newSC.Position == other.Position { 407 | if newSC.Name != "" { 408 | if newSC.Name == other.Name { 409 | log.Panicln("Unable to add subcommand because one already exists at position" + strconv.Itoa(newSC.Position) + " with name " + other.Name) 410 | } 411 | } 412 | if newSC.ShortName != "" { 413 | if newSC.ShortName == other.ShortName { 414 | log.Panicln("Unable to add subcommand because one already exists at position" + strconv.Itoa(newSC.Position) + " with name " + other.ShortName) 415 | } 416 | } 417 | } 418 | } 419 | 420 | // ensure no positionals at this depth 421 | for _, other := range sc.PositionalFlags { 422 | if newSC.Position == other.Position { 423 | log.Panicln("Unable to add subcommand because a positional value already exists at position " + strconv.Itoa(newSC.Position) + ": " + other.Name) 424 | } 425 | } 426 | 427 | sc.Subcommands = append(sc.Subcommands, newSC) 428 | } 429 | 430 | // add is a "generic" to add flags of any type. Checks the supplied parent 431 | // parser to ensure that the user isn't setting version or help flags that 432 | // conflict with the built-in help and version flag behavior. 433 | func (sc *Subcommand) add(assignmentVar interface{}, shortName string, longName string, description string) { 434 | 435 | // if the flag is already used, throw an error 436 | for _, existingFlag := range sc.Flags { 437 | if longName != "" && existingFlag.LongName == longName { 438 | log.Panicln("Flag " + longName + " added to subcommand " + sc.Name + " but the name is already assigned.") 439 | } 440 | if shortName != "" && existingFlag.ShortName == shortName { 441 | log.Panicln("Flag " + shortName + " added to subcommand " + sc.Name + " but the short name is already assigned.") 442 | } 443 | } 444 | 445 | newFlag := Flag{ 446 | AssignmentVar: assignmentVar, 447 | ShortName: shortName, 448 | LongName: longName, 449 | Description: description, 450 | } 451 | sc.Flags = append(sc.Flags, &newFlag) 452 | } 453 | 454 | // String adds a new string flag 455 | func (sc *Subcommand) String(assignmentVar *string, shortName string, longName string, description string) { 456 | sc.add(assignmentVar, shortName, longName, description) 457 | } 458 | 459 | // StringSlice adds a new slice of strings flag 460 | // Specify the flag multiple times to fill the slice 461 | func (sc *Subcommand) StringSlice(assignmentVar *[]string, shortName string, longName string, description string) { 462 | sc.add(assignmentVar, shortName, longName, description) 463 | } 464 | 465 | // Bool adds a new bool flag 466 | func (sc *Subcommand) Bool(assignmentVar *bool, shortName string, longName string, description string) { 467 | sc.add(assignmentVar, shortName, longName, description) 468 | } 469 | 470 | // BoolSlice adds a new slice of bools flag 471 | // Specify the flag multiple times to fill the slice 472 | func (sc *Subcommand) BoolSlice(assignmentVar *[]bool, shortName string, longName string, description string) { 473 | sc.add(assignmentVar, shortName, longName, description) 474 | } 475 | 476 | // ByteSlice adds a new slice of bytes flag 477 | // Specify the flag multiple times to fill the slice. Takes hex as input. 478 | func (sc *Subcommand) ByteSlice(assignmentVar *[]byte, shortName string, longName string, description string) { 479 | sc.add(assignmentVar, shortName, longName, description) 480 | } 481 | 482 | // Duration adds a new time.Duration flag. 483 | // Input format is described in time.ParseDuration(). 484 | // Example values: 1h, 1h50m, 32s 485 | func (sc *Subcommand) Duration(assignmentVar *time.Duration, shortName string, longName string, description string) { 486 | sc.add(assignmentVar, shortName, longName, description) 487 | } 488 | 489 | // DurationSlice adds a new time.Duration flag. 490 | // Input format is described in time.ParseDuration(). 491 | // Example values: 1h, 1h50m, 32s 492 | // Specify the flag multiple times to fill the slice. 493 | func (sc *Subcommand) DurationSlice(assignmentVar *[]time.Duration, shortName string, longName string, description string) { 494 | sc.add(assignmentVar, shortName, longName, description) 495 | } 496 | 497 | // Float32 adds a new float32 flag. 498 | func (sc *Subcommand) Float32(assignmentVar *float32, shortName string, longName string, description string) { 499 | sc.add(assignmentVar, shortName, longName, description) 500 | } 501 | 502 | // Float32Slice adds a new float32 flag. 503 | // Specify the flag multiple times to fill the slice. 504 | func (sc *Subcommand) Float32Slice(assignmentVar *[]float32, shortName string, longName string, description string) { 505 | sc.add(assignmentVar, shortName, longName, description) 506 | } 507 | 508 | // Float64 adds a new float64 flag. 509 | func (sc *Subcommand) Float64(assignmentVar *float64, shortName string, longName string, description string) { 510 | sc.add(assignmentVar, shortName, longName, description) 511 | } 512 | 513 | // Float64Slice adds a new float64 flag. 514 | // Specify the flag multiple times to fill the slice. 515 | func (sc *Subcommand) Float64Slice(assignmentVar *[]float64, shortName string, longName string, description string) { 516 | sc.add(assignmentVar, shortName, longName, description) 517 | } 518 | 519 | // Int adds a new int flag 520 | func (sc *Subcommand) Int(assignmentVar *int, shortName string, longName string, description string) { 521 | sc.add(assignmentVar, shortName, longName, description) 522 | } 523 | 524 | // IntSlice adds a new int slice flag. 525 | // Specify the flag multiple times to fill the slice. 526 | func (sc *Subcommand) IntSlice(assignmentVar *[]int, shortName string, longName string, description string) { 527 | sc.add(assignmentVar, shortName, longName, description) 528 | } 529 | 530 | // UInt adds a new uint flag 531 | func (sc *Subcommand) UInt(assignmentVar *uint, shortName string, longName string, description string) { 532 | sc.add(assignmentVar, shortName, longName, description) 533 | } 534 | 535 | // UIntSlice adds a new uint slice flag. 536 | // Specify the flag multiple times to fill the slice. 537 | func (sc *Subcommand) UIntSlice(assignmentVar *[]uint, shortName string, longName string, description string) { 538 | sc.add(assignmentVar, shortName, longName, description) 539 | } 540 | 541 | // UInt64 adds a new uint64 flag 542 | func (sc *Subcommand) UInt64(assignmentVar *uint64, shortName string, longName string, description string) { 543 | sc.add(assignmentVar, shortName, longName, description) 544 | } 545 | 546 | // UInt64Slice adds a new uint64 slice flag. 547 | // Specify the flag multiple times to fill the slice. 548 | func (sc *Subcommand) UInt64Slice(assignmentVar *[]uint64, shortName string, longName string, description string) { 549 | sc.add(assignmentVar, shortName, longName, description) 550 | } 551 | 552 | // UInt32 adds a new uint32 flag 553 | func (sc *Subcommand) UInt32(assignmentVar *uint32, shortName string, longName string, description string) { 554 | sc.add(assignmentVar, shortName, longName, description) 555 | } 556 | 557 | // UInt32Slice adds a new uint32 slice flag. 558 | // Specify the flag multiple times to fill the slice. 559 | func (sc *Subcommand) UInt32Slice(assignmentVar *[]uint32, shortName string, longName string, description string) { 560 | sc.add(assignmentVar, shortName, longName, description) 561 | } 562 | 563 | // UInt16 adds a new uint16 flag 564 | func (sc *Subcommand) UInt16(assignmentVar *uint16, shortName string, longName string, description string) { 565 | sc.add(assignmentVar, shortName, longName, description) 566 | } 567 | 568 | // UInt16Slice adds a new uint16 slice flag. 569 | // Specify the flag multiple times to fill the slice. 570 | func (sc *Subcommand) UInt16Slice(assignmentVar *[]uint16, shortName string, longName string, description string) { 571 | sc.add(assignmentVar, shortName, longName, description) 572 | } 573 | 574 | // UInt8 adds a new uint8 flag 575 | func (sc *Subcommand) UInt8(assignmentVar *uint8, shortName string, longName string, description string) { 576 | sc.add(assignmentVar, shortName, longName, description) 577 | } 578 | 579 | // UInt8Slice adds a new uint8 slice flag. 580 | // Specify the flag multiple times to fill the slice. 581 | func (sc *Subcommand) UInt8Slice(assignmentVar *[]uint8, shortName string, longName string, description string) { 582 | sc.add(assignmentVar, shortName, longName, description) 583 | } 584 | 585 | // Int64 adds a new int64 flag. 586 | func (sc *Subcommand) Int64(assignmentVar *int64, shortName string, longName string, description string) { 587 | sc.add(assignmentVar, shortName, longName, description) 588 | } 589 | 590 | // Int64Slice adds a new int64 slice flag. 591 | // Specify the flag multiple times to fill the slice. 592 | func (sc *Subcommand) Int64Slice(assignmentVar *[]int64, shortName string, longName string, description string) { 593 | sc.add(assignmentVar, shortName, longName, description) 594 | } 595 | 596 | // Int32 adds a new int32 flag 597 | func (sc *Subcommand) Int32(assignmentVar *int32, shortName string, longName string, description string) { 598 | sc.add(assignmentVar, shortName, longName, description) 599 | } 600 | 601 | // Int32Slice adds a new int32 slice flag. 602 | // Specify the flag multiple times to fill the slice. 603 | func (sc *Subcommand) Int32Slice(assignmentVar *[]int32, shortName string, longName string, description string) { 604 | sc.add(assignmentVar, shortName, longName, description) 605 | } 606 | 607 | // Int16 adds a new int16 flag 608 | func (sc *Subcommand) Int16(assignmentVar *int16, shortName string, longName string, description string) { 609 | sc.add(assignmentVar, shortName, longName, description) 610 | } 611 | 612 | // Int16Slice adds a new int16 slice flag. 613 | // Specify the flag multiple times to fill the slice. 614 | func (sc *Subcommand) Int16Slice(assignmentVar *[]int16, shortName string, longName string, description string) { 615 | sc.add(assignmentVar, shortName, longName, description) 616 | } 617 | 618 | // Int8 adds a new int8 flag 619 | func (sc *Subcommand) Int8(assignmentVar *int8, shortName string, longName string, description string) { 620 | sc.add(assignmentVar, shortName, longName, description) 621 | } 622 | 623 | // Int8Slice adds a new int8 slice flag. 624 | // Specify the flag multiple times to fill the slice. 625 | func (sc *Subcommand) Int8Slice(assignmentVar *[]int8, shortName string, longName string, description string) { 626 | sc.add(assignmentVar, shortName, longName, description) 627 | } 628 | 629 | // IP adds a new net.IP flag. 630 | func (sc *Subcommand) IP(assignmentVar *net.IP, shortName string, longName string, description string) { 631 | sc.add(assignmentVar, shortName, longName, description) 632 | } 633 | 634 | // IPSlice adds a new int8 slice flag. 635 | // Specify the flag multiple times to fill the slice. 636 | func (sc *Subcommand) IPSlice(assignmentVar *[]net.IP, shortName string, longName string, description string) { 637 | sc.add(assignmentVar, shortName, longName, description) 638 | } 639 | 640 | // HardwareAddr adds a new net.HardwareAddr flag. 641 | func (sc *Subcommand) HardwareAddr(assignmentVar *net.HardwareAddr, shortName string, longName string, description string) { 642 | sc.add(assignmentVar, shortName, longName, description) 643 | } 644 | 645 | // HardwareAddrSlice adds a new net.HardwareAddr slice flag. 646 | // Specify the flag multiple times to fill the slice. 647 | func (sc *Subcommand) HardwareAddrSlice(assignmentVar *[]net.HardwareAddr, shortName string, longName string, description string) { 648 | sc.add(assignmentVar, shortName, longName, description) 649 | } 650 | 651 | // IPMask adds a new net.IPMask flag. IPv4 Only. 652 | func (sc *Subcommand) IPMask(assignmentVar *net.IPMask, shortName string, longName string, description string) { 653 | sc.add(assignmentVar, shortName, longName, description) 654 | } 655 | 656 | // IPMaskSlice adds a new net.HardwareAddr slice flag. IPv4 only. 657 | // Specify the flag multiple times to fill the slice. 658 | func (sc *Subcommand) IPMaskSlice(assignmentVar *[]net.IPMask, shortName string, longName string, description string) { 659 | sc.add(assignmentVar, shortName, longName, description) 660 | } 661 | 662 | // AddPositionalValue adds a positional value to the subcommand. the 663 | // relativePosition starts at 1 and is relative to the subcommand it belongs to 664 | func (sc *Subcommand) AddPositionalValue(assignmentVar *string, name string, relativePosition int, required bool, description string) { 665 | 666 | // ensure no other positionals are at this depth 667 | for _, other := range sc.PositionalFlags { 668 | if relativePosition == other.Position { 669 | log.Panicln("Unable to add positional value " + name + " because " + other.Name + " already exists at position: " + strconv.Itoa(relativePosition)) 670 | } 671 | } 672 | 673 | // ensure no subcommands at this depth 674 | for _, other := range sc.Subcommands { 675 | if relativePosition == other.Position { 676 | log.Panicln("Unable to add positional value " + name + "because a subcommand, " + other.Name + ", already exists at position: " + strconv.Itoa(relativePosition)) 677 | } 678 | } 679 | 680 | newPositionalValue := PositionalValue{ 681 | Name: name, 682 | Position: relativePosition, 683 | AssignmentVar: assignmentVar, 684 | Required: required, 685 | Description: description, 686 | defaultValue: *assignmentVar, 687 | } 688 | sc.PositionalFlags = append(sc.PositionalFlags, &newPositionalValue) 689 | } 690 | 691 | // SetValueForKey sets the value for the specified key. If setting a bool 692 | // value, then send "true" or "false" as strings. The returned bool indicates 693 | // that a value was set. 694 | func (sc *Subcommand) SetValueForKey(key string, value string) (bool, error) { 695 | 696 | // debugPrint("Looking to set key", key, "to value", value) 697 | // check for and assign flags that match the key 698 | for _, f := range sc.Flags { 699 | // debugPrint("Evaluating string flag", f.ShortName, "==", key, "||", f.LongName, "==", key) 700 | if f.ShortName == key || f.LongName == key { 701 | // debugPrint("Setting string value for", key, "to", value) 702 | if err := f.identifyAndAssignValue(value); err != nil { 703 | return false, err 704 | } 705 | return true, nil 706 | } 707 | } 708 | 709 | // debugPrint(sc.Name, "was unable to find a key named", key, "to set to value", value) 710 | return false, nil 711 | } 712 | 713 | // ensureNoConflictWithBuiltinHelp ensures that the flags on this subcommand do 714 | // not conflict with the builtin help flags (-h or --help). Exits the program 715 | // if a conflict is found. 716 | func (sc *Subcommand) ensureNoConflictWithBuiltinHelp() { 717 | for _, f := range sc.Flags { 718 | if f.LongName == helpFlagLongName { 719 | sc.exitBecauseOfHelpFlagConflict(f.LongName) 720 | } 721 | if f.LongName == helpFlagShortName { 722 | sc.exitBecauseOfHelpFlagConflict(f.LongName) 723 | } 724 | if f.ShortName == helpFlagLongName { 725 | sc.exitBecauseOfHelpFlagConflict(f.ShortName) 726 | } 727 | if f.ShortName == helpFlagShortName { 728 | sc.exitBecauseOfHelpFlagConflict(f.ShortName) 729 | } 730 | } 731 | } 732 | 733 | // ensureNoConflictWithBuiltinVersion ensures that the flags on this subcommand do 734 | // not conflict with the builtin version flag (--version). Exits the program 735 | // if a conflict is found. 736 | func (sc *Subcommand) ensureNoConflictWithBuiltinVersion() { 737 | for _, f := range sc.Flags { 738 | if f.LongName == versionFlagLongName { 739 | sc.exitBecauseOfVersionFlagConflict(f.LongName) 740 | } 741 | if f.ShortName == versionFlagLongName { 742 | sc.exitBecauseOfVersionFlagConflict(f.ShortName) 743 | } 744 | } 745 | } 746 | 747 | // exitBecauseOfVersionFlagConflict exits the program with a message about how to prevent 748 | // flags being defined from conflicting with the builtin flags. 749 | func (sc *Subcommand) exitBecauseOfVersionFlagConflict(flagName string) { 750 | fmt.Println(`Flag with name '` + flagName + `' conflicts with the internal --version flag in flaggy. 751 | 752 | You must either change the flag's name, or disable flaggy's internal version 753 | flag with 'flaggy.DefaultParser.ShowVersionWithVersionFlag = false'. If you are using 754 | a custom parser, you must instead set '.ShowVersionWithVersionFlag = false' on it.`) 755 | exitOrPanic(1) 756 | } 757 | 758 | // exitBecauseOfHelpFlagConflict exits the program with a message about how to prevent 759 | // flags being defined from conflicting with the builtin flags. 760 | func (sc *Subcommand) exitBecauseOfHelpFlagConflict(flagName string) { 761 | fmt.Println(`Flag with name '` + flagName + `' conflicts with the internal --help or -h flag in flaggy. 762 | 763 | You must either change the flag's name, or disable flaggy's internal help 764 | flag with 'flaggy.DefaultParser.ShowHelpWithHFlag = false'. If you are using 765 | a custom parser, you must instead set '.ShowHelpWithHFlag = false' on it.`) 766 | exitOrPanic(1) 767 | } 768 | -------------------------------------------------------------------------------- /subcommand_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/integrii/flaggy" 10 | ) 11 | 12 | func TestSCNameExists(t *testing.T) { 13 | defer func() { 14 | r := recover() 15 | if r == nil { 16 | t.Fatal("Expected crash from double subcommand name use at same position") 17 | } 18 | }() 19 | flaggy.ResetParser() 20 | scA := flaggy.NewSubcommand("test") 21 | scB := flaggy.NewSubcommand("test") 22 | flaggy.AttachSubcommand(scA, 1) 23 | flaggy.AttachSubcommand(scB, 1) 24 | } 25 | 26 | func TestFlagExists(t *testing.T) { 27 | sc := flaggy.NewSubcommand("testFlagExists") 28 | e := sc.FlagExists("test") 29 | if e == true { 30 | t.Fatal("Flag exists on subcommand that should not") 31 | } 32 | var testA string 33 | 34 | sc.String(&testA, "", "test", "a test flag") 35 | e = sc.FlagExists("test") 36 | if e == false { 37 | t.Fatal("Flag does not exist on a subcommand that should") 38 | } 39 | } 40 | 41 | // TestExitOnUnknownFlag tests that when an unknown flag is supplied and the 42 | // ShowHelpOnUnexpected value is set, an error is thrown on unknown flags. 43 | func TestExitOnUnknownFlag(t *testing.T) { 44 | defer func() { 45 | r := recover() 46 | if r == nil { 47 | t.Fatal("Expected crash on unknown flag") 48 | } 49 | }() 50 | flaggy.DebugMode = true 51 | defer debugOff() 52 | var expectedFlag string 53 | var expectedPositional string 54 | flaggy.ResetParser() 55 | flaggy.String(&expectedFlag, "f", "flag", "an expected positonal flag") 56 | flaggy.AddPositionalValue(&expectedPositional, "positionalTest", 1, true, "A test positional value") 57 | flaggy.ParseArgs([]string{"positionalHere", "-f", "flagHere", "unexpectedValue"}) 58 | } 59 | 60 | // TestExitOnUnknownFlagWithValue tests that when an unknown flag with a value 61 | // is supplied and the ShowHelpOnUnexpected value is set, an error is thrown on 62 | // the unknown flags. 63 | func TestExitOnUnknownFlagWithValue(t *testing.T) { 64 | flaggy.ResetParser() 65 | flaggy.ShowHelpOnUnexpectedEnable() 66 | defer func() { 67 | r := recover() 68 | if r == nil { 69 | t.Fatal("Expected crash on unknown flag with value") 70 | } 71 | }() 72 | flaggy.DebugMode = true 73 | defer debugOff() 74 | var expectedFlag string 75 | var expectedPositional string 76 | flaggy.ResetParser() 77 | flaggy.String(&expectedFlag, "f", "flag", "an expected positonal flag") 78 | flaggy.AddPositionalValue(&expectedPositional, "positionalTest", 1, true, "A test positional value") 79 | flaggy.ParseArgs([]string{"positionalHere", "-f", "flagHere", "--unexpectedValue=true"}) 80 | } 81 | 82 | // TestDoublePositional tests errors when two positionals are 83 | // specified at the same time 84 | func TestDoublePositional(t *testing.T) { 85 | defer func() { 86 | r := recover() 87 | if r == nil { 88 | t.Fatal("Expected crash on double assignment") 89 | } 90 | }() 91 | // flaggy.DebugMode = true 92 | defer debugOff() 93 | var posTest string 94 | flaggy.ResetParser() 95 | flaggy.AddPositionalValue(&posTest, "posTest", 1, true, "First test positional") 96 | flaggy.AddPositionalValue(&posTest, "posTest2", 1, true, "Second test positional") 97 | } 98 | 99 | func TestNextArgDoesNotExist(t *testing.T) { 100 | defer func() { 101 | r := recover() 102 | if r == nil { 103 | t.Fatal("Expected crash when next arg not specifid") 104 | } 105 | }() 106 | flaggy.ResetParser() 107 | flaggy.PanicInsteadOfExit = true 108 | var test string 109 | flaggy.String(&test, "t", "test", "Description goes here") 110 | flaggy.ParseArgs([]string{"-t"}) 111 | } 112 | 113 | func TestSubcommandHidden(t *testing.T) { 114 | defer func() { 115 | r := recover() 116 | if r == nil { 117 | t.Fatal("Expected crash instead of exit. Subcommand id was set to a blank") 118 | } 119 | }() 120 | flaggy.ResetParser() 121 | sc := flaggy.NewSubcommand("") 122 | sc.Hidden = true 123 | sc.ShortName = "sc" 124 | flaggy.AttachSubcommand(sc, 1) 125 | flaggy.ParseArgs([]string{"x"}) 126 | } 127 | 128 | // TestRequiredPositional tests required positionals 129 | func TestRequiredPositional(t *testing.T) { 130 | defer func() { 131 | r := recover() 132 | if r == nil { 133 | t.Fatal("Expected crash on unused required positional") 134 | } 135 | }() 136 | // flaggy.DebugMode = true 137 | defer debugOff() 138 | var posTest string 139 | flaggy.AddPositionalValue(&posTest, "posTest", 1, true, "First test positional") 140 | flaggy.Parse() 141 | } 142 | 143 | // TestTypoSubcommand tests what happens when an invalid subcommand is passed 144 | func TestTypoSubcommand(t *testing.T) { 145 | defer func() { 146 | r := recover() 147 | if r == nil { 148 | t.Fatal("Expected crash on subcommand typo") 149 | } 150 | }() 151 | p := flaggy.NewParser("TestTypoSubcommand") 152 | p.ShowHelpOnUnexpected = true 153 | args := []string{"unexpectedArg"} 154 | newSCA := flaggy.NewSubcommand("TestTypoSubcommandA") 155 | newSCB := flaggy.NewSubcommand("TestTypoSubcommandB") 156 | p.AttachSubcommand(newSCA, 1) 157 | p.AttachSubcommand(newSCB, 1) 158 | if err := p.ParseArgs(args); err != nil { 159 | t.Fatalf("got: %s; want: no error", err) 160 | } 161 | } 162 | 163 | // TestIgnoreUnexpected tests what happens when an invalid subcommand is passed but should be ignored 164 | func TestIgnoreUnexpected(t *testing.T) { 165 | p := flaggy.NewParser("TestTypoSubcommand") 166 | p.ShowHelpOnUnexpected = false 167 | args := []string{"unexpectedArg"} 168 | newSCA := flaggy.NewSubcommand("TestTypoSubcommandA") 169 | p.AttachSubcommand(newSCA, 1) 170 | if err := p.ParseArgs(args); err != nil { 171 | t.Fatalf("got: %s; want: no error", err) 172 | } 173 | } 174 | 175 | // TestSubcommandHelp tests displaying of help on unspecified commands 176 | func TestSubcommandHelp(t *testing.T) { 177 | defer func() { 178 | r := recover() 179 | if r == nil { 180 | t.Fatal("Expected crash on subcommand help display") 181 | } 182 | }() 183 | p := flaggy.NewParser("TestSubcommandHelp") 184 | p.ShowHelpOnUnexpected = true 185 | args := []string{"unexpectedArg"} 186 | if err := p.ParseArgs(args); err != nil { 187 | t.Fatalf("got: %s; want: no error", err) 188 | } 189 | } 190 | 191 | func TestHelpWithHFlagA(t *testing.T) { 192 | defer func() { 193 | r := recover() 194 | if r == nil { 195 | t.Fatal("Expected crash on help flag use") 196 | } 197 | }() 198 | p := flaggy.NewParser("TestHelpWithHFlag") 199 | p.ShowHelpWithHFlag = true 200 | args := []string{"-h"} 201 | if err := p.ParseArgs(args); err != nil { 202 | t.Fatalf("got: %s; want: no error", err) 203 | } 204 | } 205 | 206 | func TestHelpWithHFlagB(t *testing.T) { 207 | defer func() { 208 | r := recover() 209 | if r == nil { 210 | t.Fatal("Expected crash on help flag use") 211 | } 212 | }() 213 | p := flaggy.NewParser("TestHelpWithHFlag") 214 | p.ShowHelpWithHFlag = true 215 | args := []string{"--help"} 216 | if err := p.ParseArgs(args); err != nil { 217 | t.Fatalf("got: %s; want: no error", err) 218 | } 219 | } 220 | 221 | func TestVersionWithVFlagB(t *testing.T) { 222 | defer func() { 223 | r := recover() 224 | if r == nil { 225 | t.Fatal("Expected crash on version flag use") 226 | } 227 | }() 228 | p := flaggy.NewParser("TestSubcommandVersion") 229 | p.ShowVersionWithVersionFlag = true 230 | p.Version = "TestVersionWithVFlagB 0.0.0a" 231 | args := []string{"--version"} 232 | if err := p.ParseArgs(args); err != nil { 233 | t.Fatalf("got: %s; want: no error", err) 234 | } 235 | } 236 | 237 | // TestSubcommandParse tests paring of a single subcommand 238 | func TestSubcommandParse(t *testing.T) { 239 | 240 | var positionA string 241 | 242 | // create the argument parser 243 | p := flaggy.NewParser("TestSubcommandParse") 244 | 245 | // create a subcommand 246 | newSC := flaggy.NewSubcommand("testSubcommand") 247 | 248 | // add the subcommand into the parser 249 | p.AttachSubcommand(newSC, 1) 250 | 251 | // add a positional arg onto the subcommand at relative position 1 252 | newSC.AddPositionalValue(&positionA, "positionalA", 1, false, "This is a test positional value") 253 | 254 | // override os args and parse them 255 | os.Args = []string{"binaryName", "testSubcommand", "testPositional"} 256 | if err := p.Parse(); err != nil { 257 | t.Fatalf("got: %s; want: no error", err) 258 | } 259 | 260 | // ensure subcommand and positional used 261 | if !newSC.Used { 262 | t.Fatal("Subcommand was not used, but it was expected to be") 263 | } 264 | if positionA != "testPositional" { 265 | t.Fatal("Positional argument was not set to testPositional, was:", positionA) 266 | } 267 | } 268 | 269 | func TestBadSubcommand(t *testing.T) { 270 | // create the argument parser 271 | p := flaggy.NewParser("TestBadSubcommand") 272 | 273 | // create a subcommand 274 | newSC := flaggy.NewSubcommand("testSubcommand") 275 | p.AttachSubcommand(newSC, 1) 276 | 277 | // test what happens if you add a bad subcommand 278 | os.Args = []string{"test"} 279 | if err := p.Parse(); err != nil { 280 | t.Fatalf("got: %s; want: no error", err) 281 | } 282 | } 283 | 284 | func TestBadPositional(t *testing.T) { 285 | // create the argument parser 286 | p := flaggy.NewParser("TestBadPositional") 287 | 288 | // create a subcommand 289 | // add a positional arg into the subcommand 290 | var positionA string 291 | var err error 292 | p.AddPositionalValue(&positionA, "positionalA", 1, false, "This is a test positional value") 293 | 294 | // test what happens if you add a bad subcommand 295 | os.Args = []string{"test", "badPositional"} 296 | err = p.Parse() 297 | if err != nil { 298 | t.Fatal("Threw an error when bad positional was passed, but shouldn't have") 299 | } 300 | } 301 | 302 | // TestNakedBoolFlag tests a naked boolean flag, which mean it has no 303 | // specified value beyond the flag being present. 304 | func TestNakedBool(t *testing.T) { 305 | flaggy.ResetParser() 306 | os.Args = []string{"testBinary", "-t"} 307 | 308 | // add a bool var 309 | var boolVar bool 310 | flaggy.Bool(&boolVar, "t", "boolVar", "A boolean flag for testing") 311 | flaggy.Parse() 312 | if !boolVar { 313 | t.Fatal("Boolean naked val not set to true") 314 | } 315 | } 316 | 317 | // debugOff makes defers easier 318 | func debugOff() { 319 | flaggy.DebugMode = false 320 | } 321 | 322 | // BenchmarkSubcommandParse benchmarks the creation and parsing of 323 | // a basic subcommand 324 | func BenchmarkSubcommandParse(b *testing.B) { 325 | // catch errors that may occur 326 | defer func(b *testing.B) { 327 | err := recover() 328 | if err != nil { 329 | b.Fatal("Benchmark had error:", err) 330 | } 331 | }(b) 332 | 333 | for i := 0; i < b.N; i++ { 334 | 335 | var positionA string 336 | 337 | // create the argument parser 338 | p := flaggy.NewParser("TestSubcommandParse") 339 | 340 | // create a subcommand 341 | newSC := flaggy.NewSubcommand("testSubcommand") 342 | 343 | // add the subcommand into the parser 344 | p.AttachSubcommand(newSC, 1) 345 | 346 | // add a positional arg onto the subcommand at relative position 1 347 | newSC.AddPositionalValue(&positionA, "positionalA", 1, false, "This is a test positional value") 348 | 349 | // override os args and parse them 350 | os.Args = []string{"binaryName", "testSubcommand", "testPositional"} 351 | err := p.Parse() 352 | if err != nil { 353 | b.Fatal("Error parsing args: " + err.Error()) 354 | } 355 | } 356 | } 357 | 358 | // TestSCInputParsing tests all flag types on subcommands 359 | func TestSCInputParsing(t *testing.T) { 360 | defer debugOff() 361 | flaggy.DebugMode = true 362 | 363 | flaggy.ResetParser() 364 | var err error 365 | inputArgs := []string{} 366 | 367 | // setup a subcommand for all our flags to hang on 368 | sc := flaggy.NewSubcommand("subCommand") 369 | flaggy.AttachSubcommand(sc, 1) 370 | inputArgs = append(inputArgs, "subCommand") 371 | 372 | // Setup input arguments for every input type on our subcommand 373 | var stringFlag = "defaultVar" 374 | sc.String(&stringFlag, "s", "string", "string flag") 375 | inputArgs = append(inputArgs, "-s", "flaggy") 376 | var stringFlagExpected = "flaggy" 377 | 378 | var stringSliceFlag []string 379 | sc.StringSlice(&stringSliceFlag, "ssf", "stringSlice", "string slice flag") 380 | inputArgs = append(inputArgs, "-ssf", "one", "-ssf", "two") 381 | var stringSliceFlagExpected = []string{"one", "two"} 382 | 383 | var stringSliceCommaFlag []string 384 | sc.StringSlice(&stringSliceCommaFlag, "sscf", "stringSliceComma", "string slice flag") 385 | inputArgs = append(inputArgs, "-sscf", "one,two") 386 | var stringSliceCommaFlagExpected = []string{"one", "two"} 387 | 388 | var boolFlag bool 389 | sc.Bool(&boolFlag, "bf", "bool", "bool flag") 390 | inputArgs = append(inputArgs, "-bf") 391 | var boolFlagExpected = true 392 | 393 | var boolSliceFlag []bool 394 | sc.BoolSlice(&boolSliceFlag, "bsf", "boolSlice", "bool slice flag") 395 | inputArgs = append(inputArgs, "-bsf", "-bsf") 396 | var boolSliceFlagExpected = []bool{true, true} 397 | 398 | var byteSliceFlag []byte 399 | sc.ByteSlice(&byteSliceFlag, "bysf", "byteSlice", "byte slice flag") 400 | inputArgs = append(inputArgs, "-bysf", "17", "-bysf", "18") 401 | var byteSliceFlagExpected = []uint8{17, 18} 402 | 403 | var durationFlag time.Duration 404 | sc.Duration(&durationFlag, "df", "duration", "duration flag") 405 | inputArgs = append(inputArgs, "-df", "33s") 406 | var durationFlagExpected = time.Second * 33 407 | 408 | var durationSliceFlag []time.Duration 409 | sc.DurationSlice(&durationSliceFlag, "dsf", "durationSlice", "duration slice flag") 410 | inputArgs = append(inputArgs, "-dsf", "33s", "-dsf", "1h") 411 | var durationSliceFlagExpected = []time.Duration{time.Second * 33, time.Hour} 412 | 413 | var float32Flag float32 414 | sc.Float32(&float32Flag, "f32", "float32", "float32 flag") 415 | inputArgs = append(inputArgs, "-f32", "33.343") 416 | var float32FlagExpected float32 = 33.343 417 | 418 | var float32SliceFlag []float32 419 | sc.Float32Slice(&float32SliceFlag, "f32s", "float32Slice", "float32 slice flag") 420 | inputArgs = append(inputArgs, "-f32s", "33.343", "-f32s", "33.222") 421 | var float32SliceFlagExpected = []float32{33.343, 33.222} 422 | 423 | var float64Flag float64 424 | sc.Float64(&float64Flag, "f64", "float64", "float64 flag") 425 | inputArgs = append(inputArgs, "-f64", "33.222343") 426 | var float64FlagExpected = 33.222343 427 | 428 | var float64SliceFlag []float64 429 | sc.Float64Slice(&float64SliceFlag, "f64s", "float64Slice", "float64 slice flag") 430 | inputArgs = append(inputArgs, "-f64s", "64.343", "-f64s", "64.222") 431 | var float64SliceFlagExpected = []float64{64.343, 64.222} 432 | 433 | var intFlag int 434 | sc.Int(&intFlag, "i", "int", "int flag") 435 | inputArgs = append(inputArgs, "-i", "3553") 436 | var intFlagExpected = 3553 437 | 438 | var intSliceFlag []int 439 | sc.IntSlice(&intSliceFlag, "is", "intSlice", "int slice flag") 440 | inputArgs = append(inputArgs, "-is", "6446", "-is", "64") 441 | var intSliceFlagExpected = []int{6446, 64} 442 | 443 | var uintFlag uint 444 | sc.UInt(&uintFlag, "ui", "uint", "uint flag") 445 | inputArgs = append(inputArgs, "-ui", "3553") 446 | var uintFlagExpected uint = 3553 447 | 448 | var uintSliceFlag []uint 449 | sc.UIntSlice(&uintSliceFlag, "uis", "uintSlice", "uint slice flag") 450 | inputArgs = append(inputArgs, "-uis", "6446", "-uis", "64") 451 | var uintSliceFlagExpected = []uint{6446, 64} 452 | 453 | var uint64Flag uint64 454 | sc.UInt64(&uint64Flag, "ui64", "uint64", "uint64 flag") 455 | inputArgs = append(inputArgs, "-ui64", "3553") 456 | var uint64FlagExpected uint64 = 3553 457 | 458 | var uint64SliceFlag []uint64 459 | sc.UInt64Slice(&uint64SliceFlag, "ui64s", "uint64Slice", "uint64 slice flag") 460 | inputArgs = append(inputArgs, "-ui64s", "6446", "-ui64s", "64") 461 | var uint64SliceFlagExpected = []uint64{6446, 64} 462 | 463 | var uint32Flag uint32 464 | sc.UInt32(&uint32Flag, "ui32", "uint32", "uint32 flag") 465 | inputArgs = append(inputArgs, "-ui32", "6446") 466 | var uint32FlagExpected uint32 = 6446 467 | 468 | var uint32SliceFlag []uint32 469 | sc.UInt32Slice(&uint32SliceFlag, "ui32s", "uint32Slice", "uint32 slice flag") 470 | inputArgs = append(inputArgs, "-ui32s", "6446", "-ui32s", "64") 471 | var uint32SliceFlagExpected = []uint32{6446, 64} 472 | 473 | var uint16Flag uint16 474 | sc.UInt16(&uint16Flag, "ui16", "uint16", "uint16 flag") 475 | inputArgs = append(inputArgs, "-ui16", "6446") 476 | var uint16FlagExpected uint16 = 6446 477 | 478 | var uint16SliceFlag []uint16 479 | sc.UInt16Slice(&uint16SliceFlag, "ui16s", "uint16Slice", "uint16 slice flag") 480 | inputArgs = append(inputArgs, "-ui16s", "6446", "-ui16s", "64") 481 | var uint16SliceFlagExpected = []uint16{6446, 64} 482 | 483 | var uint8Flag uint8 484 | sc.UInt8(&uint8Flag, "ui8", "uint8", "uint8 flag") 485 | inputArgs = append(inputArgs, "-ui8", "50") 486 | var uint8FlagExpected uint8 = 50 487 | 488 | var uint8SliceFlag []uint8 489 | sc.UInt8Slice(&uint8SliceFlag, "ui8s", "uint8Slice", "uint8 slice flag") 490 | inputArgs = append(inputArgs, "-ui8s", "3", "-ui8s", "2") 491 | var uint8SliceFlagExpected = []uint8{uint8(3), uint8(2)} 492 | 493 | var int64Flag int64 494 | sc.Int64(&int64Flag, "i64", "i64", "int64 flag") 495 | inputArgs = append(inputArgs, "-i64", "33445566") 496 | var int64FlagExpected int64 = 33445566 497 | 498 | var int64SliceFlag []int64 499 | sc.Int64Slice(&int64SliceFlag, "i64s", "int64Slice", "int64 slice flag") 500 | if err != nil { 501 | t.Fatal(err) 502 | } 503 | inputArgs = append(inputArgs, "-i64s", "40", "-i64s", "50") 504 | var int64SliceFlagExpected = []int64{40, 50} 505 | 506 | var int32Flag int32 507 | sc.Int32(&int32Flag, "i32", "int32", "int32 flag") 508 | inputArgs = append(inputArgs, "-i32", "445566") 509 | var int32FlagExpected int32 = 445566 510 | 511 | var int32SliceFlag []int32 512 | sc.Int32Slice(&int32SliceFlag, "i32s", "int32Slice", "uint32 slice flag") 513 | inputArgs = append(inputArgs, "-i32s", "40", "-i32s", "50") 514 | var int32SliceFlagExpected = []int32{40, 50} 515 | 516 | var int16Flag int16 517 | sc.Int16(&int16Flag, "i16", "int16", "int16 flag") 518 | if err != nil { 519 | t.Fatal(err) 520 | } 521 | inputArgs = append(inputArgs, "-i16", "5566") 522 | var int16FlagExpected int16 = 5566 523 | 524 | var int16SliceFlag []int16 525 | sc.Int16Slice(&int16SliceFlag, "i16s", "int16Slice", "int16 slice flag") 526 | inputArgs = append(inputArgs, "-i16s", "40", "-i16s", "50") 527 | var int16SliceFlagExpected = []int16{40, 50} 528 | 529 | var int8Flag int8 530 | sc.Int8(&int8Flag, "i8", "int8", "int8 flag") 531 | inputArgs = append(inputArgs, "-i8", "32") 532 | var int8FlagExpected int8 = 32 533 | 534 | var int8SliceFlag []int8 535 | sc.Int8Slice(&int8SliceFlag, "i8s", "int8Slice", "uint8 slice flag") 536 | inputArgs = append(inputArgs, "-i8s", "4", "-i8s", "2") 537 | var int8SliceFlagExpected = []int8{4, 2} 538 | 539 | var ipFlag net.IP 540 | sc.IP(&ipFlag, "ip", "ipFlag", "ip flag") 541 | inputArgs = append(inputArgs, "-ip", "1.1.1.1") 542 | var ipFlagExpected = net.IPv4(1, 1, 1, 1) 543 | 544 | var ipSliceFlag []net.IP 545 | sc.IPSlice(&ipSliceFlag, "ips", "ipFlagSlice", "ip slice flag") 546 | inputArgs = append(inputArgs, "-ips", "1.1.1.1", "-ips", "4.4.4.4") 547 | var ipSliceFlagExpected = []net.IP{net.IPv4(1, 1, 1, 1), net.IPv4(4, 4, 4, 4)} 548 | 549 | var hwFlag net.HardwareAddr 550 | sc.HardwareAddr(&hwFlag, "hw", "hwFlag", "hw flag") 551 | inputArgs = append(inputArgs, "-hw", "32:00:16:46:20:00") 552 | hwFlagExpected, err := net.ParseMAC("32:00:16:46:20:00") 553 | if err != nil { 554 | t.Fatal(err) 555 | } 556 | 557 | var hwFlagSlice []net.HardwareAddr 558 | sc.HardwareAddrSlice(&hwFlagSlice, "hws", "hwFlagSlice", "hw slice flag") 559 | inputArgs = append(inputArgs, "-hws", "32:00:16:46:20:00", "-hws", "32:00:16:46:20:01") 560 | macA, err := net.ParseMAC("32:00:16:46:20:00") 561 | if err != nil { 562 | t.Fatal(err) 563 | } 564 | macB, err := net.ParseMAC("32:00:16:46:20:01") 565 | if err != nil { 566 | t.Fatal(err) 567 | } 568 | var hwFlagSliceExpected = []net.HardwareAddr{macA, macB} 569 | 570 | var maskFlag net.IPMask 571 | sc.IPMask(&maskFlag, "m", "mFlag", "mask flag") 572 | inputArgs = append(inputArgs, "-m", "255.255.255.255") 573 | var maskFlagExpected = net.IPMask([]byte{255, 255, 255, 255}) 574 | 575 | var maskSliceFlag []net.IPMask 576 | sc.IPMaskSlice(&maskSliceFlag, "ms", "mFlagSlice", "mask slice flag") 577 | if err != nil { 578 | t.Fatal(err) 579 | } 580 | inputArgs = append(inputArgs, "-ms", "255.255.255.255", "-ms", "255.255.255.0") 581 | var maskSliceFlagExpected = []net.IPMask{net.IPMask([]byte{255, 255, 255, 255}), net.IPMask([]byte{255, 255, 255, 0})} 582 | 583 | // display help with all flags used 584 | flaggy.ShowHelp("Showing help from TestSCInputParsing test.") 585 | 586 | // Parse arguments 587 | flaggy.ParseArgs(inputArgs) 588 | 589 | // validate parsed values 590 | if stringFlag != stringFlagExpected { 591 | t.Fatal("string flag incorrect", stringFlag, stringFlagExpected) 592 | } 593 | 594 | for i, f := range stringSliceFlagExpected { 595 | if stringSliceFlag[i] != f { 596 | t.Fatal("stringSlice value incorrect", stringSliceFlag[i], f) 597 | } 598 | } 599 | for i, f := range stringSliceCommaFlagExpected { 600 | if stringSliceCommaFlag[i] != f { 601 | t.Fatal("stringSlice value incorrect", stringSliceCommaFlag[i], f) 602 | } 603 | } 604 | 605 | if boolFlag != boolFlagExpected { 606 | t.Fatal("bool flag incorrect", boolFlag, boolFlagExpected) 607 | } 608 | 609 | for i, f := range boolSliceFlagExpected { 610 | if boolSliceFlag[i] != f { 611 | t.Fatal("boolSlice value incorrect", boolSliceFlag[i], f) 612 | } 613 | } 614 | 615 | for i, f := range byteSliceFlagExpected { 616 | if byteSliceFlag[i] != f { 617 | t.Fatal("byteSlice value incorrect", boolSliceFlag[i], f) 618 | } 619 | } 620 | 621 | if durationFlag != durationFlagExpected { 622 | t.Fatal("duration flag incorrect", durationFlag, durationFlagExpected) 623 | } 624 | 625 | for i, f := range durationSliceFlagExpected { 626 | if durationSliceFlag[i] != f { 627 | t.Fatal("durationSlice value incorrect", durationSliceFlag[i], f) 628 | } 629 | } 630 | 631 | if float32Flag != float32FlagExpected { 632 | t.Fatal("float32 flag incorrect", float32Flag, float32FlagExpected) 633 | } 634 | 635 | for i, f := range float32SliceFlagExpected { 636 | if float32SliceFlag[i] != f { 637 | t.Fatal("float32Slice value incorrect", float32SliceFlag[i], f) 638 | } 639 | } 640 | 641 | if float64Flag != float64FlagExpected { 642 | t.Fatal("float64 flag incorrect", float64Flag, float64FlagExpected) 643 | } 644 | 645 | for i, f := range float64SliceFlagExpected { 646 | if float64SliceFlag[i] != f { 647 | t.Fatal("float64Slice value incorrect", float64SliceFlag[i], f) 648 | } 649 | } 650 | 651 | if intFlag != intFlagExpected { 652 | t.Fatal("int flag incorrect", intFlag, intFlagExpected) 653 | } 654 | 655 | for i, f := range intSliceFlagExpected { 656 | if intSliceFlag[i] != f { 657 | t.Fatal("intSlice value incorrect", intSliceFlag[i], f) 658 | } 659 | } 660 | 661 | if uintFlag != uintFlagExpected { 662 | t.Fatal("uint flag incorrect", uintFlag, uintFlagExpected) 663 | } 664 | 665 | for i, f := range uintSliceFlagExpected { 666 | if uintSliceFlag[i] != f { 667 | t.Fatal("uintSlice value incorrect", uintSliceFlag[i], f) 668 | } 669 | } 670 | 671 | if uint64Flag != uint64FlagExpected { 672 | t.Fatal("uint64 flag incorrect", uint64Flag, uint64FlagExpected) 673 | } 674 | 675 | for i, f := range uint64SliceFlagExpected { 676 | if uint64SliceFlag[i] != f { 677 | t.Fatal("uint64Slice value incorrect", uint64SliceFlag[i], f) 678 | } 679 | } 680 | 681 | if uint32Flag != uint32FlagExpected { 682 | t.Fatal("uint32 flag incorrect", uint32Flag, uint32FlagExpected) 683 | } 684 | 685 | for i, f := range uint32SliceFlagExpected { 686 | if uint32SliceFlag[i] != f { 687 | t.Fatal("uint32Slice value incorrect", uint32SliceFlag[i], f) 688 | } 689 | } 690 | 691 | if uint16Flag != uint16FlagExpected { 692 | t.Fatal("uint16 flag incorrect", uint16Flag, uint16FlagExpected) 693 | } 694 | 695 | for i, f := range uint16SliceFlagExpected { 696 | if uint16SliceFlag[i] != f { 697 | t.Fatal("uint16Slice value incorrect", uint16SliceFlag[i], f) 698 | } 699 | } 700 | 701 | if uint8Flag != uint8FlagExpected { 702 | t.Fatal("uint8 flag incorrect", uint8Flag, uint8FlagExpected) 703 | } 704 | 705 | for i, f := range uint8SliceFlagExpected { 706 | if uint8SliceFlag[i] != f { 707 | t.Fatal("uint8Slice value", i, "incorrect", uint8SliceFlag[i], f) 708 | } 709 | } 710 | 711 | if int64Flag != int64FlagExpected { 712 | t.Fatal("int64 flag incorrect", int64Flag, int64FlagExpected) 713 | } 714 | 715 | for i, f := range int64SliceFlagExpected { 716 | if int64SliceFlag[i] != f { 717 | t.Fatal("int64Slice value incorrect", int64SliceFlag[i], f) 718 | } 719 | } 720 | 721 | if int32Flag != int32FlagExpected { 722 | t.Fatal("int32 flag incorrect", int32Flag, int32FlagExpected) 723 | } 724 | 725 | for i, f := range int32SliceFlagExpected { 726 | if int32SliceFlag[i] != f { 727 | t.Fatal("int32Slice value incorrect", int32SliceFlag[i], f) 728 | } 729 | } 730 | 731 | if int16Flag != int16FlagExpected { 732 | t.Fatal("int16 flag incorrect", int16Flag, int16FlagExpected) 733 | } 734 | 735 | for i, f := range int16SliceFlagExpected { 736 | if int16SliceFlag[i] != f { 737 | t.Fatal("int16Slice value incorrect", int16SliceFlag[i], f) 738 | } 739 | } 740 | 741 | if int8Flag != int8FlagExpected { 742 | t.Fatal("int8 flag incorrect", int8Flag, int8FlagExpected) 743 | } 744 | 745 | for i, f := range int8SliceFlagExpected { 746 | if int8SliceFlag[i] != f { 747 | t.Fatal("int8Slice value incorrect", int8SliceFlag[i], f) 748 | } 749 | } 750 | 751 | if !ipFlag.Equal(ipFlagExpected) { 752 | t.Fatal("ip flag incorrect", ipFlag, ipFlagExpected) 753 | } 754 | 755 | for i, f := range ipSliceFlagExpected { 756 | if !f.Equal(ipSliceFlag[i]) { 757 | t.Fatal("ipSlice value incorrect", ipSliceFlag[i], f) 758 | } 759 | } 760 | 761 | if hwFlag.String() != hwFlagExpected.String() { 762 | t.Fatal("hw flag incorrect", hwFlag, hwFlagExpected) 763 | } 764 | 765 | for i, f := range hwFlagSliceExpected { 766 | if f.String() != hwFlagSlice[i].String() { 767 | t.Fatal("hw flag slice value incorrect", hwFlagSlice[i].String(), f.String()) 768 | } 769 | } 770 | 771 | if maskFlag.String() != maskFlagExpected.String() { 772 | t.Fatal("mask flag incorrect", maskFlag, maskFlagExpected) 773 | } 774 | 775 | for i, f := range maskSliceFlagExpected { 776 | if f.String() != maskSliceFlag[i].String() { 777 | t.Fatal("mask flag slice value incorrect", maskSliceFlag[i].String(), f.String()) 778 | } 779 | } 780 | } 781 | 782 | // TestSCBoolFlag tests bool flags on subcommands 783 | func TestSCBoolFlag(t *testing.T) { 784 | p := flaggy.NewParser("TestSubcommandBoolParse") 785 | newSC := flaggy.NewSubcommand("testSubcommand") 786 | 787 | var boolFlag bool 788 | newSC.Bool(&boolFlag, "f", "flag", "test bool flag on subcommand") 789 | 790 | p.AttachSubcommand(newSC, 1) 791 | 792 | os.Args = []string{"binaryName", "testSubcommand", "--flag"} 793 | err := p.Parse() 794 | if err != nil { 795 | t.Fatal("Error parsing args: " + err.Error()) 796 | } 797 | } 798 | 799 | // TestNestedSCBoolFlag tests bool flags on nested subcommands 800 | func TestNestedSCBoolFlag(t *testing.T) { 801 | p := flaggy.NewParser("TestSubcommandBoolParse") 802 | newSC := flaggy.NewSubcommand("mainSubcommand") 803 | subSC := flaggy.NewSubcommand("subSubCommand") 804 | 805 | var boolFlag bool 806 | subSC.Bool(&boolFlag, "f", "flag", "test bool flag on subcommand") 807 | 808 | newSC.AttachSubcommand(subSC, 1) 809 | p.AttachSubcommand(newSC, 1) 810 | 811 | os.Args = []string{"binaryName", "mainSubcommand", "subSubCommand", "--flag"} 812 | err := p.Parse() 813 | if err != nil { 814 | t.Fatal("Error parsing args: " + err.Error()) 815 | } 816 | } 817 | 818 | func TestParseErrorsAreReportedRegression(t *testing.T) { 819 | defer func() { 820 | r := recover() 821 | if r == nil { 822 | t.Fatal("Expected crash on invalid syntax") 823 | } 824 | }() 825 | 826 | flaggy.ResetParser() 827 | intFlag := 42 828 | flaggy.Int(&intFlag, "i", "int", "dummy") 829 | os.Args = []string{"prog", "--int", "abc"} 830 | flaggy.Parse() 831 | } 832 | --------------------------------------------------------------------------------