diff --git a/.travis.yml b/.travis.yml index 485d318..3165f00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,12 @@ install: - go build -v ./... # linting - - go get code.google.com/p/go.tools/cmd/vet + - go get golang.org/x/tools/cmd/vet - go get github.com/golang/lint - go install github.com/golang/lint/golint # code coverage - - go get code.google.com/p/go.tools/cmd/cover + - go get golang.org/x/tools/cmd/cover - go get github.com/onsi/ginkgo/ginkgo - go get github.com/modocache/gover - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then go get github.com/mattn/goveralls; fi diff --git a/README.md b/README.md index b6faef6..9378b76 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,11 @@ The flags package uses structs, reflection and struct field tags to allow users to specify command line options. This results in very simple and concise specification of your application options. For example: - type Options struct { - Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` - } +```go +type Options struct { + Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` +} +``` This specifies one option with a short name -v and a long name --verbose. When either -v or --verbose is found on the command line, a 'true' value @@ -44,88 +46,90 @@ resulting value of Verbose will be {[true, true, true]}. Example: -------- - var opts struct { - // Slice of bool will append 'true' each time the option - // is encountered (can be set multiple times, like -vvv) - Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` - - // Example of automatic marshalling to desired type (uint) - Offset uint `long:"offset" description:"Offset"` - - // Example of a callback, called each time the option is found. - Call func(string) `short:"c" description:"Call phone number"` - - // Example of a required flag - Name string `short:"n" long:"name" description:"A name" required:"true"` - - // Example of a value name - File string `short:"f" long:"file" description:"A file" value-name:"FILE"` - - // Example of a pointer - Ptr *int `short:"p" description:"A pointer to an integer"` - - // Example of a slice of strings - StringSlice []string `short:"s" description:"A slice of strings"` - - // Example of a slice of pointers - PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` - - // Example of a map - IntMap map[string]int `long:"intmap" description:"A map from string to int"` - } - - // Callback which will invoke callto: to call a number. - // Note that this works just on OS X (and probably only with - // Skype) but it shows the idea. - opts.Call = func(num string) { - cmd := exec.Command("open", "callto:"+num) - cmd.Start() - cmd.Process.Release() - } - - // Make some fake arguments to parse. - args := []string{ - "-vv", - "--offset=5", - "-n", "Me", - "-p", "3", - "-s", "hello", - "-s", "world", - "--ptrslice", "hello", - "--ptrslice", "world", - "--intmap", "a:1", - "--intmap", "b:5", - "arg1", - "arg2", - "arg3", - } - - // Parse flags from `args'. Note that here we use flags.ParseArgs for - // the sake of making a working example. Normally, you would simply use - // flags.Parse(&opts) which uses os.Args - args, err := flags.ParseArgs(&opts, args) - - if err != nil { - panic(err) - os.Exit(1) - } - - fmt.Printf("Verbosity: %v\n", opts.Verbose) - fmt.Printf("Offset: %d\n", opts.Offset) - fmt.Printf("Name: %s\n", opts.Name) - fmt.Printf("Ptr: %d\n", *opts.Ptr) - fmt.Printf("StringSlice: %v\n", opts.StringSlice) - fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) - fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) - fmt.Printf("Remaining args: %s\n", strings.Join(args, " ")) - - // Output: Verbosity: [true true] - // Offset: 5 - // Name: Me - // Ptr: 3 - // StringSlice: [hello world] - // PtrSlice: [hello world] - // IntMap: [a:1 b:5] - // Remaining args: arg1 arg2 arg3 +```go +var opts struct { + // Slice of bool will append 'true' each time the option + // is encountered (can be set multiple times, like -vvv) + Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` + + // Example of automatic marshalling to desired type (uint) + Offset uint `long:"offset" description:"Offset"` + + // Example of a callback, called each time the option is found. + Call func(string) `short:"c" description:"Call phone number"` + + // Example of a required flag + Name string `short:"n" long:"name" description:"A name" required:"true"` + + // Example of a value name + File string `short:"f" long:"file" description:"A file" value-name:"FILE"` + + // Example of a pointer + Ptr *int `short:"p" description:"A pointer to an integer"` + + // Example of a slice of strings + StringSlice []string `short:"s" description:"A slice of strings"` + + // Example of a slice of pointers + PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` + + // Example of a map + IntMap map[string]int `long:"intmap" description:"A map from string to int"` +} + +// Callback which will invoke callto: to call a number. +// Note that this works just on OS X (and probably only with +// Skype) but it shows the idea. +opts.Call = func(num string) { + cmd := exec.Command("open", "callto:"+num) + cmd.Start() + cmd.Process.Release() +} + +// Make some fake arguments to parse. +args := []string{ + "-vv", + "--offset=5", + "-n", "Me", + "-p", "3", + "-s", "hello", + "-s", "world", + "--ptrslice", "hello", + "--ptrslice", "world", + "--intmap", "a:1", + "--intmap", "b:5", + "arg1", + "arg2", + "arg3", +} + +// Parse flags from `args'. Note that here we use flags.ParseArgs for +// the sake of making a working example. Normally, you would simply use +// flags.Parse(&opts) which uses os.Args +args, err := flags.ParseArgs(&opts, args) + +if err != nil { + panic(err) + os.Exit(1) +} + +fmt.Printf("Verbosity: %v\n", opts.Verbose) +fmt.Printf("Offset: %d\n", opts.Offset) +fmt.Printf("Name: %s\n", opts.Name) +fmt.Printf("Ptr: %d\n", *opts.Ptr) +fmt.Printf("StringSlice: %v\n", opts.StringSlice) +fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) +fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) +fmt.Printf("Remaining args: %s\n", strings.Join(args, " ")) + +// Output: Verbosity: [true true] +// Offset: 5 +// Name: Me +// Ptr: 3 +// StringSlice: [hello world] +// PtrSlice: [hello world] +// IntMap: [a:1 b:5] +// Remaining args: arg1 arg2 arg3 +``` More information can be found in the godocs: diff --git a/arg.go b/arg.go index fd8db9c..d160644 100644 --- a/arg.go +++ b/arg.go @@ -12,6 +12,9 @@ type Arg struct { // A description of the positional argument (used in the help) Description string + // Whether a positional argument is required + Required int + value reflect.Value tag multiTag } diff --git a/arg_test.go b/arg_test.go index faea280..117e90e 100644 --- a/arg_test.go +++ b/arg_test.go @@ -51,3 +51,83 @@ func TestPositionalRequired(t *testing.T) { assertError(t, err, ErrRequired, "the required argument `Filename` was not provided") } + +func TestPositionalRequiredRest1Fail(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"yes"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{}) + + assertError(t, err, ErrRequired, "the required argument `Rest (at least 1 argument)` was not provided") +} + +func TestPositionalRequiredRest1Pass(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"yes"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{"rest1"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if len(opts.Positional.Rest) != 1 { + t.Fatalf("Expected 1 positional rest argument") + } + + assertString(t, opts.Positional.Rest[0], "rest1") +} + +func TestPositionalRequiredRest2Fail(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"2"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{"rest1"}) + + assertError(t, err, ErrRequired, "the required argument `Rest (at least 2 arguments, but got only 1)` was not provided") +} + +func TestPositionalRequiredRest2Pass(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Positional struct { + Rest []string `required:"2"` + } `positional-args:"yes"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{"rest1", "rest2", "rest3"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if len(opts.Positional.Rest) != 3 { + t.Fatalf("Expected 3 positional rest argument") + } + + assertString(t, opts.Positional.Rest[0], "rest1") + assertString(t, opts.Positional.Rest[1], "rest2") + assertString(t, opts.Positional.Rest[2], "rest3") +} diff --git a/command.go b/command.go index 13332ae..a30f560 100644 --- a/command.go +++ b/command.go @@ -1,5 +1,13 @@ package flags +import ( + "reflect" + "sort" + "strconv" + "strings" + "unsafe" +) + // Command represents an application command. Commands can be added to the // parser (which itself is a command) and are selected/executed when its name // is specified on the command line. The Command type embeds a Group and @@ -47,6 +55,13 @@ type Usage interface { Usage() string } +type lookup struct { + shortNames map[string]*Option + longNames map[string]*Option + + commands map[string]*Command +} + // AddCommand adds a new command to the parser with the given name and data. The // data needs to be a pointer to a struct from which the fields indicate which // options are in the command. The provided data can implement the Command and @@ -97,6 +112,32 @@ func (c *Command) Find(name string) *Command { return nil } +// Find an option that is part of the command, or any of its +// parent commands, by matching its long name +// (including the option namespace). +func (c *Command) FindOptionByLongName(longName string) (option *Option) { + for option == nil && c != nil { + option = c.Group.FindOptionByLongName(longName) + + c, _ = c.parent.(*Command) + } + + return option +} + +// Find an option that is part of the command, or any of its +// parent commands, by matching its long name +// (including the option namespace). +func (c *Command) FindOptionByShortName(shortName rune) (option *Option) { + for option == nil && c != nil { + option = c.Group.FindOptionByShortName(shortName) + + c, _ = c.parent.(*Command) + } + + return option +} + // Args returns a list of positional arguments associated with this command. func (c *Command) Args() []*Arg { ret := make([]*Arg, len(c.args)) @@ -104,3 +145,297 @@ func (c *Command) Args() []*Arg { return ret } + +func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command { + return &Command{ + Group: newGroup(shortDescription, longDescription, data), + Name: name, + } +} + +func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { + f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) { + mtag := newMultiTag(string(sfield.Tag)) + + if err := mtag.Parse(); err != nil { + return true, err + } + + positional := mtag.Get("positional-args") + + if len(positional) != 0 { + stype := realval.Type() + + for i := 0; i < stype.NumField(); i++ { + field := stype.Field(i) + + m := newMultiTag((string(field.Tag))) + + if err := m.Parse(); err != nil { + return true, err + } + + name := m.Get("positional-arg-name") + + if len(name) == 0 { + name = field.Name + } + + var required int + + sreq := m.Get("required") + + if sreq != "" { + required = 1 + + if preq, err := strconv.ParseInt(sreq, 10, 32); err == nil { + required = int(preq) + } + } + + arg := &Arg{ + Name: name, + Description: m.Get("description"), + Required: required, + + value: realval.Field(i), + tag: m, + } + + c.args = append(c.args, arg) + + if len(mtag.Get("required")) != 0 { + c.ArgsRequired = true + } + } + + return true, nil + } + + subcommand := mtag.Get("command") + + if len(subcommand) != 0 { + ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) + + shortDescription := mtag.Get("description") + longDescription := mtag.Get("long-description") + subcommandsOptional := mtag.Get("subcommands-optional") + aliases := mtag.GetMany("alias") + + subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) + if err != nil { + return true, err + } + + subc.Hidden = mtag.Get("hidden") != "" + + if len(subcommandsOptional) > 0 { + subc.SubcommandsOptional = true + } + + if len(aliases) > 0 { + subc.Aliases = aliases + } + + return true, nil + } + + return parentg.scanSubGroupHandler(realval, sfield) + } + + return f +} + +func (c *Command) scan() error { + return c.scanType(c.scanSubcommandHandler(c.Group)) +} + +func (c *Command) eachOption(f func(*Command, *Group, *Option)) { + c.eachCommand(func(c *Command) { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + f(c, g, option) + } + }) + }, true) +} + +func (c *Command) eachCommand(f func(*Command), recurse bool) { + f(c) + + for _, cc := range c.commands { + if recurse { + cc.eachCommand(f, true) + } else { + f(cc) + } + } +} + +func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) { + c.eachGroup(func(g *Group) { + f(c, g) + }) + + if c.Active != nil { + c.Active.eachActiveGroup(f) + } +} + +func (c *Command) addHelpGroups(showHelp func() error) { + if !c.hasBuiltinHelpGroup { + c.addHelpGroup(showHelp) + c.hasBuiltinHelpGroup = true + } + + for _, cc := range c.commands { + cc.addHelpGroups(showHelp) + } +} + +func (c *Command) makeLookup() lookup { + ret := lookup{ + shortNames: make(map[string]*Option), + longNames: make(map[string]*Option), + commands: make(map[string]*Command), + } + + parent := c.parent + + var parents []*Command + + for parent != nil { + if cmd, ok := parent.(*Command); ok { + parents = append(parents, cmd) + parent = cmd.parent + } else { + parent = nil + } + } + + for i := len(parents) - 1; i >= 0; i-- { + parents[i].fillLookup(&ret, true) + } + + c.fillLookup(&ret, false) + return ret +} + +func (c *Command) fillLookup(ret *lookup, onlyOptions bool) { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + if option.ShortName != 0 { + ret.shortNames[string(option.ShortName)] = option + } + + if len(option.LongName) > 0 { + ret.longNames[option.LongNameWithNamespace()] = option + } + } + }) + + if onlyOptions { + return + } + + for _, subcommand := range c.commands { + ret.commands[subcommand.Name] = subcommand + + for _, a := range subcommand.Aliases { + ret.commands[a] = subcommand + } + } +} + +func (c *Command) groupByName(name string) *Group { + if grp := c.Group.groupByName(name); grp != nil { + return grp + } + + for _, subc := range c.commands { + prefix := subc.Name + "." + + if strings.HasPrefix(name, prefix) { + if grp := subc.groupByName(name[len(prefix):]); grp != nil { + return grp + } + } else if name == subc.Name { + return subc.Group + } + } + + return nil +} + +type commandList []*Command + +func (c commandList) Less(i, j int) bool { + return c[i].Name < c[j].Name +} + +func (c commandList) Len() int { + return len(c) +} + +func (c commandList) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +func (c *Command) sortedVisibleCommands() []*Command { + ret := commandList(c.visibleCommands()) + sort.Sort(ret) + + return []*Command(ret) +} + +func (c *Command) visibleCommands() []*Command { + ret := make([]*Command, 0, len(c.commands)) + + for _, cmd := range c.commands { + if !cmd.Hidden { + ret = append(ret, cmd) + } + } + + return ret +} + +func (c *Command) match(name string) bool { + if c.Name == name { + return true + } + + for _, v := range c.Aliases { + if v == name { + return true + } + } + + return false +} + +func (c *Command) hasCliOptions() bool { + ret := false + + c.eachGroup(func(g *Group) { + if g.isBuiltinHelp { + return + } + + for _, opt := range g.options { + if opt.canCli() { + ret = true + } + } + }) + + return ret +} + +func (c *Command) fillParseState(s *parseState) { + s.positional = make([]*Arg, len(c.args)) + copy(s.positional, c.args) + + s.lookup = c.makeLookup() + s.command = c +} diff --git a/command_private.go b/command_private.go deleted file mode 100644 index 1727a30..0000000 --- a/command_private.go +++ /dev/null @@ -1,250 +0,0 @@ -package flags - -import ( - "reflect" - "sort" - "strings" - "unsafe" -) - -type lookup struct { - shortNames map[string]*Option - longNames map[string]*Option - - commands map[string]*Command -} - -func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command { - return &Command{ - Group: newGroup(shortDescription, longDescription, data), - Name: name, - } -} - -func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { - f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) { - mtag := newMultiTag(string(sfield.Tag)) - - if err := mtag.Parse(); err != nil { - return true, err - } - - positional := mtag.Get("positional-args") - - if len(positional) != 0 { - stype := realval.Type() - - for i := 0; i < stype.NumField(); i++ { - field := stype.Field(i) - - m := newMultiTag((string(field.Tag))) - - if err := m.Parse(); err != nil { - return true, err - } - - name := m.Get("name") - - if len(name) == 0 { - name = field.Name - } - - arg := &Arg{ - Name: name, - Description: m.Get("description"), - - value: realval.Field(i), - tag: m, - } - - c.args = append(c.args, arg) - - if len(mtag.Get("required")) != 0 { - c.ArgsRequired = true - } - } - - return true, nil - } - - subcommand := mtag.Get("command") - - if len(subcommand) != 0 { - ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) - - shortDescription := mtag.Get("description") - longDescription := mtag.Get("long-description") - subcommandsOptional := mtag.Get("subcommands-optional") - aliases := mtag.GetMany("alias") - - subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) - - if err != nil { - return true, err - } - - if len(subcommandsOptional) > 0 { - subc.SubcommandsOptional = true - } - - if len(aliases) > 0 { - subc.Aliases = aliases - } - - return true, nil - } - - return parentg.scanSubGroupHandler(realval, sfield) - } - - return f -} - -func (c *Command) scan() error { - return c.scanType(c.scanSubcommandHandler(c.Group)) -} - -func (c *Command) eachCommand(f func(*Command), recurse bool) { - f(c) - - for _, cc := range c.commands { - if recurse { - cc.eachCommand(f, true) - } else { - f(cc) - } - } -} - -func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) { - c.eachGroup(func(g *Group) { - f(c, g) - }) - - if c.Active != nil { - c.Active.eachActiveGroup(f) - } -} - -func (c *Command) addHelpGroups(showHelp func() error) { - if !c.hasBuiltinHelpGroup { - c.addHelpGroup(showHelp) - c.hasBuiltinHelpGroup = true - } - - for _, cc := range c.commands { - cc.addHelpGroups(showHelp) - } -} - -func (c *Command) makeLookup() lookup { - ret := lookup{ - shortNames: make(map[string]*Option), - longNames: make(map[string]*Option), - commands: make(map[string]*Command), - } - - c.eachGroup(func(g *Group) { - for _, option := range g.options { - if option.ShortName != 0 { - ret.shortNames[string(option.ShortName)] = option - } - - if len(option.LongName) > 0 { - ret.longNames[option.LongNameWithNamespace()] = option - } - } - }) - - for _, subcommand := range c.commands { - ret.commands[subcommand.Name] = subcommand - - for _, a := range subcommand.Aliases { - ret.commands[a] = subcommand - } - } - - return ret -} - -func (c *Command) groupByName(name string) *Group { - if grp := c.Group.groupByName(name); grp != nil { - return grp - } - - for _, subc := range c.commands { - prefix := subc.Name + "." - - if strings.HasPrefix(name, prefix) { - if grp := subc.groupByName(name[len(prefix):]); grp != nil { - return grp - } - } else if name == subc.Name { - return subc.Group - } - } - - return nil -} - -type commandList []*Command - -func (c commandList) Less(i, j int) bool { - return c[i].Name < c[j].Name -} - -func (c commandList) Len() int { - return len(c) -} - -func (c commandList) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} - -func (c *Command) sortedCommands() []*Command { - ret := make(commandList, len(c.commands)) - copy(ret, c.commands) - - sort.Sort(ret) - return []*Command(ret) -} - -func (c *Command) match(name string) bool { - if c.Name == name { - return true - } - - for _, v := range c.Aliases { - if v == name { - return true - } - } - - return false -} - -func (c *Command) hasCliOptions() bool { - ret := false - - c.eachGroup(func(g *Group) { - if g.isBuiltinHelp { - return - } - - for _, opt := range g.options { - if opt.canCli() { - ret = true - } - } - }) - - return ret -} - -func (c *Command) fillParseState(s *parseState) { - s.positional = make([]*Arg, len(c.args)) - copy(s.positional, c.args) - - s.lookup = c.makeLookup() - s.command = c -} diff --git a/command_test.go b/command_test.go index a093e15..e7e3089 100644 --- a/command_test.go +++ b/command_test.go @@ -95,7 +95,135 @@ func TestCommandFlagOrder2(t *testing.T) { } `command:"cmd"` }{} - assertParseFail(t, ErrUnknownFlag, "unknown flag `v'", &opts, "cmd", "-v", "-g") + assertParseSuccess(t, &opts, "cmd", "-v", "-g") + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Command.G { + t.Errorf("Expected Command.G to be true") + } +} + +func TestCommandFlagOrderSub(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + G bool `short:"g"` + + SubCommand struct { + B bool `short:"b"` + } `command:"sub"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "sub", "-v", "-g", "-b") + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Command.G { + t.Errorf("Expected Command.G to be true") + } + + if !opts.Command.SubCommand.B { + t.Errorf("Expected Command.SubCommand.B to be true") + } +} + +func TestCommandFlagOverride1(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "-v", "cmd") + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if opts.Command.Value { + t.Errorf("Expected Command.Value to be false") + } +} + +func TestCommandFlagOverride2(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "-v") + + if opts.Value { + t.Errorf("Expected Value to be false") + } + + if !opts.Command.Value { + t.Errorf("Expected Command.Value to be true") + } +} + +func TestCommandFlagOverrideSub(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + + SubCommand struct { + Value bool `short:"v"` + } `command:"sub"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "sub", "-v") + + if opts.Value { + t.Errorf("Expected Value to be false") + } + + if opts.Command.Value { + t.Errorf("Expected Command.Value to be false") + } + + if !opts.Command.SubCommand.Value { + t.Errorf("Expected Command.Value to be true") + } +} + +func TestCommandFlagOverrideSub2(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + Value bool `short:"v"` + + SubCommand struct { + G bool `short:"g"` + } `command:"sub"` + } `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "cmd", "sub", "-v") + + if opts.Value { + t.Errorf("Expected Value to be false") + } + + if !opts.Command.Value { + t.Errorf("Expected Command.Value to be true") + } } func TestCommandEstimate(t *testing.T) { @@ -302,14 +430,14 @@ func TestRequiredAllOnCommand(t *testing.T) { func TestDefaultOnCommand(t *testing.T) { var opts = struct { Command struct { - G bool `short:"g" default:"true"` + G string `short:"g" default:"value"` } `command:"cmd"` }{} assertParseSuccess(t, &opts, "cmd") - if !opts.Command.G { - t.Errorf("Expected G to be true") + if opts.Command.G != "value" { + t.Errorf("Expected G to be \"value\"") } } @@ -342,13 +470,75 @@ func TestSubcommandsOptional(t *testing.T) { func TestCommandAlias(t *testing.T) { var opts = struct { Command struct { - G bool `short:"g" default:"true"` + G string `short:"g" default:"value"` } `command:"cmd" alias:"cm"` }{} assertParseSuccess(t, &opts, "cm") - if !opts.Command.G { - t.Errorf("Expected G to be true") + if opts.Command.G != "value" { + t.Errorf("Expected G to be \"value\"") + } +} + +func TestSubCommandFindOptionByLongFlag(t *testing.T) { + var opts struct { + Testing bool `long:"testing" description:"Testing"` + } + + var cmd struct { + Other bool `long:"other" description:"Other"` + } + + p := NewParser(&opts, Default) + c, _ := p.AddCommand("command", "Short", "Long", &cmd) + + opt := c.FindOptionByLongName("other") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "other") + + opt = c.FindOptionByLongName("testing") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "testing") +} + +func TestSubCommandFindOptionByShortFlag(t *testing.T) { + var opts struct { + Testing bool `short:"t" description:"Testing"` + } + + var cmd struct { + Other bool `short:"o" description:"Other"` + } + + p := NewParser(&opts, Default) + c, _ := p.AddCommand("command", "Short", "Long", &cmd) + + opt := c.FindOptionByShortName('o') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 'o' { + t.Errorf("Expected 'o', but got %v", opt.ShortName) + } + + opt = c.FindOptionByShortName('t') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 't' { + t.Errorf("Expected 'o', but got %v", opt.ShortName) } } diff --git a/completion.go b/completion.go index d0adfe0..894f1d6 100644 --- a/completion.go +++ b/completion.go @@ -43,8 +43,6 @@ type Completer interface { type completion struct { parser *Parser - - ShowDescriptions bool } // Filename is a string alias which provides filename completion. @@ -275,19 +273,17 @@ func (c *completion) complete(args []string) []Completion { return ret } -func (c *completion) execute(args []string) { - ret := c.complete(args) - - if c.ShowDescriptions && len(ret) > 1 { +func (c *completion) print(items []Completion, showDescriptions bool) { + if showDescriptions && len(items) > 1 { maxl := 0 - for _, v := range ret { + for _, v := range items { if len(v.Item) > maxl { maxl = len(v.Item) } } - for _, v := range ret { + for _, v := range items { fmt.Printf("%s", v.Item) if len(v.Description) > 0 { @@ -297,7 +293,7 @@ func (c *completion) execute(args []string) { fmt.Printf("\n") } } else { - for _, v := range ret { + for _, v := range items { fmt.Println(v.Item) } } diff --git a/completion_test.go b/completion_test.go index 2d5a97f..f440fd7 100644 --- a/completion_test.go +++ b/completion_test.go @@ -268,6 +268,11 @@ func TestParserCompletion(t *testing.T) { p := NewParser(&completionTestOptions, None) + p.CompletionHandler = func(items []Completion) { + comp := &completion{parser: p} + comp.print(items, test.ShowDescriptions) + } + _, err := p.ParseArgs(test.Args) w.Close() diff --git a/convert.go b/convert.go index 191b5f4..938c3ac 100644 --- a/convert.go +++ b/convert.go @@ -312,46 +312,30 @@ func quoteIfNeeded(s string) string { return s } -func unquoteIfPossible(s string) (string, error) { - if len(s) == 0 || s[0] != '"' { - return s, nil +func quoteIfNeededV(s []string) []string { + ret := make([]string, len(s)) + + for i, v := range s { + ret[i] = quoteIfNeeded(v) } - return strconv.Unquote(s) + return ret } -func wrapText(s string, l int, prefix string) string { - // Basic text wrapping of s at spaces to fit in l - var ret string - - s = strings.TrimSpace(s) - - for len(s) > l { - // Try to split on space - suffix := "" - - pos := strings.LastIndex(s[:l], " ") - - if pos < 0 { - pos = l - 1 - suffix = "-\n" - } +func quoteV(s []string) []string { + ret := make([]string, len(s)) - if len(ret) != 0 { - ret += "\n" + prefix - } - - ret += strings.TrimSpace(s[:pos]) + suffix - s = strings.TrimSpace(s[pos:]) + for i, v := range s { + ret[i] = strconv.Quote(v) } - if len(s) > 0 { - if len(ret) != 0 { - ret += "\n" + prefix - } + return ret +} - return ret + s +func unquoteIfPossible(s string) (string, error) { + if len(s) == 0 || s[0] != '"' { + return s, nil } - return ret + return strconv.Unquote(s) } diff --git a/convert_test.go b/convert_test.go index 0de0eea..ef131dc 100644 --- a/convert_test.go +++ b/convert_test.go @@ -157,19 +157,3 @@ func TestConvertToStringInvalidUintBase(t *testing.T) { assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax") } - -func TestWrapText(t *testing.T) { - s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - - got := wrapText(s, 60, " ") - expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit, - sed do eiusmod tempor incididunt ut labore et dolore magna - aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. - Duis aute irure dolor in reprehenderit in voluptate velit - esse cillum dolore eu fugiat nulla pariatur. Excepteur sint - occaecat cupidatat non proident, sunt in culpa qui officia - deserunt mollit anim id est laborum.` - - assertDiff(t, got, expected, "wrapped text") -} diff --git a/error.go b/error.go index fce9d31..05528d8 100644 --- a/error.go +++ b/error.go @@ -51,6 +51,13 @@ const ( // ErrUnknownCommand indicates that an unknown command was specified. ErrUnknownCommand + + // ErrInvalidChoice indicates an invalid option value which only allows + // a certain number of choices. + ErrInvalidChoice + + // ErrInvalidTag indicates an invalid tag or invalid use of an existing tag + ErrInvalidTag ) func (e ErrorType) String() string { @@ -81,6 +88,10 @@ func (e ErrorType) String() string { return "command required" case ErrUnknownCommand: return "unknown command" + case ErrInvalidChoice: + return "invalid choice" + case ErrInvalidTag: + return "invalid tag" } return "unrecognized error type" diff --git a/flags.go b/flags.go index e3e72a3..757d42a 100644 --- a/flags.go +++ b/flags.go @@ -84,7 +84,9 @@ The following is a list of tags for struct fields supported by go-flags: displayed in generated man pages (optional) no-flag: if non-empty this field is ignored as an option (optional) - optional: whether an argument of the option is optional (optional) + optional: whether an argument of the option is optional. When an + argument is optional it can only be specified using + --option=argument (optional) optional-value: the value of an optional option when the option occurs without an argument. This tag can be specified multiple times in the case of maps or slices (optional) @@ -102,8 +104,11 @@ The following is a list of tags for struct fields supported by go-flags: env-delim: the 'env' default value from environment is split into multiple values with the given delimiter string, use with slices and maps (optional) - value-name: the name of the argument value (to be shown in the help, + value-name: the name of the argument value (to be shown in the help) (optional) + choice: limits the values for an option to a set of values. + This tag can be specified mltiple times (optional) + hidden: the option is not visible in the help or man page. base: a base (radix) used to convert strings to integer values, the default base is 10 (i.e. decimal) (optional) @@ -133,7 +138,16 @@ The following is a list of tags for struct fields supported by go-flags: then all remaining arguments will be added to it. Positional arguments are optional by default, unless the "required" tag is specified together - with the "positional-args" tag (optional) + with the "positional-args" tag. The "required" tag + can also be set on the individual rest argument + fields, to require only the first N positional + arguments. If the "required" tag is set on the + rest arguments slice, then its value determines + the minimum amount of rest arguments that needs to + be provided (e.g. `required:"2"`) (optional) + positional-arg-name: used on a field in a positional argument struct; name + of the positional argument placeholder to be shown in + the help (optional) Either the `short:` tag or the `long:` must be specified to make the field eligible as an option. @@ -179,12 +193,16 @@ the Commander interface, then its Execute method will be run with the remaining command line arguments. Command structs can have options which become valid to parse after the -command has been specified on the command line. It is currently not valid -to specify options from the parent level of the command after the command -name has occurred. Thus, given a top-level option "-v" and a command "add": +command has been specified on the command line, in addition to the options +of all the parent commands. I.e. considering a -v flag on the parser and an +add command, the following are equivalent: - Valid: ./app -v add - Invalid: ./app add -v + ./app -v add + ./app add -v + +However, if the -v flag is defined on the add command, then the first of +the two examples above would fail since the -v flag is not defined before +the add command. Completion diff --git a/group.go b/group.go index 8b609a3..6472420 100644 --- a/group.go +++ b/group.go @@ -6,7 +6,10 @@ package flags import ( "errors" + "reflect" "strings" + "unicode/utf8" + "unsafe" ) // ErrNotPointerToStruct indicates that a provided data container is not @@ -32,6 +35,9 @@ type Group struct { // The namespace of the group Namespace string + // If true, the group is not displayed in the help or man page + Hidden bool + // The parent of the group or nil if it has no parent parent interface{} @@ -47,6 +53,8 @@ type Group struct { data interface{} } +type scanHandler func(reflect.Value, *reflect.StructField) (bool, error) + // AddGroup adds a new group to the command with the given name and data. The // data needs to be a pointer to a struct from which the fields indicate which // options are in the group. @@ -89,3 +97,289 @@ func (g *Group) Find(shortDescription string) *Group { return ret } + +func (g *Group) findOption(matcher func(*Option) bool) (option *Option) { + g.eachGroup(func(g *Group) { + for _, opt := range g.options { + if option == nil && matcher(opt) { + option = opt + } + } + }) + + return option +} + +// Find an option that is part of the group, or any of its subgroups, +// by matching its long name (including the option namespace). +func (g *Group) FindOptionByLongName(longName string) *Option { + return g.findOption(func(option *Option) bool { + return option.LongNameWithNamespace() == longName + }) +} + +// Find an option that is part of the group, or any of its subgroups, +// by matching its short name. +func (g *Group) FindOptionByShortName(shortName rune) *Option { + return g.findOption(func(option *Option) bool { + return option.ShortName == shortName + }) +} + +func newGroup(shortDescription string, longDescription string, data interface{}) *Group { + return &Group{ + ShortDescription: shortDescription, + LongDescription: longDescription, + + data: data, + } +} + +func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option { + prio := 0 + var retopt *Option + + g.eachGroup(func(g *Group) { + for _, opt := range g.options { + if namematch != nil && namematch(opt, name) && prio < 4 { + retopt = opt + prio = 4 + } + + if name == opt.field.Name && prio < 3 { + retopt = opt + prio = 3 + } + + if name == opt.LongNameWithNamespace() && prio < 2 { + retopt = opt + prio = 2 + } + + if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { + retopt = opt + prio = 1 + } + } + }) + + return retopt +} + +func (g *Group) eachGroup(f func(*Group)) { + f(g) + + for _, gg := range g.groups { + gg.eachGroup(f) + } +} + +func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error { + stype := realval.Type() + + if sfield != nil { + if ok, err := handler(realval, sfield); err != nil { + return err + } else if ok { + return nil + } + } + + for i := 0; i < stype.NumField(); i++ { + field := stype.Field(i) + + // PkgName is set only for non-exported fields, which we ignore + if field.PkgPath != "" && !field.Anonymous { + continue + } + + mtag := newMultiTag(string(field.Tag)) + + if err := mtag.Parse(); err != nil { + return err + } + + // Skip fields with the no-flag tag + if mtag.Get("no-flag") != "" { + continue + } + + // Dive deep into structs or pointers to structs + kind := field.Type.Kind() + fld := realval.Field(i) + + if kind == reflect.Struct { + if err := g.scanStruct(fld, &field, handler); err != nil { + return err + } + } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { + if fld.IsNil() { + fld.Set(reflect.New(fld.Type().Elem())) + } + + if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil { + return err + } + } + + longname := mtag.Get("long") + shortname := mtag.Get("short") + + // Need at least either a short or long name + if longname == "" && shortname == "" && mtag.Get("ini-name") == "" { + continue + } + + short := rune(0) + rc := utf8.RuneCountInString(shortname) + + if rc > 1 { + return newErrorf(ErrShortNameTooLong, + "short names can only be 1 character long, not `%s'", + shortname) + + } else if rc == 1 { + short, _ = utf8.DecodeRuneInString(shortname) + } + + description := mtag.Get("description") + def := mtag.GetMany("default") + + optionalValue := mtag.GetMany("optional-value") + valueName := mtag.Get("value-name") + defaultMask := mtag.Get("default-mask") + + optional := (mtag.Get("optional") != "") + required := (mtag.Get("required") != "") + choices := mtag.GetMany("choice") + hidden := (mtag.Get("hidden") != "") + + option := &Option{ + Description: description, + ShortName: short, + LongName: longname, + Default: def, + EnvDefaultKey: mtag.Get("env"), + EnvDefaultDelim: mtag.Get("env-delim"), + OptionalArgument: optional, + OptionalValue: optionalValue, + Required: required, + ValueName: valueName, + DefaultMask: defaultMask, + Choices: choices, + Hidden: hidden, + + group: g, + + field: field, + value: realval.Field(i), + tag: mtag, + } + + if option.isBool() && option.Default != nil { + return newErrorf(ErrInvalidTag, + "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on", + option.shortAndLongName()) + } + + g.options = append(g.options, option) + } + + return nil +} + +func (g *Group) checkForDuplicateFlags() *Error { + shortNames := make(map[rune]*Option) + longNames := make(map[string]*Option) + + var duplicateError *Error + + g.eachGroup(func(g *Group) { + for _, option := range g.options { + if option.LongName != "" { + longName := option.LongNameWithNamespace() + + if otherOption, ok := longNames[longName]; ok { + duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) + return + } + longNames[longName] = option + } + if option.ShortName != 0 { + if otherOption, ok := shortNames[option.ShortName]; ok { + duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption) + return + } + shortNames[option.ShortName] = option + } + } + }) + + return duplicateError +} + +func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) { + mtag := newMultiTag(string(sfield.Tag)) + + if err := mtag.Parse(); err != nil { + return true, err + } + + subgroup := mtag.Get("group") + + if len(subgroup) != 0 { + ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) + description := mtag.Get("description") + + group, err := g.AddGroup(subgroup, description, ptrval.Interface()) + if err != nil { + return true, err + } + + group.Namespace = mtag.Get("namespace") + group.Hidden = mtag.Get("hidden") != "" + + return true, nil + } + + return false, nil +} + +func (g *Group) scanType(handler scanHandler) error { + // Get all the public fields in the data struct + ptrval := reflect.ValueOf(g.data) + + if ptrval.Type().Kind() != reflect.Ptr { + panic(ErrNotPointerToStruct) + } + + stype := ptrval.Type().Elem() + + if stype.Kind() != reflect.Struct { + panic(ErrNotPointerToStruct) + } + + realval := reflect.Indirect(ptrval) + + if err := g.scanStruct(realval, nil, handler); err != nil { + return err + } + + if err := g.checkForDuplicateFlags(); err != nil { + return err + } + + return nil +} + +func (g *Group) scan() error { + return g.scanType(g.scanSubGroupHandler) +} + +func (g *Group) groupByName(name string) *Group { + if len(name) == 0 { + return g + } + + return g.Find(name) +} diff --git a/group_private.go b/group_private.go deleted file mode 100644 index 15251ce..0000000 --- a/group_private.go +++ /dev/null @@ -1,254 +0,0 @@ -package flags - -import ( - "reflect" - "unicode/utf8" - "unsafe" -) - -type scanHandler func(reflect.Value, *reflect.StructField) (bool, error) - -func newGroup(shortDescription string, longDescription string, data interface{}) *Group { - return &Group{ - ShortDescription: shortDescription, - LongDescription: longDescription, - - data: data, - } -} - -func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option { - prio := 0 - var retopt *Option - - for _, opt := range g.options { - if namematch != nil && namematch(opt, name) && prio < 4 { - retopt = opt - prio = 4 - } - - if name == opt.field.Name && prio < 3 { - retopt = opt - prio = 3 - } - - if name == opt.LongNameWithNamespace() && prio < 2 { - retopt = opt - prio = 2 - } - - if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { - retopt = opt - prio = 1 - } - } - - return retopt -} - -func (g *Group) eachGroup(f func(*Group)) { - f(g) - - for _, gg := range g.groups { - gg.eachGroup(f) - } -} - -func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error { - stype := realval.Type() - - if sfield != nil { - if ok, err := handler(realval, sfield); err != nil { - return err - } else if ok { - return nil - } - } - - for i := 0; i < stype.NumField(); i++ { - field := stype.Field(i) - - // PkgName is set only for non-exported fields, which we ignore - if field.PkgPath != "" { - continue - } - - mtag := newMultiTag(string(field.Tag)) - - if err := mtag.Parse(); err != nil { - return err - } - - // Skip fields with the no-flag tag - if mtag.Get("no-flag") != "" { - continue - } - - // Dive deep into structs or pointers to structs - kind := field.Type.Kind() - fld := realval.Field(i) - - if kind == reflect.Struct { - if err := g.scanStruct(fld, &field, handler); err != nil { - return err - } - } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { - if fld.IsNil() { - fld.Set(reflect.New(fld.Type().Elem())) - } - - if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil { - return err - } - } - - longname := mtag.Get("long") - shortname := mtag.Get("short") - - // Need at least either a short or long name - if longname == "" && shortname == "" && mtag.Get("ini-name") == "" { - continue - } - - short := rune(0) - rc := utf8.RuneCountInString(shortname) - - if rc > 1 { - return newErrorf(ErrShortNameTooLong, - "short names can only be 1 character long, not `%s'", - shortname) - - } else if rc == 1 { - short, _ = utf8.DecodeRuneInString(shortname) - } - - description := mtag.Get("description") - def := mtag.GetMany("default") - - optionalValue := mtag.GetMany("optional-value") - valueName := mtag.Get("value-name") - defaultMask := mtag.Get("default-mask") - - optional := (mtag.Get("optional") != "") - required := (mtag.Get("required") != "") - - option := &Option{ - Description: description, - ShortName: short, - LongName: longname, - Default: def, - EnvDefaultKey: mtag.Get("env"), - EnvDefaultDelim: mtag.Get("env-delim"), - OptionalArgument: optional, - OptionalValue: optionalValue, - Required: required, - ValueName: valueName, - DefaultMask: defaultMask, - - group: g, - - field: field, - value: realval.Field(i), - tag: mtag, - } - - g.options = append(g.options, option) - } - - return nil -} - -func (g *Group) checkForDuplicateFlags() *Error { - shortNames := make(map[rune]*Option) - longNames := make(map[string]*Option) - - var duplicateError *Error - - g.eachGroup(func(g *Group) { - for _, option := range g.options { - if option.LongName != "" { - longName := option.LongNameWithNamespace() - - if otherOption, ok := longNames[longName]; ok { - duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) - return - } - longNames[longName] = option - } - if option.ShortName != 0 { - if otherOption, ok := shortNames[option.ShortName]; ok { - duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption) - return - } - shortNames[option.ShortName] = option - } - } - }) - - return duplicateError -} - -func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) { - mtag := newMultiTag(string(sfield.Tag)) - - if err := mtag.Parse(); err != nil { - return true, err - } - - subgroup := mtag.Get("group") - - if len(subgroup) != 0 { - ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr())) - description := mtag.Get("description") - - group, err := g.AddGroup(subgroup, description, ptrval.Interface()) - if err != nil { - return true, err - } - - group.Namespace = mtag.Get("namespace") - - return true, nil - } - - return false, nil -} - -func (g *Group) scanType(handler scanHandler) error { - // Get all the public fields in the data struct - ptrval := reflect.ValueOf(g.data) - - if ptrval.Type().Kind() != reflect.Ptr { - panic(ErrNotPointerToStruct) - } - - stype := ptrval.Type().Elem() - - if stype.Kind() != reflect.Struct { - panic(ErrNotPointerToStruct) - } - - realval := reflect.Indirect(ptrval) - - if err := g.scanStruct(realval, nil, handler); err != nil { - return err - } - - if err := g.checkForDuplicateFlags(); err != nil { - return err - } - - return nil -} - -func (g *Group) scan() error { - return g.scanType(g.scanSubGroupHandler) -} - -func (g *Group) groupByName(name string) *Group { - if len(name) == 0 { - return g - } - - return g.Find(name) -} diff --git a/group_test.go b/group_test.go index b5ed9d4..18cd6c1 100644 --- a/group_test.go +++ b/group_test.go @@ -185,3 +185,71 @@ func TestDuplicateLongFlags(t *testing.T) { } } } + +func TestFindOptionByLongFlag(t *testing.T) { + var opts struct { + Testing bool `long:"testing" description:"Testing"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByLongName("testing") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "testing") +} + +func TestFindOptionByShortFlag(t *testing.T) { + var opts struct { + Testing bool `short:"t" description:"Testing"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByShortName('t') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 't' { + t.Errorf("Expected 't', but got %v", opt.ShortName) + } +} + +func TestFindOptionByLongFlagInSubGroup(t *testing.T) { + var opts struct { + Group struct { + Testing bool `long:"testing" description:"Testing"` + } `group:"sub-group"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByLongName("testing") + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + assertString(t, opt.LongName, "testing") +} + +func TestFindOptionByShortFlagInSubGroup(t *testing.T) { + var opts struct { + Group struct { + Testing bool `short:"t" description:"Testing"` + } `group:"sub-group"` + } + + p := NewParser(&opts, Default) + opt := p.FindOptionByShortName('t') + + if opt == nil { + t.Errorf("Expected option, but found none") + } + + if opt.ShortName != 't' { + t.Errorf("Expected 't', but got %v", opt.ShortName) + } +} diff --git a/help.go b/help.go index e26fcd0..aac78de 100644 --- a/help.go +++ b/help.go @@ -9,7 +9,6 @@ import ( "bytes" "fmt" "io" - "reflect" "runtime" "strings" "unicode/utf8" @@ -92,13 +91,71 @@ func (p *Parser) getAlignmentInfo() alignmentInfo { ret.hasValueName = true } - ret.updateLen(info.LongNameWithNamespace()+info.ValueName, c != p.Command) + l := info.LongNameWithNamespace() + info.ValueName + + if len(info.Choices) != 0 { + l += "[" + strings.Join(info.Choices, "|") + "]" + } + + ret.updateLen(l, c != p.Command) } }) return ret } +func wrapText(s string, l int, prefix string) string { + var ret string + + // Basic text wrapping of s at spaces to fit in l + lines := strings.Split(s, "\n") + + for _, line := range lines { + var retline string + + line = strings.TrimSpace(line) + + for len(line) > l { + // Try to split on space + suffix := "" + + pos := strings.LastIndex(line[:l], " ") + + if pos < 0 { + pos = l - 1 + suffix = "-\n" + } + + if len(retline) != 0 { + retline += "\n" + prefix + } + + retline += strings.TrimSpace(line[:pos]) + suffix + line = strings.TrimSpace(line[pos:]) + } + + if len(line) > 0 { + if len(retline) != 0 { + retline += "\n" + prefix + } + + retline += line + } + + if len(ret) > 0 { + ret += "\n" + + if len(retline) > 0 { + ret += prefix + } + } + + ret += retline + } + + return ret +} + func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) { line := &bytes.Buffer{} @@ -108,6 +165,10 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig prefix += 4 } + if option.Hidden { + return + } + line.WriteString(strings.Repeat(" ", prefix)) if option.ShortName != 0 { @@ -136,6 +197,10 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig if len(option.ValueName) > 0 { line.WriteString(option.ValueName) } + + if len(option.Choices) > 0 { + line.WriteString("[" + strings.Join(option.Choices, "|") + "]") + } } written := line.Len() @@ -145,39 +210,12 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig dw := descstart - written writer.WriteString(strings.Repeat(" ", dw)) - def := "" - defs := option.Default + var def string - if len(option.DefaultMask) != 0 { - if option.DefaultMask != "-" { - def = option.DefaultMask - } - } else if len(defs) == 0 && option.canArgument() { - var showdef bool - - switch option.field.Type.Kind() { - case reflect.Func, reflect.Ptr: - showdef = !option.value.IsNil() - case reflect.Slice, reflect.String, reflect.Array: - showdef = option.value.Len() > 0 - case reflect.Map: - showdef = !option.value.IsNil() && option.value.Len() > 0 - default: - zeroval := reflect.Zero(option.field.Type) - showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) - } - - if showdef { - def, _ = convertToString(option.value, option.tag) - } - } else if len(defs) != 0 { - l := len(defs) - 1 - - for i := 0; i < l; i++ { - def += quoteIfNeeded(defs[i]) + ", " - } - - def += quoteIfNeeded(defs[l]) + if len(option.DefaultMask) != 0 && option.DefaultMask != "-" { + def = option.DefaultMask + } else { + def = option.defaultLiteral } var envDef string @@ -194,7 +232,7 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig var desc string if def != "" { - desc = fmt.Sprintf("%s (%v)%s", option.Description, def, envDef) + desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef) } else { desc = option.Description + envDef } @@ -302,10 +340,12 @@ func (p *Parser) WriteHelp(writer io.Writer) { co, cc = "<", ">" } - if len(allcmd.commands) > 3 { + visibleCommands := allcmd.visibleCommands() + + if len(visibleCommands) > 3 { fmt.Fprintf(wr, " %scommand%s", co, cc) } else { - subcommands := allcmd.sortedCommands() + subcommands := allcmd.sortedVisibleCommands() names := make([]string, len(subcommands)) for i, subc := range subcommands { @@ -342,12 +382,12 @@ func (p *Parser) WriteHelp(writer io.Writer) { // Skip built-in help group for all commands except the top-level // parser - if grp.isBuiltinHelp && c != p.Command { + if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) { return } for _, info := range grp.options { - if !info.canCli() { + if !info.canCli() || info.Hidden { continue } @@ -372,7 +412,14 @@ func (p *Parser) WriteHelp(writer io.Writer) { } }) - if len(c.args) > 0 { + var args []*Arg + for _, arg := range c.args { + if arg.Description != "" { + args = append(args, arg) + } + } + + if len(args) > 0 { if c == p.Command { fmt.Fprintf(wr, "\nArguments:\n") } else { @@ -381,7 +428,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { maxlen := aligninfo.descriptionStart() - for _, arg := range c.args { + for _, arg := range args { prefix := strings.Repeat(" ", paddingBeforeOption) fmt.Fprintf(wr, "%s%s", prefix, arg.Name) @@ -397,7 +444,7 @@ func (p *Parser) WriteHelp(writer io.Writer) { c = c.Active } - scommands := cmd.sortedCommands() + scommands := cmd.sortedVisibleCommands() if len(scommands) > 0 { maxnamelen := maxCommandLength(scommands) diff --git a/help_test.go b/help_test.go index 32220fb..1dcbe7f 100644 --- a/help_test.go +++ b/help_test.go @@ -15,11 +15,14 @@ type helpOptions struct { PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` EmptyDescription bool `long:"empty-description"` - Default string `long:"default" default:"Some\nvalue" description:"Test default value"` - DefaultArray []string `long:"default-array" default:"Some value" default:"Other\tvalue" description:"Test default array value"` - DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"` - EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"` - EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"` + Default string `long:"default" default:"Some\nvalue" description:"Test default value"` + DefaultArray []string `long:"default-array" default:"Some value" default:"Other\tvalue" description:"Test default array value"` + DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"` + EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"` + EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"` + OptionWithArgName string `long:"opt-with-arg-name" value-name:"something" description:"Option with named argument"` + OptionWithChoices string `long:"opt-with-choices" value-name:"choice" choice:"dog" choice:"cat" description:"Option with choices"` + Hidden string `long:"hidden" description:"Hidden option" hidden:"yes"` OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"` @@ -28,8 +31,13 @@ type helpOptions struct { IntMap map[string]int `long:"intmap" default:"a:1" description:"A map from string to int" ini-name:"int-map"` } `group:"Other Options"` + HiddenGroup struct { + InsideHiddenGroup string `long:"inside-hidden-group" description:"Inside hidden group"` + } `group:"Hidden group" hidden:"yes"` + Group struct { - Opt string `long:"opt" description:"This is a subgroup option"` + Opt string `long:"opt" description:"This is a subgroup option"` + HiddenInsideGroup string `long:"hidden-inside-group" description:"Hidden inside group" hidden:"yes"` Group struct { Opt string `long:"opt" description:"This is a subsubgroup option"` @@ -40,9 +48,14 @@ type helpOptions struct { ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"` } `command:"command" alias:"cm" alias:"cmd" description:"A command"` + HiddenCommand struct { + ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"` + } `command:"hidden-command" description:"A hidden command" hidden:"yes"` + Args struct { - Filename string `name:"filename" description:"A filename"` - Num int `name:"num" description:"A number"` + Filename string `positional-arg-name:"filename" description:"A filename"` + Number int `positional-arg-name:"num" description:"A number"` + HiddenInHelp float32 `positional-arg-name:"hidden-in-help" required:"yes"` } `positional-args:"yes"` } @@ -72,71 +85,90 @@ func TestHelp(t *testing.T) { if runtime.GOOS == "windows" { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] + Application Options: - /v, /verbose Show verbose debug information - /c: Call phone number - /ptrslice: A slice of pointers to string + /v, /verbose Show verbose debug information + /c: Call phone number + /ptrslice: A slice of pointers to string /empty-description - /default: Test default value ("Some\nvalue") - /default-array: Test default array value (Some value, "Other\tvalue") - /default-map: Testdefault map value (some:value, another:value) - /env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%] - /env-default2: Test env-default2 value [%ENV_DEFAULT%] + /default: Test default value (default: + "Some\nvalue") + /default-array: Test default array value (default: + Some value, "Other\tvalue") + /default-map: Testdefault map value (default: + some:value, another:value) + /env-default1: Test env-default1 value (default: + Some value) [%ENV_DEFAULT%] + /env-default2: Test env-default2 value + [%ENV_DEFAULT%] + /opt-with-arg-name:something Option with named argument + /opt-with-choices:choice[dog|cat] Option with choices Other Options: - /s: A slice of strings (some, value) - /intmap: A map from string to int (a:1) + /s: A slice of strings (default: some, + value) + /intmap: A map from string to int (default: + a:1) Subgroup: - /sip.opt: This is a subgroup option + /sip.opt: This is a subgroup option Subsubgroup: - /sip.sap.opt: This is a subsubgroup option + /sip.sap.opt: This is a subsubgroup option Help Options: - /? Show this help message - /h, /help Show this help message + /? Show this help message + /h, /help Show this help message Arguments: - filename: A filename - num: A number + filename: A filename + num: A number Available commands: command A command (aliases: cm, cmd) ` } else { expected = `Usage: - TestHelp [OPTIONS] [filename] [num] + TestHelp [OPTIONS] [filename] [num] [hidden-in-help] Application Options: - -v, --verbose Show verbose debug information - -c= Call phone number - --ptrslice= A slice of pointers to string + -v, --verbose Show verbose debug information + -c= Call phone number + --ptrslice= A slice of pointers to string --empty-description - --default= Test default value ("Some\nvalue") - --default-array= Test default array value (Some value, "Other\tvalue") - --default-map= Testdefault map value (some:value, another:value) - --env-default1= Test env-default1 value (Some value) [$ENV_DEFAULT] - --env-default2= Test env-default2 value [$ENV_DEFAULT] + --default= Test default value (default: + "Some\nvalue") + --default-array= Test default array value (default: + Some value, "Other\tvalue") + --default-map= Testdefault map value (default: + some:value, another:value) + --env-default1= Test env-default1 value (default: + Some value) [$ENV_DEFAULT] + --env-default2= Test env-default2 value + [$ENV_DEFAULT] + --opt-with-arg-name=something Option with named argument + --opt-with-choices=choice[dog|cat] Option with choices Other Options: - -s= A slice of strings (some, value) - --intmap= A map from string to int (a:1) + -s= A slice of strings (default: some, + value) + --intmap= A map from string to int (default: + a:1) Subgroup: - --sip.opt= This is a subgroup option + --sip.opt= This is a subgroup option Subsubgroup: - --sip.sap.opt= This is a subsubgroup option + --sip.sap.opt= This is a subsubgroup option Help Options: - -h, --help Show this help message + -h, --help Show this help message Arguments: - filename: A filename - num: A number + filename: A filename + num: A number Available commands: command A command (aliases: cm, cmd) @@ -167,6 +199,14 @@ func TestMan(t *testing.T) { tt := time.Now() + var envDefaultName string + + if runtime.GOOS == "windows" { + envDefaultName = "%ENV_DEFAULT%" + } else { + envDefaultName = "$ENV_DEFAULT" + } + expected := fmt.Sprintf(`.TH TestMan 1 "%s" .SH NAME TestMan \- Test manpage generation @@ -176,42 +216,48 @@ TestMan \- Test manpage generation This is a somewhat \fBlonger\fP description of what this does .SH OPTIONS .TP -\fB-v, --verbose\fP +\fB\fB\-v\fR, \fB\-\-verbose\fR\fP Show verbose debug information .TP -\fB-c\fP +\fB\fB\-c\fR\fP Call phone number .TP -\fB--ptrslice\fP +\fB\fB\-\-ptrslice\fR\fP A slice of pointers to string .TP -\fB--empty-description\fP +\fB\fB\-\-empty-description\fR\fP .TP -\fB--default\fP +\fB\fB\-\-default\fR \fP Test default value .TP -\fB--default-array\fP +\fB\fB\-\-default-array\fR \fP Test default array value .TP -\fB--default-map\fP +\fB\fB\-\-default-map\fR \fP Testdefault map value .TP -\fB--env-default1\fP +\fB\fB\-\-env-default1\fR \fP Test env-default1 value .TP -\fB--env-default2\fP +\fB\fB\-\-env-default2\fR \fP Test env-default2 value .TP -\fB-s\fP +\fB\fB\-\-opt-with-arg-name\fR \fIsomething\fR\fP +Option with named argument +.TP +\fB\fB\-\-opt-with-choices\fR \fIchoice\fR\fP +Option with choices +.TP +\fB\fB\-s\fR \fP A slice of strings .TP -\fB--intmap\fP +\fB\fB\-\-intmap\fR \fP A map from string to int .TP -\fB--sip.opt\fP +\fB\fB\-\-sip.opt\fR\fP This is a subgroup option .TP -\fB--sip.sap.opt\fP +\fB\fB\-\-sip.sap.opt\fR\fP This is a subsubgroup option .SH COMMANDS .SS command @@ -220,14 +266,14 @@ A command Longer \fBcommand\fP description \fBUsage\fP: TestMan [OPTIONS] command [command-OPTIONS] - +.TP \fBAliases\fP: cm, cmd .TP -\fB--extra-verbose\fP +\fB\fB\-\-extra-verbose\fR\fP Use for extra verbosity -`, tt.Format("2 January 2006")) +`, tt.Format("2 January 2006"), envDefaultName) assertDiff(t, got, expected, "man page") } @@ -281,3 +327,136 @@ Help Options: assertDiff(t, e.Message, expected, "help message") } } + +func TestHelpDefaults(t *testing.T) { + var expected string + + if runtime.GOOS == "windows" { + expected = `Usage: + TestHelpDefaults [OPTIONS] + +Application Options: + /with-default: With default (default: default-value) + /without-default: Without default + /with-programmatic-default: With programmatic default (default: + default-value) + +Help Options: + /? Show this help message + /h, /help Show this help message +` + } else { + expected = `Usage: + TestHelpDefaults [OPTIONS] + +Application Options: + --with-default= With default (default: default-value) + --without-default= Without default + --with-programmatic-default= With programmatic default (default: + default-value) + +Help Options: + -h, --help Show this help message +` + } + + tests := []struct { + Args []string + Output string + }{ + { + Args: []string{"-h"}, + Output: expected, + }, + { + Args: []string{"--with-default", "other-value", "--with-programmatic-default", "other-value", "-h"}, + Output: expected, + }, + } + + for _, test := range tests { + var opts struct { + WithDefault string `long:"with-default" default:"default-value" description:"With default"` + WithoutDefault string `long:"without-default" description:"Without default"` + WithProgrammaticDefault string `long:"with-programmatic-default" description:"With programmatic default"` + } + + opts.WithProgrammaticDefault = "default-value" + + p := NewNamedParser("TestHelpDefaults", HelpFlag) + p.AddGroup("Application Options", "The application options", &opts) + + _, err := p.ParseArgs(test.Args) + + if err == nil { + t.Fatalf("Expected help error") + } + + if e, ok := err.(*Error); !ok { + t.Fatalf("Expected flags.Error, but got %T", err) + } else { + if e.Type != ErrHelp { + t.Errorf("Expected flags.ErrHelp type, but got %s", e.Type) + } + + assertDiff(t, e.Message, test.Output, "help message") + } + } +} + +func TestHelpRestArgs(t *testing.T) { + opts := struct { + Verbose bool `short:"v"` + }{} + + p := NewNamedParser("TestHelpDefaults", HelpFlag) + p.AddGroup("Application Options", "The application options", &opts) + + retargs, err := p.ParseArgs([]string{"-h", "-v", "rest"}) + + if err == nil { + t.Fatalf("Expected help error") + } + + assertStringArray(t, retargs, []string{"-v", "rest"}) +} + +func TestWrapText(t *testing.T) { + s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + + got := wrapText(s, 60, " ") + expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit + esse cillum dolore eu fugiat nulla pariatur. Excepteur sint + occaecat cupidatat non proident, sunt in culpa qui officia + deserunt mollit anim id est laborum.` + + assertDiff(t, got, expected, "wrapped text") +} + +func TestWrapParagraph(t *testing.T) { + s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n\n" + s += "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\n" + s += "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n\n" + s += "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n" + + got := wrapText(s, 60, " ") + expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. + + Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. + + Duis aute irure dolor in reprehenderit in voluptate velit + esse cillum dolore eu fugiat nulla pariatur. + + Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. +` + + assertDiff(t, got, expected, "wrapped paragraph") +} diff --git a/ini.go b/ini.go index 7225052..cfdf57c 100644 --- a/ini.go +++ b/ini.go @@ -1,8 +1,14 @@ package flags import ( + "bufio" "fmt" "io" + "os" + "reflect" + "sort" + "strconv" + "strings" ) // IniError contains location information on where an error occured. @@ -55,6 +61,20 @@ type IniParser struct { parser *Parser } +type iniValue struct { + Name string + Value string + Quoted bool + LineNumber uint +} + +type iniSection []iniValue + +type ini struct { + File string + Sections map[string]iniSection +} + // NewIniParser creates a new ini parser for a given Parser. func NewIniParser(p *Parser) *IniParser { return &IniParser{ @@ -138,3 +158,436 @@ func (i *IniParser) WriteFile(filename string, options IniOptions) error { func (i *IniParser) Write(writer io.Writer, options IniOptions) { writeIni(i, writer, options) } + +func readFullLine(reader *bufio.Reader) (string, error) { + var line []byte + + for { + l, more, err := reader.ReadLine() + + if err != nil { + return "", err + } + + if line == nil && !more { + return string(l), nil + } + + line = append(line, l...) + + if !more { + break + } + } + + return string(line), nil +} + +func optionIniName(option *Option) string { + name := option.tag.Get("_read-ini-name") + + if len(name) != 0 { + return name + } + + name = option.tag.Get("ini-name") + + if len(name) != 0 { + return name + } + + return option.field.Name +} + +func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) { + var sname string + + if len(namespace) != 0 { + sname = namespace + } + + if cmd.Group != group && len(group.ShortDescription) != 0 { + if len(sname) != 0 { + sname += "." + } + + sname += group.ShortDescription + } + + sectionwritten := false + comments := (options & IniIncludeComments) != IniNone + + for _, option := range group.options { + if option.isFunc() || option.Hidden { + continue + } + + if len(option.tag.Get("no-ini")) != 0 { + continue + } + + val := option.value + + if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() { + continue + } + + if !sectionwritten { + fmt.Fprintf(writer, "[%s]\n", sname) + sectionwritten = true + } + + if comments && len(option.Description) != 0 { + fmt.Fprintf(writer, "; %s\n", option.Description) + } + + oname := optionIniName(option) + + commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault() + + kind := val.Type().Kind() + switch kind { + case reflect.Slice: + kind = val.Type().Elem().Kind() + + if val.Len() == 0 { + writeOption(writer, oname, kind, "", "", true, option.iniQuote) + } else { + for idx := 0; idx < val.Len(); idx++ { + v, _ := convertToString(val.Index(idx), option.tag) + + writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) + } + } + case reflect.Map: + kind = val.Type().Elem().Kind() + + if val.Len() == 0 { + writeOption(writer, oname, kind, "", "", true, option.iniQuote) + } else { + mkeys := val.MapKeys() + keys := make([]string, len(val.MapKeys())) + kkmap := make(map[string]reflect.Value) + + for i, k := range mkeys { + keys[i], _ = convertToString(k, option.tag) + kkmap[keys[i]] = k + } + + sort.Strings(keys) + + for _, k := range keys { + v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag) + + writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote) + } + } + default: + v, _ := convertToString(val, option.tag) + + writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) + } + + if comments { + fmt.Fprintln(writer) + } + } + + if sectionwritten && !comments { + fmt.Fprintln(writer) + } +} + +func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) { + if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) { + optionValue = strconv.Quote(optionValue) + } + + comment := "" + if commentOption { + comment = "; " + } + + fmt.Fprintf(writer, "%s%s =", comment, optionName) + + if optionKey != "" { + fmt.Fprintf(writer, " %s:%s", optionKey, optionValue) + } else if optionValue != "" { + fmt.Fprintf(writer, " %s", optionValue) + } + + fmt.Fprintln(writer) +} + +func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { + command.eachGroup(func(group *Group) { + if !group.Hidden { + writeGroupIni(command, group, namespace, writer, options) + } + }) + + for _, c := range command.commands { + var nns string + + if c.Hidden { + continue + } + + if len(namespace) != 0 { + nns = c.Name + "." + nns + } else { + nns = c.Name + } + + writeCommandIni(c, nns, writer, options) + } +} + +func writeIni(parser *IniParser, writer io.Writer, options IniOptions) { + writeCommandIni(parser.parser.Command, "", writer, options) +} + +func writeIniToFile(parser *IniParser, filename string, options IniOptions) error { + file, err := os.Create(filename) + + if err != nil { + return err + } + + defer file.Close() + + writeIni(parser, file, options) + + return nil +} + +func readIniFromFile(filename string) (*ini, error) { + file, err := os.Open(filename) + + if err != nil { + return nil, err + } + + defer file.Close() + + return readIni(file, filename) +} + +func readIni(contents io.Reader, filename string) (*ini, error) { + ret := &ini{ + File: filename, + Sections: make(map[string]iniSection), + } + + reader := bufio.NewReader(contents) + + // Empty global section + section := make(iniSection, 0, 10) + sectionname := "" + + ret.Sections[sectionname] = section + + var lineno uint + + for { + line, err := readFullLine(reader) + + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + lineno++ + line = strings.TrimSpace(line) + + // Skip empty lines and lines starting with ; (comments) + if len(line) == 0 || line[0] == ';' || line[0] == '#' { + continue + } + + if line[0] == '[' { + if line[0] != '[' || line[len(line)-1] != ']' { + return nil, &IniError{ + Message: "malformed section header", + File: filename, + LineNumber: lineno, + } + } + + name := strings.TrimSpace(line[1 : len(line)-1]) + + if len(name) == 0 { + return nil, &IniError{ + Message: "empty section name", + File: filename, + LineNumber: lineno, + } + } + + sectionname = name + section = ret.Sections[name] + + if section == nil { + section = make(iniSection, 0, 10) + ret.Sections[name] = section + } + + continue + } + + // Parse option here + keyval := strings.SplitN(line, "=", 2) + + if len(keyval) != 2 { + return nil, &IniError{ + Message: fmt.Sprintf("malformed key=value (%s)", line), + File: filename, + LineNumber: lineno, + } + } + + name := strings.TrimSpace(keyval[0]) + value := strings.TrimSpace(keyval[1]) + quoted := false + + if len(value) != 0 && value[0] == '"' { + if v, err := strconv.Unquote(value); err == nil { + value = v + + quoted = true + } else { + return nil, &IniError{ + Message: err.Error(), + File: filename, + LineNumber: lineno, + } + } + } + + section = append(section, iniValue{ + Name: name, + Value: value, + Quoted: quoted, + LineNumber: lineno, + }) + + ret.Sections[sectionname] = section + } + + return ret, nil +} + +func (i *IniParser) matchingGroups(name string) []*Group { + if len(name) == 0 { + var ret []*Group + + i.parser.eachGroup(func(g *Group) { + ret = append(ret, g) + }) + + return ret + } + + g := i.parser.groupByName(name) + + if g != nil { + return []*Group{g} + } + + return nil +} + +func (i *IniParser) parse(ini *ini) error { + p := i.parser + + var quotesLookup = make(map[*Option]bool) + + for name, section := range ini.Sections { + groups := i.matchingGroups(name) + + if len(groups) == 0 { + return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) + } + + for _, inival := range section { + var opt *Option + + for _, group := range groups { + opt = group.optionByName(inival.Name, func(o *Option, n string) bool { + return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n) + }) + + if opt != nil && len(opt.tag.Get("no-ini")) != 0 { + opt = nil + } + + if opt != nil { + break + } + } + + if opt == nil { + if (p.Options & IgnoreUnknown) == None { + return &IniError{ + Message: fmt.Sprintf("unknown option: %s", inival.Name), + File: ini.File, + LineNumber: inival.LineNumber, + } + } + + continue + } + + pval := &inival.Value + + if !opt.canArgument() && len(inival.Value) == 0 { + pval = nil + } else { + if opt.value.Type().Kind() == reflect.Map { + parts := strings.SplitN(inival.Value, ":", 2) + + // only handle unquoting + if len(parts) == 2 && parts[1][0] == '"' { + if v, err := strconv.Unquote(parts[1]); err == nil { + parts[1] = v + + inival.Quoted = true + } else { + return &IniError{ + Message: err.Error(), + File: ini.File, + LineNumber: inival.LineNumber, + } + } + + s := parts[0] + ":" + parts[1] + + pval = &s + } + } + } + + if err := opt.set(pval); err != nil { + return &IniError{ + Message: err.Error(), + File: ini.File, + LineNumber: inival.LineNumber, + } + } + + // either all INI values are quoted or only values who need quoting + if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { + quotesLookup[opt] = inival.Quoted + } + + opt.tag.Set("_read-ini-name", inival.Name) + } + } + + for opt, quoted := range quotesLookup { + opt.iniQuote = quoted + } + + return nil +} diff --git a/ini_private.go b/ini_private.go deleted file mode 100644 index 887aa76..0000000 --- a/ini_private.go +++ /dev/null @@ -1,452 +0,0 @@ -package flags - -import ( - "bufio" - "fmt" - "io" - "os" - "reflect" - "sort" - "strconv" - "strings" -) - -type iniValue struct { - Name string - Value string - Quoted bool - LineNumber uint -} - -type iniSection []iniValue -type ini struct { - File string - Sections map[string]iniSection -} - -func readFullLine(reader *bufio.Reader) (string, error) { - var line []byte - - for { - l, more, err := reader.ReadLine() - - if err != nil { - return "", err - } - - if line == nil && !more { - return string(l), nil - } - - line = append(line, l...) - - if !more { - break - } - } - - return string(line), nil -} - -func optionIniName(option *Option) string { - name := option.tag.Get("_read-ini-name") - - if len(name) != 0 { - return name - } - - name = option.tag.Get("ini-name") - - if len(name) != 0 { - return name - } - - return option.field.Name -} - -func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) { - var sname string - - if len(namespace) != 0 { - sname = namespace - } - - if cmd.Group != group && len(group.ShortDescription) != 0 { - if len(sname) != 0 { - sname += "." - } - - sname += group.ShortDescription - } - - sectionwritten := false - comments := (options & IniIncludeComments) != IniNone - - for _, option := range group.options { - if option.isFunc() { - continue - } - - if len(option.tag.Get("no-ini")) != 0 { - continue - } - - val := option.value - - if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() { - continue - } - - if !sectionwritten { - fmt.Fprintf(writer, "[%s]\n", sname) - sectionwritten = true - } - - if comments && len(option.Description) != 0 { - fmt.Fprintf(writer, "; %s\n", option.Description) - } - - oname := optionIniName(option) - - commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault() - - kind := val.Type().Kind() - switch kind { - case reflect.Slice: - kind = val.Type().Elem().Kind() - - if val.Len() == 0 { - writeOption(writer, oname, kind, "", "", true, option.iniQuote) - } else { - for idx := 0; idx < val.Len(); idx++ { - v, _ := convertToString(val.Index(idx), option.tag) - - writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) - } - } - case reflect.Map: - kind = val.Type().Elem().Kind() - - if val.Len() == 0 { - writeOption(writer, oname, kind, "", "", true, option.iniQuote) - } else { - mkeys := val.MapKeys() - keys := make([]string, len(val.MapKeys())) - kkmap := make(map[string]reflect.Value) - - for i, k := range mkeys { - keys[i], _ = convertToString(k, option.tag) - kkmap[keys[i]] = k - } - - sort.Strings(keys) - - for _, k := range keys { - v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag) - - writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote) - } - } - default: - v, _ := convertToString(val, option.tag) - - writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) - } - - if comments { - fmt.Fprintln(writer) - } - } - - if sectionwritten && !comments { - fmt.Fprintln(writer) - } -} - -func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) { - if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) { - optionValue = strconv.Quote(optionValue) - } - - comment := "" - if commentOption { - comment = "; " - } - - fmt.Fprintf(writer, "%s%s =", comment, optionName) - - if optionKey != "" { - fmt.Fprintf(writer, " %s:%s", optionKey, optionValue) - } else if optionValue != "" { - fmt.Fprintf(writer, " %s", optionValue) - } - - fmt.Fprintln(writer) -} - -func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { - command.eachGroup(func(group *Group) { - writeGroupIni(command, group, namespace, writer, options) - }) - - for _, c := range command.commands { - var nns string - - if len(namespace) != 0 { - nns = c.Name + "." + nns - } else { - nns = c.Name - } - - writeCommandIni(c, nns, writer, options) - } -} - -func writeIni(parser *IniParser, writer io.Writer, options IniOptions) { - writeCommandIni(parser.parser.Command, "", writer, options) -} - -func writeIniToFile(parser *IniParser, filename string, options IniOptions) error { - file, err := os.Create(filename) - - if err != nil { - return err - } - - defer file.Close() - - writeIni(parser, file, options) - - return nil -} - -func readIniFromFile(filename string) (*ini, error) { - file, err := os.Open(filename) - - if err != nil { - return nil, err - } - - defer file.Close() - - return readIni(file, filename) -} - -func readIni(contents io.Reader, filename string) (*ini, error) { - ret := &ini{ - File: filename, - Sections: make(map[string]iniSection), - } - - reader := bufio.NewReader(contents) - - // Empty global section - section := make(iniSection, 0, 10) - sectionname := "" - - ret.Sections[sectionname] = section - - var lineno uint - - for { - line, err := readFullLine(reader) - - if err == io.EOF { - break - } else if err != nil { - return nil, err - } - - lineno++ - line = strings.TrimSpace(line) - - // Skip empty lines and lines starting with ; (comments) - if len(line) == 0 || line[0] == ';' || line[0] == '#' { - continue - } - - if line[0] == '[' { - if line[0] != '[' || line[len(line)-1] != ']' { - return nil, &IniError{ - Message: "malformed section header", - File: filename, - LineNumber: lineno, - } - } - - name := strings.TrimSpace(line[1 : len(line)-1]) - - if len(name) == 0 { - return nil, &IniError{ - Message: "empty section name", - File: filename, - LineNumber: lineno, - } - } - - sectionname = name - section = ret.Sections[name] - - if section == nil { - section = make(iniSection, 0, 10) - ret.Sections[name] = section - } - - continue - } - - // Parse option here - keyval := strings.SplitN(line, "=", 2) - - if len(keyval) != 2 { - return nil, &IniError{ - Message: fmt.Sprintf("malformed key=value (%s)", line), - File: filename, - LineNumber: lineno, - } - } - - name := strings.TrimSpace(keyval[0]) - value := strings.TrimSpace(keyval[1]) - quoted := false - - if len(value) != 0 && value[0] == '"' { - if v, err := strconv.Unquote(value); err == nil { - value = v - - quoted = true - } else { - return nil, &IniError{ - Message: err.Error(), - File: filename, - LineNumber: lineno, - } - } - } - - section = append(section, iniValue{ - Name: name, - Value: value, - Quoted: quoted, - LineNumber: lineno, - }) - - ret.Sections[sectionname] = section - } - - return ret, nil -} - -func (i *IniParser) matchingGroups(name string) []*Group { - if len(name) == 0 { - var ret []*Group - - i.parser.eachGroup(func(g *Group) { - ret = append(ret, g) - }) - - return ret - } - - g := i.parser.groupByName(name) - - if g != nil { - return []*Group{g} - } - - return nil -} - -func (i *IniParser) parse(ini *ini) error { - p := i.parser - - var quotesLookup = make(map[*Option]bool) - - for name, section := range ini.Sections { - groups := i.matchingGroups(name) - - if len(groups) == 0 { - return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) - } - - for _, inival := range section { - var opt *Option - - for _, group := range groups { - opt = group.optionByName(inival.Name, func(o *Option, n string) bool { - return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n) - }) - - if opt != nil && len(opt.tag.Get("no-ini")) != 0 { - opt = nil - } - - if opt != nil { - break - } - } - - if opt == nil { - if (p.Options & IgnoreUnknown) == None { - return &IniError{ - Message: fmt.Sprintf("unknown option: %s", inival.Name), - File: ini.File, - LineNumber: inival.LineNumber, - } - } - - continue - } - - pval := &inival.Value - - if !opt.canArgument() && len(inival.Value) == 0 { - pval = nil - } else { - if opt.value.Type().Kind() == reflect.Map { - parts := strings.SplitN(inival.Value, ":", 2) - - // only handle unquoting - if len(parts) == 2 && parts[1][0] == '"' { - if v, err := strconv.Unquote(parts[1]); err == nil { - parts[1] = v - - inival.Quoted = true - } else { - return &IniError{ - Message: err.Error(), - File: ini.File, - LineNumber: inival.LineNumber, - } - } - - s := parts[0] + ":" + parts[1] - - pval = &s - } - } - } - - if err := opt.set(pval); err != nil { - return &IniError{ - Message: err.Error(), - File: ini.File, - LineNumber: inival.LineNumber, - } - } - - // either all INI values are quoted or only values who need quoting - if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { - quotesLookup[opt] = inival.Quoted - } - - opt.tag.Set("_read-ini-name", inival.Name) - } - } - - for opt, quoted := range quotesLookup { - opt.iniQuote = quoted - } - - return nil -} diff --git a/ini_test.go b/ini_test.go index 8c3176c..dfe49ce 100644 --- a/ini_test.go +++ b/ini_test.go @@ -21,7 +21,7 @@ func TestWriteIni(t *testing.T) { expected string }{ { - []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"}, + []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"}, IniDefault, `[Application Options] ; Show verbose debug information @@ -42,7 +42,7 @@ int-map = b:3 `, }, { - []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"}, + []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults, `[Application Options] ; Show verbose debug information @@ -71,6 +71,12 @@ EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def +; Option with named argument +OptionWithArgName = + +; Option with choices +OptionWithChoices = + ; Option only available in ini only-ini = @@ -98,7 +104,7 @@ Opt = `, }, { - []string{"filename", "0", "command"}, + []string{"filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults | IniCommentDefaults, `[Application Options] ; Show verbose debug information @@ -126,6 +132,12 @@ EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def +; Option with named argument +; OptionWithArgName = + +; Option with choices +; OptionWithChoices = + ; Option only available in ini ; only-ini = @@ -152,7 +164,7 @@ EnvDefault2 = env-def `, }, { - []string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "command"}, + []string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "3.14", "command"}, IniDefault | IniIncludeDefaults | IniCommentDefaults, `[Application Options] ; Show verbose debug information @@ -178,6 +190,12 @@ EnvDefault1 = env-def ; Test env-default2 value EnvDefault2 = env-def +; Option with named argument +; OptionWithArgName = + +; Option with choices +; OptionWithChoices = + ; Option only available in ini ; only-ini = @@ -230,6 +248,92 @@ EnvDefault2 = env-def } } +func TestReadIni_flagEquivalent(t *testing.T) { + type options struct { + Opt1 bool `long:"opt1"` + + Group1 struct { + Opt2 bool `long:"opt2"` + } `group:"group1"` + + Group2 struct { + Opt3 bool `long:"opt3"` + } `group:"group2" namespace:"ns1"` + + Cmd1 struct { + Opt4 bool `long:"opt4"` + Opt5 bool `long:"foo.opt5"` + + Group1 struct { + Opt6 bool `long:"opt6"` + Opt7 bool `long:"foo.opt7"` + } `group:"group1"` + + Group2 struct { + Opt8 bool `long:"opt8"` + } `group:"group2" namespace:"ns1"` + } `command:"cmd1"` + } + + a := ` +opt1=true + +[group1] +opt2=true + +[group2] +ns1.opt3=true + +[cmd1] +opt4=true +foo.opt5=true + +[cmd1.group1] +opt6=true +foo.opt7=true + +[cmd1.group2] +ns1.opt8=true +` + b := ` +opt1=true +opt2=true +ns1.opt3=true + +[cmd1] +opt4=true +foo.opt5=true +opt6=true +foo.opt7=true +ns1.opt8=true +` + + parse := func(readIni string) (opts options, writeIni string) { + p := NewNamedParser("TestIni", Default) + p.AddGroup("Application Options", "The application options", &opts) + + inip := NewIniParser(p) + err := inip.Parse(strings.NewReader(readIni)) + + if err != nil { + t.Fatalf("Unexpected error: %s\n\nFile:\n%s", err, readIni) + } + + var b bytes.Buffer + inip.Write(&b, Default) + + return opts, b.String() + } + + aOpt, aIni := parse(a) + bOpt, bIni := parse(b) + + assertDiff(t, aIni, bIni, "") + if !reflect.DeepEqual(aOpt, bOpt) { + t.Errorf("not equal") + } +} + func TestReadIni(t *testing.T) { var opts helpOptions @@ -569,7 +673,8 @@ other = subgroup func TestIniNoIni(t *testing.T) { var opts struct { - Value string `short:"v" long:"value" no-ini:"yes"` + NoValue string `short:"n" long:"novalue" no-ini:"yes"` + Value string `short:"v" long:"value"` } p := NewNamedParser("TestIni", Default) @@ -577,8 +682,10 @@ func TestIniNoIni(t *testing.T) { inip := NewIniParser(p) + // read INI inic := `[Application Options] -value = some value +novalue = some value +value = some other value ` b := strings.NewReader(inic) @@ -594,9 +701,33 @@ value = some value t.Errorf("Expected opts.Add.Name to be %d, but got %d", v, iniError.LineNumber) } - if v := "unknown option: value"; iniError.Message != v { + if v := "unknown option: novalue"; iniError.Message != v { t.Errorf("Expected opts.Add.Name to be %s, but got %s", v, iniError.Message) } + + // write INI + opts.NoValue = "some value" + opts.Value = "some other value" + + file, err := ioutil.TempFile("", "") + if err != nil { + t.Fatalf("Cannot create temporary file: %s", err) + } + defer os.Remove(file.Name()) + + err = inip.WriteFile(file.Name(), IniIncludeDefaults) + if err != nil { + t.Fatalf("Could not write ini file: %s", err) + } + + found, err := ioutil.ReadFile(file.Name()) + if err != nil { + t.Fatalf("Could not read written ini file: %s", err) + } + + expected := "[Application Options]\nValue = some other value\n\n" + + assertDiff(t, string(found), expected, "ini content") } func TestIniParse(t *testing.T) { @@ -627,6 +758,94 @@ func TestIniParse(t *testing.T) { } } +func TestIniCliOverrides(t *testing.T) { + file, err := ioutil.TempFile("", "") + + if err != nil { + t.Fatalf("Cannot create temporary file: %s", err) + } + + defer os.Remove(file.Name()) + + _, err = file.WriteString("values = 123\n") + _, err = file.WriteString("values = 456\n") + + if err != nil { + t.Fatalf("Cannot write to temporary file: %s", err) + } + + file.Close() + + var opts struct { + Values []int `long:"values"` + } + + p := NewParser(&opts, Default) + err = NewIniParser(p).ParseFile(file.Name()) + + if err != nil { + t.Fatalf("Could not parse ini: %s", err) + } + + _, err = p.ParseArgs([]string{"--values", "111", "--values", "222"}) + + if err != nil { + t.Fatalf("Failed to parse arguments: %s", err) + } + + if len(opts.Values) != 2 { + t.Fatalf("Expected Values to contain two elements, but got %d", len(opts.Values)) + } + + if opts.Values[0] != 111 { + t.Fatalf("Expected Values[0] to be 111, but got '%d'", opts.Values[0]) + } + + if opts.Values[1] != 222 { + t.Fatalf("Expected Values[0] to be 222, but got '%d'", opts.Values[1]) + } +} + +func TestIniOverrides(t *testing.T) { + file, err := ioutil.TempFile("", "") + + if err != nil { + t.Fatalf("Cannot create temporary file: %s", err) + } + + defer os.Remove(file.Name()) + + _, err = file.WriteString("value-with-default = \"ini-value\"\n") + _, err = file.WriteString("value-with-default-override-cli = \"ini-value\"\n") + + if err != nil { + t.Fatalf("Cannot write to temporary file: %s", err) + } + + file.Close() + + var opts struct { + ValueWithDefault string `long:"value-with-default" default:"value"` + ValueWithDefaultOverrideCli string `long:"value-with-default-override-cli" default:"value"` + } + + p := NewParser(&opts, Default) + err = NewIniParser(p).ParseFile(file.Name()) + + if err != nil { + t.Fatalf("Could not parse ini: %s", err) + } + + _, err = p.ParseArgs([]string{"--value-with-default-override-cli", "cli-value"}) + + if err != nil { + t.Fatalf("Failed to parse arguments: %s", err) + } + + assertString(t, opts.ValueWithDefault, "ini-value") + assertString(t, opts.ValueWithDefaultOverrideCli, "cli-value") +} + func TestWriteFile(t *testing.T) { file, err := ioutil.TempFile("", "") if err != nil { diff --git a/man.go b/man.go index e8e5916..8e4a8b7 100644 --- a/man.go +++ b/man.go @@ -3,38 +3,47 @@ package flags import ( "fmt" "io" + "runtime" "strings" "time" ) +func manQuote(s string) string { + return strings.Replace(s, "\\", "\\\\", -1) +} + func formatForMan(wr io.Writer, s string) { for { idx := strings.IndexRune(s, '`') if idx < 0 { - fmt.Fprintf(wr, "%s", s) + fmt.Fprintf(wr, "%s", manQuote(s)) break } - fmt.Fprintf(wr, "%s", s[:idx]) + fmt.Fprintf(wr, "%s", manQuote(s[:idx])) s = s[idx+1:] idx = strings.IndexRune(s, '\'') if idx < 0 { - fmt.Fprintf(wr, "%s", s) + fmt.Fprintf(wr, "%s", manQuote(s)) break } - fmt.Fprintf(wr, "\\fB%s\\fP", s[:idx]) + fmt.Fprintf(wr, "\\fB%s\\fP", manQuote(s[:idx])) s = s[idx+1:] } } func writeManPageOptions(wr io.Writer, grp *Group) { grp.eachGroup(func(group *Group) { + if group.Hidden { + return + } + for _, opt := range group.options { - if !opt.canCli() { + if !opt.canCli() || opt.Hidden { continue } @@ -42,7 +51,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) { fmt.Fprintf(wr, "\\fB") if opt.ShortName != 0 { - fmt.Fprintf(wr, "-%c", opt.ShortName) + fmt.Fprintf(wr, "\\fB\\-%c\\fR", opt.ShortName) } if len(opt.LongName) != 0 { @@ -50,10 +59,33 @@ func writeManPageOptions(wr io.Writer, grp *Group) { fmt.Fprintf(wr, ", ") } - fmt.Fprintf(wr, "--%s", opt.LongNameWithNamespace()) + fmt.Fprintf(wr, "\\fB\\-\\-%s\\fR", manQuote(opt.LongNameWithNamespace())) + } + + if len(opt.ValueName) != 0 || opt.OptionalArgument { + if opt.OptionalArgument { + fmt.Fprintf(wr, " [\\fI%s=%s\\fR]", manQuote(opt.ValueName), manQuote(strings.Join(quoteV(opt.OptionalValue), ", "))) + } else { + fmt.Fprintf(wr, " \\fI%s\\fR", manQuote(opt.ValueName)) + } + } + + if len(opt.Default) != 0 { + fmt.Fprintf(wr, " ", manQuote(strings.Join(quoteV(opt.Default), ", "))) + } else if len(opt.EnvDefaultKey) != 0 { + if runtime.GOOS == "windows" { + fmt.Fprintf(wr, " ", manQuote(opt.EnvDefaultKey)) + } else { + fmt.Fprintf(wr, " ", manQuote(opt.EnvDefaultKey)) + } + } + + if opt.Required { + fmt.Fprintf(wr, " (\\fIrequired\\fR)") } fmt.Fprintln(wr, "\\fP") + if len(opt.Description) != 0 { formatForMan(wr, opt.Description) fmt.Fprintln(wr, "") @@ -63,11 +95,15 @@ func writeManPageOptions(wr io.Writer, grp *Group) { } func writeManPageSubcommands(wr io.Writer, name string, root *Command) { - commands := root.sortedCommands() + commands := root.sortedVisibleCommands() for _, c := range commands { var nn string + if c.Hidden { + continue + } + if len(name) != 0 { nn = name + " " + c.Name } else { @@ -85,10 +121,10 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm if len(command.LongDescription) > 0 { fmt.Fprintln(wr, "") - cmdstart := fmt.Sprintf("The %s command", command.Name) + cmdstart := fmt.Sprintf("The %s command", manQuote(command.Name)) if strings.HasPrefix(command.LongDescription, cmdstart) { - fmt.Fprintf(wr, "The \\fI%s\\fP command", command.Name) + fmt.Fprintf(wr, "The \\fI%s\\fP command", manQuote(command.Name)) formatForMan(wr, command.LongDescription[len(cmdstart):]) fmt.Fprintln(wr, "") @@ -113,11 +149,11 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm } if len(usage) > 0 { - fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n\n", pre, usage) + fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n.TP\n", manQuote(pre), manQuote(usage)) } if len(command.Aliases) > 0 { - fmt.Fprintf(wr, "\n\\fBAliases\\fP: %s\n\n", strings.Join(command.Aliases, ", ")) + fmt.Fprintf(wr, "\n\\fBAliases\\fP: %s\n\n", manQuote(strings.Join(command.Aliases, ", "))) } writeManPageOptions(wr, command.Group) @@ -129,9 +165,9 @@ func writeManPageCommand(wr io.Writer, name string, root *Command, command *Comm func (p *Parser) WriteManPage(wr io.Writer) { t := time.Now() - fmt.Fprintf(wr, ".TH %s 1 \"%s\"\n", p.Name, t.Format("2 January 2006")) + fmt.Fprintf(wr, ".TH %s 1 \"%s\"\n", manQuote(p.Name), t.Format("2 January 2006")) fmt.Fprintln(wr, ".SH NAME") - fmt.Fprintf(wr, "%s \\- %s\n", p.Name, p.ShortDescription) + fmt.Fprintf(wr, "%s \\- %s\n", manQuote(p.Name), manQuote(p.ShortDescription)) fmt.Fprintln(wr, ".SH SYNOPSIS") usage := p.Usage @@ -140,7 +176,7 @@ func (p *Parser) WriteManPage(wr io.Writer) { usage = "[OPTIONS]" } - fmt.Fprintf(wr, "\\fB%s\\fP %s\n", p.Name, usage) + fmt.Fprintf(wr, "\\fB%s\\fP %s\n", manQuote(p.Name), manQuote(usage)) fmt.Fprintln(wr, ".SH DESCRIPTION") formatForMan(wr, p.LongDescription) @@ -150,7 +186,7 @@ func (p *Parser) WriteManPage(wr io.Writer) { writeManPageOptions(wr, p.Command.Group) - if len(p.commands) > 0 { + if len(p.visibleCommands()) > 0 { fmt.Fprintln(wr, ".SH COMMANDS") writeManPageSubcommands(wr, "", p.Command) diff --git a/marshal_test.go b/marshal_test.go index 59c9cce..095e9e4 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -5,13 +5,13 @@ import ( "testing" ) -type marshalled bool +type marshalled string func (m *marshalled) UnmarshalFlag(value string) error { if value == "yes" { - *m = true + *m = "true" } else if value == "no" { - *m = false + *m = "false" } else { return fmt.Errorf("`%s' is not a valid value, please specify `yes' or `no'", value) } @@ -20,7 +20,7 @@ func (m *marshalled) UnmarshalFlag(value string) error { } func (m marshalled) MarshalFlag() (string, error) { - if m { + if m == "true" { return "yes", nil } @@ -42,8 +42,8 @@ func TestUnmarshal(t *testing.T) { assertStringArray(t, ret, []string{}) - if !opts.Value { - t.Errorf("Expected Value to be true") + if opts.Value != "true" { + t.Errorf("Expected Value to be \"true\"") } } @@ -56,8 +56,8 @@ func TestUnmarshalDefault(t *testing.T) { assertStringArray(t, ret, []string{}) - if !opts.Value { - t.Errorf("Expected Value to be true") + if opts.Value != "true" { + t.Errorf("Expected Value to be \"true\"") } } @@ -70,8 +70,8 @@ func TestUnmarshalOptional(t *testing.T) { assertStringArray(t, ret, []string{}) - if !opts.Value { - t.Errorf("Expected Value to be true") + if opts.Value != "true" { + t.Errorf("Expected Value to be \"true\"") } } diff --git a/option.go b/option.go index 29e702c..b2a69c7 100644 --- a/option.go +++ b/option.go @@ -1,8 +1,11 @@ package flags import ( + "bytes" "fmt" "reflect" + "strings" + "syscall" "unicode/utf8" ) @@ -35,7 +38,7 @@ type Option struct { // If true, specifies that the argument to an option flag is optional. // When no argument to the flag is specified on the command line, the - // value of Default will be set in the field this option represents. + // value of OptionalValue will be set in the field this option represents. // This is only valid for non-boolean options. OptionalArgument bool @@ -59,6 +62,12 @@ type Option struct { // passwords. DefaultMask string + // If non empty, only a certain set of values is allowed for an option. + Choices []string + + // If true, the option is not displayed in the help or man page + Hidden bool + // The group which the option belongs to group *Group @@ -71,8 +80,11 @@ type Option struct { // Determines if the option will be always quoted in the INI output iniQuote bool - tag multiTag - isSet bool + tag multiTag + isSet bool + preventDefault bool + + defaultLiteral string } // LongNameWithNamespace returns the option's long name with the group namespaces @@ -155,3 +167,268 @@ func (option *Option) String() string { func (option *Option) Value() interface{} { return option.value.Interface() } + +// IsSet returns true if option has been set +func (option *Option) IsSet() bool { + return option.isSet +} + +// Set the value of an option to the specified value. An error will be returned +// if the specified value could not be converted to the corresponding option +// value type. +func (option *Option) set(value *string) error { + kind := option.value.Type().Kind() + + if (kind == reflect.Map || kind == reflect.Slice) && !option.isSet { + option.empty() + } + + option.isSet = true + option.preventDefault = true + + if len(option.Choices) != 0 { + found := false + + for _, choice := range option.Choices { + if choice == *value { + found = true + break + } + } + + if !found { + allowed := strings.Join(option.Choices[0:len(option.Choices)-1], ", ") + + if len(option.Choices) > 1 { + allowed += " or " + option.Choices[len(option.Choices)-1] + } + + return newErrorf(ErrInvalidChoice, + "Invalid value `%s' for option `%s'. Allowed values are: %s", + *value, option, allowed) + } + } + + if option.isFunc() { + return option.call(value) + } else if value != nil { + return convert(*value, option.value, option.tag) + } + + return convert("", option.value, option.tag) +} + +func (option *Option) canCli() bool { + return option.ShortName != 0 || len(option.LongName) != 0 +} + +func (option *Option) canArgument() bool { + if u := option.isUnmarshaler(); u != nil { + return true + } + + return !option.isBool() +} + +func (option *Option) emptyValue() reflect.Value { + tp := option.value.Type() + + if tp.Kind() == reflect.Map { + return reflect.MakeMap(tp) + } + + return reflect.Zero(tp) +} + +func (option *Option) empty() { + if !option.isFunc() { + option.value.Set(option.emptyValue()) + } +} + +func (option *Option) clearDefault() { + usedDefault := option.Default + + if envKey := option.EnvDefaultKey; envKey != "" { + // os.Getenv() makes no distinction between undefined and + // empty values, so we use syscall.Getenv() + if value, ok := syscall.Getenv(envKey); ok { + if option.EnvDefaultDelim != "" { + usedDefault = strings.Split(value, + option.EnvDefaultDelim) + } else { + usedDefault = []string{value} + } + } + } + + if len(usedDefault) > 0 { + option.empty() + + for _, d := range usedDefault { + option.set(&d) + } + } else { + tp := option.value.Type() + + switch tp.Kind() { + case reflect.Map: + if option.value.IsNil() { + option.empty() + } + case reflect.Slice: + if option.value.IsNil() { + option.empty() + } + } + } +} + +func (option *Option) valueIsDefault() bool { + // Check if the value of the option corresponds to its + // default value + emptyval := option.emptyValue() + + checkvalptr := reflect.New(emptyval.Type()) + checkval := reflect.Indirect(checkvalptr) + + checkval.Set(emptyval) + + if len(option.Default) != 0 { + for _, v := range option.Default { + convert(v, checkval, option.tag) + } + } + + return reflect.DeepEqual(option.value.Interface(), checkval.Interface()) +} + +func (option *Option) isUnmarshaler() Unmarshaler { + v := option.value + + for { + if !v.CanInterface() { + break + } + + i := v.Interface() + + if u, ok := i.(Unmarshaler); ok { + return u + } + + if !v.CanAddr() { + break + } + + v = v.Addr() + } + + return nil +} + +func (option *Option) isBool() bool { + tp := option.value.Type() + + for { + switch tp.Kind() { + case reflect.Bool: + return true + case reflect.Slice: + return (tp.Elem().Kind() == reflect.Bool) + case reflect.Func: + return tp.NumIn() == 0 + case reflect.Ptr: + tp = tp.Elem() + default: + return false + } + } +} + +func (option *Option) isFunc() bool { + return option.value.Type().Kind() == reflect.Func +} + +func (option *Option) call(value *string) error { + var retval []reflect.Value + + if value == nil { + retval = option.value.Call(nil) + } else { + tp := option.value.Type().In(0) + + val := reflect.New(tp) + val = reflect.Indirect(val) + + if err := convert(*value, val, option.tag); err != nil { + return err + } + + retval = option.value.Call([]reflect.Value{val}) + } + + if len(retval) == 1 && retval[0].Type() == reflect.TypeOf((*error)(nil)).Elem() { + if retval[0].Interface() == nil { + return nil + } + + return retval[0].Interface().(error) + } + + return nil +} + +func (option *Option) updateDefaultLiteral() { + defs := option.Default + def := "" + + if len(defs) == 0 && option.canArgument() { + var showdef bool + + switch option.field.Type.Kind() { + case reflect.Func, reflect.Ptr: + showdef = !option.value.IsNil() + case reflect.Slice, reflect.String, reflect.Array: + showdef = option.value.Len() > 0 + case reflect.Map: + showdef = !option.value.IsNil() && option.value.Len() > 0 + default: + zeroval := reflect.Zero(option.field.Type) + showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) + } + + if showdef { + def, _ = convertToString(option.value, option.tag) + } + } else if len(defs) != 0 { + l := len(defs) - 1 + + for i := 0; i < l; i++ { + def += quoteIfNeeded(defs[i]) + ", " + } + + def += quoteIfNeeded(defs[l]) + } + + option.defaultLiteral = def +} + +func (option *Option) shortAndLongName() string { + ret := &bytes.Buffer{} + + if option.ShortName != 0 { + ret.WriteRune(defaultShortOptDelimiter) + ret.WriteRune(option.ShortName) + } + + if len(option.LongName) != 0 { + if option.ShortName != 0 { + ret.WriteRune('/') + } + + ret.WriteString(option.LongName) + } + + return ret.String() +} diff --git a/option_private.go b/option_private.go deleted file mode 100644 index d36c841..0000000 --- a/option_private.go +++ /dev/null @@ -1,182 +0,0 @@ -package flags - -import ( - "reflect" - "strings" - "syscall" -) - -// Set the value of an option to the specified value. An error will be returned -// if the specified value could not be converted to the corresponding option -// value type. -func (option *Option) set(value *string) error { - option.isSet = true - - if option.isFunc() { - return option.call(value) - } else if value != nil { - return convert(*value, option.value, option.tag) - } - - return convert("", option.value, option.tag) -} - -func (option *Option) canCli() bool { - return option.ShortName != 0 || len(option.LongName) != 0 -} - -func (option *Option) canArgument() bool { - if u := option.isUnmarshaler(); u != nil { - return true - } - - return !option.isBool() -} - -func (option *Option) emptyValue() reflect.Value { - tp := option.value.Type() - - if tp.Kind() == reflect.Map { - return reflect.MakeMap(tp) - } - - return reflect.Zero(tp) -} - -func (option *Option) empty() { - if !option.isFunc() { - option.value.Set(option.emptyValue()) - } -} - -func (option *Option) clearDefault() { - usedDefault := option.Default - if envKey := option.EnvDefaultKey; envKey != "" { - // os.Getenv() makes no distinction between undefined and - // empty values, so we use syscall.Getenv() - if value, ok := syscall.Getenv(envKey); ok { - if option.EnvDefaultDelim != "" { - usedDefault = strings.Split(value, - option.EnvDefaultDelim) - } else { - usedDefault = []string{value} - } - } - } - - if len(usedDefault) > 0 { - option.empty() - - for _, d := range usedDefault { - option.set(&d) - } - } else { - tp := option.value.Type() - - switch tp.Kind() { - case reflect.Map: - if option.value.IsNil() { - option.empty() - } - case reflect.Slice: - if option.value.IsNil() { - option.empty() - } - } - } -} - -func (option *Option) valueIsDefault() bool { - // Check if the value of the option corresponds to its - // default value - emptyval := option.emptyValue() - - checkvalptr := reflect.New(emptyval.Type()) - checkval := reflect.Indirect(checkvalptr) - - checkval.Set(emptyval) - - if len(option.Default) != 0 { - for _, v := range option.Default { - convert(v, checkval, option.tag) - } - } - - return reflect.DeepEqual(option.value.Interface(), checkval.Interface()) -} - -func (option *Option) isUnmarshaler() Unmarshaler { - v := option.value - - for { - if !v.CanInterface() { - break - } - - i := v.Interface() - - if u, ok := i.(Unmarshaler); ok { - return u - } - - if !v.CanAddr() { - break - } - - v = v.Addr() - } - - return nil -} - -func (option *Option) isBool() bool { - tp := option.value.Type() - - for { - switch tp.Kind() { - case reflect.Bool: - return true - case reflect.Slice: - return (tp.Elem().Kind() == reflect.Bool) - case reflect.Func: - return tp.NumIn() == 0 - case reflect.Ptr: - tp = tp.Elem() - default: - return false - } - } -} - -func (option *Option) isFunc() bool { - return option.value.Type().Kind() == reflect.Func -} - -func (option *Option) call(value *string) error { - var retval []reflect.Value - - if value == nil { - retval = option.value.Call(nil) - } else { - tp := option.value.Type().In(0) - - val := reflect.New(tp) - val = reflect.Indirect(val) - - if err := convert(*value, val, option.tag); err != nil { - return err - } - - retval = option.value.Call([]reflect.Value{val}) - } - - if len(retval) == 1 && retval[0].Type() == reflect.TypeOf((*error)(nil)).Elem() { - if retval[0].Interface() == nil { - return nil - } - - return retval[0].Interface().(error) - } - - return nil -} diff --git a/parser.go b/parser.go index 6f45a03..f9e07ee 100644 --- a/parser.go +++ b/parser.go @@ -5,8 +5,13 @@ package flags import ( + "bytes" + "fmt" "os" "path" + "sort" + "strings" + "unicode/utf8" ) // A Parser provides command line option parsing. It can contain several @@ -32,6 +37,11 @@ type Parser struct { // or an error to indicate a parse failure. UnknownOptionHandler func(option string, arg SplitArgument, args []string) ([]string, error) + // CompletionHandler is a function gets called to handle the completion of + // items. By default, the items are printed and the application is exited. + // You can override this default behavior by specifying a custom CompletionHandler. + CompletionHandler func(items []Completion) + internalError error } @@ -93,6 +103,17 @@ const ( Default = HelpFlag | PrintErrors | PassDoubleDash ) +type parseState struct { + arg string + args []string + retargs []string + positional []*Arg + err error + + command *Command + lookup lookup +} + // Parse is a convenience function to parse command line options with default // settings. The provided data is a pointer to a struct representing the // default option group (named "Application Options"). For more control, use @@ -162,14 +183,18 @@ func (p *Parser) Parse() ([]string, error) { // // When the common help group has been added (AddHelp) and either -h or --help // was specified in the command line arguments, a help message will be -// automatically printed. Furthermore, the special error type ErrHelp is returned. +// automatically printed if the PrintErrors option is enabled. +// Furthermore, the special error type ErrHelp is returned. // It is up to the caller to exit the program if so desired. func (p *Parser) ParseArgs(args []string) ([]string, error) { if p.internalError != nil { return nil, p.internalError } - p.clearIsSet() + p.eachOption(func(c *Command, g *Group, option *Option) { + option.isSet = false + option.updateDefaultLiteral() + }) // Add built-in help group to all commands if necessary if (p.Options & HelpFlag) != None { @@ -180,13 +205,15 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { if len(compval) != 0 { comp := &completion{parser: p} + items := comp.complete(args) - if compval == "verbose" { - comp.ShowDescriptions = true + if p.CompletionHandler != nil { + p.CompletionHandler(items) + } else { + comp.print(items, compval == "verbose") + os.Exit(0) } - comp.execute(args) - return nil, nil } @@ -253,17 +280,13 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if s.err == nil { - p.eachCommand(func(c *Command) { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - if option.isSet { - continue - } + p.eachOption(func(c *Command, g *Group, option *Option) { + if option.preventDefault { + return + } - option.clearDefault() - } - }) - }, true) + option.clearDefault() + }) s.checkRequired(p) } @@ -279,8 +302,351 @@ func (p *Parser) ParseArgs(args []string) ([]string, error) { } if reterr != nil { - return append([]string{s.arg}, s.args...), p.printError(reterr) + var retargs []string + + if ourErr, ok := reterr.(*Error); !ok || ourErr.Type != ErrHelp { + retargs = append([]string{s.arg}, s.args...) + } else { + retargs = s.args + } + + return retargs, p.printError(reterr) } return s.retargs, nil } + +func (p *parseState) eof() bool { + return len(p.args) == 0 +} + +func (p *parseState) pop() string { + if p.eof() { + return "" + } + + p.arg = p.args[0] + p.args = p.args[1:] + + return p.arg +} + +func (p *parseState) peek() string { + if p.eof() { + return "" + } + + return p.args[0] +} + +func (p *parseState) checkRequired(parser *Parser) error { + c := parser.Command + + var required []*Option + + for c != nil { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + if !option.isSet && option.Required { + required = append(required, option) + } + } + }) + + c = c.Active + } + + if len(required) == 0 { + if len(p.positional) > 0 { + var reqnames []string + + for _, arg := range p.positional { + argRequired := (!arg.isRemaining() && p.command.ArgsRequired) || arg.Required != 0 + + if !argRequired { + continue + } + + if arg.isRemaining() { + if arg.value.Len() < arg.Required { + var arguments string + + if arg.Required > 1 { + arguments = "arguments, but got only " + fmt.Sprintf("%d", arg.value.Len()) + } else { + arguments = "argument" + } + + reqnames = append(reqnames, "`"+arg.Name+" (at least "+fmt.Sprintf("%d", arg.Required)+" "+arguments+")`") + } + } else { + reqnames = append(reqnames, "`"+arg.Name+"`") + } + } + + if len(reqnames) == 0 { + return nil + } + + var msg string + + if len(reqnames) == 1 { + msg = fmt.Sprintf("the required argument %s was not provided", reqnames[0]) + } else { + msg = fmt.Sprintf("the required arguments %s and %s were not provided", + strings.Join(reqnames[:len(reqnames)-1], ", "), reqnames[len(reqnames)-1]) + } + + p.err = newError(ErrRequired, msg) + return p.err + } + + return nil + } + + names := make([]string, 0, len(required)) + + for _, k := range required { + names = append(names, "`"+k.String()+"'") + } + + sort.Strings(names) + + var msg string + + if len(names) == 1 { + msg = fmt.Sprintf("the required flag %s was not specified", names[0]) + } else { + msg = fmt.Sprintf("the required flags %s and %s were not specified", + strings.Join(names[:len(names)-1], ", "), names[len(names)-1]) + } + + p.err = newError(ErrRequired, msg) + return p.err +} + +func (p *parseState) estimateCommand() error { + commands := p.command.sortedVisibleCommands() + cmdnames := make([]string, len(commands)) + + for i, v := range commands { + cmdnames[i] = v.Name + } + + var msg string + var errtype ErrorType + + if len(p.retargs) != 0 { + c, l := closestChoice(p.retargs[0], cmdnames) + msg = fmt.Sprintf("Unknown command `%s'", p.retargs[0]) + errtype = ErrUnknownCommand + + if float32(l)/float32(len(c)) < 0.5 { + msg = fmt.Sprintf("%s, did you mean `%s'?", msg, c) + } else if len(cmdnames) == 1 { + msg = fmt.Sprintf("%s. You should use the %s command", + msg, + cmdnames[0]) + } else { + msg = fmt.Sprintf("%s. Please specify one command of: %s or %s", + msg, + strings.Join(cmdnames[:len(cmdnames)-1], ", "), + cmdnames[len(cmdnames)-1]) + } + } else { + errtype = ErrCommandRequired + + if len(cmdnames) == 1 { + msg = fmt.Sprintf("Please specify the %s command", cmdnames[0]) + } else { + msg = fmt.Sprintf("Please specify one command of: %s or %s", + strings.Join(cmdnames[:len(cmdnames)-1], ", "), + cmdnames[len(cmdnames)-1]) + } + } + + return newError(errtype, msg) +} + +func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) { + if !option.canArgument() { + if argument != nil { + return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) + } + + err = option.set(nil) + } else if argument != nil || (canarg && !s.eof()) { + var arg string + + if argument != nil { + arg = *argument + } else { + arg = s.pop() + + if argumentIsOption(arg) { + return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got option `%s'", option, arg) + } else if p.Options&PassDoubleDash != 0 && arg == "--" { + return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got double dash `--'", option) + } + } + + if option.tag.Get("unquote") != "false" { + arg, err = unquoteIfPossible(arg) + } + + if err == nil { + err = option.set(&arg) + } + } else if option.OptionalArgument { + option.empty() + + for _, v := range option.OptionalValue { + err = option.set(&v) + + if err != nil { + break + } + } + } else { + err = newErrorf(ErrExpectedArgument, "expected argument for flag `%s'", option) + } + + if err != nil { + if _, ok := err.(*Error); !ok { + err = newErrorf(ErrMarshal, "invalid argument for flag `%s' (expected %s): %s", + option, + option.value.Type(), + err.Error()) + } + } + + return err +} + +func (p *Parser) parseLong(s *parseState, name string, argument *string) error { + if option := s.lookup.longNames[name]; option != nil { + // Only long options that are required can consume an argument + // from the argument list + canarg := !option.OptionalArgument + + return p.parseOption(s, name, option, canarg, argument) + } + + return newErrorf(ErrUnknownFlag, "unknown flag `%s'", name) +} + +func (p *Parser) splitShortConcatArg(s *parseState, optname string) (string, *string) { + c, n := utf8.DecodeRuneInString(optname) + + if n == len(optname) { + return optname, nil + } + + first := string(c) + + if option := s.lookup.shortNames[first]; option != nil && option.canArgument() { + arg := optname[n:] + return first, &arg + } + + return optname, nil +} + +func (p *Parser) parseShort(s *parseState, optname string, argument *string) error { + if argument == nil { + optname, argument = p.splitShortConcatArg(s, optname) + } + + for i, c := range optname { + shortname := string(c) + + if option := s.lookup.shortNames[shortname]; option != nil { + // Only the last short argument can consume an argument from + // the arguments list, and only if it's non optional + canarg := (i+utf8.RuneLen(c) == len(optname)) && !option.OptionalArgument + + if err := p.parseOption(s, shortname, option, canarg, argument); err != nil { + return err + } + } else { + return newErrorf(ErrUnknownFlag, "unknown flag `%s'", shortname) + } + + // Only the first option can have a concatted argument, so just + // clear argument here + argument = nil + } + + return nil +} + +func (p *parseState) addArgs(args ...string) error { + for len(p.positional) > 0 && len(args) > 0 { + arg := p.positional[0] + + if err := convert(args[0], arg.value, arg.tag); err != nil { + return err + } + + if !arg.isRemaining() { + p.positional = p.positional[1:] + } + + args = args[1:] + } + + p.retargs = append(p.retargs, args...) + return nil +} + +func (p *Parser) parseNonOption(s *parseState) error { + if len(s.positional) > 0 { + return s.addArgs(s.arg) + } + + if cmd := s.lookup.commands[s.arg]; cmd != nil { + s.command.Active = cmd + cmd.fillParseState(s) + } else if (p.Options & PassAfterNonOption) != None { + // If PassAfterNonOption is set then all remaining arguments + // are considered positional + if err := s.addArgs(s.arg); err != nil { + return err + } + + if err := s.addArgs(s.args...); err != nil { + return err + } + + s.args = []string{} + } else { + return s.addArgs(s.arg) + } + + return nil +} + +func (p *Parser) showBuiltinHelp() error { + var b bytes.Buffer + + p.WriteHelp(&b) + return newError(ErrHelp, b.String()) +} + +func (p *Parser) printError(err error) error { + if err != nil && (p.Options&PrintErrors) != None { + fmt.Fprintln(os.Stderr, err) + } + + return err +} + +func (p *Parser) clearIsSet() { + p.eachCommand(func(c *Command) { + c.eachGroup(func(g *Group) { + for _, option := range g.options { + option.isSet = false + } + }) + }, true) +} diff --git a/parser_private.go b/parser_private.go deleted file mode 100644 index 76be4a7..0000000 --- a/parser_private.go +++ /dev/null @@ -1,340 +0,0 @@ -package flags - -import ( - "bytes" - "fmt" - "os" - "sort" - "strings" - "unicode/utf8" -) - -type parseState struct { - arg string - args []string - retargs []string - positional []*Arg - err error - - command *Command - lookup lookup -} - -func (p *parseState) eof() bool { - return len(p.args) == 0 -} - -func (p *parseState) pop() string { - if p.eof() { - return "" - } - - p.arg = p.args[0] - p.args = p.args[1:] - - return p.arg -} - -func (p *parseState) peek() string { - if p.eof() { - return "" - } - - return p.args[0] -} - -func (p *parseState) checkRequired(parser *Parser) error { - c := parser.Command - - var required []*Option - - for c != nil { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - if !option.isSet && option.Required { - required = append(required, option) - } - } - }) - - c = c.Active - } - - if len(required) == 0 { - if len(p.positional) > 0 && p.command.ArgsRequired { - var reqnames []string - - for _, arg := range p.positional { - if arg.isRemaining() { - break - } - - reqnames = append(reqnames, "`"+arg.Name+"`") - } - - if len(reqnames) == 0 { - return nil - } - - var msg string - - if len(reqnames) == 1 { - msg = fmt.Sprintf("the required argument %s was not provided", reqnames[0]) - } else { - msg = fmt.Sprintf("the required arguments %s and %s were not provided", - strings.Join(reqnames[:len(reqnames)-1], ", "), reqnames[len(reqnames)-1]) - } - - p.err = newError(ErrRequired, msg) - return p.err - } - - return nil - } - - names := make([]string, 0, len(required)) - - for _, k := range required { - names = append(names, "`"+k.String()+"'") - } - - sort.Strings(names) - - var msg string - - if len(names) == 1 { - msg = fmt.Sprintf("the required flag %s was not specified", names[0]) - } else { - msg = fmt.Sprintf("the required flags %s and %s were not specified", - strings.Join(names[:len(names)-1], ", "), names[len(names)-1]) - } - - p.err = newError(ErrRequired, msg) - return p.err -} - -func (p *parseState) estimateCommand() error { - commands := p.command.sortedCommands() - cmdnames := make([]string, len(commands)) - - for i, v := range commands { - cmdnames[i] = v.Name - } - - var msg string - var errtype ErrorType - - if len(p.retargs) != 0 { - c, l := closestChoice(p.retargs[0], cmdnames) - msg = fmt.Sprintf("Unknown command `%s'", p.retargs[0]) - errtype = ErrUnknownCommand - - if float32(l)/float32(len(c)) < 0.5 { - msg = fmt.Sprintf("%s, did you mean `%s'?", msg, c) - } else if len(cmdnames) == 1 { - msg = fmt.Sprintf("%s. You should use the %s command", - msg, - cmdnames[0]) - } else { - msg = fmt.Sprintf("%s. Please specify one command of: %s or %s", - msg, - strings.Join(cmdnames[:len(cmdnames)-1], ", "), - cmdnames[len(cmdnames)-1]) - } - } else { - errtype = ErrCommandRequired - - if len(cmdnames) == 1 { - msg = fmt.Sprintf("Please specify the %s command", cmdnames[0]) - } else { - msg = fmt.Sprintf("Please specify one command of: %s or %s", - strings.Join(cmdnames[:len(cmdnames)-1], ", "), - cmdnames[len(cmdnames)-1]) - } - } - - return newError(errtype, msg) -} - -func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (err error) { - if !option.canArgument() { - if argument != nil { - return newErrorf(ErrNoArgumentForBool, "bool flag `%s' cannot have an argument", option) - } - - err = option.set(nil) - } else if argument != nil || (canarg && !s.eof()) { - var arg string - - if argument != nil { - arg = *argument - } else { - arg = s.pop() - - if argumentIsOption(arg) { - return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got option `%s'", option, arg) - } else if p.Options&PassDoubleDash != 0 && arg == "--" { - return newErrorf(ErrExpectedArgument, "expected argument for flag `%s', but got double dash `--'", option) - } - } - - if option.tag.Get("unquote") != "false" { - arg, err = unquoteIfPossible(arg) - } - - if err == nil { - err = option.set(&arg) - } - } else if option.OptionalArgument { - option.empty() - - for _, v := range option.OptionalValue { - err = option.set(&v) - - if err != nil { - break - } - } - } else { - err = newErrorf(ErrExpectedArgument, "expected argument for flag `%s'", option) - } - - if err != nil { - if _, ok := err.(*Error); !ok { - err = newErrorf(ErrMarshal, "invalid argument for flag `%s' (expected %s): %s", - option, - option.value.Type(), - err.Error()) - } - } - - return err -} - -func (p *Parser) parseLong(s *parseState, name string, argument *string) error { - if option := s.lookup.longNames[name]; option != nil { - // Only long options that are required can consume an argument - // from the argument list - canarg := !option.OptionalArgument - - return p.parseOption(s, name, option, canarg, argument) - } - - return newErrorf(ErrUnknownFlag, "unknown flag `%s'", name) -} - -func (p *Parser) splitShortConcatArg(s *parseState, optname string) (string, *string) { - c, n := utf8.DecodeRuneInString(optname) - - if n == len(optname) { - return optname, nil - } - - first := string(c) - - if option := s.lookup.shortNames[first]; option != nil && option.canArgument() { - arg := optname[n:] - return first, &arg - } - - return optname, nil -} - -func (p *Parser) parseShort(s *parseState, optname string, argument *string) error { - if argument == nil { - optname, argument = p.splitShortConcatArg(s, optname) - } - - for i, c := range optname { - shortname := string(c) - - if option := s.lookup.shortNames[shortname]; option != nil { - // Only the last short argument can consume an argument from - // the arguments list, and only if it's non optional - canarg := (i+utf8.RuneLen(c) == len(optname)) && !option.OptionalArgument - - if err := p.parseOption(s, shortname, option, canarg, argument); err != nil { - return err - } - } else { - return newErrorf(ErrUnknownFlag, "unknown flag `%s'", shortname) - } - - // Only the first option can have a concatted argument, so just - // clear argument here - argument = nil - } - - return nil -} - -func (p *parseState) addArgs(args ...string) error { - for len(p.positional) > 0 && len(args) > 0 { - arg := p.positional[0] - - if err := convert(args[0], arg.value, arg.tag); err != nil { - return err - } - - if !arg.isRemaining() { - p.positional = p.positional[1:] - } - - args = args[1:] - } - - p.retargs = append(p.retargs, args...) - return nil -} - -func (p *Parser) parseNonOption(s *parseState) error { - if len(s.positional) > 0 { - return s.addArgs(s.arg) - } - - if cmd := s.lookup.commands[s.arg]; cmd != nil { - s.command.Active = cmd - cmd.fillParseState(s) - } else if (p.Options & PassAfterNonOption) != None { - // If PassAfterNonOption is set then all remaining arguments - // are considered positional - if err := s.addArgs(s.arg); err != nil { - return err - } - - if err := s.addArgs(s.args...); err != nil { - return err - } - - s.args = []string{} - } else { - return s.addArgs(s.arg) - } - - return nil -} - -func (p *Parser) showBuiltinHelp() error { - var b bytes.Buffer - - p.WriteHelp(&b) - return newError(ErrHelp, b.String()) -} - -func (p *Parser) printError(err error) error { - if err != nil && (p.Options&PrintErrors) != None { - fmt.Fprintln(os.Stderr, err) - } - - return err -} - -func (p *Parser) clearIsSet() { - p.eachCommand(func(c *Command) { - c.eachGroup(func(g *Group) { - for _, option := range g.options { - option.isSet = false - } - }) - }, true) -} diff --git a/parser_test.go b/parser_test.go index 5792872..32afc69 100644 --- a/parser_test.go +++ b/parser_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "reflect" + "runtime" "strconv" "strings" "testing" @@ -14,6 +15,11 @@ type defaultOptions struct { Int int `long:"i"` IntDefault int `long:"id" default:"1"` + Float64 float64 `long:"f"` + Float64Default float64 `long:"fd" default:"-3.14"` + + NumericFlag bool `short:"3"` + String string `long:"str"` StringDefault string `long:"strd" default:"abc"` StringNotUnquoted string `long:"strnot" unquote:"false"` @@ -41,6 +47,11 @@ func TestDefaults(t *testing.T) { Int: 0, IntDefault: 1, + Float64: 0.0, + Float64Default: -3.14, + + NumericFlag: false, + String: "", StringDefault: "abc", @@ -56,11 +67,16 @@ func TestDefaults(t *testing.T) { }, { msg: "non-zero value arguments, expecting overwritten arguments", - args: []string{"--i=3", "--id=3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, + args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "-3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, expected: defaultOptions{ Int: 3, IntDefault: 3, + Float64: -2.71, + Float64Default: 2.71, + + NumericFlag: true, + String: "def", StringDefault: "def", @@ -76,11 +92,14 @@ func TestDefaults(t *testing.T) { }, { msg: "zero value arguments, expecting overwritten arguments", - args: []string{"--i=0", "--id=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, + args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, expected: defaultOptions{ Int: 0, IntDefault: 0, + Float64: 0, + Float64Default: 0, + String: "", StringDefault: "", @@ -114,6 +133,18 @@ func TestDefaults(t *testing.T) { } } +func TestNoDefaultsForBools(t *testing.T) { + var opts struct { + DefaultBool bool `short:"d" default:"true"` + } + + if runtime.GOOS == "windows" { + assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts) + } else { + assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts) + } +} + func TestUnquoting(t *testing.T) { var tests = []struct { arg string @@ -320,21 +351,21 @@ func TestOptionAsArgument(t *testing.T) { args: []string{"--string-slice", "foobar", "--string-slice", "-o"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got option `-o'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'", }, { // long option must not be accepted as argument args: []string{"--string-slice", "foobar", "--string-slice", "--other-option"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got option `--other-option'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'", }, { // long option must not be accepted as argument args: []string{"--string-slice", "--"}, expectError: true, errType: ErrExpectedArgument, - errMsg: "expected argument for flag `--string-slice', but got double dash `--'", + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'", }, { // quoted and appended option should be accepted as argument (even if it looks like an option) @@ -344,6 +375,20 @@ func TestOptionAsArgument(t *testing.T) { // Accept any single character arguments including '-' args: []string{"--string-slice", "-"}, }, + { + // Do not accept arguments which start with '-' even if the next character is a digit + args: []string{"--string-slice", "-3.14"}, + expectError: true, + errType: ErrExpectedArgument, + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'", + }, + { + // Do not accept arguments which start with '-' if the next character is not a digit + args: []string{"--string-slice", "-character"}, + expectError: true, + errType: ErrExpectedArgument, + errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'", + }, { args: []string{"-o", "-", "-"}, rest: []string{"-", "-"}, @@ -429,3 +474,27 @@ func TestUnknownFlagHandler(t *testing.T) { assertErrorf(t, "Parser should have returned error, but returned nil") } } + +func TestChoices(t *testing.T) { + var opts struct { + Choice string `long:"choose" choice:"v1" choice:"v2"` + } + + assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid") + assertParseSuccess(t, &opts, "--choose", "v2") + assertString(t, opts.Choice, "v2") +} + +func TestEmbedded(t *testing.T) { + type embedded struct { + V bool `short:"v"` + } + var opts struct { + embedded + } + + assertParseSuccess(t, &opts, "-v") + if !opts.V { + t.Errorf("Expected V to be true") + } +}