├── go.sum ├── examples ├── sliceFlag │ ├── .gitignore │ └── main.go ├── trailingArguments │ ├── .gitignore │ └── main.go ├── .gitignore ├── simple │ └── main.go ├── positionalValue │ └── main.go ├── subcommand │ └── main.go ├── complex │ └── main.go ├── customParser │ └── main.go └── customTemplate │ └── main.go ├── assets ├── logo.png └── flaggy-gopher.png ├── byte_types.go ├── go.mod ├── .travis.yml ├── help.go ├── .gitignore ├── .github └── workflows │ └── go-tests.yml ├── positionalValue.go ├── argumentParser.go ├── parsedValue.go ├── main_test.go ├── LICENSE ├── CONTRIBUTING.md ├── scan_result.go ├── helpValues_whitebox_test.go ├── AGENTS.md ├── help_format_dump_test.go ├── parser_test.go ├── help_sort_test.go ├── parser_completion_cli_test.go ├── completion_test.go ├── flaggy_test.go ├── benchmark_test.go ├── helpValues_blackbox_test.go ├── parser_refactor_regression_test.go ├── examples_test.go ├── README.md ├── parser.go ├── completion.go ├── helpValues.go ├── flaggy.go ├── flag_test.go └── flag.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/sliceFlag/.gitignore: -------------------------------------------------------------------------------- 1 | sliceFlag 2 | -------------------------------------------------------------------------------- /examples/trailingArguments/.gitignore: -------------------------------------------------------------------------------- 1 | trailingArguments 2 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/integrii/flaggy/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/flaggy-gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/integrii/flaggy/HEAD/assets/flaggy-gopher.png -------------------------------------------------------------------------------- /byte_types.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | // Base64Bytes is a []byte interpreted as base64 when parsed from flags. 4 | type Base64Bytes []byte 5 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | complex/complex 2 | customParser/customParser 3 | positionalValue/positionalValue 4 | simple/simple 5 | subcommand/subcommand 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/integrii/flaggy 2 | 3 | go 1.25 4 | 5 | retract ( 6 | v1.6.0 // Release version was used twice, causing go proxy errors. 7 | ) 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 = `{{range $idx, $line := .Lines}}{{if gt $idx 0}} 7 | {{end}}{{$line}}{{end}}` 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/go-tests.yml: -------------------------------------------------------------------------------- 1 | name: Go Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: '1.25' 17 | - name: Run tests 18 | run: go test -v ./... 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /parsedValue.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | // parsedValue represents a flag or subcommand that was parsed. Primarily 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 | ConsumesNext bool // indicates that parsing this value consumed the following CLI token 11 | } 12 | 13 | // newParsedValue creates and returns a new parsedValue struct with the 14 | // supplied values set 15 | func newParsedValue(key string, value string, isPositional bool, consumesNext bool) parsedValue { 16 | if len(key) == 0 && len(value) == 0 { 17 | panic("can't add parsed value with no key or value") 18 | } 19 | return parsedValue{ 20 | Key: key, 21 | Value: value, 22 | IsPositional: isPositional, 23 | ConsumesNext: consumesNext, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 passed 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Flaggy 2 | 3 | Thanks for your interest in improving Flaggy! The following guide outlines the standard workflow for proposing and landing a change. Following these steps helps maintainers review contributions efficiently and keeps the project healthy. 4 | 5 | * **Getting Started** 6 | - **Open an issue** describing the problem you want to fix or the feature you plan to add. This gives us a chance to discuss the proposal before you start coding. 7 | - **Fork the repository** to your own GitHub account so you can develop the change independently of the main project. 8 | * **Implementing Your Change** 9 | - Make your modifications in your fork (create a topic branch if that helps keep things organized). 10 | - Add or update tests so your change is well covered. 11 | - Run the full test suite locally and ensure everything passes (`go test ./...`). 12 | * **Opening a Pull Request** 13 | - Push your updates to your fork. 14 | - Open a pull request against the main repository, referencing the issue created earlier. Include context about what the change does and any testing performed. 15 | - Participate in the review process and incorporate any requested changes. Keep your branch up to date with the main branch as needed. 16 | 17 | We appreciate your contributions and look forward to collaborating with you! 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scan_result.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | // flagScanResult summarizes the outcome of scanning arguments for a parser. 4 | type flagScanResult struct { 5 | // Positionals lists positional tokens (subcommands or positional args) in 6 | // the order they were encountered, along with their indexes in the source 7 | // argument slice. 8 | Positionals []positionalToken 9 | // ForwardArgs contains arguments that were intentionally left untouched so 10 | // that downstream parsers can process them. These tokens maintain their 11 | // original order. 12 | ForwardArgs []string 13 | // HelpRequested reports whether a help flag (-h/--help) was encountered 14 | // while scanning this parser. 15 | HelpRequested bool 16 | // Subcommand holds the first subcommand encountered while scanning. When 17 | // non-nil, scanning stops and the remaining arguments are handed off to the 18 | // referenced parser. 19 | Subcommand *subcommandMatch 20 | } 21 | 22 | // positionalToken tracks a positional argument's value and the index it was 23 | // read from in the source slice. 24 | type positionalToken struct { 25 | Value string 26 | Index int 27 | } 28 | 29 | // subcommandMatch captures the metadata necessary to hand control over to a 30 | // downstream subcommand parser. 31 | type subcommandMatch struct { 32 | // Command references the subcommand that matched the positional token. 33 | Command *Subcommand 34 | // Token points to the positional token that triggered the match. 35 | Token positionalToken 36 | // RelativeDepth tracks the positional depth (1-based) where the match was 37 | // found. This mirrors how subcommand positions are configured. 38 | RelativeDepth int 39 | } 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 properties 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 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Flaggy Contribution Guidelines for Agents 2 | 3 | This repository provides a zero-dependency command-line parsing library that must always rely exclusively on the Go standard library for runtime and test code. The core principles that follow preserve the project's lightweight nature and its focus on easily understandable, flat code. 4 | 5 | ## Core Principles 6 | - Keep the codebase dependency-free beyond the Go standard library. Adding third-party modules for any purpose, including testing, is not permitted. 7 | - Prefer flat, straightforward control flow. Avoid `else` statements when possible and limit indentation depth so examples remain approachable for beginners. 8 | - Optimize every change for readability. Favor descriptive names, small logical blocks, and explanatory comments that teach users how the parser behaves. 9 | 10 | ## Documentation Expectations 11 | - Maintain clear, beginner-friendly explanations throughout the codebase. Add comments to **every** function and test describing what they do and why they matter to the overall library. 12 | - Annotate each stanza of code with concise comments, even when the logic appears self-explanatory. 13 | - Keep primary documentation accurate. Update `README.md` and `CONTRIBUTING.md` whenever your modifications alter usage instructions, contribution workflows, or observable behavior. 14 | 15 | ## Tooling Requirements 16 | - Always run `go fmt`, `go vet`, and `goimports` over affected packages before committing. 17 | - Favor consistent formatting and import organization that highlight the minimal surface area of each example. 18 | 19 | ## Testing Guidance 20 | - When writing tests, ensure the accompanying comment explains exactly what is being verified. 21 | - Leave benchmarks and examples with clarifying comments so readers immediately understand the intent and scope of each scenario. 22 | 23 | Following these guidelines keeps Flaggy's codebase welcoming to newcomers and aligned with its lightweight philosophy. 24 | -------------------------------------------------------------------------------- /help_format_dump_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/integrii/flaggy" 9 | ) 10 | 11 | // TestPrintHelpToStdout builds a representative parser and prints the current 12 | // help output to stdout for manual inspection. Run with: 13 | // 14 | // go test -run TestPrintHelpToStdout -v 15 | func TestPrintHelpToStdout(t *testing.T) { 16 | p := flaggy.NewParser("help-dump") 17 | p.Description = "Sample command showing current help formatting." 18 | 19 | // Set up a couple of subcommands 20 | scA := flaggy.NewSubcommand("subA") 21 | scA.ShortName = "a" 22 | scA.Description = "Subcommand A description." 23 | 24 | scB := flaggy.NewSubcommand("subB") 25 | scB.ShortName = "b" 26 | scB.Description = "Subcommand B description." 27 | 28 | p.AttachSubcommand(scA, 1) 29 | scA.AttachSubcommand(scB, 1) 30 | 31 | // Add a positional to demonstrate the section 32 | var posA = "defaultPosA" 33 | scA.AddPositionalValue(&posA, "posA", 2, false, "Example positional for A.") 34 | 35 | // Add a few flags of different types 36 | var s string = "defaultStringHere" 37 | var i int 38 | var b bool 39 | var d time.Duration 40 | p.String(&s, "s", "stringFlag", "Example string flag.") 41 | p.Int(&i, "i", "intFlag", "Example int flag.") 42 | p.Bool(&b, "b", "boolFlag", "Example bool flag.") 43 | p.Duration(&d, "d", "durationFlag", "Example duration flag.") 44 | 45 | // Optional extra help lines to show placement in template 46 | p.AdditionalHelpPrepend = "This is a prepend for help" 47 | p.AdditionalHelpAppend = "This is an append for help" 48 | 49 | // Parse to set subcommand context to scB 50 | if err := p.ParseArgs([]string{"subA", "subB"}); err != nil { 51 | t.Fatalf("parse: unexpected error: %v", err) 52 | } 53 | 54 | // Redirect help output from stderr to stdout for visibility under `go test -v`. 55 | savedStderr := os.Stderr 56 | os.Stderr = os.Stdout 57 | defer func() { os.Stderr = savedStderr }() 58 | 59 | // Print current help to stdout 60 | p.ShowHelpWithMessage("This is a help message on exit") 61 | } 62 | -------------------------------------------------------------------------------- /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 | ConsumesNext: true, 52 | }, 53 | } 54 | args = []string{"--flaggy", "testing", "unusedFlag"} 55 | unusedArgs = findArgsNotInParsedValues(args, parsedValues) 56 | t.Log(unusedArgs) 57 | if len(unusedArgs) == 0 { 58 | t.Fatal("Found no args as unused when --flaggy=testing should have been detected") 59 | } 60 | if len(unusedArgs) != 1 { 61 | t.Fatal("Invalid number of unused args found. Expected 1 but found", len(unusedArgs)) 62 | } 63 | } 64 | 65 | func TestFindArgsNotInParsedValuesSkipsEmptyConsumedValues(t *testing.T) { 66 | t.Parallel() 67 | 68 | args := []string{"-log.file.dir", ""} 69 | parsedValues := []parsedValue{ 70 | { 71 | Key: "log.file.dir", 72 | Value: "", 73 | ConsumesNext: true, 74 | }, 75 | } 76 | 77 | unusedArgs := findArgsNotInParsedValues(args, parsedValues) 78 | if len(unusedArgs) != 0 { 79 | t.Fatalf("expected no unused args, found %v", unusedArgs) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /help_sort_test.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestHelpFlagsSortedWhenEnabled(t *testing.T) { 10 | // Use default parser and functions to enable sort 11 | ResetParser() 12 | DefaultParser.ShowHelpWithHFlag = false 13 | DefaultParser.ShowVersionWithVersionFlag = false 14 | SortFlagsByLongName() 15 | 16 | var a, b, z string 17 | // Intentionally add in non-sorted order 18 | String(&z, "z", "zeta", "") 19 | String(&a, "a", "alpha", "") 20 | String(&b, "b", "beta", "") 21 | 22 | rd, wr, err := os.Pipe() 23 | if err != nil { 24 | t.Fatalf("pipe error: %v", err) 25 | } 26 | saved := os.Stderr 27 | os.Stderr = wr 28 | defer func() { os.Stderr = saved }() 29 | 30 | DefaultParser.ShowHelp() 31 | 32 | buf := make([]byte, 4096) 33 | n, err := rd.Read(buf) 34 | if err != nil { 35 | t.Fatalf("read error: %v", err) 36 | } 37 | lines := strings.Split(string(buf[:n]), "\n") 38 | 39 | // collect just the flag lines (start with two spaces then a dash or spaces then --) 40 | var flagLines []string 41 | inFlags := false 42 | for _, l := range lines { 43 | if strings.HasPrefix(l, " Flags:") { 44 | inFlags = true 45 | continue 46 | } 47 | if inFlags { 48 | if strings.TrimSpace(l) == "" { 49 | break 50 | } 51 | flagLines = append(flagLines, l) 52 | } 53 | } 54 | if len(flagLines) < 3 { 55 | t.Fatalf("expected at least 3 flag lines, got %d: %q", len(flagLines), flagLines) 56 | } 57 | 58 | // find the three of interest 59 | var idxAlpha, idxBeta, idxZeta = -1, -1, -1 60 | for i, l := range flagLines { 61 | if strings.Contains(l, "--alpha") { 62 | idxAlpha = i 63 | } 64 | if strings.Contains(l, "--beta") { 65 | idxBeta = i 66 | } 67 | if strings.Contains(l, "--zeta") { 68 | idxZeta = i 69 | } 70 | } 71 | if idxAlpha == -1 || idxBeta == -1 || idxZeta == -1 { 72 | t.Fatalf("expected to find alpha, beta, zeta in flags; got: %q", flagLines) 73 | } 74 | if !(idxAlpha < idxBeta && idxBeta < idxZeta) { 75 | t.Fatalf("flags not sorted: alpha=%d beta=%d zeta=%d; lines=%q", idxAlpha, idxBeta, idxZeta, flagLines) 76 | } 77 | } 78 | 79 | func TestHelpFlagsSortedReversed(t *testing.T) { 80 | // Use default parser and reversed sort 81 | ResetParser() 82 | DefaultParser.ShowHelpWithHFlag = false 83 | DefaultParser.ShowVersionWithVersionFlag = false 84 | SortFlagsByLongNameReversed() 85 | 86 | var a, b, z string 87 | // Intentionally add in non-sorted order 88 | String(&z, "z", "zeta", "") 89 | String(&a, "a", "alpha", "") 90 | String(&b, "b", "beta", "") 91 | 92 | rd, wr, err := os.Pipe() 93 | if err != nil { 94 | t.Fatalf("pipe error: %v", err) 95 | } 96 | saved := os.Stderr 97 | os.Stderr = wr 98 | defer func() { os.Stderr = saved }() 99 | 100 | DefaultParser.ShowHelp() 101 | 102 | buf := make([]byte, 4096) 103 | n, err := rd.Read(buf) 104 | if err != nil { 105 | t.Fatalf("read error: %v", err) 106 | } 107 | lines := strings.Split(string(buf[:n]), "\n") 108 | 109 | var flagLines []string 110 | inFlags := false 111 | for _, l := range lines { 112 | if strings.HasPrefix(l, " Flags:") { 113 | inFlags = true 114 | continue 115 | } 116 | if inFlags { 117 | if strings.TrimSpace(l) == "" { 118 | break 119 | } 120 | flagLines = append(flagLines, l) 121 | } 122 | } 123 | if len(flagLines) < 3 { 124 | t.Fatalf("expected at least 3 flag lines, got %d: %q", len(flagLines), flagLines) 125 | } 126 | 127 | var idxAlpha, idxBeta, idxZeta = -1, -1, -1 128 | for i, l := range flagLines { 129 | if strings.Contains(l, "--alpha") { 130 | idxAlpha = i 131 | } 132 | if strings.Contains(l, "--beta") { 133 | idxBeta = i 134 | } 135 | if strings.Contains(l, "--zeta") { 136 | idxZeta = i 137 | } 138 | } 139 | if idxAlpha == -1 || idxBeta == -1 || idxZeta == -1 { 140 | t.Fatalf("expected to find alpha, beta, zeta in flags; got: %q", flagLines) 141 | } 142 | if !(idxZeta < idxBeta && idxBeta < idxAlpha) { 143 | t.Fatalf("flags not reverse-sorted: alpha=%d beta=%d zeta=%d; lines=%q", idxAlpha, idxBeta, idxZeta, flagLines) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /parser_completion_cli_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/integrii/flaggy" 10 | ) 11 | 12 | // runParserWithArgs executes a new parser using the provided os.Args tail while capturing 13 | // stdout, stderr, and the panic payload triggered by exitOrPanic so tests can assert behavior. 14 | func runParserWithArgs(t *testing.T, args []string) (string, string, any) { 15 | t.Helper() 16 | 17 | originalArgs := os.Args 18 | os.Args = append([]string{"starfleet"}, args...) 19 | defer func() { 20 | os.Args = originalArgs 21 | }() 22 | 23 | parser := flaggy.NewParser("starfleet") 24 | 25 | originalStdout := os.Stdout 26 | stdoutReader, stdoutWriter, err := os.Pipe() 27 | if err != nil { 28 | t.Fatalf("failed to create stdout pipe: %v", err) 29 | } 30 | 31 | originalStderr := os.Stderr 32 | stderrReader, stderrWriter, err := os.Pipe() 33 | if err != nil { 34 | t.Fatalf("failed to create stderr pipe: %v", err) 35 | } 36 | 37 | os.Stdout = stdoutWriter 38 | os.Stderr = stderrWriter 39 | defer func() { 40 | os.Stdout = originalStdout 41 | os.Stderr = originalStderr 42 | }() 43 | 44 | originalPanicSetting := flaggy.PanicInsteadOfExit 45 | flaggy.PanicInsteadOfExit = true 46 | defer func() { 47 | flaggy.PanicInsteadOfExit = originalPanicSetting 48 | }() 49 | 50 | var recovered any 51 | func() { 52 | defer func() { 53 | recovered = recover() 54 | }() 55 | _ = parser.Parse() 56 | }() 57 | 58 | stdoutWriter.Close() 59 | stderrWriter.Close() 60 | 61 | stdoutBytes, err := io.ReadAll(stdoutReader) 62 | if err != nil { 63 | t.Fatalf("failed to read stdout pipe: %v", err) 64 | } 65 | stderrBytes, err := io.ReadAll(stderrReader) 66 | if err != nil { 67 | t.Fatalf("failed to read stderr pipe: %v", err) 68 | } 69 | 70 | stdoutReader.Close() 71 | stderrReader.Close() 72 | 73 | return string(stdoutBytes), string(stderrBytes), recovered 74 | } 75 | 76 | // TestParseCompletionRequiresShell ensures that invoking the completion command without a shell 77 | // prints guidance that lists every supported shell so users know their options. 78 | func TestParseCompletionRequiresShell(t *testing.T) { 79 | _, stderr, recovered := runParserWithArgs(t, []string{"completion"}) 80 | if recovered == nil { 81 | t.Fatalf("expected parser to exit when no shell provided") 82 | } 83 | expectedExit := "Panic instead of exit with code: 2" 84 | if recovered != expectedExit { 85 | t.Fatalf("expected exit code 2 when shell missing: %v", recovered) 86 | } 87 | want := "Supported shells: bash zsh fish powershell nushell" 88 | if !strings.Contains(stderr, want) { 89 | t.Fatalf("expected stderr to describe all supported shells: %s", stderr) 90 | } 91 | } 92 | 93 | // TestParseCompletionSupportsAllShells confirms that every advertised completion shell runs 94 | // through the parser and emits shell-specific output when provided on the command line. 95 | func TestParseCompletionSupportsAllShells(t *testing.T) { 96 | cases := []struct { 97 | shell string 98 | expected string 99 | }{ 100 | {shell: "bash", expected: "# bash completion"}, 101 | {shell: "zsh", expected: "#compdef"}, 102 | {shell: "fish", expected: "# fish completion"}, 103 | {shell: "powershell", expected: "# PowerShell completion"}, 104 | {shell: "nushell", expected: "# nushell completion"}, 105 | } 106 | 107 | for _, tc := range cases { 108 | stdout, stderr, recovered := runParserWithArgs(t, []string{"completion", tc.shell}) 109 | if recovered == nil { 110 | t.Fatalf("expected parser to exit after writing %s completion", tc.shell) 111 | } 112 | expectedExit := "Panic instead of exit with code: 0" 113 | if recovered != expectedExit { 114 | t.Fatalf("expected exit code 0 for %s completion: %v", tc.shell, recovered) 115 | } 116 | if len(stderr) > 0 { 117 | t.Fatalf("expected stderr to be empty for %s completion: %s", tc.shell, stderr) 118 | } 119 | if !strings.Contains(stdout, tc.expected) { 120 | t.Fatalf("expected stdout for %s completion to include %q: %s", tc.shell, tc.expected, stdout) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /completion_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/integrii/flaggy" 8 | ) 9 | 10 | // newCompletionParser builds a parser populated with the fictitious fleet commands used 11 | // to validate shell completion generators across every supported shell variant. 12 | func newCompletionParser() (*flaggy.Parser, []string) { 13 | p := flaggy.NewParser("starfleet") 14 | var route string 15 | p.String(&route, "w", "warp", "Enable warp calibration during deployment") 16 | p.AddPositionalValue(&route, "sector", 2, false, "Target sector coordinate") 17 | 18 | commands := []string{"deploy", "destroy", "diagnose", "dock"} 19 | for _, name := range commands { 20 | sub := flaggy.NewSubcommand(name) 21 | sub.Description = "Handle " + name + " operations" 22 | p.AttachSubcommand(sub, 1) 23 | } 24 | 25 | return p, commands 26 | } 27 | 28 | // verifyCompletionCoverage ensures that each fictitious command surfaces within the 29 | // produced completion output so developers can trust the generator to describe real CLIs. 30 | func verifyCompletionCoverage(t *testing.T, output string, commands []string, shell string) { 31 | t.Helper() 32 | for _, name := range commands { 33 | if !strings.Contains(output, name) { 34 | t.Fatalf("expected %s completion to list command %s: %s", shell, name, output) 35 | } 36 | } 37 | } 38 | 39 | // TestGenerateBashCompletion exercises bash completions with the shared fleet commands. 40 | func TestGenerateBashCompletion(t *testing.T) { 41 | p, commands := newCompletionParser() 42 | out := flaggy.GenerateBashCompletion(p) 43 | verifyCompletionCoverage(t, out, commands, "bash") 44 | if !strings.Contains(out, "--warp") { 45 | t.Fatalf("expected bash completion to include long flag name: %s", out) 46 | } 47 | if !strings.Contains(out, "-w") { 48 | t.Fatalf("expected bash completion to include short flag name: %s", out) 49 | } 50 | if !strings.Contains(out, "sector") { 51 | t.Fatalf("expected bash completion to include positional name: %s", out) 52 | } 53 | } 54 | 55 | // TestGenerateZshCompletion exercises zsh completions with the shared fleet commands. 56 | func TestGenerateZshCompletion(t *testing.T) { 57 | p, commands := newCompletionParser() 58 | out := flaggy.GenerateZshCompletion(p) 59 | verifyCompletionCoverage(t, out, commands, "zsh") 60 | if !strings.Contains(out, "--warp") { 61 | t.Fatalf("expected zsh completion to include long flag name: %s", out) 62 | } 63 | if !strings.Contains(out, "-w") { 64 | t.Fatalf("expected zsh completion to include short flag name: %s", out) 65 | } 66 | if !strings.Contains(out, "sector") { 67 | t.Fatalf("expected zsh completion to include positional name: %s", out) 68 | } 69 | } 70 | 71 | // TestGenerateFishCompletion exercises fish completions with the shared fleet commands. 72 | func TestGenerateFishCompletion(t *testing.T) { 73 | p, commands := newCompletionParser() 74 | out := flaggy.GenerateFishCompletion(p) 75 | verifyCompletionCoverage(t, out, commands, "fish") 76 | if !strings.Contains(out, "complete -c starfleet") { 77 | t.Fatalf("expected fish completion to target starfleet: %s", out) 78 | } 79 | if !strings.Contains(out, "-l warp") { 80 | t.Fatalf("expected fish completion to include long flag name: %s", out) 81 | } 82 | if !strings.Contains(out, "-s w") { 83 | t.Fatalf("expected fish completion to include short flag name: %s", out) 84 | } 85 | if !strings.Contains(out, "sector") { 86 | t.Fatalf("expected fish completion to include positional name: %s", out) 87 | } 88 | } 89 | 90 | // TestGeneratePowerShellCompletion exercises PowerShell completions with fleet commands. 91 | func TestGeneratePowerShellCompletion(t *testing.T) { 92 | p, commands := newCompletionParser() 93 | out := flaggy.GeneratePowerShellCompletion(p) 94 | verifyCompletionCoverage(t, out, commands, "powershell") 95 | if !strings.Contains(out, "Register-ArgumentCompleter") { 96 | t.Fatalf("expected powershell completion to register completer: %s", out) 97 | } 98 | if !strings.Contains(out, "--warp") { 99 | t.Fatalf("expected powershell completion to include long flag name: %s", out) 100 | } 101 | if !strings.Contains(out, "-w") { 102 | t.Fatalf("expected powershell completion to include short flag name: %s", out) 103 | } 104 | if !strings.Contains(out, "sector") { 105 | t.Fatalf("expected powershell completion to include positional name: %s", out) 106 | } 107 | } 108 | 109 | // TestGenerateNushellCompletion exercises Nushell completions with the fleet commands. 110 | func TestGenerateNushellCompletion(t *testing.T) { 111 | p, commands := newCompletionParser() 112 | out := flaggy.GenerateNushellCompletion(p) 113 | verifyCompletionCoverage(t, out, commands, "nushell") 114 | if !strings.Contains(out, "extern \"starfleet\"") { 115 | t.Fatalf("expected nushell completion to expose extern signature: %s", out) 116 | } 117 | if !strings.Contains(out, "\"--warp\"") { 118 | t.Fatalf("expected nushell completion to include long flag name: %s", out) 119 | } 120 | if !strings.Contains(out, "\"-w\"") { 121 | t.Fatalf("expected nushell completion to include short flag name: %s", out) 122 | } 123 | if !strings.Contains(out, "\"sector\"") { 124 | t.Fatalf("expected nushell completion to include positional name: %s", out) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /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 each other. 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 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | // BenchmarkParseSingleFlag measures parsing a single string flag so we can ensure 9 | // the simplest CLI scenario stays efficient for users building tiny utilities. 10 | func BenchmarkParseSingleFlag(b *testing.B) { 11 | // Track allocations to verify the parser remains lightweight. 12 | b.ReportAllocs() 13 | 14 | // Provide a minimal argument slice that exercises a lone string flag. 15 | args := []string{"--name", "benchmark"} 16 | 17 | // Execute the parse flow repeatedly to gather benchmark statistics. 18 | for i := 0; i < b.N; i++ { 19 | // Reset the parser so each iteration starts from a clean state. 20 | ResetParser() 21 | 22 | // Capture the string flag value during parsing. 23 | var name string 24 | 25 | // Declare the flag that the benchmark will parse. 26 | String(&name, "n", "name", "collects a name for benchmarking") 27 | 28 | // Parse the synthetic arguments. 29 | ParseArgs(args) 30 | 31 | // Fail fast if the parsed value is not the expected string. 32 | if name != "benchmark" { 33 | b.Fatalf("expected name to equal benchmark, got %q", name) 34 | } 35 | } 36 | } 37 | 38 | // BenchmarkParseSubcommandWithTwoFlags tracks the performance of parsing a 39 | // subcommand that defines two flags so teams can gauge realistic CLI workloads. 40 | func BenchmarkParseSubcommandWithTwoFlags(b *testing.B) { 41 | // Record allocations during the benchmark to detect regressions. 42 | b.ReportAllocs() 43 | 44 | // Arrange a subcommand invocation with host and port flags populated. 45 | args := []string{"serve", "--host", "localhost", "--port", "8080"} 46 | 47 | // Drive the parser repeatedly to gather benchmark metrics. 48 | for i := 0; i < b.N; i++ { 49 | // Reset state so each run configures fresh flag bindings. 50 | ResetParser() 51 | 52 | // Create the subcommand and variables that store parsed values. 53 | serve := NewSubcommand("serve") 54 | var host string 55 | var port int 56 | 57 | // Declare the host and port flags on the subcommand. 58 | serve.String(&host, "h", "host", "host name to bind to") 59 | serve.Int(&port, "p", "port", "port to listen on") 60 | 61 | // Attach the subcommand so the parser can discover it. 62 | AttachSubcommand(serve, 1) 63 | 64 | // Parse the prepared argument slice. 65 | ParseArgs(args) 66 | 67 | // Ensure the subcommand was detected and values were populated. 68 | if !serve.Used { 69 | b.Fatal("expected serve subcommand to be used") 70 | } 71 | if host != "localhost" || port != 8080 { 72 | b.Fatalf("unexpected host %q or port %d", host, port) 73 | } 74 | } 75 | } 76 | 77 | // BenchmarkParseNestedSubcommandsWithTrailingArgs covers a complex hierarchy of 78 | // three subcommands, mixed flag types, and trailing arguments to validate that 79 | // rich CLIs remain performant. 80 | func BenchmarkParseNestedSubcommandsWithTrailingArgs(b *testing.B) { 81 | // Track allocation counts while exercising the full parser feature set. 82 | b.ReportAllocs() 83 | 84 | // Define arguments that walk through alpha -> beta -> gamma subcommands 85 | // and include trailing values after a -- separator. 86 | args := []string{ 87 | "alpha", "--name", "alpha-task", "--count", "3", "--enabled", "--speed", "1.5", "--timeout", "500ms", 88 | "beta", "--label", "release", "--label", "candidate", "--level", "9", "--active", "--ratio", "1.75", "--interval", "45s", 89 | "gamma", "--mode", "auto", "--max", "100", "--debug", "--threshold", "0.8", "--window", "1m30s", 90 | "--", "artifact-one", "artifact-two", "artifact-three", 91 | } 92 | 93 | // Loop to exercise the parser repeatedly during the benchmark. 94 | for i := 0; i < b.N; i++ { 95 | // Reset state before wiring up the subcommand hierarchy again. 96 | ResetParser() 97 | 98 | // Prepare variables that will hold parsed values from each layer. 99 | var ( 100 | alphaName string 101 | alphaCount int 102 | alphaEnabled bool 103 | alphaSpeed float64 104 | alphaTimeout time.Duration 105 | 106 | betaLabels []string 107 | betaLevel int 108 | betaActive bool 109 | betaRatio float32 110 | betaInterval time.Duration 111 | 112 | gammaMode string 113 | gammaMax uint 114 | gammaDebug bool 115 | gammaThreshold float64 116 | gammaWindow time.Duration 117 | ) 118 | 119 | // Configure the alpha subcommand with mixed flag types. 120 | alpha := NewSubcommand("alpha") 121 | alpha.String(&alphaName, "n", "name", "name for the alpha subcommand") 122 | alpha.Int(&alphaCount, "c", "count", "number of repetitions") 123 | alpha.Bool(&alphaEnabled, "e", "enabled", "whether alpha is enabled") 124 | alpha.Float64(&alphaSpeed, "s", "speed", "speed multiplier") 125 | alpha.Duration(&alphaTimeout, "t", "timeout", "how long alpha should wait") 126 | AttachSubcommand(alpha, 1) 127 | 128 | // Configure the beta subcommand, including slice and duration flags. 129 | beta := NewSubcommand("beta") 130 | beta.StringSlice(&betaLabels, "l", "label", "labels to apply") 131 | beta.Int(&betaLevel, "v", "level", "an integer setting") 132 | beta.Bool(&betaActive, "a", "active", "whether beta is active") 133 | beta.Float32(&betaRatio, "r", "ratio", "ratio value for calculations") 134 | beta.Duration(&betaInterval, "i", "interval", "time between runs") 135 | alpha.AttachSubcommand(beta, 1) 136 | 137 | // Configure the gamma subcommand to complete the hierarchy. 138 | gamma := NewSubcommand("gamma") 139 | gamma.String(&gammaMode, "m", "mode", "mode of operation") 140 | gamma.UInt(&gammaMax, "x", "max", "maximum allowed value") 141 | gamma.Bool(&gammaDebug, "d", "debug", "enable debug logging") 142 | gamma.Float64(&gammaThreshold, "h", "threshold", "threshold used for alerts") 143 | gamma.Duration(&gammaWindow, "w", "window", "time window to inspect") 144 | beta.AttachSubcommand(gamma, 1) 145 | 146 | // Parse the synthetic arguments that exercise the full tree. 147 | ParseArgs(args) 148 | 149 | // Assert that each subcommand was used during parsing. 150 | if !alpha.Used || !beta.Used || !gamma.Used { 151 | b.Fatalf("expected alpha, beta, gamma to be used but got alpha=%t beta=%t gamma=%t", alpha.Used, beta.Used, gamma.Used) 152 | } 153 | 154 | // Confirm the beta subcommand gathered labels and the active flag. 155 | if len(betaLabels) != 2 || !betaActive { 156 | b.Fatalf("expected beta labels and active flag to be parsed correctly") 157 | } 158 | 159 | // Confirm gamma parsed every value correctly, including numeric caps. 160 | if !gammaDebug || gammaMode == "" || gammaMax != 100 { 161 | b.Fatalf("gamma values not parsed as expected: debug=%t mode=%q max=%d", gammaDebug, gammaMode, gammaMax) 162 | } 163 | 164 | // Confirm trailing arguments flowed through after the -- separator. 165 | if len(TrailingArguments) != 3 { 166 | b.Fatalf("expected 3 trailing arguments, got %d", len(TrailingArguments)) 167 | } 168 | 169 | // Validate every alpha flag to ensure full coverage of parsed values. 170 | if alphaName == "" || alphaCount != 3 || !alphaEnabled || alphaTimeout != 500*time.Millisecond { 171 | b.Fatalf("alpha values not parsed correctly") 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /helpValues_blackbox_test.go: -------------------------------------------------------------------------------- 1 | package flaggy_test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | "testing" 8 | "time" 9 | 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 | // Updated to match current help template output (single leading/trailing blank line) 36 | want := []string{ 37 | "", 38 | "TestMinimalHelpOutput", 39 | "", 40 | " Subcommands:", 41 | " completion Generate shell completion script for bash or zsh.", 42 | "", 43 | " Flags:", 44 | " --version Displays the program version string.", 45 | " -h --help Displays help with available flag, subcommand, and positional value parameters.", 46 | "", 47 | } 48 | 49 | if len(got) != len(want) { 50 | t.Fatalf("help length mismatch: got %d lines, want %d lines\nGot:\n%q\nWant:\n%q", len(got), len(want), got, want) 51 | } 52 | for i := range want { 53 | if got[i] != want[i] { 54 | t.Fatalf("help line %d mismatch:\nGot: %q\nWant: %q", i, got[i], want[i]) 55 | } 56 | } 57 | } 58 | 59 | func TestShowHelpBeforeParseIncludesSubcommands(t *testing.T) { 60 | p := flaggy.NewParser("root-help") 61 | alpha := flaggy.NewSubcommand("alpha") 62 | alpha.ShortName = "a" 63 | beta := flaggy.NewSubcommand("beta") 64 | beta.ShortName = "b" 65 | p.AttachSubcommand(alpha, 1) 66 | p.AttachSubcommand(beta, 2) 67 | 68 | rd, wr, err := os.Pipe() 69 | if err != nil { 70 | t.Fatalf("pipe: error: %s", err) 71 | } 72 | savedStderr := os.Stderr 73 | os.Stderr = wr 74 | 75 | p.ShowHelp() 76 | 77 | if err := wr.Close(); err != nil { 78 | t.Fatalf("close: error: %s", err) 79 | } 80 | os.Stderr = savedStderr 81 | 82 | data, err := io.ReadAll(rd) 83 | if err != nil { 84 | t.Fatalf("read all: error: %s", err) 85 | } 86 | output := string(data) 87 | 88 | if !strings.Contains(output, "alpha") { 89 | t.Fatalf("expected alpha subcommand in help, got:\n%s", output) 90 | } 91 | if !strings.Contains(output, "beta") { 92 | t.Fatalf("expected beta subcommand in help, got:\n%s", output) 93 | } 94 | } 95 | 96 | func TestRootHelpDoesNotShowGlobalFlagsSection(t *testing.T) { 97 | p := flaggy.NewParser("rootCmd") 98 | p.Description = "Root description" 99 | 100 | var rootString string 101 | var rootBool bool 102 | p.String(&rootString, "s", "string", "Root string flag") 103 | p.Bool(&rootBool, "b", "bool", "Root bool flag") 104 | 105 | sub := flaggy.NewSubcommand("child") 106 | var subFlag string 107 | sub.String(&subFlag, "", "sub-flag", "Subcommand flag") 108 | p.AttachSubcommand(sub, 1) 109 | 110 | rd, wr, err := os.Pipe() 111 | if err != nil { 112 | t.Fatalf("pipe: error: %s", err) 113 | } 114 | savedStderr := os.Stderr 115 | os.Stderr = wr 116 | 117 | p.ShowHelp() 118 | 119 | if err := wr.Close(); err != nil { 120 | t.Fatalf("close: error: %s", err) 121 | } 122 | os.Stderr = savedStderr 123 | 124 | data, err := io.ReadAll(rd) 125 | if err != nil { 126 | t.Fatalf("read: error: %s", err) 127 | } 128 | output := string(data) 129 | 130 | if strings.Contains(output, "Global Flags:") { 131 | t.Fatalf("root help should not contain 'Global Flags:' section:\n%s", output) 132 | } 133 | if !strings.Contains(output, " Flags:") { 134 | t.Fatalf("expected root Flags section in help output:\n%s", output) 135 | } 136 | if !strings.Contains(output, "--string") || !strings.Contains(output, "--bool") { 137 | t.Fatalf("expected root flags in output:\n%s", output) 138 | } 139 | } 140 | 141 | func TestHelpWithMissingSCName(t *testing.T) { 142 | defer func() { 143 | r := recover() 144 | gotMsg := r.(string) 145 | wantMsg := "Panic instead of exit with code: 2" 146 | if gotMsg != wantMsg { 147 | t.Fatalf("error: got: %s; want: %s", gotMsg, wantMsg) 148 | } 149 | }() 150 | flaggy.ResetParser() 151 | flaggy.PanicInsteadOfExit = true 152 | sc := flaggy.NewSubcommand("") 153 | sc.ShortName = "sn" 154 | flaggy.AttachSubcommand(sc, 1) 155 | flaggy.ParseArgs([]string{"x"}) 156 | } 157 | 158 | // TestHelpOutput tests the display of help with -h 159 | func TestHelpOutput(t *testing.T) { 160 | flaggy.ResetParser() 161 | // flaggy.DebugMode = true 162 | // defer debugOff() 163 | 164 | p := flaggy.NewParser("testCommand") 165 | p.Description = "Description goes here. Get more information at https://github.com/integrii/flaggy." 166 | scA := flaggy.NewSubcommand("subcommandA") 167 | scA.ShortName = "a" 168 | scA.Description = "Subcommand A is a command that does stuff" 169 | scB := flaggy.NewSubcommand("subcommandB") 170 | scB.ShortName = "b" 171 | scB.Description = "Subcommand B is a command that does other stuff" 172 | scX := flaggy.NewSubcommand("subcommandX") 173 | scX.Description = "This should be hidden." 174 | scX.Hidden = true 175 | 176 | var posA = "defaultPosA" 177 | var posB string 178 | p.AttachSubcommand(scA, 1) 179 | scA.AttachSubcommand(scB, 1) 180 | scA.AddPositionalValue(&posA, "testPositionalA", 2, false, "Test positional A does some things with a positional value.") 181 | scB.AddPositionalValue(&posB, "hiddenPositional", 1, false, "Hidden test positional B does some less than serious things with a positional value.") 182 | scB.PositionalFlags[0].Hidden = true 183 | var stringFlag = "defaultStringHere" 184 | var intFlag int 185 | var boolFlag bool 186 | var durationFlag time.Duration 187 | p.String(&stringFlag, "s", "stringFlag", "This is a test string flag that does some stringy string stuff.") 188 | p.Int(&intFlag, "i", "intFlg", "This is a test int flag that does some interesting int stuff.") 189 | p.Bool(&boolFlag, "b", "boolFlag", "This is a test bool flag that does some booly bool stuff.") 190 | p.Duration(&durationFlag, "d", "durationFlag", "This is a test duration flag that does some untimely stuff.") 191 | var subFlag string 192 | scB.String(&subFlag, "", "subFlag", "This is a subcommand-specific flag.") 193 | p.AdditionalHelpPrepend = "This is a prepend for help" 194 | p.AdditionalHelpAppend = "This is an append for help" 195 | 196 | rd, wr, err := os.Pipe() 197 | if err != nil { 198 | t.Fatalf("pipe: error: %s", err) 199 | } 200 | savedStderr := os.Stderr 201 | os.Stderr = wr 202 | 203 | defer func() { 204 | os.Stderr = savedStderr 205 | }() 206 | 207 | if err := p.ParseArgs([]string{"subcommandA", "subcommandB", "hiddenPositional1"}); err != nil { 208 | t.Fatalf("got: %s; want: no error", err) 209 | } 210 | p.ShowHelpWithMessage("This is a help message on exit") 211 | 212 | buf := make([]byte, 1024) 213 | n, err := rd.Read(buf) 214 | if err != nil { 215 | t.Fatalf("read: error: %s", err) 216 | } 217 | got := strings.Split(string(buf[:n]), "\n") 218 | // Updated to match current help template output without completion subcommand on nested help. 219 | want := []string{ 220 | "", 221 | "subcommandB - Subcommand B is a command that does other stuff", 222 | "", 223 | " Flags:", 224 | " --subFlag This is a subcommand-specific flag.", 225 | "", 226 | " Global Flags:", 227 | " --version Displays the program version string.", 228 | " -h --help Displays help with available flag, subcommand, and positional value parameters.", 229 | " -s --stringFlag This is a test string flag that does some stringy string stuff. (default: defaultStringHere)", 230 | " -i --intFlg This is a test int flag that does some interesting int stuff. (default: 0)", 231 | " -b --boolFlag This is a test bool flag that does some booly bool stuff.", 232 | " -d --durationFlag This is a test duration flag that does some untimely stuff. (default: 0s)", 233 | "", 234 | "This is a help message on exit", 235 | "", 236 | } 237 | 238 | if len(got) != len(want) { 239 | t.Fatalf("help length mismatch: got %d lines, want %d lines\nGot:\n%q\nWant:\n%q", len(got), len(want), got, want) 240 | } 241 | for i := range want { 242 | if got[i] != want[i] { 243 | t.Fatalf("help line %d mismatch:\nGot: %q\nWant: %q", i, got[i], want[i]) 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /parser_refactor_regression_test.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestNestedSubcommandsParseLocalFlags(t *testing.T) { 9 | // Command: ./app subcommandA subcommandB -flagA 5 10 | // Expect: subcommandA and subcommandB used; flagA parsed as int 5 on subcommandB. 11 | t.Parallel() 12 | 13 | // Setup root parser and nested subcommands to mirror the CLI hierarchy. 14 | p := NewParser("app") 15 | subA := NewSubcommand("subcommandA") 16 | subB := NewSubcommand("subcommandB") 17 | p.AttachSubcommand(subA, 1) 18 | subA.AttachSubcommand(subB, 1) 19 | 20 | // Define flagA on subcommandB with storage for the parsed integer. 21 | var flagA int 22 | subB.Int(&flagA, "", "flagA", "int flag scoped to subcommandB") 23 | 24 | // Parse the CLI input exactly as described in the command comment. 25 | if err := p.ParseArgs([]string{"subcommandA", "subcommandB", "-flagA", "5"}); err != nil { 26 | t.Fatalf("parse failed: %v", err) 27 | } 28 | 29 | // Assert both subcommands are marked as used and the flag value is correct. 30 | if !subA.Used || !subB.Used { 31 | t.Fatalf("expected subcommands to be marked used: subA=%v subB=%v", subA.Used, subB.Used) 32 | } 33 | if flagA != 5 { 34 | t.Fatalf("expected flagA to be 5, got %d", flagA) 35 | } 36 | } 37 | 38 | func TestRootAndChildFlagsAreIsolated(t *testing.T) { 39 | // Command: ./app -flagA=5 --flagB 5 subcommandA --flagC hello 40 | // Expect: root flagA=5, root flagB=5, subcommandA used with flagC="hello". 41 | t.Parallel() 42 | 43 | // Setup root parser with a single subcommand to host child-scoped flags. 44 | p := NewParser("app") 45 | subA := NewSubcommand("subcommandA") 46 | p.AttachSubcommand(subA, 1) 47 | 48 | // Define storage locations for the root and child flag values. 49 | var flagA int 50 | var flagB int 51 | var flagC string 52 | 53 | // Register two root flags and one child flag matching the command contract. 54 | p.Int(&flagA, "", "flagA", "root int flag") 55 | p.Int(&flagB, "", "flagB", "second root int flag") 56 | subA.String(&flagC, "", "flagC", "child string flag") 57 | 58 | // Parse the CLI input exactly as described in the command comment. 59 | args := []string{"-flagA=5", "--flagB", "5", "subcommandA", "--flagC", "hello"} 60 | if err := p.ParseArgs(args); err != nil { 61 | t.Fatalf("parse failed: %v", err) 62 | } 63 | 64 | // Verify root flags resolve correctly and the child flag receives "hello". 65 | if flagA != 5 { 66 | t.Fatalf("expected flagA to be 5, got %d", flagA) 67 | } 68 | if flagB != 5 { 69 | t.Fatalf("expected flagB to be 5, got %d", flagB) 70 | } 71 | if flagC != "hello" { 72 | t.Fatalf("expected flagC to be hello, got %q", flagC) 73 | } 74 | // Confirm the subcommand was invoked during parsing. 75 | if !subA.Used { 76 | t.Fatalf("expected subcommandA to be used") 77 | } 78 | } 79 | 80 | func TestFlagNameCollisionWithSubcommand(t *testing.T) { 81 | // Command: ./app -flagA=test flagA 82 | // Expect: root flagA string set to "test" while flagA subcommand is used. 83 | t.Parallel() 84 | 85 | // Setup root parser with a subcommand whose name collides with a root flag. 86 | p := NewParser("app") 87 | subFlagA := NewSubcommand("flagA") 88 | p.AttachSubcommand(subFlagA, 1) 89 | 90 | // Register the root-level string flag sharing the subcommand's name. 91 | var rootFlag string 92 | p.String(&rootFlag, "", "flagA", "root string flag") 93 | 94 | // Parse the CLI input exactly as described in the command comment. 95 | if err := p.ParseArgs([]string{"-flagA=test", "flagA"}); err != nil { 96 | t.Fatalf("parse failed: %v", err) 97 | } 98 | 99 | // Validate both the root flag value and the subcommand usage status. 100 | if rootFlag != "test" { 101 | t.Fatalf("expected root flag to be \"test\", got %q", rootFlag) 102 | } 103 | if !subFlagA.Used { 104 | t.Fatalf("expected subcommand flagA to be used") 105 | } 106 | } 107 | 108 | func TestBlankAndWhitespaceValues(t *testing.T) { 109 | // Command: ./app -flagA "" subcommandA -a "" -b " " -c XYZ 110 | // Expect: root flagA blank, -a blank, -b single space, -c "XYZ", subcommandA used. 111 | t.Parallel() 112 | 113 | // Setup root parser with subcommandA to hold scoped string flags. 114 | p := NewParser("app") 115 | subA := NewSubcommand("subcommandA") 116 | p.AttachSubcommand(subA, 1) 117 | 118 | // Define storage for root and subcommand flag values. 119 | var rootFlag string 120 | var flagA string 121 | var flagB string 122 | var flagC string 123 | 124 | // Register the root flag and subcommand flags matching the CLI usage. 125 | p.String(&rootFlag, "", "flagA", "root string flag") 126 | subA.String(&flagA, "a", "", "blank string flag") 127 | subA.String(&flagB, "b", "", "single space flag") 128 | subA.String(&flagC, "c", "", "non blank flag") 129 | 130 | // Parse the CLI input exactly as described in the command comment. 131 | args := []string{"-flagA", "", "subcommandA", "-a", "", "-b", " ", "-c", "XYZ"} 132 | if err := p.ParseArgs(args); err != nil { 133 | t.Fatalf("parse failed: %v", err) 134 | } 135 | 136 | // Assert blank and whitespace values were preserved for each flag. 137 | if rootFlag != "" { 138 | t.Fatalf("expected root flagA to be blank, got %q", rootFlag) 139 | } 140 | if flagA != "" { 141 | t.Fatalf("expected -a flag to be blank, got %q", flagA) 142 | } 143 | if flagB != " " { 144 | t.Fatalf("expected -b flag to be a single space, got %q", flagB) 145 | } 146 | if flagC != "XYZ" { 147 | t.Fatalf("expected -c flag to be XYZ, got %q", flagC) 148 | } 149 | // Confirm the subcommand was invoked during parsing. 150 | if !subA.Used { 151 | t.Fatalf("expected subcommandA to be used") 152 | } 153 | } 154 | 155 | func TestIssue96EmptyStringShortFlagValue(t *testing.T) { 156 | // Issue 96: ./app -log.file.dir "" -log.logstash.level INFO 157 | // Expect: no parse error, log.file.dir stored as "", log.logstash.level stored as "INFO". 158 | t.Parallel() 159 | 160 | // Setup parser mirroring the issue-96 configuration with dotted short flag names. 161 | p := NewParser("app") 162 | 163 | var fileDir string 164 | var logstashLevel string 165 | 166 | // Register both flags using the same short/long names as the regression report. 167 | p.String(&fileDir, "log.file.dir", "logFileDir", "Directory for log files (issue 96)") 168 | p.String(&logstashLevel, "log.logstash.level", "logLogstashLevel", "Logstash level (issue 96)") 169 | 170 | // Parse the exact CLI from the GitHub issue to guard against regression. 171 | args := []string{"-log.file.dir", "", "-log.logstash.level", "INFO"} 172 | if err := p.ParseArgs(args); err != nil { 173 | t.Fatalf("parse failed for issue 96 reproduction: %v", err) 174 | } 175 | 176 | if fileDir != "" { 177 | t.Fatalf("expected log.file.dir to remain an empty string, got %q", fileDir) 178 | } 179 | if logstashLevel != "INFO" { 180 | t.Fatalf("expected log.logstash.level to be \"INFO\", got %q", logstashLevel) 181 | } 182 | } 183 | 184 | func TestRootBoolAfterSubcommand(t *testing.T) { 185 | // Command: ./app subcommandA --output 186 | // Expect: subcommandA used; root --output bool flag set to true. 187 | t.Parallel() 188 | 189 | // Setup root parser with subcommandA to mirror the CLI input. 190 | p := NewParser("app") 191 | subA := NewSubcommand("subcommandA") 192 | p.AttachSubcommand(subA, 1) 193 | 194 | // Register the root-level bool flag that follows the subcommand. 195 | var output bool 196 | p.Bool(&output, "", "output", "root bool flag") 197 | 198 | // Parse the CLI input exactly as described in the command comment. 199 | if err := p.ParseArgs([]string{"subcommandA", "--output"}); err != nil { 200 | t.Fatalf("parse failed: %v", err) 201 | } 202 | 203 | // Assert the bool flag is true and the subcommand is marked as used. 204 | if !output { 205 | t.Fatalf("expected --output to set output to true") 206 | } 207 | if !subA.Used { 208 | t.Fatalf("expected subcommandA to be used") 209 | } 210 | } 211 | 212 | func TestNestedSubcommandTrailingArguments(t *testing.T) { 213 | // Command: ./app one two --test -- abc 123 xyz 214 | // Expect: subcommands one & two used; --test bool true; trailing args joined to "abc 123 xyz". 215 | t.Parallel() 216 | 217 | // Setup root parser with nested subcommands to reflect the CLI command. 218 | p := NewParser("app") 219 | subOne := NewSubcommand("one") 220 | subTwo := NewSubcommand("two") 221 | p.AttachSubcommand(subOne, 1) 222 | subOne.AttachSubcommand(subTwo, 1) 223 | 224 | // Register the bool flag on the deepest subcommand per the command contract. 225 | var test bool 226 | subTwo.Bool(&test, "", "test", "bool flag on nested subcommand") 227 | 228 | // Parse the CLI input exactly as described in the command comment. 229 | args := []string{"one", "two", "--test", "--", "abc", "123", "xyz"} 230 | if err := p.ParseArgs(args); err != nil { 231 | t.Fatalf("parse failed: %v", err) 232 | } 233 | 234 | // Validate subcommands were used, flag set to true, and trailing args preserved. 235 | if !subOne.Used || !subTwo.Used { 236 | t.Fatalf("expected nested subcommands to be used: one=%v two=%v", subOne.Used, subTwo.Used) 237 | } 238 | if !test { 239 | t.Fatalf("expected --test to set the nested bool flag to true") 240 | } 241 | if got := strings.Join(p.TrailingArguments, " "); got != "abc 123 xyz" { 242 | t.Fatalf("expected trailing arguments to join to %q, got %q", "abc 123 xyz", got) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /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 | 214 | // ExampleTrailingArguments shows how to read values that appear after the 215 | // double-dash (--) separator. Any arguments after -- are stored as trailing 216 | // arguments so you can forward them to another command or treat them as raw 217 | // input. 218 | func ExampleTrailingArguments() { 219 | 220 | // Reset the parser and provide some beginner-friendly help text so anyone 221 | // running `./app --help` understands what this example demonstrates. 222 | flaggy.ResetParser() 223 | flaggy.DefaultParser.Description = "Collects file names that appear after -- as trailing arguments." 224 | flaggy.DefaultParser.AdditionalHelpPrepend = "Usage: ./app -- [ ...]" 225 | flaggy.DefaultParser.AdditionalHelpAppend = "Example: ./app -- notes.txt todo.md" 226 | 227 | // Pretend the CLI input looks like `./app -- notes.txt todo.md`. 228 | os.Args = []string{"app", "--", "notes.txt", "todo.md"} 229 | 230 | // Parse the input arguments from the OS (os.Args). 231 | flaggy.Parse() 232 | 233 | // Trailing arguments are available even if you did not define explicit 234 | // positional values or flags for them. 235 | fmt.Printf("Trailing arguments (%d):\n", len(flaggy.TrailingArguments)) 236 | for index, argument := range flaggy.TrailingArguments { 237 | fmt.Printf(" %d. %s\n", index+1, argument) 238 | } 239 | // Output: 240 | // Trailing arguments (2): 241 | // 1. notes.txt 242 | // 2. todo.md 243 | } 244 | 245 | // ExampleStringSlice shows how to gather repeated flag values into a slice of 246 | // strings. This example fulfills the "ExampleSliceString" request by showing 247 | // how repeated string flags build a slice of values. 248 | func ExampleStringSlice() { 249 | 250 | // Reset the parser and describe the command so that --help output is clear 251 | // for new users experimenting with the example. 252 | flaggy.ResetParser() 253 | flaggy.DefaultParser.Description = "Stores every -a/--add value in a slice." 254 | flaggy.DefaultParser.AdditionalHelpPrepend = "Usage: ./app -a [-a ...]" 255 | flaggy.DefaultParser.AdditionalHelpAppend = "Example: ./app -a test -a another -a again" 256 | 257 | // Simulate invoking the binary as `./app -a test -a another -a again`. 258 | os.Args = []string{"app", "-a", "test", "-a", "another", "-a", "again"} 259 | 260 | var sliceFlag []string 261 | flaggy.StringSlice(&sliceFlag, "a", "add", "Collect values for this slice") 262 | 263 | flaggy.Parse() 264 | 265 | fmt.Printf("String slice has %d items:\n", len(sliceFlag)) 266 | for index, value := range sliceFlag { 267 | fmt.Printf(" %d. %s\n", index+1, value) 268 | } 269 | // Output: 270 | // String slice has 3 items: 271 | // 1. test 272 | // 2. another 273 | // 3. again 274 | } 275 | -------------------------------------------------------------------------------- /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 | - bash, zsh, fish, PowerShell, and Nushell shell completion generation by default 46 | - It's _fast_. All flag and subcommand parsing takes less than `1ms` in most programs. 47 | 48 | # Example Help Output 49 | 50 | ``` 51 | testCommand - Description goes here. Get more information at http://flaggy. 52 | This is a prepend for help 53 | 54 | Usage: 55 | testCommand [subcommandA|subcommandB|subcommandC] [testPositionalA] [testPositionalB] 56 | 57 | Positional Variables: 58 | testPositionalA Test positional A does some things with a positional value. (Required) 59 | testPositionalB Test positional B does some less than serious things with a positional value. 60 | 61 | Subcommands: 62 | subcommandA (a) Subcommand A is a command that does stuff 63 | subcommandB (b) Subcommand B is a command that does other stuff 64 | subcommandC (c) Subcommand C is a command that does SERIOUS stuff 65 | 66 | Flags: 67 | --version Displays the program version string. 68 | -h --help Displays help with available flag, subcommand, and positional value parameters. 69 | -s --stringFlag This is a test string flag that does some stringy string stuff. 70 | -i --intFlg This is a test int flag that does some interesting int stuff. (default: 5) 71 | -b --boolFlag This is a test bool flag that does some booly bool stuff. (default: true) 72 | -d --durationFlag This is a test duration flag that does some untimely stuff. (default: 1h23s) 73 | 74 | This is an append for help 75 | This is a help add-on message 76 | ``` 77 | 78 | # Super Simple Example 79 | 80 | `./yourApp -f test` 81 | 82 | ```go 83 | // Declare variables and their defaults 84 | var stringFlag = "defaultValue" 85 | 86 | // Add a flag 87 | flaggy.String(&stringFlag, "f", "flag", "A test string flag") 88 | 89 | // Parse the flag 90 | flaggy.Parse() 91 | 92 | // Use the flag 93 | print(stringFlag) 94 | ``` 95 | 96 | 97 | # Example with Subcommand 98 | 99 | `./yourApp subcommandExample -f test` 100 | 101 | ```go 102 | // Declare variables and their defaults 103 | var stringFlag = "defaultValue" 104 | 105 | // Create the subcommand 106 | subcommand := flaggy.NewSubcommand("subcommandExample") 107 | 108 | // Add a flag to the subcommand 109 | subcommand.String(&stringFlag, "f", "flag", "A test string flag") 110 | 111 | // Add the subcommand to the parser at position 1 112 | flaggy.AttachSubcommand(subcommand, 1) 113 | 114 | // Parse the subcommand and all flags 115 | flaggy.Parse() 116 | 117 | // Use the flag 118 | print(stringFlag) 119 | ``` 120 | 121 | # Example with Nested Subcommands, Various Flags and Trailing Arguments 122 | 123 | `./yourApp subcommandExample --flag=5 nestedSubcommand -t test -y -- trailingArg` 124 | 125 | ```go 126 | // Declare variables and their defaults 127 | var stringFlagF = "defaultValueF" 128 | var intFlagT = 3 129 | var boolFlagB bool 130 | 131 | // Create the subcommands 132 | subcommandExample := flaggy.NewSubcommand("subcommandExample") 133 | nestedSubcommand := flaggy.NewSubcommand("nestedSubcommand") 134 | 135 | // Add a flag to both subcommands 136 | subcommandExample.String(&stringFlagF, "t", "testFlag", "A test string flag") 137 | nestedSubcommand.Int(&intFlagT, "f", "flag", "A test int flag") 138 | 139 | // add a global bool flag for fun 140 | flaggy.Bool(&boolFlagB, "y", "yes", "A sample boolean flag") 141 | 142 | // attach the nested subcommand to the parent subcommand at position 1 143 | subcommandExample.AttachSubcommand(nestedSubcommand, 1) 144 | // attach the base subcommand to the parser at position 1 145 | flaggy.AttachSubcommand(subcommandExample, 1) 146 | 147 | // Parse everything, then use the flags and trailing arguments 148 | flaggy.Parse() 149 | print(stringFlagF) 150 | print(intFlagT) 151 | print(boolFlagB) 152 | print(flaggy.TrailingArguments[0]) 153 | ``` 154 | 155 | # Supported Flag Types 156 | 157 | Flaggy has specific flag types for all basic Go types as well as slice variants, plus a selection of helpful standard library structures. You can target any of the following assignments when defining a flag: 158 | 159 | - Text and truthy values: `string`, `[]string`, `bool`, `[]bool` 160 | - Signed integers: `int`, `int64`, `int32`, `int16`, `int8`, and a slice form for each type 161 | - Unsigned integers: `uint`, `uint64`, `uint32`, `uint16`, `uint8` (aka `byte`), and slice forms for each type 162 | - Floating point numbers: `float64`, `float32`, and slices of both precisions 163 | - Time utilities: `time.Duration`, `[]time.Duration`, `time.Time`, `time.Location`, `time.Month`, `time.Weekday` 164 | - Network primitives: `net.IP`, `[]net.IP`, `net.HardwareAddr`, `[]net.HardwareAddr`, `net.IPMask`, `[]net.IPMask`, `net.IPNet`, `net.TCPAddr`, `net.UDPAddr` 165 | - Modern IP types: `netip.Addr`, `netip.Prefix`, `netip.AddrPort` 166 | - URLs and filesystem helpers: `url.URL`, `os.FileMode` 167 | - Pattern and math types: `regexp.Regexp`, `big.Int`, `big.Rat` 168 | - Encoded byte helpers: `Base64Bytes` (a base64-decoded `[]byte`) 169 | 170 | # Shell Completion 171 | 172 | Flaggy generates `bash`, `zsh`, `fish`, `PowerShell`, and `Nushell` completion scripts automatically. 173 | 174 | ```bash 175 | # Bash 176 | source <(./app completion bash) 177 | 178 | # Zsh 179 | source <(./app completion zsh) 180 | 181 | # Fish 182 | ./app completion fish | source 183 | 184 | # PowerShell 185 | ./app completion powershell | Out-String | Invoke-Expression 186 | 187 | # Nushell 188 | ./app completion nushell | save --force ~/.cache/app-completions.nu 189 | source ~/.cache/app-completions.nu 190 | ``` 191 | 192 | # An Example Program 193 | 194 | Best practice when using flaggy includes setting your program's name, description, and version (at build time) as shown in this example program. 195 | 196 | ```go 197 | package main 198 | 199 | import "github.com/integrii/flaggy" 200 | 201 | // Make a variable for the version which will be set at build time. 202 | var version = "unknown" 203 | 204 | // Keep subcommands as globals so you can easily check if they were used later on. 205 | var mySubcommand *flaggy.Subcommand 206 | 207 | // Setup the variables you want your incoming flags to set. 208 | var testVar string 209 | 210 | // If you would like an environment variable as the default for a value, just populate the flag 211 | // with the value of the environment by default. If the flag corresponding to this value is not 212 | // used, then it will not be changed. 213 | var myVar = os.Getenv("MY_VAR") 214 | 215 | 216 | func init() { 217 | // Set your program's name and description. These appear in help output. 218 | flaggy.SetName("Test Program") 219 | flaggy.SetDescription("A little example program") 220 | 221 | // You can disable various things by changing bools on the default parser 222 | // (or your own parser if you have created one). 223 | flaggy.DefaultParser.ShowHelpOnUnexpected = false 224 | 225 | // You can set a help prepend or append on the default parser. 226 | flaggy.DefaultParser.AdditionalHelpPrepend = "http://github.com/integrii/flaggy" 227 | 228 | // Add a flag to the main program (this will be available in all subcommands as well). 229 | flaggy.String(&testVar, "tv", "testVariable", "A variable just for testing things!") 230 | 231 | // Create any subcommands and set their parameters. 232 | mySubcommand = flaggy.NewSubcommand("mySubcommand") 233 | mySubcommand.Description = "My great subcommand!" 234 | 235 | // Add a flag to the subcommand. 236 | mySubcommand.String(&myVar, "mv", "myVariable", "A variable just for me!") 237 | 238 | // Set the version and parse all inputs into variables. 239 | flaggy.SetVersion(version) 240 | flaggy.Parse() 241 | } 242 | 243 | func main(){ 244 | if mySubcommand.Used { 245 | ... 246 | } 247 | } 248 | ``` 249 | 250 | Then, you can use the following build command to set the `version` variable in the above program at build time. 251 | 252 | ```bash 253 | # build your app and set the version string 254 | $ go build -ldflags='-X main.version=1.0.3-a3db3' 255 | $ ./yourApp version 256 | Version: 1.0.3-a3db3 257 | $ ./yourApp --help 258 | Test Program - A little example program 259 | http://github.com/integrii/flaggy 260 | ``` 261 | 262 | # Contributions 263 | 264 | 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. 265 | -------------------------------------------------------------------------------- /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 | initialSubcommandContext *Subcommand // points to the initial help context prior to parsing 28 | ShowCompletion bool // indicates that bash and zsh completion output is possible 29 | SortFlags bool // when true, help output flags are sorted alphabetically 30 | SortFlagsReverse bool // when true with SortFlags, sort order is reversed (Z..A) 31 | } 32 | 33 | // supportedCompletionShells lists every shell that can receive generated completion output. 34 | var supportedCompletionShells = []string{"bash", "zsh", "fish", "powershell", "nushell"} 35 | 36 | // completionShellList joins the supported completion shell names into a space separated string. 37 | func completionShellList() string { 38 | return strings.Join(supportedCompletionShells, " ") 39 | } 40 | 41 | // isSupportedCompletionShell reports whether the provided shell is eligible for generated completions. 42 | func isSupportedCompletionShell(shell string) bool { 43 | for _, supported := range supportedCompletionShells { 44 | if shell == supported { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | // TrailingSubcommand returns the last and most specific subcommand invoked. 52 | func (p *Parser) TrailingSubcommand() *Subcommand { 53 | return p.subcommandContext 54 | } 55 | 56 | // NewParser creates a new ArgumentParser ready to parse inputs 57 | func NewParser(name string) *Parser { 58 | // this can not be done inline because of struct embedding 59 | p := &Parser{} 60 | p.Name = name 61 | p.Version = defaultVersion 62 | p.ShowHelpOnUnexpected = true 63 | p.ShowHelpWithHFlag = true 64 | p.ShowVersionWithVersionFlag = true 65 | p.ShowCompletion = true 66 | p.SortFlags = false 67 | p.SortFlagsReverse = false 68 | p.SetHelpTemplate(DefaultHelpTemplate) 69 | initialContext := &Subcommand{} 70 | p.subcommandContext = initialContext 71 | p.initialSubcommandContext = initialContext 72 | return p 73 | } 74 | 75 | // isTopLevelHelpContext returns true when help output should be shown for the top 76 | // level parser instead of a specific subcommand. 77 | func (p *Parser) isTopLevelHelpContext() bool { 78 | if p.subcommandContext == nil { 79 | return true 80 | } 81 | if p.subcommandContext == &p.Subcommand { 82 | return true 83 | } 84 | if p.initialSubcommandContext != nil && p.subcommandContext == p.initialSubcommandContext { 85 | return true 86 | } 87 | return false 88 | } 89 | 90 | // SortFlagsByLongName enables alphabetical sorting by long flag name 91 | // (case-insensitive) for help output on this parser. 92 | func (p *Parser) SortFlagsByLongName() { 93 | p.SortFlags = true 94 | p.SortFlagsReverse = false 95 | } 96 | 97 | // SortFlagsByLongNameReversed enables reverse alphabetical sorting by 98 | // long flag name (case-insensitive) for help output on this parser. 99 | func (p *Parser) SortFlagsByLongNameReversed() { 100 | p.SortFlags = true 101 | p.SortFlagsReverse = true 102 | } 103 | 104 | // ParseArgs parses as if the passed args were the os.Args, but without the 105 | // binary at the 0 position in the array. An error is returned if there 106 | // is a low level issue converting flags to their proper type. No error 107 | // is returned for invalid arguments or missing require subcommands. 108 | func (p *Parser) ParseArgs(args []string) error { 109 | if p.parsed { 110 | return errors.New("Parser.Parse() called twice on parser with name: " + " " + p.Name + " " + p.ShortName) 111 | } 112 | p.parsed = true 113 | 114 | // Handle shell completion before any parsing to avoid unknown-argument exits. 115 | if p.ShowCompletion { 116 | if len(args) >= 1 && strings.EqualFold(args[0], "completion") { 117 | // no shell provided 118 | if len(args) < 2 { 119 | fmt.Fprintf(os.Stderr, "Please specify a shell for completion. Supported shells: %s\n", completionShellList()) 120 | exitOrPanic(2) 121 | } 122 | 123 | shell := strings.ToLower(args[1]) 124 | if isSupportedCompletionShell(shell) { 125 | p.Completion(shell) 126 | exitOrPanic(0) 127 | } 128 | fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\nSupported shells: %s\n", args[1], completionShellList()) 129 | exitOrPanic(2) 130 | } 131 | } 132 | 133 | debugPrint("Kicking off parsing with args:", args) 134 | err := p.parse(p, args) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | // if we are set to exit on unexpected args, look for those here 140 | if p.ShowHelpOnUnexpected { 141 | parsedValues := p.findAllParsedValues() 142 | debugPrint("parsedValues:", parsedValues) 143 | argsNotParsed := findArgsNotInParsedValues(args, parsedValues) 144 | if len(argsNotParsed) > 0 { 145 | // flatten out unused args for our error message 146 | var argsNotParsedFlat string 147 | for _, a := range argsNotParsed { 148 | argsNotParsedFlat = argsNotParsedFlat + " " + a 149 | } 150 | p.ShowHelpAndExit("Unknown arguments supplied: " + argsNotParsedFlat) 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | 157 | // Completion takes in a shell type and outputs the completion script for 158 | // that shell. 159 | func (p *Parser) Completion(completionType string) { 160 | switch strings.ToLower(completionType) { 161 | case "bash": 162 | fmt.Print(GenerateBashCompletion(p)) 163 | case "zsh": 164 | fmt.Print(GenerateZshCompletion(p)) 165 | case "fish": 166 | fmt.Print(GenerateFishCompletion(p)) 167 | case "powershell": 168 | fmt.Print(GeneratePowerShellCompletion(p)) 169 | case "nushell": 170 | fmt.Print(GenerateNushellCompletion(p)) 171 | default: 172 | fmt.Fprintf(os.Stderr, "Unsupported shell specified for completion: %s\nSupported shells: %s\n", completionType, completionShellList()) 173 | } 174 | } 175 | 176 | // findArgsNotInParsedValues finds arguments not used in parsed values. The 177 | // incoming args should be in the order supplied by the user and should not 178 | // include the invoked binary, which is normally the first thing in os.Args. 179 | func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []string { 180 | // DebugMode = true 181 | // defer func() { 182 | // DebugMode = false 183 | // }() 184 | 185 | var argsNotUsed []string 186 | var skipNext bool 187 | 188 | for i := 0; i < len(args); i++ { 189 | a := args[i] 190 | 191 | // if the final argument (--) is seen, then we stop checking because all 192 | // further values are trailing arguments. 193 | if determineArgType(a) == argIsFinal { 194 | return argsNotUsed 195 | } 196 | 197 | // allow for skipping the next arg when needed 198 | if skipNext { 199 | skipNext = false 200 | continue 201 | } 202 | 203 | // Determine token type and normalized key/value 204 | arg := parseFlagToName(a) 205 | isFlagToken := strings.HasPrefix(a, "-") 206 | 207 | // skip args that start with 'test.' because they are injected with go test 208 | debugPrint("flagsNotParsed: checking arg for test prefix:", arg) 209 | if strings.HasPrefix(arg, "test.") { 210 | debugPrint("skipping test. prefixed arg has test prefix:", arg) 211 | continue 212 | } 213 | debugPrint("flagsNotParsed: flag is not a test. flag:", arg) 214 | 215 | // indicates that we found this arg used in one of the parsed values. Used 216 | // to indicate which values should be added to argsNotUsed. 217 | var foundArgUsed bool 218 | 219 | // For flag tokens, only allow non-positional (flag) matches. 220 | if isFlagToken { 221 | for _, pv := range parsedValues { 222 | debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") 223 | if !pv.IsPositional && pv.Key == arg { 224 | debugPrint("Found matching parsed flag for " + pv.Key) 225 | foundArgUsed = true 226 | if pv.ConsumesNext { 227 | skipNext = true 228 | } else if i+1 < len(args) && pv.Value == args[i+1] { 229 | skipNext = true 230 | } 231 | break 232 | } 233 | } 234 | if foundArgUsed { 235 | continue 236 | } 237 | } 238 | 239 | // For non-flag tokens, prefer positional matches first. 240 | if !isFlagToken { 241 | for _, pv := range parsedValues { 242 | debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") 243 | if pv.IsPositional && pv.Value == arg { 244 | debugPrint("Found matching parsed positional for " + pv.Value) 245 | foundArgUsed = true 246 | break 247 | } 248 | } 249 | if foundArgUsed { 250 | continue 251 | } 252 | 253 | // Fallback for non-flag tokens: allow matching a non-positional flag by bare name. 254 | for _, pv := range parsedValues { 255 | debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")") 256 | if !pv.IsPositional && pv.Key == arg { 257 | debugPrint("Found matching parsed flag for " + pv.Key) 258 | foundArgUsed = true 259 | if pv.ConsumesNext { 260 | skipNext = true 261 | } else if i+1 < len(args) && pv.Value == args[i+1] { 262 | skipNext = true 263 | } 264 | break 265 | } 266 | } 267 | if foundArgUsed { 268 | continue 269 | } 270 | } 271 | 272 | // if the arg was not used in any parsed values, then we add it to the slice 273 | // of arguments not used 274 | if !foundArgUsed { 275 | argsNotUsed = append(argsNotUsed, arg) 276 | } 277 | } 278 | 279 | return argsNotUsed 280 | } 281 | 282 | // ShowVersionAndExit shows the version of this parser 283 | func (p *Parser) ShowVersionAndExit() { 284 | fmt.Println("Version:", p.Version) 285 | exitOrPanic(0) 286 | } 287 | 288 | // SetHelpTemplate sets the go template this parser will use when rendering 289 | // Help. 290 | func (p *Parser) SetHelpTemplate(tmpl string) error { 291 | var err error 292 | p.HelpTemplate = template.New(helpFlagLongName) 293 | p.HelpTemplate, err = p.HelpTemplate.Parse(tmpl) 294 | if err != nil { 295 | return err 296 | } 297 | return nil 298 | } 299 | 300 | // Parse calculates all flags and subcommands 301 | func (p *Parser) Parse() error { 302 | return p.ParseArgs(os.Args[1:]) 303 | } 304 | 305 | // ShowHelp shows Help without an error message 306 | func (p *Parser) ShowHelp() { 307 | debugPrint("showing help for", p.subcommandContext.Name) 308 | p.ShowHelpWithMessage("") 309 | } 310 | 311 | // ShowHelpAndExit shows parser help and exits with status code 2 312 | func (p *Parser) ShowHelpAndExit(message string) { 313 | p.ShowHelpWithMessage(message) 314 | exitOrPanic(2) 315 | } 316 | 317 | // ShowHelpWithMessage shows the Help for this parser with an optional string error 318 | // message as a header. The supplied subcommand will be the context of Help 319 | // displayed to the user. 320 | func (p *Parser) ShowHelpWithMessage(message string) { 321 | 322 | // create a new Help values template and extract values into it 323 | help := Help{} 324 | help.ExtractValues(p, message) 325 | err := p.HelpTemplate.Execute(os.Stderr, help) 326 | if err != nil { 327 | fmt.Fprintln(os.Stderr, "Error rendering Help template:", err) 328 | } 329 | } 330 | 331 | // DisableShowVersionWithVersion disables the showing of version information 332 | // with --version. It is enabled by default. 333 | func (p *Parser) DisableShowVersionWithVersion() { 334 | p.ShowVersionWithVersionFlag = false 335 | } 336 | -------------------------------------------------------------------------------- /completion.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // EnableCompletion enables shell autocomplete outputs to be generated. 9 | func EnableCompletion() { 10 | DefaultParser.ShowCompletion = true 11 | } 12 | 13 | // DisableCompletion disallows shell autocomplete outputs to be generated. 14 | func DisableCompletion() { 15 | DefaultParser.ShowCompletion = false 16 | } 17 | 18 | // GenerateBashCompletion returns a bash completion script for the parser. 19 | func GenerateBashCompletion(p *Parser) string { 20 | var b strings.Builder 21 | funcName := "_" + sanitizeName(p.Name) + "_complete" 22 | b.WriteString("# bash completion for " + p.Name + "\n") 23 | b.WriteString(funcName + "() {\n") 24 | b.WriteString(" local cur prev\n") 25 | b.WriteString(" COMPREPLY=()\n") 26 | b.WriteString(" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n") 27 | b.WriteString(" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n") 28 | b.WriteString(" case \"$prev\" in\n") 29 | bashCaseEntries(&p.Subcommand, &b) 30 | rootOpts := collectOptions(&p.Subcommand) 31 | b.WriteString(" *)\n COMPREPLY=( $(compgen -W \"" + rootOpts + "\" -- \"$cur\") )\n return 0\n ;;\n esac\n}\n") 32 | b.WriteString("complete -F " + funcName + " " + p.Name + "\n") 33 | return b.String() 34 | } 35 | 36 | // GenerateZshCompletion returns a zsh completion script for the parser. 37 | func GenerateZshCompletion(p *Parser) string { 38 | var b strings.Builder 39 | funcName := "_" + sanitizeName(p.Name) 40 | b.WriteString("#compdef " + p.Name + "\n\n") 41 | b.WriteString(funcName + "() {\n") 42 | b.WriteString(" local cur prev\n") 43 | b.WriteString(" cur=${words[CURRENT]}\n") 44 | b.WriteString(" prev=${words[CURRENT-1]}\n") 45 | b.WriteString(" case \"$prev\" in\n") 46 | zshCaseEntries(&p.Subcommand, &b) 47 | rootOpts := collectOptions(&p.Subcommand) 48 | b.WriteString(" *)\n compadd -- " + rootOpts + "\n ;;\n esac\n}\n") 49 | b.WriteString("compdef " + funcName + " " + p.Name + "\n") 50 | return b.String() 51 | } 52 | 53 | // GenerateFishCompletion returns a fish completion script for the parser. 54 | func GenerateFishCompletion(p *Parser) string { 55 | var b strings.Builder 56 | b.WriteString("# fish completion for " + p.Name + "\n") 57 | writeFishEntries(&p.Subcommand, &b, p.Name, nil) 58 | return b.String() 59 | } 60 | 61 | // GeneratePowerShellCompletion returns a PowerShell completion script for the parser. 62 | func GeneratePowerShellCompletion(p *Parser) string { 63 | var b strings.Builder 64 | b.WriteString("# PowerShell completion for " + p.Name + "\n") 65 | b.WriteString("Register-ArgumentCompleter -CommandName '" + p.Name + "' -ScriptBlock {\n") 66 | b.WriteString(" param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)\n") 67 | b.WriteString(" $completions = @(\n") 68 | writePowerShellEntries(&p.Subcommand, &b) 69 | b.WriteString(" )\n") 70 | b.WriteString(" $completions | Where-Object { $_.CompletionText -like \"$wordToComplete*\" }\n") 71 | b.WriteString("}\n") 72 | return b.String() 73 | } 74 | 75 | // GenerateNushellCompletion returns a Nushell completion script for the parser. 76 | func GenerateNushellCompletion(p *Parser) string { 77 | var b strings.Builder 78 | command := p.Name 79 | funcName := "nu-complete " + command 80 | b.WriteString("# nushell completion for " + command + "\n") 81 | b.WriteString("def \"" + funcName + "\" [] {\n") 82 | b.WriteString(" [\n") 83 | writeNushellEntries(&p.Subcommand, &b) 84 | b.WriteString(" ]\n") 85 | b.WriteString("}\n\n") 86 | b.WriteString("extern \"" + command + "\" [\n") 87 | writeNushellFlagSignature(&p.Subcommand, &b) 88 | b.WriteString(" command?: string@\"" + funcName + "\"\n") 89 | b.WriteString("]\n") 90 | return b.String() 91 | } 92 | 93 | // collectOptions builds a space-delimited list of flags, subcommands, and positional values 94 | // for the provided subcommand. 95 | func collectOptions(sc *Subcommand) string { 96 | var opts []string 97 | for _, f := range sc.Flags { 98 | if len(f.ShortName) > 0 { 99 | opts = append(opts, "-"+f.ShortName) 100 | } 101 | if len(f.LongName) > 0 { 102 | opts = append(opts, "--"+f.LongName) 103 | } 104 | } 105 | for _, p := range sc.PositionalFlags { 106 | if p.Name != "" { 107 | opts = append(opts, p.Name) 108 | } 109 | } 110 | for _, s := range sc.Subcommands { 111 | if s.Hidden { 112 | continue 113 | } 114 | if s.Name != "" { 115 | opts = append(opts, s.Name) 116 | } 117 | if s.ShortName != "" { 118 | opts = append(opts, s.ShortName) 119 | } 120 | } 121 | return strings.Join(opts, " ") 122 | } 123 | 124 | func bashCaseEntries(sc *Subcommand, b *strings.Builder) { 125 | for _, s := range sc.Subcommands { 126 | if s.Hidden { 127 | continue 128 | } 129 | opts := collectOptions(s) 130 | b.WriteString(" " + s.Name + ")\n COMPREPLY=( $(compgen -W \"" + opts + "\" -- \"$cur\") )\n return 0\n ;;\n") 131 | if s.ShortName != "" { 132 | b.WriteString(" " + s.ShortName + ")\n COMPREPLY=( $(compgen -W \"" + opts + "\" -- \"$cur\") )\n return 0\n ;;\n") 133 | } 134 | bashCaseEntries(s, b) 135 | } 136 | } 137 | 138 | func zshCaseEntries(sc *Subcommand, b *strings.Builder) { 139 | for _, s := range sc.Subcommands { 140 | if s.Hidden { 141 | continue 142 | } 143 | opts := collectOptions(s) 144 | b.WriteString(" " + s.Name + ")\n compadd -- " + opts + "\n return\n ;;\n") 145 | if s.ShortName != "" { 146 | b.WriteString(" " + s.ShortName + ")\n compadd -- " + opts + "\n return\n ;;\n") 147 | } 148 | zshCaseEntries(s, b) 149 | } 150 | } 151 | 152 | func sanitizeName(n string) string { 153 | return strings.ReplaceAll(n, "-", "_") 154 | } 155 | 156 | // writeFishEntries builds the fish completion statements for the provided subcommand path so 157 | // the generated script mirrors Flaggy's flag and subcommand hierarchy for interactive use. 158 | func writeFishEntries(sc *Subcommand, b *strings.Builder, command string, path []string) { 159 | condition := fishConditionForFlags(path) 160 | for _, f := range sc.Flags { 161 | if f.Hidden { 162 | continue 163 | } 164 | line := "complete -c " + command 165 | if condition != "" { 166 | line += " -n '" + condition + "'" 167 | } 168 | if f.ShortName != "" { 169 | line += " -s " + f.ShortName 170 | } 171 | if f.LongName != "" { 172 | line += " -l " + f.LongName 173 | } 174 | if f.Description != "" { 175 | line += " -d '" + escapeSingleQuotes(f.Description) + "'" 176 | } 177 | line += "\n" 178 | b.WriteString(line) 179 | } 180 | for _, p := range sc.PositionalFlags { 181 | if p.Hidden { 182 | continue 183 | } 184 | if p.Name == "" { 185 | continue 186 | } 187 | line := "complete -c " + command 188 | if condition != "" { 189 | line += " -n '" + condition + "'" 190 | } 191 | line += " -a '" + escapeSingleQuotes(p.Name) + "'" 192 | if p.Description != "" { 193 | line += " -d '" + escapeSingleQuotes(p.Description) + "'" 194 | } 195 | line += "\n" 196 | b.WriteString(line) 197 | } 198 | subCondition := fishConditionForSubcommands(path) 199 | for _, sub := range sc.Subcommands { 200 | if sub.Hidden { 201 | continue 202 | } 203 | line := "complete -c " + command 204 | if subCondition != "" { 205 | line += " -n '" + subCondition + "'" 206 | } 207 | line += " -a '" + escapeSingleQuotes(sub.Name) + "'" 208 | if sub.Description != "" { 209 | line += " -d '" + escapeSingleQuotes(sub.Description) + "'" 210 | } 211 | line += "\n" 212 | b.WriteString(line) 213 | if sub.ShortName != "" { 214 | aliasLine := "complete -c " + command 215 | if subCondition != "" { 216 | aliasLine += " -n '" + subCondition + "'" 217 | } 218 | aliasLine += " -a '" + escapeSingleQuotes(sub.ShortName) + "'" 219 | if sub.Description != "" { 220 | aliasLine += " -d '" + escapeSingleQuotes(sub.Description) + "'" 221 | } 222 | aliasLine += "\n" 223 | b.WriteString(aliasLine) 224 | } 225 | nextPath := appendPath(path, sub.Name) 226 | writeFishEntries(sub, b, command, nextPath) 227 | } 228 | } 229 | 230 | // fishConditionForFlags returns the fish condition needed to scope flag suggestions to the 231 | // current subcommand path while leaving root flags globally available. 232 | func fishConditionForFlags(path []string) string { 233 | if len(path) == 0 { 234 | return "" 235 | } 236 | return "__fish_seen_subcommand_from " + path[len(path)-1] 237 | } 238 | 239 | // fishConditionForSubcommands returns the fish condition that ensures subcommand suggestions 240 | // appear only after their parent command token has been entered. 241 | func fishConditionForSubcommands(path []string) string { 242 | if len(path) == 0 { 243 | return "__fish_use_subcommand" 244 | } 245 | return "__fish_seen_subcommand_from " + path[len(path)-1] 246 | } 247 | 248 | // appendPath creates a new slice with the next subcommand name appended so recursive 249 | // completion builders can keep the traversal stack immutable. 250 | func appendPath(path []string, value string) []string { 251 | next := make([]string, len(path)+1) 252 | copy(next, path) 253 | next[len(path)] = value 254 | return next 255 | } 256 | 257 | // escapeSingleQuotes prepares text for inclusion in single-quoted shell strings so flag 258 | // descriptions render safely in the generated scripts. 259 | func escapeSingleQuotes(s string) string { 260 | return strings.ReplaceAll(s, "'", "\\'") 261 | } 262 | 263 | // escapeDoubleQuotes prepares text for inclusion in double-quoted shell strings which is 264 | // required for PowerShell and Nushell emission. 265 | func escapeDoubleQuotes(s string) string { 266 | return strings.ReplaceAll(s, "\"", "\\\"") 267 | } 268 | 269 | // writePowerShellEntries walks the parser tree and emits CompletionResult entries so the 270 | // PowerShell script can surface flags, positionals, and subcommands interactively. 271 | func writePowerShellEntries(sc *Subcommand, b *strings.Builder) { 272 | for _, f := range sc.Flags { 273 | if f.Hidden { 274 | continue 275 | } 276 | if f.LongName != "" { 277 | writePowerShellLine("--"+f.LongName, f.Description, "ParameterName", b) 278 | } 279 | if f.ShortName != "" { 280 | writePowerShellLine("-"+f.ShortName, f.Description, "ParameterName", b) 281 | } 282 | } 283 | for _, p := range sc.PositionalFlags { 284 | if p.Hidden { 285 | continue 286 | } 287 | if p.Name == "" { 288 | continue 289 | } 290 | writePowerShellLine(p.Name, p.Description, "ParameterValue", b) 291 | } 292 | for _, sub := range sc.Subcommands { 293 | if sub.Hidden { 294 | continue 295 | } 296 | writePowerShellLine(sub.Name, sub.Description, "Command", b) 297 | if sub.ShortName != "" { 298 | writePowerShellLine(sub.ShortName, sub.Description, "Command", b) 299 | } 300 | writePowerShellEntries(sub, b) 301 | } 302 | } 303 | 304 | // writePowerShellLine emits a single CompletionResult definition with the supplied tooltip and 305 | // completion type for consumption by Register-ArgumentCompleter. 306 | func writePowerShellLine(value, description, kind string, b *strings.Builder) { 307 | tooltip := description 308 | if tooltip == "" { 309 | tooltip = value 310 | } 311 | line := fmt.Sprintf(" [System.Management.Automation.CompletionResult]::new(\"%s\", \"%s\", \"%s\", \"%s\")\n", escapeDoubleQuotes(value), escapeDoubleQuotes(value), kind, escapeDoubleQuotes(tooltip)) 312 | b.WriteString(line) 313 | } 314 | 315 | // writeNushellEntries collects all completion values into Nushell's structured format so 316 | // external commands can expose their interactive help inside the shell. 317 | func writeNushellEntries(sc *Subcommand, b *strings.Builder) { 318 | for _, f := range sc.Flags { 319 | if f.Hidden { 320 | continue 321 | } 322 | if f.LongName != "" { 323 | writeNushellLine("--"+f.LongName, f.Description, b) 324 | } 325 | if f.ShortName != "" { 326 | writeNushellLine("-"+f.ShortName, f.Description, b) 327 | } 328 | } 329 | for _, p := range sc.PositionalFlags { 330 | if p.Hidden { 331 | continue 332 | } 333 | if p.Name == "" { 334 | continue 335 | } 336 | writeNushellLine(p.Name, p.Description, b) 337 | } 338 | for _, sub := range sc.Subcommands { 339 | if sub.Hidden { 340 | continue 341 | } 342 | writeNushellLine(sub.Name, sub.Description, b) 343 | if sub.ShortName != "" { 344 | writeNushellLine(sub.ShortName, sub.Description, b) 345 | } 346 | writeNushellEntries(sub, b) 347 | } 348 | } 349 | 350 | // writeNushellLine emits a single structured completion item for Nushell with a value and 351 | // friendly description. 352 | func writeNushellLine(value, description string, b *strings.Builder) { 353 | tooltip := description 354 | if tooltip == "" { 355 | tooltip = value 356 | } 357 | line := fmt.Sprintf(" { value: \"%s\", description: \"%s\" }\n", escapeDoubleQuotes(value), escapeDoubleQuotes(tooltip)) 358 | b.WriteString(line) 359 | } 360 | 361 | // writeNushellFlagSignature appends flag signature stubs so Nushell understands which 362 | // switches are available when invoking the external command. 363 | func writeNushellFlagSignature(sc *Subcommand, b *strings.Builder) { 364 | for _, f := range sc.Flags { 365 | if f.Hidden { 366 | continue 367 | } 368 | if f.LongName != "" || f.ShortName != "" { 369 | line := " " 370 | if f.LongName != "" { 371 | line += "--" + f.LongName 372 | } 373 | if f.ShortName != "" { 374 | if f.LongName != "" { 375 | line += "(-" + f.ShortName + ")" 376 | } else { 377 | line += "-" + f.ShortName 378 | } 379 | } 380 | line += "\n" 381 | b.WriteString(line) 382 | } 383 | } 384 | for _, sub := range sc.Subcommands { 385 | if sub.Hidden { 386 | continue 387 | } 388 | writeNushellFlagSignature(sub, b) 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /helpValues.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | "unicode/utf8" 10 | ) 11 | 12 | // Help represents the values needed to render a Help page 13 | type Help struct { 14 | Subcommands []HelpSubcommand 15 | Positionals []HelpPositional 16 | Flags []HelpFlag 17 | GlobalFlags []HelpFlag 18 | UsageString string 19 | CommandName string 20 | PrependMessage string 21 | AppendMessage string 22 | ShowCompletion bool 23 | Message string 24 | Description string 25 | Lines []string 26 | } 27 | 28 | // HelpSubcommand is used to template subcommand Help output 29 | type HelpSubcommand struct { 30 | ShortName string 31 | LongName string 32 | Description string 33 | Position int 34 | Spacer string 35 | } 36 | 37 | // HelpPositional is used to template positional Help output 38 | type HelpPositional struct { 39 | Name string 40 | Description string 41 | Required bool 42 | Position int 43 | DefaultValue string 44 | Spacer string 45 | } 46 | 47 | // HelpFlag is used to template string flag Help output 48 | type HelpFlag struct { 49 | ShortName string 50 | LongName string 51 | Description string 52 | DefaultValue string 53 | ShortDisplay string 54 | LongDisplay string 55 | } 56 | 57 | // ExtractValues extracts Help template values from a subcommand and its parent 58 | // parser. The parser is required in order to detect default flag settings 59 | // for help and version output. 60 | func (h *Help) ExtractValues(p *Parser, message string) { 61 | // accept message string for output 62 | h.Message = message 63 | 64 | ctx := p.subcommandContext 65 | if ctx == nil || ctx == p.initialSubcommandContext { 66 | ctx = &p.Subcommand 67 | } 68 | isRootContext := ctx == &p.Subcommand 69 | 70 | // extract Help values from the current subcommand in context 71 | // prependMessage string 72 | h.PrependMessage = ctx.AdditionalHelpPrepend 73 | // appendMessage string 74 | h.AppendMessage = ctx.AdditionalHelpAppend 75 | // command name 76 | h.CommandName = ctx.Name 77 | // description 78 | h.Description = ctx.Description 79 | // shell completion 80 | showCompletion := p.ShowCompletion && p.isTopLevelHelpContext() 81 | h.ShowCompletion = showCompletion 82 | 83 | // determine the max length of subcommand names for spacer calculation. 84 | maxLength := getLongestNameLength(ctx.Subcommands, 0) 85 | // include the synthetic completion subcommand in spacer calculation 86 | if showCompletion { 87 | if l := len("completion"); l > maxLength { 88 | maxLength = l 89 | } 90 | } 91 | 92 | // subcommands []HelpSubcommand 93 | for _, cmd := range ctx.Subcommands { 94 | if cmd.Hidden { 95 | continue 96 | } 97 | newHelpSubcommand := HelpSubcommand{ 98 | ShortName: cmd.ShortName, 99 | LongName: cmd.Name, 100 | Description: cmd.Description, 101 | Position: cmd.Position, 102 | Spacer: makeSpacer(cmd.Name, maxLength), 103 | } 104 | h.Subcommands = append(h.Subcommands, newHelpSubcommand) 105 | } 106 | 107 | // Append a synthetic completion subcommand at the end when enabled. 108 | // This shows users the correct invocation: "./appName completion [bash|zsh]". 109 | if showCompletion { 110 | completionHelp := HelpSubcommand{ 111 | ShortName: "", 112 | LongName: "completion", 113 | Description: "Generate shell completion script for bash or zsh.", 114 | Position: 0, 115 | Spacer: makeSpacer("completion", maxLength), 116 | } 117 | h.Subcommands = append(h.Subcommands, completionHelp) 118 | } 119 | 120 | maxLength = getLongestNameLength(ctx.PositionalFlags, 0) 121 | 122 | // parse positional flags into help output structs 123 | for _, pos := range ctx.PositionalFlags { 124 | if pos.Hidden { 125 | continue 126 | } 127 | newHelpPositional := HelpPositional{ 128 | Name: pos.Name, 129 | Position: pos.Position, 130 | Description: pos.Description, 131 | Required: pos.Required, 132 | DefaultValue: pos.defaultValue, 133 | Spacer: makeSpacer(pos.Name, maxLength), 134 | } 135 | h.Positionals = append(h.Positionals, newHelpPositional) 136 | } 137 | 138 | // if the built-in version flag is enabled, then add it to the appropriate help collection 139 | if p.ShowVersionWithVersionFlag { 140 | defaultVersionFlag := HelpFlag{ 141 | ShortName: "", 142 | LongName: versionFlagLongName, 143 | Description: "Displays the program version string.", 144 | DefaultValue: "", 145 | } 146 | if isRootContext { 147 | h.addFlagToSlice(&h.Flags, defaultVersionFlag) 148 | } else { 149 | h.addFlagToSlice(&h.GlobalFlags, defaultVersionFlag) 150 | } 151 | } 152 | 153 | // if the built-in help flag exists, then add it as a help flag 154 | if p.ShowHelpWithHFlag { 155 | defaultHelpFlag := HelpFlag{ 156 | ShortName: helpFlagShortName, 157 | LongName: helpFlagLongName, 158 | Description: "Displays help with available flag, subcommand, and positional value parameters.", 159 | DefaultValue: "", 160 | } 161 | if isRootContext { 162 | h.addFlagToSlice(&h.Flags, defaultHelpFlag) 163 | } else { 164 | h.addFlagToSlice(&h.GlobalFlags, defaultHelpFlag) 165 | } 166 | } 167 | 168 | // go through every flag in the subcommand and add it to help output 169 | h.parseFlagsToHelpFlags(ctx.Flags, &h.Flags) 170 | 171 | // go through every flag in the parent parser and add it to help output 172 | if isRootContext { 173 | h.parseFlagsToHelpFlags(p.Flags, &h.Flags) 174 | } else { 175 | h.parseFlagsToHelpFlags(p.Flags, &h.GlobalFlags) 176 | } 177 | 178 | // Optionally sort flags alphabetically by long name (fallback to short name) 179 | if p.SortFlags { 180 | sort.SliceStable(h.Flags, func(i, j int) bool { 181 | a := h.Flags[i] 182 | b := h.Flags[j] 183 | aName := strings.ToLower(strings.TrimSpace(a.LongName)) 184 | bName := strings.ToLower(strings.TrimSpace(b.LongName)) 185 | if aName == "" { 186 | aName = strings.ToLower(strings.TrimSpace(a.ShortName)) 187 | } 188 | if bName == "" { 189 | bName = strings.ToLower(strings.TrimSpace(b.ShortName)) 190 | } 191 | if p.SortFlagsReverse { 192 | return aName > bName 193 | } 194 | return aName < bName 195 | }) 196 | sort.SliceStable(h.GlobalFlags, func(i, j int) bool { 197 | a := h.GlobalFlags[i] 198 | b := h.GlobalFlags[j] 199 | aName := strings.ToLower(strings.TrimSpace(a.LongName)) 200 | bName := strings.ToLower(strings.TrimSpace(b.LongName)) 201 | if aName == "" { 202 | aName = strings.ToLower(strings.TrimSpace(a.ShortName)) 203 | } 204 | if bName == "" { 205 | bName = strings.ToLower(strings.TrimSpace(b.ShortName)) 206 | } 207 | if p.SortFlagsReverse { 208 | return aName > bName 209 | } 210 | return aName < bName 211 | }) 212 | } 213 | 214 | // formulate the usage string 215 | // first, we capture all the command and positional names by position 216 | commandsByPosition := make(map[int]string) 217 | for _, pos := range ctx.PositionalFlags { 218 | if pos.Hidden { 219 | continue 220 | } 221 | if len(commandsByPosition[pos.Position]) > 0 { 222 | commandsByPosition[pos.Position] = commandsByPosition[pos.Position] + "|" + pos.Name 223 | } else { 224 | commandsByPosition[pos.Position] = pos.Name 225 | } 226 | } 227 | for _, cmd := range ctx.Subcommands { 228 | if cmd.Hidden { 229 | continue 230 | } 231 | if len(commandsByPosition[cmd.Position]) > 0 { 232 | commandsByPosition[cmd.Position] = commandsByPosition[cmd.Position] + "|" + cmd.Name 233 | } else { 234 | commandsByPosition[cmd.Position] = cmd.Name 235 | } 236 | } 237 | 238 | // find the highest position count in the map 239 | var highestPosition int 240 | for i := range commandsByPosition { 241 | if i > highestPosition { 242 | highestPosition = i 243 | } 244 | } 245 | 246 | // only have a usage string if there are positional items 247 | var usageString string 248 | if highestPosition > 0 { 249 | // find each positional value and make our final string 250 | usageString = ctx.Name 251 | for i := 1; i <= highestPosition; i++ { 252 | if len(commandsByPosition[i]) > 0 { 253 | usageString = usageString + " [" + commandsByPosition[i] + "]" 254 | } else { 255 | // dont keep listing after the first position without any properties 256 | // it will be impossible to reach anything beyond here anyway 257 | break 258 | } 259 | } 260 | } 261 | 262 | h.UsageString = usageString 263 | 264 | alignHelpFlags(h.Flags) 265 | alignHelpFlags(h.GlobalFlags) 266 | h.composeLines() 267 | } 268 | 269 | // parseFlagsToHelpFlags parses the specified slice of flags into 270 | // help flags on the the calling help command 271 | func (h *Help) parseFlagsToHelpFlags(flags []*Flag, dest *[]HelpFlag) { 272 | for _, f := range flags { 273 | if f.Hidden { 274 | continue 275 | } 276 | 277 | // parse help values out if the flag hasn't been parsed yet 278 | if !f.parsed { 279 | f.parsed = true 280 | // parse the default value as a string and remember it for help output 281 | f.defaultValue, _ = f.returnAssignmentVarValueAsString() 282 | } 283 | 284 | // determine the default value based on the assignment variable 285 | defaultValue := f.defaultValue 286 | 287 | // dont show nils 288 | if defaultValue == "" { 289 | defaultValue = "" 290 | } 291 | 292 | // for bools, dont show a default of false 293 | _, isBool := f.AssignmentVar.(*bool) 294 | if isBool { 295 | b := f.AssignmentVar.(*bool) 296 | if !*b { 297 | defaultValue = "" 298 | } 299 | } 300 | 301 | newHelpFlag := HelpFlag{ 302 | ShortName: f.ShortName, 303 | LongName: f.LongName, 304 | Description: f.Description, 305 | DefaultValue: defaultValue, 306 | } 307 | h.addFlagToSlice(dest, newHelpFlag) 308 | } 309 | } 310 | 311 | // addFlagToSlice adds a flag to the provided slice if it does not exist already. 312 | func (h *Help) addFlagToSlice(dest *[]HelpFlag, f HelpFlag) { 313 | for _, existingFlag := range *dest { 314 | if len(existingFlag.ShortName) > 0 && existingFlag.ShortName == f.ShortName { 315 | return 316 | } 317 | if len(existingFlag.LongName) > 0 && existingFlag.LongName == f.LongName { 318 | return 319 | } 320 | } 321 | *dest = append(*dest, f) 322 | } 323 | 324 | // getLongestNameLength takes a slice of any supported flag and returns the length of the longest of their names 325 | func getLongestNameLength(slice interface{}, min int) int { 326 | var maxLength = min 327 | 328 | s := reflect.ValueOf(slice) 329 | if s.Kind() != reflect.Slice { 330 | log.Panicf("Parameter given to getLongestNameLength() is of type %s. Expected slice", s.Kind()) 331 | } 332 | 333 | for i := 0; i < s.Len(); i++ { 334 | option := s.Index(i).Interface() 335 | var name string 336 | switch t := option.(type) { 337 | case *Subcommand: 338 | name = t.Name 339 | case *Flag: 340 | name = t.LongName 341 | case *PositionalValue: 342 | name = t.Name 343 | default: 344 | log.Panicf("Unexpected type %T found in slice passed to getLongestNameLength(). Possible types: *Subcommand, *Flag, *PositionalValue", t) 345 | } 346 | length := len(name) 347 | if length > maxLength { 348 | maxLength = length 349 | } 350 | } 351 | 352 | return maxLength 353 | } 354 | 355 | // makeSpacer creates a string of whitespaces, with a length of the given 356 | // maxLength minus the length of the given name 357 | func makeSpacer(name string, maxLength int) string { 358 | length := maxLength - utf8.RuneCountInString(name) 359 | if length < 0 { 360 | length = 0 361 | } 362 | return strings.Repeat(" ", length) 363 | } 364 | 365 | func alignHelpFlags(flags []HelpFlag) { 366 | if len(flags) == 0 { 367 | return 368 | } 369 | 370 | shortWidth := 0 371 | longWidth := 0 372 | 373 | for _, flag := range flags { 374 | shortCol := flagShortColumn(flag.ShortName) 375 | longCol := flagLongColumn(flag.LongName) 376 | if l := utf8.RuneCountInString(shortCol); l > shortWidth { 377 | shortWidth = l 378 | } 379 | if l := utf8.RuneCountInString(longCol); l > longWidth { 380 | longWidth = l 381 | } 382 | } 383 | 384 | const shortGap = " " 385 | const descGap = " " 386 | 387 | for i := range flags { 388 | shortCol := flagShortColumn(flags[i].ShortName) 389 | longCol := flagLongColumn(flags[i].LongName) 390 | 391 | if shortWidth > 0 { 392 | flags[i].ShortDisplay = padRight(shortCol, shortWidth) + shortGap 393 | } else { 394 | flags[i].ShortDisplay = shortGap 395 | } 396 | 397 | if longWidth > 0 { 398 | flags[i].LongDisplay = padRight(longCol, longWidth) + descGap 399 | } else { 400 | flags[i].LongDisplay = descGap 401 | } 402 | } 403 | } 404 | 405 | func flagShortColumn(shortName string) string { 406 | if shortName == "" { 407 | return "" 408 | } 409 | return "-" + shortName 410 | } 411 | 412 | func flagLongColumn(longName string) string { 413 | if longName == "" { 414 | return "" 415 | } 416 | return "--" + longName 417 | } 418 | 419 | func padRight(input string, width int) string { 420 | delta := width - utf8.RuneCountInString(input) 421 | if delta <= 0 { 422 | return input 423 | } 424 | return input + strings.Repeat(" ", delta) 425 | } 426 | 427 | func (h *Help) composeLines() { 428 | lines := make([]string, 0, 16) 429 | 430 | appendBlank := func() { 431 | if len(lines) > 0 && lines[len(lines)-1] != "" { 432 | lines = append(lines, "") 433 | } 434 | } 435 | 436 | if h.CommandName != "" || h.Description != "" { 437 | header := h.CommandName 438 | if h.Description != "" { 439 | if header != "" { 440 | header += " - " 441 | } 442 | header += h.Description 443 | } 444 | lines = append(lines, header) 445 | } 446 | 447 | if h.PrependMessage != "" { 448 | lines = append(lines, splitLines(h.PrependMessage)...) 449 | } 450 | 451 | appendSection := func(section []string) { 452 | if len(section) == 0 { 453 | return 454 | } 455 | appendBlank() 456 | lines = append(lines, section...) 457 | } 458 | 459 | if h.UsageString != "" { 460 | section := []string{ 461 | " Usage:", 462 | " " + h.UsageString, 463 | } 464 | appendSection(section) 465 | } 466 | 467 | if len(h.Positionals) > 0 { 468 | section := []string{" Positional Variables:"} 469 | for _, pos := range h.Positionals { 470 | line := " " + pos.Name + " " + pos.Spacer 471 | if pos.Description != "" { 472 | line += " " + pos.Description 473 | } 474 | if pos.DefaultValue != "" { 475 | line += " (default: " + pos.DefaultValue + ")" 476 | } else if pos.Required { 477 | line += " (Required)" 478 | } 479 | section = append(section, line) 480 | } 481 | appendSection(section) 482 | } 483 | 484 | if len(h.Subcommands) > 0 { 485 | section := []string{" Subcommands:"} 486 | for _, sub := range h.Subcommands { 487 | line := " " + sub.LongName 488 | if sub.ShortName != "" { 489 | line += " (" + sub.ShortName + ")" 490 | } 491 | if sub.Position > 1 { 492 | line += " (position " + strconv.Itoa(sub.Position) + ")" 493 | } 494 | if sub.Description != "" { 495 | line += " " + sub.Spacer + sub.Description 496 | } 497 | section = append(section, line) 498 | } 499 | appendSection(section) 500 | } 501 | 502 | if len(h.Flags) > 0 { 503 | section := []string{" Flags:"} 504 | for _, flag := range h.Flags { 505 | line := " " + flag.ShortDisplay + flag.LongDisplay 506 | descAdded := false 507 | if flag.Description != "" { 508 | line += flag.Description 509 | descAdded = true 510 | } 511 | if flag.DefaultValue != "" { 512 | if descAdded { 513 | line += " (default: " + flag.DefaultValue + ")" 514 | } else { 515 | line += "(default: " + flag.DefaultValue + ")" 516 | } 517 | } 518 | section = append(section, line) 519 | } 520 | appendSection(section) 521 | } 522 | 523 | if len(h.GlobalFlags) > 0 { 524 | section := []string{" Global Flags:"} 525 | for _, flag := range h.GlobalFlags { 526 | line := " " + flag.ShortDisplay + flag.LongDisplay 527 | descAdded := false 528 | if flag.Description != "" { 529 | line += flag.Description 530 | descAdded = true 531 | } 532 | if flag.DefaultValue != "" { 533 | if descAdded { 534 | line += " (default: " + flag.DefaultValue + ")" 535 | } else { 536 | line += "(default: " + flag.DefaultValue + ")" 537 | } 538 | } 539 | section = append(section, line) 540 | } 541 | appendSection(section) 542 | } 543 | 544 | appendText := func(text string) { 545 | if text == "" { 546 | return 547 | } 548 | appendBlank() 549 | lines = append(lines, splitLines(text)...) 550 | } 551 | 552 | appendText(h.AppendMessage) 553 | appendText(h.Message) 554 | 555 | if len(lines) == 0 { 556 | lines = append(lines, "") 557 | } else { 558 | if lines[0] != "" { 559 | lines = append([]string{""}, lines...) 560 | } 561 | if lines[len(lines)-1] != "" { 562 | lines = append(lines, "") 563 | } 564 | } 565 | 566 | h.Lines = lines 567 | } 568 | 569 | func splitLines(input string) []string { 570 | if input == "" { 571 | return nil 572 | } 573 | return strings.Split(input, "\n") 574 | } 575 | -------------------------------------------------------------------------------- /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 | "math/big" 13 | "net" 14 | netip "net/netip" 15 | "net/url" 16 | "os" 17 | "regexp" 18 | "strconv" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | // strings used for builtin help and version flags both short and long 24 | const versionFlagLongName = "version" 25 | const helpFlagLongName = "help" 26 | const helpFlagShortName = "h" 27 | 28 | // defaultVersion is applied to parsers when they are created 29 | const defaultVersion = "0.0.0" 30 | 31 | // DebugMode indicates that debug output should be enabled 32 | var DebugMode bool 33 | 34 | // DefaultHelpTemplate is the help template that will be used 35 | // for newly created subcommands and commands 36 | var DefaultHelpTemplate = defaultHelpTemplate 37 | 38 | // DefaultParser is the default parser that is used with the package-level public 39 | // functions 40 | var DefaultParser *Parser 41 | 42 | // TrailingArguments holds trailing arguments in the main parser after parsing 43 | // has been run. 44 | var TrailingArguments []string 45 | 46 | func init() { 47 | 48 | // set the default help template 49 | // allow usage like flaggy.StringVar by enabling a default Parser 50 | ResetParser() 51 | } 52 | 53 | // ResetParser resets the default parser to a fresh instance. Uses the 54 | // name of the binary executing as the program name by default. 55 | func ResetParser() { 56 | if len(os.Args) > 0 { 57 | chunks := strings.Split(os.Args[0], "/") 58 | DefaultParser = NewParser(chunks[len(chunks)-1]) 59 | return 60 | } 61 | DefaultParser = NewParser("default") 62 | } 63 | 64 | // SortFlagsByLongName enables alphabetical sorting of flags by long name 65 | // in help output on the default parser. 66 | func SortFlagsByLongName() { 67 | DefaultParser.SortFlagsByLongName() 68 | } 69 | 70 | // SortFlagsByLongNameReversed enables reverse alphabetical sorting of flags 71 | // by long name in help output on the default parser. 72 | func SortFlagsByLongNameReversed() { 73 | DefaultParser.SortFlagsByLongNameReversed() 74 | } 75 | 76 | // Parse parses flags as requested in the default package parser. All trailing arguments 77 | // that result from parsing are placed in the global TrailingArguments variable. 78 | func Parse() { 79 | err := DefaultParser.Parse() 80 | TrailingArguments = DefaultParser.TrailingArguments 81 | if err != nil { 82 | log.Panicln("Error from argument parser:", err) 83 | } 84 | } 85 | 86 | // ParseArgs parses the passed args as if they were the arguments to the 87 | // running binary. Targets the default main parser for the package. All trailing 88 | // arguments are set in the global TrailingArguments variable. 89 | func ParseArgs(args []string) { 90 | err := DefaultParser.ParseArgs(args) 91 | TrailingArguments = DefaultParser.TrailingArguments 92 | if err != nil { 93 | log.Panicln("Error from argument parser:", err) 94 | } 95 | } 96 | 97 | // String adds a new string flag 98 | func String(assignmentVar *string, shortName string, longName string, description string) { 99 | DefaultParser.add(assignmentVar, shortName, longName, description) 100 | } 101 | 102 | // StringSlice adds a new slice of strings flag 103 | // Specify the flag multiple times to fill the slice 104 | func StringSlice(assignmentVar *[]string, shortName string, longName string, description string) { 105 | DefaultParser.add(assignmentVar, shortName, longName, description) 106 | } 107 | 108 | // Bool adds a new bool flag 109 | func Bool(assignmentVar *bool, shortName string, longName string, description string) { 110 | DefaultParser.add(assignmentVar, shortName, longName, description) 111 | } 112 | 113 | // BoolSlice adds a new slice of bools flag 114 | // Specify the flag multiple times to fill the slice 115 | func BoolSlice(assignmentVar *[]bool, shortName string, longName string, description string) { 116 | DefaultParser.add(assignmentVar, shortName, longName, description) 117 | } 118 | 119 | // ByteSlice adds a new slice of bytes flag 120 | // Specify the flag multiple times to fill the slice. Takes hex as input. 121 | func ByteSlice(assignmentVar *[]byte, shortName string, longName string, description string) { 122 | DefaultParser.add(assignmentVar, shortName, longName, description) 123 | } 124 | 125 | // BytesBase64 adds a new []byte flag parsed from base64 input. 126 | func BytesBase64(assignmentVar *Base64Bytes, shortName string, longName string, description string) { 127 | DefaultParser.add(assignmentVar, shortName, longName, description) 128 | } 129 | 130 | // Duration adds a new time.Duration flag. 131 | // Input format is described in time.ParseDuration(). 132 | // Example values: 1h, 1h50m, 32s 133 | func Duration(assignmentVar *time.Duration, shortName string, longName string, description string) { 134 | DefaultParser.add(assignmentVar, shortName, longName, description) 135 | } 136 | 137 | // DurationSlice adds a new time.Duration flag. 138 | // Input format is described in time.ParseDuration(). 139 | // Example values: 1h, 1h50m, 32s 140 | // Specify the flag multiple times to fill the slice. 141 | func DurationSlice(assignmentVar *[]time.Duration, shortName string, longName string, description string) { 142 | DefaultParser.add(assignmentVar, shortName, longName, description) 143 | } 144 | 145 | // Float32 adds a new float32 flag. 146 | func Float32(assignmentVar *float32, shortName string, longName string, description string) { 147 | DefaultParser.add(assignmentVar, shortName, longName, description) 148 | } 149 | 150 | // Float32Slice adds a new float32 flag. 151 | // Specify the flag multiple times to fill the slice. 152 | func Float32Slice(assignmentVar *[]float32, shortName string, longName string, description string) { 153 | DefaultParser.add(assignmentVar, shortName, longName, description) 154 | } 155 | 156 | // Float64 adds a new float64 flag. 157 | func Float64(assignmentVar *float64, shortName string, longName string, description string) { 158 | DefaultParser.add(assignmentVar, shortName, longName, description) 159 | } 160 | 161 | // Float64Slice adds a new float64 flag. 162 | // Specify the flag multiple times to fill the slice. 163 | func Float64Slice(assignmentVar *[]float64, shortName string, longName string, description string) { 164 | DefaultParser.add(assignmentVar, shortName, longName, description) 165 | } 166 | 167 | // Int adds a new int flag 168 | func Int(assignmentVar *int, shortName string, longName string, description string) { 169 | DefaultParser.add(assignmentVar, shortName, longName, description) 170 | } 171 | 172 | // IntSlice adds a new int slice flag. 173 | // Specify the flag multiple times to fill the slice. 174 | func IntSlice(assignmentVar *[]int, shortName string, longName string, description string) { 175 | DefaultParser.add(assignmentVar, shortName, longName, description) 176 | } 177 | 178 | // UInt adds a new uint flag 179 | func UInt(assignmentVar *uint, shortName string, longName string, description string) { 180 | DefaultParser.add(assignmentVar, shortName, longName, description) 181 | } 182 | 183 | // UIntSlice adds a new uint slice flag. 184 | // Specify the flag multiple times to fill the slice. 185 | func UIntSlice(assignmentVar *[]uint, shortName string, longName string, description string) { 186 | DefaultParser.add(assignmentVar, shortName, longName, description) 187 | } 188 | 189 | // UInt64 adds a new uint64 flag 190 | func UInt64(assignmentVar *uint64, shortName string, longName string, description string) { 191 | DefaultParser.add(assignmentVar, shortName, longName, description) 192 | } 193 | 194 | // UInt64Slice adds a new uint64 slice flag. 195 | // Specify the flag multiple times to fill the slice. 196 | func UInt64Slice(assignmentVar *[]uint64, shortName string, longName string, description string) { 197 | DefaultParser.add(assignmentVar, shortName, longName, description) 198 | } 199 | 200 | // UInt32 adds a new uint32 flag 201 | func UInt32(assignmentVar *uint32, shortName string, longName string, description string) { 202 | DefaultParser.add(assignmentVar, shortName, longName, description) 203 | } 204 | 205 | // UInt32Slice adds a new uint32 slice flag. 206 | // Specify the flag multiple times to fill the slice. 207 | func UInt32Slice(assignmentVar *[]uint32, shortName string, longName string, description string) { 208 | DefaultParser.add(assignmentVar, shortName, longName, description) 209 | } 210 | 211 | // UInt16 adds a new uint16 flag 212 | func UInt16(assignmentVar *uint16, shortName string, longName string, description string) { 213 | DefaultParser.add(assignmentVar, shortName, longName, description) 214 | } 215 | 216 | // UInt16Slice adds a new uint16 slice flag. 217 | // Specify the flag multiple times to fill the slice. 218 | func UInt16Slice(assignmentVar *[]uint16, shortName string, longName string, description string) { 219 | DefaultParser.add(assignmentVar, shortName, longName, description) 220 | } 221 | 222 | // UInt8 adds a new uint8 flag 223 | func UInt8(assignmentVar *uint8, shortName string, longName string, description string) { 224 | DefaultParser.add(assignmentVar, shortName, longName, description) 225 | } 226 | 227 | // UInt8Slice adds a new uint8 slice flag. 228 | // Specify the flag multiple times to fill the slice. 229 | func UInt8Slice(assignmentVar *[]uint8, shortName string, longName string, description string) { 230 | DefaultParser.add(assignmentVar, shortName, longName, description) 231 | } 232 | 233 | // Int64 adds a new int64 flag 234 | func Int64(assignmentVar *int64, shortName string, longName string, description string) { 235 | DefaultParser.add(assignmentVar, shortName, longName, description) 236 | } 237 | 238 | // Int64Slice adds a new int64 slice flag. 239 | // Specify the flag multiple times to fill the slice. 240 | func Int64Slice(assignmentVar *[]int64, shortName string, longName string, description string) { 241 | DefaultParser.add(assignmentVar, shortName, longName, description) 242 | } 243 | 244 | // Int32 adds a new int32 flag 245 | func Int32(assignmentVar *int32, shortName string, longName string, description string) { 246 | DefaultParser.add(assignmentVar, shortName, longName, description) 247 | } 248 | 249 | // Int32Slice adds a new int32 slice flag. 250 | // Specify the flag multiple times to fill the slice. 251 | func Int32Slice(assignmentVar *[]int32, shortName string, longName string, description string) { 252 | DefaultParser.add(assignmentVar, shortName, longName, description) 253 | } 254 | 255 | // Int16 adds a new int16 flag 256 | func Int16(assignmentVar *int16, shortName string, longName string, description string) { 257 | DefaultParser.add(assignmentVar, shortName, longName, description) 258 | } 259 | 260 | // Int16Slice adds a new int16 slice flag. 261 | // Specify the flag multiple times to fill the slice. 262 | func Int16Slice(assignmentVar *[]int16, shortName string, longName string, description string) { 263 | DefaultParser.add(assignmentVar, shortName, longName, description) 264 | } 265 | 266 | // Int8 adds a new int8 flag 267 | func Int8(assignmentVar *int8, shortName string, longName string, description string) { 268 | DefaultParser.add(assignmentVar, shortName, longName, description) 269 | } 270 | 271 | // Int8Slice adds a new int8 slice flag. 272 | // Specify the flag multiple times to fill the slice. 273 | func Int8Slice(assignmentVar *[]int8, shortName string, longName string, description string) { 274 | DefaultParser.add(assignmentVar, shortName, longName, description) 275 | } 276 | 277 | // IP adds a new net.IP flag. 278 | func IP(assignmentVar *net.IP, shortName string, longName string, description string) { 279 | DefaultParser.add(assignmentVar, shortName, longName, description) 280 | } 281 | 282 | // IPSlice adds a new int8 slice flag. 283 | // Specify the flag multiple times to fill the slice. 284 | func IPSlice(assignmentVar *[]net.IP, shortName string, longName string, description string) { 285 | DefaultParser.add(assignmentVar, shortName, longName, description) 286 | } 287 | 288 | // HardwareAddr adds a new net.HardwareAddr flag. 289 | func HardwareAddr(assignmentVar *net.HardwareAddr, shortName string, longName string, description string) { 290 | DefaultParser.add(assignmentVar, shortName, longName, description) 291 | } 292 | 293 | // HardwareAddrSlice adds a new net.HardwareAddr slice flag. 294 | // Specify the flag multiple times to fill the slice. 295 | func HardwareAddrSlice(assignmentVar *[]net.HardwareAddr, shortName string, longName string, description string) { 296 | DefaultParser.add(assignmentVar, shortName, longName, description) 297 | } 298 | 299 | // IPMask adds a new net.IPMask flag. IPv4 Only. 300 | func IPMask(assignmentVar *net.IPMask, shortName string, longName string, description string) { 301 | DefaultParser.add(assignmentVar, shortName, longName, description) 302 | } 303 | 304 | // IPMaskSlice adds a new net.HardwareAddr slice flag. IPv4 only. 305 | // Specify the flag multiple times to fill the slice. 306 | func IPMaskSlice(assignmentVar *[]net.IPMask, shortName string, longName string, description string) { 307 | DefaultParser.add(assignmentVar, shortName, longName, description) 308 | } 309 | 310 | // Time adds a new time.Time flag. Supports RFC3339/RFC3339Nano, RFC1123, and unix seconds. 311 | func Time(assignmentVar *time.Time, shortName string, longName string, description string) { 312 | DefaultParser.add(assignmentVar, shortName, longName, description) 313 | } 314 | 315 | // URL adds a new url.URL flag. 316 | func URL(assignmentVar *url.URL, shortName string, longName string, description string) { 317 | DefaultParser.add(assignmentVar, shortName, longName, description) 318 | } 319 | 320 | // IPNet adds a new net.IPNet flag parsed from CIDR. 321 | func IPNet(assignmentVar *net.IPNet, shortName string, longName string, description string) { 322 | DefaultParser.add(assignmentVar, shortName, longName, description) 323 | } 324 | 325 | // TCPAddr adds a new net.TCPAddr flag parsed from host:port. 326 | func TCPAddr(assignmentVar *net.TCPAddr, shortName string, longName string, description string) { 327 | DefaultParser.add(assignmentVar, shortName, longName, description) 328 | } 329 | 330 | // UDPAddr adds a new net.UDPAddr flag parsed from host:port. 331 | func UDPAddr(assignmentVar *net.UDPAddr, shortName string, longName string, description string) { 332 | DefaultParser.add(assignmentVar, shortName, longName, description) 333 | } 334 | 335 | // FileMode adds a new os.FileMode flag parsed from octal/decimal (base auto-detected). 336 | func FileMode(assignmentVar *os.FileMode, shortName string, longName string, description string) { 337 | DefaultParser.add(assignmentVar, shortName, longName, description) 338 | } 339 | 340 | // Regexp adds a new regexp.Regexp flag. 341 | func Regexp(assignmentVar *regexp.Regexp, shortName string, longName string, description string) { 342 | DefaultParser.add(assignmentVar, shortName, longName, description) 343 | } 344 | 345 | // Location adds a new time.Location flag. 346 | func Location(assignmentVar *time.Location, shortName string, longName string, description string) { 347 | DefaultParser.add(assignmentVar, shortName, longName, description) 348 | } 349 | 350 | // Month adds a new time.Month flag. 351 | func Month(assignmentVar *time.Month, shortName string, longName string, description string) { 352 | DefaultParser.add(assignmentVar, shortName, longName, description) 353 | } 354 | 355 | // Weekday adds a new time.Weekday flag. 356 | func Weekday(assignmentVar *time.Weekday, shortName string, longName string, description string) { 357 | DefaultParser.add(assignmentVar, shortName, longName, description) 358 | } 359 | 360 | // BigInt adds a new big.Int flag. 361 | func BigInt(assignmentVar *big.Int, shortName string, longName string, description string) { 362 | DefaultParser.add(assignmentVar, shortName, longName, description) 363 | } 364 | 365 | // BigRat adds a new big.Rat flag. 366 | func BigRat(assignmentVar *big.Rat, shortName string, longName string, description string) { 367 | DefaultParser.add(assignmentVar, shortName, longName, description) 368 | } 369 | 370 | // NetipAddr adds a new netip.Addr flag. 371 | func NetipAddr(assignmentVar *netip.Addr, shortName string, longName string, description string) { 372 | DefaultParser.add(assignmentVar, shortName, longName, description) 373 | } 374 | 375 | // NetipPrefix adds a new netip.Prefix flag. 376 | func NetipPrefix(assignmentVar *netip.Prefix, shortName string, longName string, description string) { 377 | DefaultParser.add(assignmentVar, shortName, longName, description) 378 | } 379 | 380 | // NetipAddrPort adds a new netip.AddrPort flag. 381 | func NetipAddrPort(assignmentVar *netip.AddrPort, shortName string, longName string, description string) { 382 | DefaultParser.add(assignmentVar, shortName, longName, description) 383 | } 384 | 385 | // AttachSubcommand adds a subcommand for parsing 386 | func AttachSubcommand(subcommand *Subcommand, relativePosition int) { 387 | DefaultParser.AttachSubcommand(subcommand, relativePosition) 388 | } 389 | 390 | // ShowHelp shows parser help 391 | func ShowHelp(message string) { 392 | DefaultParser.ShowHelpWithMessage(message) 393 | } 394 | 395 | // SetDescription sets the description of the default package command parser 396 | func SetDescription(description string) { 397 | DefaultParser.Description = description 398 | } 399 | 400 | // SetVersion sets the version of the default package command parser 401 | func SetVersion(version string) { 402 | DefaultParser.Version = version 403 | } 404 | 405 | // SetName sets the name of the default package command parser 406 | func SetName(name string) { 407 | DefaultParser.Name = name 408 | } 409 | 410 | // ShowHelpAndExit shows parser help and exits with status code 2 411 | func ShowHelpAndExit(message string) { 412 | ShowHelp(message) 413 | exitOrPanic(2) 414 | } 415 | 416 | // PanicInsteadOfExit is used when running tests 417 | var PanicInsteadOfExit bool 418 | 419 | // exitOrPanic panics instead of calling os.Exit so that tests can catch 420 | // more failures 421 | func exitOrPanic(code int) { 422 | if PanicInsteadOfExit { 423 | panic("Panic instead of exit with code: " + strconv.Itoa(code)) 424 | } 425 | os.Exit(code) 426 | } 427 | 428 | // ShowHelpOnUnexpectedEnable enables the ShowHelpOnUnexpected behavior on the 429 | // default parser. This causes unknown inputs to error out. 430 | func ShowHelpOnUnexpectedEnable() { 431 | DefaultParser.ShowHelpOnUnexpected = true 432 | } 433 | 434 | // ShowHelpOnUnexpectedDisable disables the ShowHelpOnUnexpected behavior on the 435 | // default parser. This causes unknown inputs to error out. 436 | func ShowHelpOnUnexpectedDisable() { 437 | DefaultParser.ShowHelpOnUnexpected = false 438 | } 439 | 440 | // AddPositionalValue adds a positional value to the main parser at the global 441 | // context 442 | func AddPositionalValue(assignmentVar *string, name string, relativePosition int, required bool, description string) { 443 | DefaultParser.AddPositionalValue(assignmentVar, name, relativePosition, required, description) 444 | } 445 | 446 | // debugPrint prints if debugging is enabled 447 | func debugPrint(i ...interface{}) { 448 | if DebugMode { 449 | fmt.Println(i...) 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /flag_test.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "net" 7 | netip "net/netip" 8 | "net/url" 9 | "os" 10 | "regexp" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | // debugOff makes defers easier and turns off debug mode 16 | func debugOff() { 17 | DebugMode = false 18 | } 19 | 20 | // debugOn turns on debug mode 21 | func debugOn() { 22 | DebugMode = true 23 | } 24 | 25 | func TestGlobs(t *testing.T) { 26 | for _, a := range os.Args { 27 | fmt.Println(a) 28 | } 29 | } 30 | 31 | func TestParseArgWithValue(t *testing.T) { 32 | 33 | testCases := make(map[string][]string) 34 | testCases["-f=test"] = []string{"f", "test"} 35 | testCases["--f=test"] = []string{"f", "test"} 36 | testCases["--flag=test"] = []string{"flag", "test"} 37 | testCases["-flag=test"] = []string{"flag", "test"} 38 | testCases["----flag=--test"] = []string{"--flag", "--test"} 39 | testCases["-b"] = []string{"b", ""} 40 | testCases["--bool"] = []string{"bool", ""} 41 | 42 | for arg, correctValues := range testCases { 43 | key, value := parseArgWithValue(arg) 44 | if key != correctValues[0] { 45 | t.Fatalf("Flag %s parsed key as %s but expected key %s", arg, key, correctValues[0]) 46 | } 47 | if value != correctValues[1] { 48 | t.Fatalf("Flag %s parsed value as %s but expected value %s", arg, value, correctValues[1]) 49 | } 50 | t.Logf("Flag %s parsed key as %s and value as %s correctly", arg, key, value) 51 | } 52 | } 53 | 54 | func TestDetermineArgType(t *testing.T) { 55 | 56 | testCases := make(map[string]string) 57 | testCases["-f"] = argIsFlagWithSpace 58 | testCases["--f"] = argIsFlagWithSpace 59 | testCases["-flag"] = argIsFlagWithSpace 60 | testCases["--flag"] = argIsFlagWithSpace 61 | testCases["positionalArg"] = argIsPositional 62 | testCases["subcommand"] = argIsPositional 63 | testCases["sub--+/\\324command"] = argIsPositional 64 | testCases["--flag=CONTENT"] = argIsFlagWithValue 65 | testCases["-flag=CONTENT"] = argIsFlagWithValue 66 | testCases["-anotherfl-ag=CONTENT"] = argIsFlagWithValue 67 | testCases["--anotherfl-ag=CONTENT"] = argIsFlagWithValue 68 | testCases["1--anotherfl-ag=CONTENT"] = argIsPositional 69 | 70 | for arg, correctArgType := range testCases { 71 | argType := determineArgType(arg) 72 | if argType != correctArgType { 73 | t.Fatalf("Flag %s determined to be type %s but expected type %s", arg, argType, correctArgType) 74 | } else { 75 | t.Logf("Flag %s correctly determined to be type %s", arg, argType) 76 | } 77 | } 78 | } 79 | 80 | // TestInputParsing tests all flag types. 81 | func TestInputParsing(t *testing.T) { 82 | defer debugOff() 83 | DebugMode = true 84 | 85 | ResetParser() 86 | var err error 87 | inputArgs := []string{} 88 | 89 | // Setup input arguments for every input type 90 | 91 | var stringFlag = "defaultVar" 92 | String(&stringFlag, "s", "string", "string flag") 93 | inputArgs = append(inputArgs, "-s", "flaggy") 94 | var stringFlagExpected = "flaggy" 95 | 96 | var stringSliceFlag []string 97 | StringSlice(&stringSliceFlag, "ssf", "stringSlice", "string slice flag") 98 | inputArgs = append(inputArgs, "-ssf", "one", "-ssf", "two") 99 | var stringSliceFlagExpected = []string{"one", "two"} 100 | 101 | var boolFlag bool 102 | Bool(&boolFlag, "bf", "bool", "bool flag") 103 | inputArgs = append(inputArgs, "-bf") 104 | var boolFlagExpected = true 105 | 106 | var boolSliceFlag []bool 107 | BoolSlice(&boolSliceFlag, "bsf", "boolSlice", "bool slice flag") 108 | inputArgs = append(inputArgs, "-bsf", "-bsf") 109 | var boolSliceFlagExpected = []bool{true, true} 110 | 111 | var byteSliceFlag []byte 112 | ByteSlice(&byteSliceFlag, "bysf", "byteSlice", "byte slice flag") 113 | inputArgs = append(inputArgs, "-bysf", "17", "-bysf", "18") 114 | var byteSliceFlagExpected = []uint8{17, 18} 115 | 116 | var durationFlag time.Duration 117 | Duration(&durationFlag, "df", "duration", "duration flag") 118 | inputArgs = append(inputArgs, "-df", "33s") 119 | var durationFlagExpected = time.Second * 33 120 | 121 | var durationSliceFlag []time.Duration 122 | DurationSlice(&durationSliceFlag, "dsf", "durationSlice", "duration slice flag") 123 | inputArgs = append(inputArgs, "-dsf", "33s", "-dsf", "1h") 124 | var durationSliceFlagExpected = []time.Duration{time.Second * 33, time.Hour} 125 | 126 | var float32Flag float32 127 | Float32(&float32Flag, "f32", "float32", "float32 flag") 128 | inputArgs = append(inputArgs, "-f32", "33.343") 129 | var float32FlagExpected float32 = 33.343 130 | 131 | var float32SliceFlag []float32 132 | Float32Slice(&float32SliceFlag, "f32s", "float32Slice", "float32 slice flag") 133 | inputArgs = append(inputArgs, "-f32s", "33.343", "-f32s", "33.222") 134 | var float32SliceFlagExpected = []float32{33.343, 33.222} 135 | 136 | var float64Flag float64 137 | Float64(&float64Flag, "f64", "float64", "float64 flag") 138 | inputArgs = append(inputArgs, "-f64", "33.222343") 139 | var float64FlagExpected = 33.222343 140 | 141 | var float64SliceFlag []float64 142 | Float64Slice(&float64SliceFlag, "f64s", "float64Slice", "float64 slice flag") 143 | inputArgs = append(inputArgs, "-f64s", "64.343", "-f64s", "64.222") 144 | var float64SliceFlagExpected = []float64{64.343, 64.222} 145 | 146 | var intFlag int 147 | Int(&intFlag, "i", "int", "int flag") 148 | inputArgs = append(inputArgs, "-i", "3553") 149 | var intFlagExpected = 3553 150 | 151 | var intSliceFlag []int 152 | IntSlice(&intSliceFlag, "is", "intSlice", "int slice flag") 153 | inputArgs = append(inputArgs, "-is", "6446", "-is", "64") 154 | var intSliceFlagExpected = []int{6446, 64} 155 | 156 | var uintFlag uint 157 | UInt(&uintFlag, "ui", "uint", "uint flag") 158 | inputArgs = append(inputArgs, "-ui", "3553") 159 | var uintFlagExpected uint = 3553 160 | 161 | var uintSliceFlag []uint 162 | UIntSlice(&uintSliceFlag, "uis", "uintSlice", "uint slice flag") 163 | inputArgs = append(inputArgs, "-uis", "6446", "-uis", "64") 164 | var uintSliceFlagExpected = []uint{6446, 64} 165 | 166 | var uint64Flag uint64 167 | UInt64(&uint64Flag, "ui64", "uint64", "uint64 flag") 168 | inputArgs = append(inputArgs, "-ui64", "3553") 169 | var uint64FlagExpected uint64 = 3553 170 | 171 | var uint64SliceFlag []uint64 172 | UInt64Slice(&uint64SliceFlag, "ui64s", "uint64Slice", "uint64 slice flag") 173 | inputArgs = append(inputArgs, "-ui64s", "6446", "-ui64s", "64") 174 | var uint64SliceFlagExpected = []uint64{6446, 64} 175 | 176 | var uint32Flag uint32 177 | UInt32(&uint32Flag, "ui32", "uint32", "uint32 flag") 178 | inputArgs = append(inputArgs, "-ui32", "6446") 179 | var uint32FlagExpected uint32 = 6446 180 | 181 | var uint32SliceFlag []uint32 182 | UInt32Slice(&uint32SliceFlag, "ui32s", "uint32Slice", "uint32 slice flag") 183 | inputArgs = append(inputArgs, "-ui32s", "6446", "-ui32s", "64") 184 | var uint32SliceFlagExpected = []uint32{6446, 64} 185 | 186 | var uint16Flag uint16 187 | UInt16(&uint16Flag, "ui16", "uint16", "uint16 flag") 188 | inputArgs = append(inputArgs, "-ui16", "6446") 189 | var uint16FlagExpected uint16 = 6446 190 | 191 | var uint16SliceFlag []uint16 192 | UInt16Slice(&uint16SliceFlag, "ui16s", "uint16Slice", "uint16 slice flag") 193 | inputArgs = append(inputArgs, "-ui16s", "6446", "-ui16s", "64") 194 | var uint16SliceFlagExpected = []uint16{6446, 64} 195 | 196 | var uint8Flag uint8 197 | UInt8(&uint8Flag, "ui8", "uint8", "uint8 flag") 198 | inputArgs = append(inputArgs, "-ui8", "50") 199 | var uint8FlagExpected uint8 = 50 200 | 201 | var uint8SliceFlag []uint8 202 | UInt8Slice(&uint8SliceFlag, "ui8s", "uint8Slice", "uint8 slice flag") 203 | inputArgs = append(inputArgs, "-ui8s", "3", "-ui8s", "2") 204 | var uint8SliceFlagExpected = []uint8{uint8(3), uint8(2)} 205 | 206 | var int64Flag int64 207 | Int64(&int64Flag, "i64", "i64", "int64 flag") 208 | inputArgs = append(inputArgs, "-i64", "33445566") 209 | var int64FlagExpected int64 = 33445566 210 | 211 | var int64SliceFlag []int64 212 | Int64Slice(&int64SliceFlag, "i64s", "int64Slice", "int64 slice flag") 213 | inputArgs = append(inputArgs, "-i64s", "40", "-i64s", "50") 214 | var int64SliceFlagExpected = []int64{40, 50} 215 | 216 | var int32Flag int32 217 | Int32(&int32Flag, "i32", "int32", "int32 flag") 218 | inputArgs = append(inputArgs, "-i32", "445566") 219 | var int32FlagExpected int32 = 445566 220 | 221 | var int32SliceFlag []int32 222 | Int32Slice(&int32SliceFlag, "i32s", "int32Slice", "uint32 slice flag") 223 | inputArgs = append(inputArgs, "-i32s", "40", "-i32s", "50") 224 | var int32SliceFlagExpected = []int32{40, 50} 225 | 226 | var int16Flag int16 227 | Int16(&int16Flag, "i16", "int16", "int16 flag") 228 | inputArgs = append(inputArgs, "-i16", "5566") 229 | var int16FlagExpected int16 = 5566 230 | 231 | var int16SliceFlag []int16 232 | Int16Slice(&int16SliceFlag, "i16s", "int16Slice", "int16 slice flag") 233 | inputArgs = append(inputArgs, "-i16s", "40", "-i16s", "50") 234 | var int16SliceFlagExpected = []int16{40, 50} 235 | 236 | var int8Flag int8 237 | Int8(&int8Flag, "i8", "int8", "int8 flag") 238 | inputArgs = append(inputArgs, "-i8", "32") 239 | var int8FlagExpected int8 = 32 240 | 241 | var int8SliceFlag []int8 242 | Int8Slice(&int8SliceFlag, "i8s", "int8Slice", "uint8 slice flag") 243 | inputArgs = append(inputArgs, "-i8s", "4", "-i8s", "2") 244 | var int8SliceFlagExpected = []int8{4, 2} 245 | 246 | var ipFlag net.IP 247 | IP(&ipFlag, "ip", "ipFlag", "ip flag") 248 | inputArgs = append(inputArgs, "-ip", "1.1.1.1") 249 | var ipFlagExpected = net.IPv4(1, 1, 1, 1) 250 | 251 | var ipSliceFlag []net.IP 252 | IPSlice(&ipSliceFlag, "ips", "ipFlagSlice", "ip slice flag") 253 | inputArgs = append(inputArgs, "-ips", "1.1.1.1", "-ips", "4.4.4.4") 254 | var ipSliceFlagExpected = []net.IP{net.IPv4(1, 1, 1, 1), net.IPv4(4, 4, 4, 4)} 255 | 256 | var hwFlag net.HardwareAddr 257 | HardwareAddr(&hwFlag, "hw", "hwFlag", "hw flag") 258 | inputArgs = append(inputArgs, "-hw", "32:00:16:46:20:00") 259 | hwFlagExpected, err := net.ParseMAC("32:00:16:46:20:00") 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | 264 | var hwFlagSlice []net.HardwareAddr 265 | HardwareAddrSlice(&hwFlagSlice, "hws", "hwFlagSlice", "hw slice flag") 266 | inputArgs = append(inputArgs, "-hws", "32:00:16:46:20:00", "-hws", "32:00:16:46:20:01") 267 | macA, err := net.ParseMAC("32:00:16:46:20:00") 268 | if err != nil { 269 | t.Fatal(err) 270 | } 271 | macB, err := net.ParseMAC("32:00:16:46:20:01") 272 | if err != nil { 273 | t.Fatal(err) 274 | } 275 | var hwFlagSliceExpected = []net.HardwareAddr{macA, macB} 276 | 277 | var maskFlag net.IPMask 278 | IPMask(&maskFlag, "m", "mFlag", "mask flag") 279 | inputArgs = append(inputArgs, "-m", "255.255.255.255") 280 | var maskFlagExpected = net.IPMask([]byte{255, 255, 255, 255}) 281 | 282 | var maskSliceFlag []net.IPMask 283 | IPMaskSlice(&maskSliceFlag, "ms", "mFlagSlice", "mask slice flag") 284 | if err != nil { 285 | t.Fatal(err) 286 | } 287 | inputArgs = append(inputArgs, "-ms", "255.255.255.255", "-ms", "255.255.255.0") 288 | var maskSliceFlagExpected = []net.IPMask{net.IPMask([]byte{255, 255, 255, 255}), net.IPMask([]byte{255, 255, 255, 0})} 289 | 290 | // time.Time via unix seconds 291 | var timeFlag time.Time 292 | Time(&timeFlag, "ttm", "timeFlag", "time flag") 293 | inputArgs = append(inputArgs, "-ttm", "1717171717") 294 | var timeFlagExpected = time.Unix(1717171717, 0).UTC() 295 | 296 | // url.URL 297 | var urlFlag url.URL 298 | URL(&urlFlag, "urlf", "urlFlag", "url flag") 299 | inputArgs = append(inputArgs, "-urlf", "https://example.com/x?y=1#z") 300 | var urlFlagExpected = "https://example.com/x?y=1#z" 301 | 302 | // net.IPNet CIDR 303 | var cidrFlag net.IPNet 304 | IPNet(&cidrFlag, "cidrf", "cidrFlag", "cidr flag") 305 | inputArgs = append(inputArgs, "-cidrf", "192.168.0.0/16") 306 | var cidrFlagExpected = "192.168.0.0/16" 307 | 308 | // net.TCPAddr 309 | var tcpAddr net.TCPAddr 310 | TCPAddr(&tcpAddr, "tcpa", "tcpAddr", "tcp addr") 311 | inputArgs = append(inputArgs, "-tcpa", "127.0.0.1:8080") 312 | var tcpIPExpected = net.IPv4(127, 0, 0, 1) 313 | var tcpPortExpected = 8080 314 | 315 | // net.UDPAddr 316 | var udpAddr net.UDPAddr 317 | UDPAddr(&udpAddr, "udpa", "udpAddr", "udp addr") 318 | inputArgs = append(inputArgs, "-udpa", "127.0.0.1:5353") 319 | var udpIPExpected = net.IPv4(127, 0, 0, 1) 320 | var udpPortExpected = 5353 321 | 322 | // os.FileMode 323 | var fileModeFlag os.FileMode 324 | FileMode(&fileModeFlag, "fmode", "fileMode", "file mode flag") 325 | inputArgs = append(inputArgs, "-fmode", "0755") 326 | var fileModeExpected os.FileMode = 0o755 327 | 328 | // regexp.Regexp 329 | var regexFlag regexp.Regexp 330 | Regexp(®exFlag, "re", "regexp", "regex flag") 331 | inputArgs = append(inputArgs, "-re", "^ab+$") 332 | 333 | // time.Location 334 | var locFlag time.Location 335 | Location(&locFlag, "tz", "timezone", "timezone flag") 336 | inputArgs = append(inputArgs, "-tz", "+02:00") 337 | var locFlagExpected = "UTC+02:00" 338 | 339 | // time.Month 340 | var monthFlag time.Month 341 | Month(&monthFlag, "mon", "month", "month flag") 342 | inputArgs = append(inputArgs, "-mon", "February") 343 | var monthFlagExpected = time.February 344 | 345 | // time.Weekday 346 | var weekdayFlag time.Weekday 347 | Weekday(&weekdayFlag, "wday", "weekday", "weekday flag") 348 | inputArgs = append(inputArgs, "-wday", "Tuesday") 349 | var weekdayFlagExpected = time.Tuesday 350 | 351 | // big.Int 352 | var bigIntFlag big.Int 353 | BigInt(&bigIntFlag, "bigi", "bigint", "big int flag") 354 | inputArgs = append(inputArgs, "-bigi", "0xFF") 355 | var bigIntExpected = big.NewInt(255) 356 | 357 | // big.Rat 358 | var bigRatFlag big.Rat 359 | BigRat(&bigRatFlag, "bigr", "bigrat", "big rat flag") 360 | inputArgs = append(inputArgs, "-bigr", "1/8") 361 | var bigRatExpected = big.NewRat(1, 8) 362 | 363 | // netip.Addr 364 | var netipAddrFlag netip.Addr 365 | NetipAddr(&netipAddrFlag, "nip", "netipaddr", "netip addr flag") 366 | inputArgs = append(inputArgs, "-nip", "192.0.2.1") 367 | var netipAddrExpected = netip.MustParseAddr("192.0.2.1") 368 | 369 | // netip.Prefix 370 | var netipPrefixFlag netip.Prefix 371 | NetipPrefix(&netipPrefixFlag, "nipr", "netipprefix", "netip prefix flag") 372 | inputArgs = append(inputArgs, "-nipr", "2001:db8::/32") 373 | var netipPrefixExpected = netip.MustParsePrefix("2001:db8::/32") 374 | 375 | // netip.AddrPort 376 | var netipAddrPortFlag netip.AddrPort 377 | NetipAddrPort(&netipAddrPortFlag, "niap", "netipaddrport", "netip addrport flag") 378 | inputArgs = append(inputArgs, "-niap", "127.0.0.1:80") 379 | var netipAddrPortExpected = netip.MustParseAddrPort("127.0.0.1:80") 380 | 381 | // Base64 bytes 382 | var b64Flag Base64Bytes 383 | BytesBase64(&b64Flag, "b64", "base64", "base64 bytes flag") 384 | inputArgs = append(inputArgs, "-b64", "SGVsbG8=") 385 | var b64Expected = []byte("Hello") 386 | 387 | // display help with all flags used 388 | ShowHelp("Showing help for test: " + t.Name()) 389 | 390 | // Parse arguments 391 | ParseArgs(inputArgs) 392 | 393 | // validate parsed values 394 | if stringFlag != stringFlagExpected { 395 | t.Fatal("string flag incorrect", stringFlag, stringFlagExpected) 396 | } 397 | 398 | for i, f := range stringSliceFlagExpected { 399 | if stringSliceFlag[i] != f { 400 | t.Fatal("stringSlice value incorrect", stringSliceFlag[i], f) 401 | } 402 | } 403 | 404 | if boolFlag != boolFlagExpected { 405 | t.Fatal("bool flag incorrect", boolFlag, boolFlagExpected) 406 | } 407 | 408 | for i, f := range boolSliceFlagExpected { 409 | if boolSliceFlag[i] != f { 410 | t.Fatal("boolSlice value incorrect", boolSliceFlag[i], f) 411 | } 412 | } 413 | 414 | for i, f := range byteSliceFlagExpected { 415 | if byteSliceFlag[i] != f { 416 | t.Fatal("byteSlice value incorrect", boolSliceFlag[i], f) 417 | } 418 | } 419 | 420 | if durationFlag != durationFlagExpected { 421 | t.Fatal("duration flag incorrect", durationFlag, durationFlagExpected) 422 | } 423 | 424 | for i, f := range durationSliceFlagExpected { 425 | if durationSliceFlag[i] != f { 426 | t.Fatal("durationSlice value incorrect", durationSliceFlag[i], f) 427 | } 428 | } 429 | 430 | if float32Flag != float32FlagExpected { 431 | t.Fatal("float32 flag incorrect", float32Flag, float32FlagExpected) 432 | } 433 | 434 | for i, f := range float32SliceFlagExpected { 435 | if float32SliceFlag[i] != f { 436 | t.Fatal("float32Slice value incorrect", float32SliceFlag[i], f) 437 | } 438 | } 439 | 440 | if float64Flag != float64FlagExpected { 441 | t.Fatal("float64 flag incorrect", float64Flag, float64FlagExpected) 442 | } 443 | 444 | for i, f := range float64SliceFlagExpected { 445 | if float64SliceFlag[i] != f { 446 | t.Fatal("float64Slice value incorrect", float64SliceFlag[i], f) 447 | } 448 | } 449 | 450 | if intFlag != intFlagExpected { 451 | t.Fatal("int flag incorrect", intFlag, intFlagExpected) 452 | } 453 | 454 | for i, f := range intSliceFlagExpected { 455 | if intSliceFlag[i] != f { 456 | t.Fatal("intSlice value incorrect", intSliceFlag[i], f) 457 | } 458 | } 459 | 460 | if uintFlag != uintFlagExpected { 461 | t.Fatal("uint flag incorrect", uintFlag, uintFlagExpected) 462 | } 463 | 464 | for i, f := range uintSliceFlagExpected { 465 | if uintSliceFlag[i] != f { 466 | t.Fatal("uintSlice value incorrect", uintSliceFlag[i], f) 467 | } 468 | } 469 | 470 | if uint64Flag != uint64FlagExpected { 471 | t.Fatal("uint64 flag incorrect", uint64Flag, uint64FlagExpected) 472 | } 473 | 474 | for i, f := range uint64SliceFlagExpected { 475 | if uint64SliceFlag[i] != f { 476 | t.Fatal("uint64Slice value incorrect", uint64SliceFlag[i], f) 477 | } 478 | } 479 | 480 | if uint32Flag != uint32FlagExpected { 481 | t.Fatal("uint32 flag incorrect", uint32Flag, uint32FlagExpected) 482 | } 483 | 484 | for i, f := range uint32SliceFlagExpected { 485 | if uint32SliceFlag[i] != f { 486 | t.Fatal("uint32Slice value incorrect", uint32SliceFlag[i], f) 487 | } 488 | } 489 | 490 | if uint16Flag != uint16FlagExpected { 491 | t.Fatal("uint16 flag incorrect", uint16Flag, uint16FlagExpected) 492 | } 493 | 494 | for i, f := range uint16SliceFlagExpected { 495 | if uint16SliceFlag[i] != f { 496 | t.Fatal("uint16Slice value incorrect", uint16SliceFlag[i], f) 497 | } 498 | } 499 | 500 | if uint8Flag != uint8FlagExpected { 501 | t.Fatal("uint8 flag incorrect", uint8Flag, uint8FlagExpected) 502 | } 503 | 504 | for i, f := range uint8SliceFlagExpected { 505 | if uint8SliceFlag[i] != f { 506 | t.Fatal("uint8Slice value", i, "incorrect", uint8SliceFlag[i], f) 507 | } 508 | } 509 | 510 | if int64Flag != int64FlagExpected { 511 | t.Fatal("int64 flag incorrect", int64Flag, int64FlagExpected) 512 | } 513 | 514 | for i, f := range int64SliceFlagExpected { 515 | if int64SliceFlag[i] != f { 516 | t.Fatal("int64Slice value incorrect", int64SliceFlag[i], f) 517 | } 518 | } 519 | 520 | if int32Flag != int32FlagExpected { 521 | t.Fatal("int32 flag incorrect", int32Flag, int32FlagExpected) 522 | } 523 | 524 | for i, f := range int32SliceFlagExpected { 525 | if int32SliceFlag[i] != f { 526 | t.Fatal("int32Slice value incorrect", int32SliceFlag[i], f) 527 | } 528 | } 529 | 530 | if int16Flag != int16FlagExpected { 531 | t.Fatal("int16 flag incorrect", int16Flag, int16FlagExpected) 532 | } 533 | 534 | for i, f := range int16SliceFlagExpected { 535 | if int16SliceFlag[i] != f { 536 | t.Fatal("int16Slice value incorrect", int16SliceFlag[i], f) 537 | } 538 | } 539 | 540 | if int8Flag != int8FlagExpected { 541 | t.Fatal("int8 flag incorrect", int8Flag, int8FlagExpected) 542 | } 543 | 544 | for i, f := range int8SliceFlagExpected { 545 | if int8SliceFlag[i] != f { 546 | t.Fatal("int8Slice value incorrect", int8SliceFlag[i], f) 547 | } 548 | } 549 | 550 | if !ipFlag.Equal(ipFlagExpected) { 551 | t.Fatal("ip flag incorrect", ipFlag, ipFlagExpected) 552 | } 553 | 554 | for i, f := range ipSliceFlagExpected { 555 | if !f.Equal(ipSliceFlag[i]) { 556 | t.Fatal("ipSlice value incorrect", ipSliceFlag[i], f) 557 | } 558 | } 559 | 560 | if hwFlag.String() != hwFlagExpected.String() { 561 | t.Fatal("hw flag incorrect", hwFlag, hwFlagExpected) 562 | } 563 | 564 | for i, f := range hwFlagSliceExpected { 565 | if f.String() != hwFlagSlice[i].String() { 566 | t.Fatal("hw flag slice value incorrect", hwFlagSlice[i].String(), f.String()) 567 | } 568 | } 569 | 570 | if maskFlag.String() != maskFlagExpected.String() { 571 | t.Fatal("mask flag incorrect", maskFlag, maskFlagExpected) 572 | } 573 | 574 | for i, f := range maskSliceFlagExpected { 575 | if f.String() != maskSliceFlag[i].String() { 576 | t.Fatal("mask flag slice value incorrect", maskSliceFlag[i].String(), f.String()) 577 | } 578 | } 579 | 580 | // time.Time 581 | if !timeFlag.Equal(timeFlagExpected) { 582 | t.Fatal("time flag incorrect", timeFlag, timeFlagExpected) 583 | } 584 | 585 | // url.URL 586 | if urlFlag.String() != urlFlagExpected { 587 | t.Fatal("url flag incorrect", urlFlag.String(), urlFlagExpected) 588 | } 589 | 590 | // CIDR 591 | if cidrFlag.String() != cidrFlagExpected { 592 | t.Fatal("cidr flag incorrect", cidrFlag.String(), cidrFlagExpected) 593 | } 594 | 595 | // TCP addr 596 | if !tcpAddr.IP.Equal(tcpIPExpected) || tcpAddr.Port != tcpPortExpected { 597 | t.Fatal("tcp addr incorrect", tcpAddr, tcpIPExpected, tcpPortExpected) 598 | } 599 | 600 | // UDP addr 601 | if !udpAddr.IP.Equal(udpIPExpected) || udpAddr.Port != udpPortExpected { 602 | t.Fatal("udp addr incorrect", udpAddr, udpIPExpected, udpPortExpected) 603 | } 604 | 605 | // File mode 606 | if fileModeFlag != fileModeExpected { 607 | t.Fatal("file mode incorrect", fileModeFlag, fileModeExpected) 608 | } 609 | 610 | // Regexp 611 | if !regexFlag.MatchString("abbb") || regexFlag.MatchString("ac") { 612 | t.Fatal("regexp flag incorrect") 613 | } 614 | 615 | // Location 616 | if locFlag.String() != locFlagExpected { 617 | t.Fatal("location flag incorrect", locFlag.String(), locFlagExpected) 618 | } 619 | 620 | // Month 621 | if monthFlag != monthFlagExpected { 622 | t.Fatal("month flag incorrect", monthFlag, monthFlagExpected) 623 | } 624 | 625 | // Weekday 626 | if weekdayFlag != weekdayFlagExpected { 627 | t.Fatal("weekday flag incorrect", weekdayFlag, weekdayFlagExpected) 628 | } 629 | 630 | // big.Int 631 | if bigIntFlag.Cmp(bigIntExpected) != 0 { 632 | t.Fatal("bigint flag incorrect", bigIntFlag.String(), bigIntExpected.String()) 633 | } 634 | 635 | // big.Rat 636 | if bigRatFlag.Cmp(bigRatExpected) != 0 { 637 | t.Fatal("bigrat flag incorrect", bigRatFlag.RatString(), bigRatExpected.RatString()) 638 | } 639 | 640 | // netip.Addr 641 | if netipAddrFlag != netipAddrExpected { 642 | t.Fatal("netip addr incorrect", netipAddrFlag.String(), netipAddrExpected.String()) 643 | } 644 | 645 | // netip.Prefix 646 | if netipPrefixFlag.String() != netipPrefixExpected.String() { 647 | t.Fatal("netip prefix incorrect", netipPrefixFlag.String(), netipPrefixExpected.String()) 648 | } 649 | 650 | // netip.AddrPort 651 | if netipAddrPortFlag.String() != netipAddrPortExpected.String() { 652 | t.Fatal("netip addrport incorrect", netipAddrPortFlag.String(), netipAddrPortExpected.String()) 653 | } 654 | 655 | // Base64 bytes 656 | if string([]byte(b64Flag)) != string(b64Expected) { 657 | t.Fatal("base64 bytes flag incorrect", []byte(b64Flag), b64Expected) 658 | } 659 | } 660 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package flaggy 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "math/big" 8 | "net" 9 | netip "net/netip" 10 | "net/url" 11 | "os" 12 | "reflect" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // Flag holds the base methods for all flag types 20 | type Flag struct { 21 | ShortName string 22 | LongName string 23 | Description string 24 | rawValue string // the value as a string before being parsed 25 | Hidden bool // indicates this flag should be hidden from help and suggestions 26 | AssignmentVar interface{} 27 | defaultValue string // the value (as a string), that was set by default before any parsing and assignment 28 | parsed bool // indicates that this flag has already been parsed 29 | } 30 | 31 | // HasName indicates that this flag's short or long name matches the 32 | // supplied name string 33 | func (f *Flag) HasName(name string) bool { 34 | name = strings.TrimSpace(name) 35 | if f.ShortName == name || f.LongName == name { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | // identifyAndAssignValue identifies the type of the incoming value 42 | // and assigns it to the AssignmentVar pointer's target value. If 43 | // the value is a type that needs parsing, that is performed as well. 44 | func (f *Flag) identifyAndAssignValue(value string) error { 45 | 46 | var err error 47 | 48 | // Only parse this flag default value once. This keeps us from 49 | // overwriting the default value in help output 50 | if !f.parsed { 51 | f.parsed = true 52 | // parse the default value as a string and remember it for help output 53 | f.defaultValue, err = f.returnAssignmentVarValueAsString() 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | 59 | debugPrint("attempting to assign value", value, "to flag", f.LongName) 60 | f.rawValue = value // remember the raw value 61 | 62 | // depending on the type of the assignment variable, we convert the 63 | // incoming string and assign it. We only use pointers to variables 64 | // in flagy. No returning vars by value. 65 | switch f.AssignmentVar.(type) { 66 | case *string: 67 | v, _ := (f.AssignmentVar).(*string) 68 | *v = value 69 | case *[]string: 70 | v := f.AssignmentVar.(*[]string) 71 | new := append(*v, value) 72 | *v = new 73 | case *bool: 74 | v, err := strconv.ParseBool(value) 75 | if err != nil { 76 | return err 77 | } 78 | a, _ := (f.AssignmentVar).(*bool) 79 | *a = v 80 | case *[]bool: 81 | // parse the incoming bool 82 | b, err := strconv.ParseBool(value) 83 | if err != nil { 84 | return err 85 | } 86 | // cast the assignment var 87 | existing := f.AssignmentVar.(*[]bool) 88 | // deref the assignment var and append to it 89 | v := append(*existing, b) 90 | // pointer the new value and assign it 91 | a, _ := (f.AssignmentVar).(*[]bool) 92 | *a = v 93 | case *time.Duration: 94 | v, err := time.ParseDuration(value) 95 | if err != nil { 96 | return err 97 | } 98 | a, _ := (f.AssignmentVar).(*time.Duration) 99 | *a = v 100 | case *[]time.Duration: 101 | t, err := time.ParseDuration(value) 102 | if err != nil { 103 | return err 104 | } 105 | existing := f.AssignmentVar.(*[]time.Duration) 106 | // deref the assignment var and append to it 107 | v := append(*existing, t) 108 | // pointer the new value and assign it 109 | a, _ := (f.AssignmentVar).(*[]time.Duration) 110 | *a = v 111 | case *float32: 112 | v, err := strconv.ParseFloat(value, 32) 113 | if err != nil { 114 | return err 115 | } 116 | float := float32(v) 117 | a, _ := (f.AssignmentVar).(*float32) 118 | *a = float 119 | case *[]float32: 120 | v, err := strconv.ParseFloat(value, 32) 121 | if err != nil { 122 | return err 123 | } 124 | float := float32(v) 125 | existing := f.AssignmentVar.(*[]float32) 126 | new := append(*existing, float) 127 | *existing = new 128 | case *float64: 129 | v, err := strconv.ParseFloat(value, 64) 130 | if err != nil { 131 | return err 132 | } 133 | a, _ := (f.AssignmentVar).(*float64) 134 | *a = v 135 | case *[]float64: 136 | v, err := strconv.ParseFloat(value, 64) 137 | if err != nil { 138 | return err 139 | } 140 | existing := f.AssignmentVar.(*[]float64) 141 | new := append(*existing, v) 142 | 143 | *existing = new 144 | case *int: 145 | v, err := strconv.Atoi(value) 146 | if err != nil { 147 | return err 148 | } 149 | e := f.AssignmentVar.(*int) 150 | *e = v 151 | case *[]int: 152 | v, err := strconv.Atoi(value) 153 | if err != nil { 154 | return err 155 | } 156 | existing := f.AssignmentVar.(*[]int) 157 | new := append(*existing, v) 158 | *existing = new 159 | case *uint: 160 | v, err := strconv.ParseUint(value, 10, 64) 161 | if err != nil { 162 | return err 163 | } 164 | existing := f.AssignmentVar.(*uint) 165 | *existing = uint(v) 166 | case *[]uint: 167 | v, err := strconv.ParseUint(value, 10, 64) 168 | if err != nil { 169 | return err 170 | } 171 | existing := f.AssignmentVar.(*[]uint) 172 | new := append(*existing, uint(v)) 173 | *existing = new 174 | case *uint64: 175 | v, err := strconv.ParseUint(value, 10, 64) 176 | if err != nil { 177 | return err 178 | } 179 | existing := f.AssignmentVar.(*uint64) 180 | *existing = v 181 | case *[]uint64: 182 | v, err := strconv.ParseUint(value, 10, 64) 183 | if err != nil { 184 | return err 185 | } 186 | existing := f.AssignmentVar.(*[]uint64) 187 | new := append(*existing, v) 188 | *existing = new 189 | case *uint32: 190 | v, err := strconv.ParseUint(value, 10, 32) 191 | if err != nil { 192 | return err 193 | } 194 | existing := f.AssignmentVar.(*uint32) 195 | *existing = uint32(v) 196 | case *[]uint32: 197 | v, err := strconv.ParseUint(value, 10, 32) 198 | if err != nil { 199 | return err 200 | } 201 | existing := f.AssignmentVar.(*[]uint32) 202 | new := append(*existing, uint32(v)) 203 | *existing = new 204 | case *uint16: 205 | v, err := strconv.ParseUint(value, 10, 16) 206 | if err != nil { 207 | return err 208 | } 209 | val := uint16(v) 210 | existing := f.AssignmentVar.(*uint16) 211 | *existing = val 212 | case *[]uint16: 213 | v, err := strconv.ParseUint(value, 10, 16) 214 | if err != nil { 215 | return err 216 | } 217 | existing := f.AssignmentVar.(*[]uint16) 218 | new := append(*existing, uint16(v)) 219 | *existing = new 220 | case *uint8: 221 | v, err := strconv.ParseUint(value, 10, 8) 222 | if err != nil { 223 | return err 224 | } 225 | val := uint8(v) 226 | existing := f.AssignmentVar.(*uint8) 227 | *existing = val 228 | case *[]uint8: 229 | var newSlice []uint8 230 | 231 | v, err := strconv.ParseUint(value, 10, 8) 232 | if err != nil { 233 | return err 234 | } 235 | newV := uint8(v) 236 | existing := f.AssignmentVar.(*[]uint8) 237 | newSlice = append(*existing, newV) 238 | *existing = newSlice 239 | case *int64: 240 | v, err := strconv.ParseInt(value, 10, 64) 241 | if err != nil { 242 | return err 243 | } 244 | existing := f.AssignmentVar.(*int64) 245 | *existing = v 246 | case *[]int64: 247 | v, err := strconv.ParseInt(value, 10, 64) 248 | if err != nil { 249 | return err 250 | } 251 | existingSlice := f.AssignmentVar.(*[]int64) 252 | newSlice := append(*existingSlice, v) 253 | *existingSlice = newSlice 254 | case *int32: 255 | v, err := strconv.ParseInt(value, 10, 32) 256 | if err != nil { 257 | return err 258 | } 259 | converted := int32(v) 260 | existing := f.AssignmentVar.(*int32) 261 | *existing = converted 262 | case *[]int32: 263 | v, err := strconv.ParseInt(value, 10, 32) 264 | if err != nil { 265 | return err 266 | } 267 | existingSlice := f.AssignmentVar.(*[]int32) 268 | newSlice := append(*existingSlice, int32(v)) 269 | *existingSlice = newSlice 270 | case *int16: 271 | v, err := strconv.ParseInt(value, 10, 16) 272 | if err != nil { 273 | return err 274 | } 275 | converted := int16(v) 276 | existing := f.AssignmentVar.(*int16) 277 | *existing = converted 278 | case *[]int16: 279 | v, err := strconv.ParseInt(value, 10, 16) 280 | if err != nil { 281 | return err 282 | } 283 | existingSlice := f.AssignmentVar.(*[]int16) 284 | newSlice := append(*existingSlice, int16(v)) 285 | *existingSlice = newSlice 286 | case *int8: 287 | v, err := strconv.ParseInt(value, 10, 8) 288 | if err != nil { 289 | return err 290 | } 291 | converted := int8(v) 292 | existing := f.AssignmentVar.(*int8) 293 | *existing = converted 294 | case *[]int8: 295 | v, err := strconv.ParseInt(value, 10, 8) 296 | if err != nil { 297 | return err 298 | } 299 | existingSlice := f.AssignmentVar.(*[]int8) 300 | newSlice := append(*existingSlice, int8(v)) 301 | *existingSlice = newSlice 302 | case *net.IP: 303 | v := net.ParseIP(value) 304 | existing := f.AssignmentVar.(*net.IP) 305 | *existing = v 306 | case *[]net.IP: 307 | v := net.ParseIP(value) 308 | existing := f.AssignmentVar.(*[]net.IP) 309 | new := append(*existing, v) 310 | *existing = new 311 | case *net.HardwareAddr: 312 | v, err := net.ParseMAC(value) 313 | if err != nil { 314 | return err 315 | } 316 | existing := f.AssignmentVar.(*net.HardwareAddr) 317 | *existing = v 318 | case *[]net.HardwareAddr: 319 | v, err := net.ParseMAC(value) 320 | if err != nil { 321 | return err 322 | } 323 | existing := f.AssignmentVar.(*[]net.HardwareAddr) 324 | new := append(*existing, v) 325 | *existing = new 326 | case *net.IPMask: 327 | v := net.IPMask(net.ParseIP(value).To4()) 328 | existing := f.AssignmentVar.(*net.IPMask) 329 | *existing = v 330 | case *[]net.IPMask: 331 | v := net.IPMask(net.ParseIP(value).To4()) 332 | existing := f.AssignmentVar.(*[]net.IPMask) 333 | new := append(*existing, v) 334 | *existing = new 335 | case *time.Time: 336 | // Support unix seconds if numeric, else try common layouts 337 | if isAllDigits(value) { 338 | sec, err := strconv.ParseInt(value, 10, 64) 339 | if err != nil { 340 | return err 341 | } 342 | t := time.Unix(sec, 0).UTC() 343 | a := f.AssignmentVar.(*time.Time) 344 | *a = t 345 | return nil 346 | } 347 | var parsed time.Time 348 | var err error 349 | layouts := []string{time.RFC3339Nano, time.RFC3339, time.RFC1123Z, time.RFC1123} 350 | for _, layout := range layouts { 351 | parsed, err = time.Parse(layout, value) 352 | if err == nil { 353 | a := f.AssignmentVar.(*time.Time) 354 | *a = parsed 355 | return nil 356 | } 357 | } 358 | return err 359 | case *url.URL: 360 | u, err := url.Parse(value) 361 | if err != nil { 362 | return err 363 | } 364 | a := f.AssignmentVar.(*url.URL) 365 | *a = *u 366 | case *net.IPNet: 367 | _, ipnet, err := net.ParseCIDR(value) 368 | if err != nil { 369 | return err 370 | } 371 | a := f.AssignmentVar.(*net.IPNet) 372 | *a = *ipnet 373 | case *net.TCPAddr: 374 | host, portStr, err := net.SplitHostPort(value) 375 | if err != nil { 376 | return err 377 | } 378 | port, err := strconv.Atoi(portStr) 379 | if err != nil { 380 | return err 381 | } 382 | var ip net.IP 383 | if len(host) > 0 { 384 | ip = net.ParseIP(host) 385 | } 386 | addr := net.TCPAddr{IP: ip, Port: port} 387 | a := f.AssignmentVar.(*net.TCPAddr) 388 | *a = addr 389 | case *net.UDPAddr: 390 | host, portStr, err := net.SplitHostPort(value) 391 | if err != nil { 392 | return err 393 | } 394 | port, err := strconv.Atoi(portStr) 395 | if err != nil { 396 | return err 397 | } 398 | var ip net.IP 399 | if len(host) > 0 { 400 | ip = net.ParseIP(host) 401 | } 402 | addr := net.UDPAddr{IP: ip, Port: port} 403 | a := f.AssignmentVar.(*net.UDPAddr) 404 | *a = addr 405 | case *os.FileMode: 406 | v, err := strconv.ParseUint(value, 0, 32) 407 | if err != nil { 408 | return err 409 | } 410 | a := f.AssignmentVar.(*os.FileMode) 411 | *a = os.FileMode(v) 412 | case *regexp.Regexp: 413 | r, err := regexp.Compile(value) 414 | if err != nil { 415 | return err 416 | } 417 | a := f.AssignmentVar.(*regexp.Regexp) 418 | *a = *r 419 | case *time.Location: 420 | // Try IANA name, with fallback to UTC offset like +02:00 or -0700 421 | if loc, err := time.LoadLocation(value); err == nil { 422 | a := f.AssignmentVar.(*time.Location) 423 | *a = *loc 424 | return nil 425 | } 426 | if off, ok := parseUTCOffset(value); ok { 427 | name := offsetName(off) 428 | loc := time.FixedZone(name, off) 429 | a := f.AssignmentVar.(*time.Location) 430 | *a = *loc 431 | return nil 432 | } 433 | return fmt.Errorf("invalid time.Location: %s", value) 434 | case *time.Month: 435 | if m, ok := parseMonth(value); ok { 436 | a := f.AssignmentVar.(*time.Month) 437 | *a = m 438 | return nil 439 | } 440 | return fmt.Errorf("invalid time.Month: %s", value) 441 | case *time.Weekday: 442 | if d, ok := parseWeekday(value); ok { 443 | a := f.AssignmentVar.(*time.Weekday) 444 | *a = d 445 | return nil 446 | } 447 | return fmt.Errorf("invalid time.Weekday: %s", value) 448 | case *big.Int: 449 | bi := f.AssignmentVar.(*big.Int) 450 | if _, ok := bi.SetString(value, 0); !ok { 451 | return fmt.Errorf("invalid big.Int: %s", value) 452 | } 453 | case *big.Rat: 454 | br := f.AssignmentVar.(*big.Rat) 455 | if _, ok := br.SetString(value); !ok { 456 | return fmt.Errorf("invalid big.Rat: %s", value) 457 | } 458 | case *Base64Bytes: 459 | // Try standard then URL encoding 460 | decoded, err := base64.StdEncoding.DecodeString(value) 461 | if err == nil { 462 | a := f.AssignmentVar.(*Base64Bytes) 463 | *a = Base64Bytes(decoded) 464 | return nil 465 | } 466 | if decodedURL, errURL := base64.URLEncoding.DecodeString(value); errURL == nil { 467 | a := f.AssignmentVar.(*Base64Bytes) 468 | *a = Base64Bytes(decodedURL) 469 | return nil 470 | } 471 | return err 472 | case *netip.Addr: 473 | addr, err := netip.ParseAddr(value) 474 | if err != nil { 475 | return err 476 | } 477 | a := f.AssignmentVar.(*netip.Addr) 478 | *a = addr 479 | case *netip.Prefix: 480 | pfx, err := netip.ParsePrefix(value) 481 | if err != nil { 482 | return err 483 | } 484 | a := f.AssignmentVar.(*netip.Prefix) 485 | *a = pfx 486 | case *netip.AddrPort: 487 | ap, err := netip.ParseAddrPort(value) 488 | if err != nil { 489 | return err 490 | } 491 | a := f.AssignmentVar.(*netip.AddrPort) 492 | *a = ap 493 | default: 494 | return errors.New("Unknown flag assignmentVar supplied in flag " + f.LongName + " " + f.ShortName) 495 | } 496 | 497 | return err 498 | } 499 | 500 | const argIsPositional = "positional" // subcommand or positional value 501 | const argIsFlagWithSpace = "flagWithSpace" // -f path or --file path 502 | const argIsFlagWithValue = "flagWithValue" // -f=path or --file=path 503 | const argIsFinal = "final" // the final argument only '--' 504 | 505 | // determineArgType determines if the specified arg is a flag with space 506 | // separated value, a flag with a connected value, or neither (positional) 507 | func determineArgType(arg string) string { 508 | 509 | // if the arg is --, then its the final arg 510 | if arg == "--" { 511 | return argIsFinal 512 | } 513 | 514 | // if it has the prefix --, then its a long flag 515 | if strings.HasPrefix(arg, "--") { 516 | // if it contains an equals, it is a joined value 517 | if strings.Contains(arg, "=") { 518 | return argIsFlagWithValue 519 | } 520 | return argIsFlagWithSpace 521 | } 522 | 523 | // if it has the prefix -, then its a short flag 524 | if strings.HasPrefix(arg, "-") { 525 | // if it contains an equals, it is a joined value 526 | if strings.Contains(arg, "=") { 527 | return argIsFlagWithValue 528 | } 529 | return argIsFlagWithSpace 530 | } 531 | 532 | return argIsPositional 533 | } 534 | 535 | // parseArgWithValue parses a key=value concatenated argument into a key and 536 | // value 537 | func parseArgWithValue(arg string) (key string, value string) { 538 | 539 | // remove up to two minuses from start of flag 540 | arg = strings.TrimPrefix(arg, "-") 541 | arg = strings.TrimPrefix(arg, "-") 542 | 543 | // debugPrint("parseArgWithValue parsing", arg) 544 | 545 | // break at the equals 546 | args := strings.SplitN(arg, "=", 2) 547 | 548 | // if its a bool arg, with no explicit value, we return a blank 549 | if len(args) == 1 { 550 | return args[0], "" 551 | } 552 | 553 | // if its a key and value pair, we return those 554 | if len(args) == 2 { 555 | // debugPrint("parseArgWithValue parsed", args[0], args[1]) 556 | return args[0], args[1] 557 | } 558 | 559 | fmt.Println("Warning: attempted to parseArgWithValue but did not have correct parameter count.", arg, "->", args) 560 | return "", "" 561 | } 562 | 563 | // parseFlagToName parses a flag with space value down to a key name: 564 | // 565 | // --path -> path 566 | // -p -> p 567 | func parseFlagToName(arg string) string { 568 | // remove minus from start 569 | arg = strings.TrimLeft(arg, "-") 570 | arg = strings.TrimLeft(arg, "-") 571 | return arg 572 | } 573 | 574 | // collectAllNestedFlags recurses through the command tree to get all 575 | // 576 | // flags specified on a subcommand and its descending subcommands 577 | func collectAllNestedFlags(sc *Subcommand) []*Flag { 578 | fullList := sc.Flags 579 | for _, sc := range sc.Subcommands { 580 | fullList = append(fullList, sc.Flags...) 581 | fullList = append(fullList, collectAllNestedFlags(sc)...) 582 | } 583 | return fullList 584 | } 585 | 586 | // flagIsBool determines if the flag is a bool within the specified parser 587 | // and subcommand's context 588 | func flagIsBool(sc *Subcommand, p *Parser, key string) bool { 589 | for _, f := range sc.Flags { 590 | if f.HasName(key) { 591 | _, isBool := f.AssignmentVar.(*bool) 592 | _, isBoolSlice := f.AssignmentVar.(*[]bool) 593 | if isBool || isBoolSlice { 594 | return true 595 | } 596 | } 597 | } 598 | 599 | for _, f := range p.Flags { 600 | if f.HasName(key) { 601 | _, isBool := f.AssignmentVar.(*bool) 602 | _, isBoolSlice := f.AssignmentVar.(*[]bool) 603 | if isBool || isBoolSlice { 604 | return true 605 | } 606 | } 607 | } 608 | 609 | // by default, the answer is false 610 | return false 611 | } 612 | 613 | // flagIsDefined reports whether a flag with the provided key is registered on 614 | // the supplied subcommand or parser. 615 | func flagIsDefined(sc *Subcommand, p *Parser, key string) bool { 616 | for _, f := range sc.Flags { 617 | if f.HasName(key) { 618 | return true 619 | } 620 | } 621 | 622 | for _, f := range p.Flags { 623 | if f.HasName(key) { 624 | return true 625 | } 626 | } 627 | 628 | return false 629 | } 630 | 631 | // returnAssignmentVarValueAsString returns the value of the flag's 632 | // assignment variable as a string. This is used to display the 633 | // default value of flags before they are assigned (like when help is output). 634 | func (f *Flag) returnAssignmentVarValueAsString() (string, error) { 635 | 636 | debugPrint("returning current value of assignment var of flag", f.LongName) 637 | 638 | var err error 639 | 640 | // depending on the type of the assignment variable, we convert the 641 | // incoming string and assign it. We only use pointers to variables 642 | // in flagy. No returning vars by value. 643 | switch f.AssignmentVar.(type) { 644 | case *string: 645 | v, _ := (f.AssignmentVar).(*string) 646 | return *v, err 647 | case *[]string: 648 | v := f.AssignmentVar.(*[]string) 649 | return strings.Join(*v, ","), err 650 | case *bool: 651 | a, _ := (f.AssignmentVar).(*bool) 652 | return strconv.FormatBool(*a), err 653 | case *[]bool: 654 | value := f.AssignmentVar.(*[]bool) 655 | var ss []string 656 | for _, b := range *value { 657 | ss = append(ss, strconv.FormatBool(b)) 658 | } 659 | return strings.Join(ss, ","), err 660 | case *time.Duration: 661 | a := f.AssignmentVar.(*time.Duration) 662 | return (*a).String(), err 663 | case *[]time.Duration: 664 | tds := f.AssignmentVar.(*[]time.Duration) 665 | var asSlice []string 666 | for _, td := range *tds { 667 | asSlice = append(asSlice, td.String()) 668 | } 669 | return strings.Join(asSlice, ","), err 670 | case *float32: 671 | a := f.AssignmentVar.(*float32) 672 | return strconv.FormatFloat(float64(*a), 'f', 2, 32), err 673 | case *[]float32: 674 | v := f.AssignmentVar.(*[]float32) 675 | var strSlice []string 676 | for _, f := range *v { 677 | formatted := strconv.FormatFloat(float64(f), 'f', 2, 32) 678 | strSlice = append(strSlice, formatted) 679 | } 680 | return strings.Join(strSlice, ","), err 681 | case *float64: 682 | a := f.AssignmentVar.(*float64) 683 | return strconv.FormatFloat(float64(*a), 'f', 2, 64), err 684 | case *[]float64: 685 | v := f.AssignmentVar.(*[]float64) 686 | var strSlice []string 687 | for _, f := range *v { 688 | formatted := strconv.FormatFloat(float64(f), 'f', 2, 64) 689 | strSlice = append(strSlice, formatted) 690 | } 691 | return strings.Join(strSlice, ","), err 692 | case *int: 693 | a := f.AssignmentVar.(*int) 694 | return strconv.Itoa(*a), err 695 | case *[]int: 696 | val := f.AssignmentVar.(*[]int) 697 | var strSlice []string 698 | for _, i := range *val { 699 | str := strconv.Itoa(i) 700 | strSlice = append(strSlice, str) 701 | } 702 | return strings.Join(strSlice, ","), err 703 | case *uint: 704 | v := f.AssignmentVar.(*uint) 705 | return strconv.FormatUint(uint64(*v), 10), err 706 | case *[]uint: 707 | values := f.AssignmentVar.(*[]uint) 708 | var strVars []string 709 | for _, i := range *values { 710 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 711 | } 712 | return strings.Join(strVars, ","), err 713 | case *uint64: 714 | v := f.AssignmentVar.(*uint64) 715 | return strconv.FormatUint(*v, 10), err 716 | case *[]uint64: 717 | values := f.AssignmentVar.(*[]uint64) 718 | var strVars []string 719 | for _, i := range *values { 720 | strVars = append(strVars, strconv.FormatUint(i, 10)) 721 | } 722 | return strings.Join(strVars, ","), err 723 | case *uint32: 724 | v := f.AssignmentVar.(*uint32) 725 | return strconv.FormatUint(uint64(*v), 10), err 726 | case *[]uint32: 727 | values := f.AssignmentVar.(*[]uint32) 728 | var strVars []string 729 | for _, i := range *values { 730 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 731 | } 732 | return strings.Join(strVars, ","), err 733 | case *uint16: 734 | v := f.AssignmentVar.(*uint16) 735 | return strconv.FormatUint(uint64(*v), 10), err 736 | case *[]uint16: 737 | values := f.AssignmentVar.(*[]uint16) 738 | var strVars []string 739 | for _, i := range *values { 740 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 741 | } 742 | return strings.Join(strVars, ","), err 743 | case *uint8: 744 | v := f.AssignmentVar.(*uint8) 745 | return strconv.FormatUint(uint64(*v), 10), err 746 | case *[]uint8: 747 | values := f.AssignmentVar.(*[]uint8) 748 | var strVars []string 749 | for _, i := range *values { 750 | strVars = append(strVars, strconv.FormatUint(uint64(i), 10)) 751 | } 752 | return strings.Join(strVars, ","), err 753 | case *int64: 754 | v := f.AssignmentVar.(*int64) 755 | return strconv.FormatInt(int64(*v), 10), err 756 | case *[]int64: 757 | values := f.AssignmentVar.(*[]int64) 758 | var strVars []string 759 | for _, i := range *values { 760 | strVars = append(strVars, strconv.FormatInt(i, 10)) 761 | } 762 | return strings.Join(strVars, ","), err 763 | case *int32: 764 | v := f.AssignmentVar.(*int32) 765 | return strconv.FormatInt(int64(*v), 10), err 766 | case *[]int32: 767 | values := f.AssignmentVar.(*[]int32) 768 | var strVars []string 769 | for _, i := range *values { 770 | strVars = append(strVars, strconv.FormatInt(int64(i), 10)) 771 | } 772 | return strings.Join(strVars, ","), err 773 | case *int16: 774 | v := f.AssignmentVar.(*int16) 775 | return strconv.FormatInt(int64(*v), 10), err 776 | case *[]int16: 777 | values := f.AssignmentVar.(*[]int16) 778 | var strVars []string 779 | for _, i := range *values { 780 | strVars = append(strVars, strconv.FormatInt(int64(i), 10)) 781 | } 782 | return strings.Join(strVars, ","), err 783 | case *int8: 784 | v := f.AssignmentVar.(*int8) 785 | return strconv.FormatInt(int64(*v), 10), err 786 | case *[]int8: 787 | values := f.AssignmentVar.(*[]int8) 788 | var strVars []string 789 | for _, i := range *values { 790 | strVars = append(strVars, strconv.FormatInt(int64(i), 10)) 791 | } 792 | return strings.Join(strVars, ","), err 793 | case *net.IP: 794 | val := f.AssignmentVar.(*net.IP) 795 | return val.String(), err 796 | case *[]net.IP: 797 | val := f.AssignmentVar.(*[]net.IP) 798 | var strSlice []string 799 | for _, ip := range *val { 800 | strSlice = append(strSlice, ip.String()) 801 | } 802 | return strings.Join(strSlice, ","), err 803 | case *net.HardwareAddr: 804 | val := f.AssignmentVar.(*net.HardwareAddr) 805 | return val.String(), err 806 | case *[]net.HardwareAddr: 807 | val := f.AssignmentVar.(*[]net.HardwareAddr) 808 | var strSlice []string 809 | for _, mac := range *val { 810 | strSlice = append(strSlice, mac.String()) 811 | } 812 | return strings.Join(strSlice, ","), err 813 | case *net.IPMask: 814 | val := f.AssignmentVar.(*net.IPMask) 815 | return val.String(), err 816 | case *[]net.IPMask: 817 | val := f.AssignmentVar.(*[]net.IPMask) 818 | var strSlice []string 819 | for _, m := range *val { 820 | strSlice = append(strSlice, m.String()) 821 | } 822 | return strings.Join(strSlice, ","), err 823 | case *time.Time: 824 | v := f.AssignmentVar.(*time.Time) 825 | if v.IsZero() { 826 | return "", err 827 | } 828 | return v.UTC().Format(time.RFC3339Nano), err 829 | case *url.URL: 830 | v := f.AssignmentVar.(*url.URL) 831 | return v.String(), err 832 | case *net.IPNet: 833 | v := f.AssignmentVar.(*net.IPNet) 834 | return v.String(), err 835 | case *net.TCPAddr: 836 | v := f.AssignmentVar.(*net.TCPAddr) 837 | return v.String(), err 838 | case *net.UDPAddr: 839 | v := f.AssignmentVar.(*net.UDPAddr) 840 | return v.String(), err 841 | case *os.FileMode: 842 | v := f.AssignmentVar.(*os.FileMode) 843 | return fmt.Sprintf("%#o", *v), err 844 | case *regexp.Regexp: 845 | v := f.AssignmentVar.(*regexp.Regexp) 846 | return v.String(), err 847 | case *time.Location: 848 | v := f.AssignmentVar.(*time.Location) 849 | return v.String(), err 850 | case *time.Month: 851 | v := f.AssignmentVar.(*time.Month) 852 | if *v == 0 { 853 | return "", err 854 | } 855 | return v.String(), err 856 | case *time.Weekday: 857 | v := f.AssignmentVar.(*time.Weekday) 858 | return v.String(), err 859 | case *big.Int: 860 | v := f.AssignmentVar.(*big.Int) 861 | return v.String(), err 862 | case *big.Rat: 863 | v := f.AssignmentVar.(*big.Rat) 864 | return v.RatString(), err 865 | case *Base64Bytes: 866 | v := f.AssignmentVar.(*Base64Bytes) 867 | if v == nil || len(*v) == 0 { 868 | return "", err 869 | } 870 | return base64.StdEncoding.EncodeToString([]byte(*v)), err 871 | case *netip.Addr: 872 | v := f.AssignmentVar.(*netip.Addr) 873 | return v.String(), err 874 | case *netip.Prefix: 875 | v := f.AssignmentVar.(*netip.Prefix) 876 | return v.String(), err 877 | case *netip.AddrPort: 878 | v := f.AssignmentVar.(*netip.AddrPort) 879 | return v.String(), err 880 | default: 881 | return "", errors.New("Unknown flag assignmentVar found in flag " + f.LongName + " " + f.ShortName + ". Type not supported: " + reflect.TypeOf(f.AssignmentVar).String()) 882 | } 883 | } 884 | 885 | // helpers 886 | func isAllDigits(s string) bool { 887 | if len(s) == 0 { 888 | return false 889 | } 890 | for _, r := range s { 891 | if r < '0' || r > '9' { 892 | return false 893 | } 894 | } 895 | return true 896 | } 897 | 898 | func parseUTCOffset(s string) (int, bool) { 899 | // Supports formats: +HH, -HH, +HHMM, -HHMM, +HH:MM, -HH:MM, Z 900 | if s == "Z" || s == "z" || strings.EqualFold(s, "UTC") { 901 | return 0, true 902 | } 903 | if len(s) < 2 { 904 | return 0, false 905 | } 906 | sign := 1 907 | switch s[0] { 908 | case '+': 909 | sign = 1 910 | case '-': 911 | sign = -1 912 | default: 913 | return 0, false 914 | } 915 | rest := s[1:] 916 | rest = strings.ReplaceAll(rest, ":", "") 917 | if len(rest) != 2 && len(rest) != 4 { 918 | return 0, false 919 | } 920 | hh, err := strconv.Atoi(rest[:2]) 921 | if err != nil { 922 | return 0, false 923 | } 924 | mm := 0 925 | if len(rest) == 4 { 926 | mm, err = strconv.Atoi(rest[2:]) 927 | if err != nil { 928 | return 0, false 929 | } 930 | } 931 | if hh < 0 || hh > 23 || mm < 0 || mm > 59 { 932 | return 0, false 933 | } 934 | return sign * (hh*3600 + mm*60), true 935 | } 936 | 937 | func offsetName(offset int) string { 938 | if offset == 0 { 939 | return "UTC" 940 | } 941 | sign := "+" 942 | if offset < 0 { 943 | sign = "-" 944 | offset = -offset 945 | } 946 | hh := offset / 3600 947 | mm := (offset % 3600) / 60 948 | return fmt.Sprintf("UTC%s%02d:%02d", sign, hh, mm) 949 | } 950 | 951 | func parseMonth(s string) (time.Month, bool) { 952 | // Try name 953 | names := map[string]time.Month{ 954 | "january": time.January, "february": time.February, "march": time.March, "april": time.April, 955 | "may": time.May, "june": time.June, "july": time.July, "august": time.August, 956 | "september": time.September, "october": time.October, "november": time.November, "december": time.December, 957 | } 958 | if m, ok := names[strings.ToLower(s)]; ok { 959 | return m, true 960 | } 961 | // Try number 1-12 962 | n, err := strconv.Atoi(s) 963 | if err == nil && n >= 1 && n <= 12 { 964 | return time.Month(n), true 965 | } 966 | return 0, false 967 | } 968 | 969 | func parseWeekday(s string) (time.Weekday, bool) { 970 | names := map[string]time.Weekday{ 971 | "sunday": time.Sunday, "monday": time.Monday, "tuesday": time.Tuesday, "wednesday": time.Wednesday, 972 | "thursday": time.Thursday, "friday": time.Friday, "saturday": time.Saturday, 973 | } 974 | if d, ok := names[strings.ToLower(s)]; ok { 975 | return d, true 976 | } 977 | n, err := strconv.Atoi(s) 978 | if err == nil { 979 | // Accept 0-6 as Sunday-Saturday 980 | if n >= 0 && n <= 6 { 981 | return time.Weekday(n), true 982 | } 983 | // Also accept 1-7 as Monday-Sunday 984 | if n >= 1 && n <= 7 { 985 | v := (n % 7) // 7->0 986 | return time.Weekday(v), true 987 | } 988 | } 989 | return 0, false 990 | } 991 | --------------------------------------------------------------------------------