From cd2040a7d23b2ae91869e239f3ed6395dc9d3b76 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 23 Dec 2013 11:18:46 -0500 Subject: [PATCH] Pull in go-flags, modified to build on Solaris --- github.com/jessevdk/go-flags/LICENSE | 26 ++ github.com/jessevdk/go-flags/README.md | 128 +++++++ github.com/jessevdk/go-flags/assert_test.go | 82 +++++ .../jessevdk/go-flags/check_crosscompile.sh | 16 + github.com/jessevdk/go-flags/closest.go | 61 ++++ github.com/jessevdk/go-flags/command.go | 84 +++++ .../jessevdk/go-flags/command_private.go | 161 +++++++++ github.com/jessevdk/go-flags/command_test.go | 255 ++++++++++++++ github.com/jessevdk/go-flags/convert.go | 315 +++++++++++++++++ github.com/jessevdk/go-flags/error.go | 113 ++++++ github.com/jessevdk/go-flags/example_test.go | 95 +++++ github.com/jessevdk/go-flags/examples/add.go | 23 ++ github.com/jessevdk/go-flags/examples/main.go | 75 ++++ github.com/jessevdk/go-flags/examples/rm.go | 23 ++ github.com/jessevdk/go-flags/flags.go | 141 ++++++++ github.com/jessevdk/go-flags/group.go | 80 +++++ github.com/jessevdk/go-flags/group_private.go | 263 ++++++++++++++ github.com/jessevdk/go-flags/group_test.go | 160 +++++++++ github.com/jessevdk/go-flags/help.go | 275 +++++++++++++++ github.com/jessevdk/go-flags/help_test.go | 153 ++++++++ github.com/jessevdk/go-flags/ini.go | 146 ++++++++ github.com/jessevdk/go-flags/ini_private.go | 333 ++++++++++++++++++ github.com/jessevdk/go-flags/ini_test.go | 170 +++++++++ github.com/jessevdk/go-flags/long_test.go | 85 +++++ github.com/jessevdk/go-flags/man.go | 134 +++++++ github.com/jessevdk/go-flags/marshal_test.go | 78 ++++ github.com/jessevdk/go-flags/multitag.go | 140 ++++++++ github.com/jessevdk/go-flags/option.go | 95 +++++ .../jessevdk/go-flags/option_private.go | 125 +++++++ github.com/jessevdk/go-flags/options_test.go | 45 +++ .../jessevdk/go-flags/optstyle_other.go | 54 +++ .../jessevdk/go-flags/optstyle_windows.go | 85 +++++ github.com/jessevdk/go-flags/parser.go | 212 +++++++++++ .../jessevdk/go-flags/parser_private.go | 243 +++++++++++++ github.com/jessevdk/go-flags/pointer_test.go | 81 +++++ github.com/jessevdk/go-flags/short_test.go | 169 +++++++++ github.com/jessevdk/go-flags/tag_test.go | 39 ++ github.com/jessevdk/go-flags/termsize.go | 5 + github.com/jessevdk/go-flags/unknown_test.go | 66 ++++ main.go | 2 +- 40 files changed, 4835 insertions(+), 1 deletion(-) create mode 100644 github.com/jessevdk/go-flags/LICENSE create mode 100644 github.com/jessevdk/go-flags/README.md create mode 100644 github.com/jessevdk/go-flags/assert_test.go create mode 100755 github.com/jessevdk/go-flags/check_crosscompile.sh create mode 100644 github.com/jessevdk/go-flags/closest.go create mode 100644 github.com/jessevdk/go-flags/command.go create mode 100644 github.com/jessevdk/go-flags/command_private.go create mode 100644 github.com/jessevdk/go-flags/command_test.go create mode 100644 github.com/jessevdk/go-flags/convert.go create mode 100644 github.com/jessevdk/go-flags/error.go create mode 100644 github.com/jessevdk/go-flags/example_test.go create mode 100644 github.com/jessevdk/go-flags/examples/add.go create mode 100644 github.com/jessevdk/go-flags/examples/main.go create mode 100644 github.com/jessevdk/go-flags/examples/rm.go create mode 100644 github.com/jessevdk/go-flags/flags.go create mode 100644 github.com/jessevdk/go-flags/group.go create mode 100644 github.com/jessevdk/go-flags/group_private.go create mode 100644 github.com/jessevdk/go-flags/group_test.go create mode 100644 github.com/jessevdk/go-flags/help.go create mode 100644 github.com/jessevdk/go-flags/help_test.go create mode 100644 github.com/jessevdk/go-flags/ini.go create mode 100644 github.com/jessevdk/go-flags/ini_private.go create mode 100644 github.com/jessevdk/go-flags/ini_test.go create mode 100644 github.com/jessevdk/go-flags/long_test.go create mode 100644 github.com/jessevdk/go-flags/man.go create mode 100644 github.com/jessevdk/go-flags/marshal_test.go create mode 100644 github.com/jessevdk/go-flags/multitag.go create mode 100644 github.com/jessevdk/go-flags/option.go create mode 100644 github.com/jessevdk/go-flags/option_private.go create mode 100644 github.com/jessevdk/go-flags/options_test.go create mode 100644 github.com/jessevdk/go-flags/optstyle_other.go create mode 100644 github.com/jessevdk/go-flags/optstyle_windows.go create mode 100644 github.com/jessevdk/go-flags/parser.go create mode 100644 github.com/jessevdk/go-flags/parser_private.go create mode 100644 github.com/jessevdk/go-flags/pointer_test.go create mode 100644 github.com/jessevdk/go-flags/short_test.go create mode 100644 github.com/jessevdk/go-flags/tag_test.go create mode 100644 github.com/jessevdk/go-flags/termsize.go create mode 100644 github.com/jessevdk/go-flags/unknown_test.go diff --git a/github.com/jessevdk/go-flags/LICENSE b/github.com/jessevdk/go-flags/LICENSE new file mode 100644 index 000000000..bcca0d521 --- /dev/null +++ b/github.com/jessevdk/go-flags/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2012 Jesse van den Kieboom. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/github.com/jessevdk/go-flags/README.md b/github.com/jessevdk/go-flags/README.md new file mode 100644 index 000000000..4194e2c41 --- /dev/null +++ b/github.com/jessevdk/go-flags/README.md @@ -0,0 +1,128 @@ +go-flags: a go library for parsing command line arguments +========================================================= + +This library provides similar functionality to the builtin flag library of +go, but provides much more functionality and nicer formatting. From the +documentation: + +Package flags provides an extensive command line option parser. +The flags package is similar in functionality to the go builtin flag package +but provides more options and uses reflection to provide a convenient and +succinct way of specifying command line options. + +Supported features: +* Options with short names (-v) +* Options with long names (--verbose) +* Options with and without arguments (bool v.s. other type) +* Options with optional arguments and default values +* Multiple option groups each containing a set of options +* Generate and print well-formatted help message +* Passing remaining command line arguments after -- (optional) +* Ignoring unknown command line options (optional) +* Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification +* Supports multiple short options -aux +* Supports all primitive go types (string, int{8..64}, uint{8..64}, float) +* Supports same option multiple times (can store in slice or last option counts) +* Supports maps +* Supports function callbacks + +The flags package uses structs, reflection and struct field tags +to allow users to specify command line options. This results in very simple +and consise specification of your application options. For example: + + 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 +will be appended to the Verbose field. e.g. when specifying -vvv, the +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 + +More information can be found in the godocs: diff --git a/github.com/jessevdk/go-flags/assert_test.go b/github.com/jessevdk/go-flags/assert_test.go new file mode 100644 index 000000000..49d093c34 --- /dev/null +++ b/github.com/jessevdk/go-flags/assert_test.go @@ -0,0 +1,82 @@ +package flags + +import ( + "testing" +) + +func assertString(t *testing.T, a string, b string) { + if a != b { + t.Errorf("Expected %#v, but got %#v", b, a) + } +} +func assertStringArray(t *testing.T, a []string, b []string) { + if len(a) != len(b) { + t.Errorf("Expected %#v, but got %#v", b, a) + return + } + + for i, v := range a { + if b[i] != v { + t.Errorf("Expected %#v, but got %#v", b, a) + return + } + } +} + +func assertBoolArray(t *testing.T, a []bool, b []bool) { + if len(a) != len(b) { + t.Errorf("Expected %#v, but got %#v", b, a) + return + } + + for i, v := range a { + if b[i] != v { + t.Errorf("Expected %#v, but got %#v", b, a) + return + } + } +} + +func assertParserSuccess(t *testing.T, data interface{}, args ...string) (*Parser, []string) { + parser := NewParser(data, Default&^PrintErrors) + ret, err := parser.ParseArgs(args) + + if err != nil { + t.Fatalf("Unexpected parse error: %s", err) + return nil, nil + } + + return parser, ret +} + +func assertParseSuccess(t *testing.T, data interface{}, args ...string) []string { + _, ret := assertParserSuccess(t, data, args...) + return ret +} + +func assertError(t *testing.T, err error, typ ErrorType, msg string) { + if err == nil { + t.Fatalf("Expected error: %s", msg) + return + } + + if e, ok := err.(*Error); !ok { + t.Fatalf("Expected Error type, but got %#v", err) + return + } else { + if e.Type != typ { + t.Errorf("Expected error type {%s}, but got {%s}", typ, e.Type) + } + + if e.Message != msg { + t.Errorf("Expected error message %#v, but got %#v", msg, e.Message) + } + } +} + +func assertParseFail(t *testing.T, typ ErrorType, msg string, data interface{}, args ...string) { + parser := NewParser(data, Default&^PrintErrors) + _, err := parser.ParseArgs(args) + + assertError(t, err, typ, msg) +} diff --git a/github.com/jessevdk/go-flags/check_crosscompile.sh b/github.com/jessevdk/go-flags/check_crosscompile.sh new file mode 100755 index 000000000..c494f6119 --- /dev/null +++ b/github.com/jessevdk/go-flags/check_crosscompile.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +echo '# linux arm7' +GOARM=7 GOARCH=arm GOOS=linux go build +echo '# linux arm5' +GOARM=5 GOARCH=arm GOOS=linux go build +echo '# windows 386' +GOARCH=386 GOOS=windows go build +echo '# windows amd64' +GOARCH=amd64 GOOS=windows go build +echo '# darwin' +GOARCH=amd64 GOOS=darwin go build +echo '# freebsd' +GOARCH=amd64 GOOS=freebsd go build diff --git a/github.com/jessevdk/go-flags/closest.go b/github.com/jessevdk/go-flags/closest.go new file mode 100644 index 000000000..ba6f5f0c6 --- /dev/null +++ b/github.com/jessevdk/go-flags/closest.go @@ -0,0 +1,61 @@ +package flags + +func levenshtein(s string, t string) int { + if len(s) == 0 { + return len(t) + } + + if len(t) == 0 { + return len(s) + } + + var l1, l2, l3 int + + if len(s) == 1 { + l1 = len(t) + 1 + } else { + l1 = levenshtein(s[1:len(s)-1], t) + 1 + } + + if len(t) == 1 { + l2 = len(s) + 1 + } else { + l2 = levenshtein(t[1:len(t)-1], s) + 1 + } + + l3 = levenshtein(s[1:len(s)], t[1:len(t)]) + + if s[0] != t[0] { + l3 += 1 + } + + if l2 < l1 { + l1 = l2 + } + + if l1 < l3 { + return l1 + } + + return l3 +} + +func closestChoice(cmd string, choices []string) (string, int) { + if len(choices) == 0 { + return "", 0 + } + + mincmd := -1 + mindist := -1 + + for i, c := range choices { + l := levenshtein(cmd, c) + + if mincmd < 0 || l < mindist { + mindist = l + mincmd = i + } + } + + return choices[mincmd], mindist +} diff --git a/github.com/jessevdk/go-flags/command.go b/github.com/jessevdk/go-flags/command.go new file mode 100644 index 000000000..5e99cd663 --- /dev/null +++ b/github.com/jessevdk/go-flags/command.go @@ -0,0 +1,84 @@ +package flags + +// 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 +// therefore also carries a set of command specific options. +type Command struct { + // Embedded, see Group for more information + *Group + + // The name by which the command can be invoked + Name string + + // The active sub command (set by parsing) or nil + Active *Command + + commands []*Command + hasBuiltinHelpGroup bool +} + +// Commander is an interface which can be implemented by any command added in +// the options. When implemented, the Execute method will be called for the last +// specified (sub)command providing the remaining command line arguments. +type Commander interface { + // Execute will be called for the last active (sub)command. The + // args argument contains the remaining command line arguments. The + // error that Execute returns will be eventually passed out of the + // Parse method of the Parser. + Execute(args []string) error +} + +// Usage is an interface which can be implemented to show a custom usage string +// in the help message shown for a command. +type Usage interface { + // Usage is called for commands to allow customized printing of command + // usage in the generated help message. + Usage() string +} + +// 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 +// Usage interfaces. +func (c *Command) AddCommand(command string, shortDescription string, longDescription string, data interface{}) (*Command, error) { + cmd := newCommand(command, shortDescription, longDescription, data) + + if err := cmd.scan(); err != nil { + return nil, err + } + + c.commands = append(c.commands, cmd) + return cmd, nil +} + +// 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. +func (c *Command) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) { + group := newGroup(shortDescription, longDescription, data) + + if err := group.scanType(c.scanSubCommandHandler(group)); err != nil { + return nil, err + } + + c.groups = append(c.groups, group) + return group, nil +} + +// Commands returns a list of subcommands of this command. +func (c *Command) Commands() []*Command { + return c.commands +} + +// Find locates the subcommand with the given name and returns it. If no such +// command can be found Find will return nil. +func (c *Command) Find(name string) *Command { + for _, cc := range c.commands { + if cc.Name == name { + return cc + } + } + + return nil +} diff --git a/github.com/jessevdk/go-flags/command_private.go b/github.com/jessevdk/go-flags/command_private.go new file mode 100644 index 000000000..dcb82fd87 --- /dev/null +++ b/github.com/jessevdk/go-flags/command_private.go @@ -0,0 +1,161 @@ +package flags + +import ( + "reflect" + "sort" + "strings" + "unsafe" +) + +type lookup struct { + shortNames map[string]*Option + longNames map[string]*Option + + required map[*Option]bool + 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 + } + + 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") + + if _, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()); err != nil { + return true, err + } + + 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(g *Group)) { + c.eachGroup(f) + + 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), + + required: make(map[*Option]bool), + commands: make(map[string]*Command), + } + + c.eachGroup(func(g *Group) { + for _, option := range g.options { + if option.Required && option.canCli() { + ret.required[option] = true + } + + if option.ShortName != 0 { + ret.shortNames[string(option.ShortName)] = option + } + + if len(option.LongName) > 0 { + ret.longNames[option.LongName] = option + } + } + }) + + for _, subcommand := range c.commands { + ret.commands[subcommand.Name] = 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) +} diff --git a/github.com/jessevdk/go-flags/command_test.go b/github.com/jessevdk/go-flags/command_test.go new file mode 100644 index 000000000..f58031d06 --- /dev/null +++ b/github.com/jessevdk/go-flags/command_test.go @@ -0,0 +1,255 @@ +package flags + +import ( + "testing" +) + +func TestCommandInline(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + G bool `short:"g"` + } `command:"cmd"` + }{} + + p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g") + + assertStringArray(t, ret, []string{}) + + if p.Active == nil { + t.Errorf("Expected active command") + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Command.G { + t.Errorf("Expected Command.G to be true") + } + + if p.Command.Find("cmd") != p.Active { + t.Errorf("Expected to find command `cmd' to be active") + } +} + +func TestCommandInlineMulti(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + C1 struct { + } `command:"c1"` + + C2 struct { + G bool `short:"g"` + } `command:"c2"` + }{} + + p, ret := assertParserSuccess(t, &opts, "-v", "c2", "-g") + + assertStringArray(t, ret, []string{}) + + if p.Active == nil { + t.Errorf("Expected active command") + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.C2.G { + t.Errorf("Expected C2.G to be true") + } + + if p.Command.Find("c1") == nil { + t.Errorf("Expected to find command `c1'") + } + + if c2 := p.Command.Find("c2"); c2 == nil { + t.Errorf("Expected to find command `c2'") + } else if c2 != p.Active { + t.Errorf("Expected to find command `c2' to be active") + } +} + +func TestCommandFlagOrder1(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + G bool `short:"g"` + } `command:"cmd"` + }{} + + assertParseFail(t, ErrUnknownFlag, "unknown flag `g'", &opts, "-v", "-g", "cmd") +} + +func TestCommandFlagOrder2(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + G bool `short:"g"` + } `command:"cmd"` + }{} + + assertParseFail(t, ErrUnknownFlag, "unknown flag `v'", &opts, "cmd", "-v", "-g") +} + +func TestCommandEstimate(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Cmd1 struct { + } `command:"remove"` + + Cmd2 struct { + } `command:"add"` + }{} + + p := NewParser(&opts, None) + _, err := p.ParseArgs([]string{}) + + assertError(t, err, ErrRequired, "Please specify one command of: add or remove") +} + +type testCommand struct { + G bool `short:"g"` + Executed bool + EArgs []string +} + +func (c *testCommand) Execute(args []string) error { + c.Executed = true + c.EArgs = args + + return nil +} + +func TestCommandExecute(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command testCommand `command:"cmd"` + }{} + + assertParseSuccess(t, &opts, "-v", "cmd", "-g", "a", "b") + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Command.Executed { + t.Errorf("Did not execute command") + } + + if !opts.Command.G { + t.Errorf("Expected Command.C to be true") + } + + assertStringArray(t, opts.Command.EArgs, []string{"a", "b"}) +} + +func TestCommandClosest(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Cmd1 struct { + } `command:"remove"` + + Cmd2 struct { + } `command:"add"` + }{} + + assertParseFail(t, ErrRequired, "Unknown command `addd', did you mean `add'?", &opts, "-v", "addd") +} + +func TestCommandAdd(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + }{} + + var cmd = struct { + G bool `short:"g"` + }{} + + p := NewParser(&opts, Default) + c, err := p.AddCommand("cmd", "", "", &cmd) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + ret, err := p.ParseArgs([]string{"-v", "cmd", "-g", "rest"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + assertStringArray(t, ret, []string{"rest"}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !cmd.G { + t.Errorf("Expected Command.G to be true") + } + + if p.Command.Find("cmd") != c { + t.Errorf("Expected to find command `cmd'") + } + + if p.Commands()[0] != c { + t.Errorf("Espected command #v, but got #v", c, p.Commands()[0]) + } + + if c.Options()[0].ShortName != 'g' { + t.Errorf("Expected short name `g' but got %v", c.Options()[0].ShortName) + } +} + +func TestCommandNestedInline(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Command struct { + G bool `short:"g"` + + Nested struct { + N string `long:"n"` + } `command:"nested"` + } `command:"cmd"` + }{} + + p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g", "nested", "--n", "n", "rest") + + assertStringArray(t, ret, []string{"rest"}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Command.G { + t.Errorf("Expected Command.G to be true") + } + + assertString(t, opts.Command.Nested.N, "n") + + if c := p.Command.Find("cmd"); c == nil { + t.Errorf("Expected to find command `cmd'") + } else { + if c != p.Active { + t.Errorf("Expected `cmd' to be the active parser command") + } + + if nested := c.Find("nested"); nested == nil { + t.Errorf("Expected to find command `nested'") + } else if nested != c.Active { + t.Errorf("Expected to find command `nested' to be the active `cmd' command") + } + } +} diff --git a/github.com/jessevdk/go-flags/convert.go b/github.com/jessevdk/go-flags/convert.go new file mode 100644 index 000000000..948a50682 --- /dev/null +++ b/github.com/jessevdk/go-flags/convert.go @@ -0,0 +1,315 @@ +// Copyright 2012 Jesse van den Kieboom. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package flags + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +// Marshaler is the interface implemented by types that can marshal themselves +// to a string representation of the flag. +type Marshaler interface { + // MarshalFlag marshals a flag value to its string representation. + MarshalFlag() (string, error) +} + +// Unmarshaler is the interface implemented by types that can unmarshal a flag +// argument to themselves. The provided value is directly passed from the +// command line. +type Unmarshaler interface { + // UnmarshalFlag unmarshals a string value representation to the flag + // value (which therefore needs to be a pointer receiver). + UnmarshalFlag(value string) error +} + +func getBase(options multiTag, base int) (int, error) { + sbase := options.Get("base") + + var err error + var ivbase int64 + + if sbase != "" { + ivbase, err = strconv.ParseInt(sbase, 10, 32) + base = int(ivbase) + } + + return base, err +} + +func convertMarshal(val reflect.Value) (bool, string, error) { + // Check first for the Marshaler interface + if val.Type().NumMethod() > 0 && val.CanInterface() { + if marshaler, ok := val.Interface().(Marshaler); ok { + ret, err := marshaler.MarshalFlag() + return true, ret, err + } + } + + return false, "", nil +} + +func convertToString(val reflect.Value, options multiTag) (string, error) { + if ok, ret, err := convertMarshal(val); ok { + return ret, err + } + + tp := val.Type() + + // Support for time.Duration + if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() { + stringer := val.Interface().(fmt.Stringer) + return stringer.String(), nil + } + + switch tp.Kind() { + case reflect.String: + return val.String(), nil + case reflect.Bool: + if val.Bool() { + return "true", nil + } + + return "false", nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + base, _ := getBase(options, 10) + return strconv.FormatInt(val.Int(), base), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + base, _ := getBase(options, 10) + return strconv.FormatUint(val.Uint(), base), nil + case reflect.Float32, reflect.Float64: + return strconv.FormatFloat(val.Float(), 'g', -1, tp.Bits()), nil + case reflect.Slice: + if val.Len() == 0 { + return "", nil + } + + ret := "[" + + for i := 0; i < val.Len(); i++ { + if i != 0 { + ret += ", " + } + + item, err := convertToString(val.Index(i), options) + + if err != nil { + return "", err + } + + ret += item + } + + return ret + "]", nil + case reflect.Map: + ret := "{" + + for i, key := range val.MapKeys() { + if i != 0 { + ret += ", " + } + + item, err := convertToString(val.MapIndex(key), options) + + if err != nil { + return "", err + } + + ret += item + } + + return ret + "}", nil + case reflect.Ptr: + return convertToString(reflect.Indirect(val), options) + case reflect.Interface: + if !val.IsNil() { + return convertToString(val.Elem(), options) + } + } + + return "", nil +} + +func convertUnmarshal(val string, retval reflect.Value) (bool, error) { + if retval.Type().NumMethod() > 0 && retval.CanInterface() { + if unmarshaler, ok := retval.Interface().(Unmarshaler); ok { + return true, unmarshaler.UnmarshalFlag(val) + } + } + + if retval.Type().Kind() != reflect.Ptr && retval.CanAddr() { + return convertUnmarshal(val, retval.Addr()) + } + + if retval.Type().Kind() == reflect.Interface && !retval.IsNil() { + return convertUnmarshal(val, retval.Elem()) + } + + return false, nil +} + +func convert(val string, retval reflect.Value, options multiTag) error { + if ok, err := convertUnmarshal(val, retval); ok { + return err + } + + tp := retval.Type() + + // Support for time.Duration + if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() { + parsed, err := time.ParseDuration(val) + + if err != nil { + return err + } + + retval.SetInt(int64(parsed)) + return nil + } + + switch tp.Kind() { + case reflect.String: + retval.SetString(val) + case reflect.Bool: + if val == "" { + retval.SetBool(true) + } else { + b, err := strconv.ParseBool(val) + + if err != nil { + return err + } + + retval.SetBool(b) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + base, err := getBase(options, 10) + + if err != nil { + return err + } + + parsed, err := strconv.ParseInt(val, base, tp.Bits()) + + if err != nil { + return err + } + + retval.SetInt(parsed) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + base, err := getBase(options, 10) + + if err != nil { + return err + } + + parsed, err := strconv.ParseUint(val, base, tp.Bits()) + + if err != nil { + return err + } + + retval.SetUint(parsed) + case reflect.Float32, reflect.Float64: + parsed, err := strconv.ParseFloat(val, tp.Bits()) + + if err != nil { + return err + } + + retval.SetFloat(parsed) + case reflect.Slice: + elemtp := tp.Elem() + + elemvalptr := reflect.New(elemtp) + elemval := reflect.Indirect(elemvalptr) + + if err := convert(val, elemval, options); err != nil { + return err + } + + retval.Set(reflect.Append(retval, elemval)) + case reflect.Map: + parts := strings.SplitN(val, ":", 2) + + key := parts[0] + var value string + + if len(parts) == 2 { + value = parts[1] + } + + keytp := tp.Key() + keyval := reflect.New(keytp) + + if err := convert(key, keyval, options); err != nil { + return err + } + + valuetp := tp.Elem() + valueval := reflect.New(valuetp) + + if err := convert(value, valueval, options); err != nil { + return err + } + + if retval.IsNil() { + retval.Set(reflect.MakeMap(tp)) + } + + retval.SetMapIndex(reflect.Indirect(keyval), reflect.Indirect(valueval)) + case reflect.Ptr: + if retval.IsNil() { + retval.Set(reflect.New(retval.Type().Elem())) + } + + return convert(val, reflect.Indirect(retval), options) + case reflect.Interface: + if !retval.IsNil() { + return convert(val, retval.Elem(), options) + } + } + + return nil +} + +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" + } + + if len(ret) != 0 { + ret += "\n" + prefix + } + + ret += strings.TrimSpace(s[:pos]) + suffix + s = strings.TrimSpace(s[pos:]) + } + + if len(s) > 0 { + if len(ret) != 0 { + ret += "\n" + prefix + } + + return ret + s + } + + return ret +} diff --git a/github.com/jessevdk/go-flags/error.go b/github.com/jessevdk/go-flags/error.go new file mode 100644 index 000000000..14db093e2 --- /dev/null +++ b/github.com/jessevdk/go-flags/error.go @@ -0,0 +1,113 @@ +package flags + +import ( + "fmt" +) + +// ErrorType represents the type of error. +type ErrorType uint + +const ( + // ErrUnknown indicates a generic error. + ErrUnknown ErrorType = iota + + // ErrExpectedArgument indicates that an argument was expected. + ErrExpectedArgument + + // ErrUnknownFlag indicates an unknown flag. + ErrUnknownFlag + + // ErrUnknownGroup indicates an unknown group. + ErrUnknownGroup + + // ErrMarshal indicates a marshalling error while converting values. + ErrMarshal + + // ErrHelp indicates that the builtin help was shown (the error + // contains the help message). + ErrHelp + + // ErrNoArgumentForBool indicates that an argument was given for a + // boolean flag (which don't not take any arguments). + ErrNoArgumentForBool + + // ErrRequired indicates that a required flag was not provided. + ErrRequired + + // ErrShortNameTooLong indicates that a short flag name was specified, + // longer than one character. + ErrShortNameTooLong + + // ErrDuplicatedFlag indicates that a short or long flag has been + // defined more than once + ErrDuplicatedFlag + + // ErrTag indicates an error while parsing flag tags. + ErrTag +) + +// String returns a string representation of the error type. +func (e ErrorType) String() string { + switch e { + case ErrUnknown: + return "unknown" + case ErrExpectedArgument: + return "expected argument" + case ErrUnknownFlag: + return "unknown flag" + case ErrUnknownGroup: + return "unknown group" + case ErrMarshal: + return "marshal" + case ErrHelp: + return "help" + case ErrNoArgumentForBool: + return "no argument for bool" + case ErrRequired: + return "required" + case ErrShortNameTooLong: + return "short name too long" + case ErrDuplicatedFlag: + return "duplicated flag" + case ErrTag: + return "tag" + } + + return "unknown" +} + +// Error represents a parser error. The error returned from Parse is of this +// type. The error contains both a Type and Message. +type Error struct { + // The type of error + Type ErrorType + + // The error message + Message string +} + +// Error returns the error's message +func (e *Error) Error() string { + return e.Message +} + +func newError(tp ErrorType, message string) *Error { + return &Error{ + Type: tp, + Message: message, + } +} + +func newErrorf(tp ErrorType, format string, args ...interface{}) *Error { + return newError(tp, fmt.Sprintf(format, args...)) +} + +func wrapError(err error) *Error { + ret, ok := err.(*Error) + + if !ok { + return newError(ErrUnknown, err.Error()) + } + + return ret +} diff --git a/github.com/jessevdk/go-flags/example_test.go b/github.com/jessevdk/go-flags/example_test.go new file mode 100644 index 000000000..5de7bff7e --- /dev/null +++ b/github.com/jessevdk/go-flags/example_test.go @@ -0,0 +1,95 @@ +// Example of use of the flags package. +package flags + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func 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 := 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 +} diff --git a/github.com/jessevdk/go-flags/examples/add.go b/github.com/jessevdk/go-flags/examples/add.go new file mode 100644 index 000000000..57d8f232b --- /dev/null +++ b/github.com/jessevdk/go-flags/examples/add.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" +) + +type AddCommand struct { + All bool `short:"a" long:"all" description:"Add all files"` +} + +var addCommand AddCommand + +func (x *AddCommand) Execute(args []string) error { + fmt.Printf("Adding (all=%v): %#v\n", x.All, args) + return nil +} + +func init() { + parser.AddCommand("add", + "Add a file", + "The add command adds a file to the repository. Use -a to add all files.", + &addCommand) +} diff --git a/github.com/jessevdk/go-flags/examples/main.go b/github.com/jessevdk/go-flags/examples/main.go new file mode 100644 index 000000000..7132bba12 --- /dev/null +++ b/github.com/jessevdk/go-flags/examples/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "errors" + "fmt" + "github.com/calmh/syncthing/github.com/jessevdk/go-flags" + "os" + "strconv" + "strings" +) + +type EditorOptions struct { + Input string `short:"i" long:"input" description:"Input file" default:"-"` + Output string `short:"o" long:"output" description:"Output file" default:"-"` +} + +type Point struct { + X, Y int +} + +func (p *Point) UnmarshalFlag(value string) error { + parts := strings.Split(value, ",") + + if len(parts) != 2 { + return errors.New("Expected two numbers separated by a ,") + } + + x, err := strconv.ParseInt(parts[0], 10, 32) + + if err != nil { + return err + } + + y, err := strconv.ParseInt(parts[1], 10, 32) + + if err != nil { + return err + } + + p.X = int(x) + p.Y = int(y) + + return nil +} + +func (p Point) MarshalFlag() (string, error) { + return fmt.Sprintf("%d,%d", p.X, p.Y), nil +} + +type Options struct { + // Example of verbosity with level + Verbose []bool `short:"v" long:"verbose" description:"Verbose output"` + + // Example of optional value + User string `short:"u" long:"user" description:"User name" optional:"yes" optional-value:"pancake"` + + // Example of map with multiple default values + Users map[string]string `long:"users" description:"User e-mail map" default:"system:system@example.org" default:"admin:admin@example.org"` + + // Example of option group + Editor EditorOptions `group:"Editor Options"` + + // Example of custom type Marshal/Unmarshal + Point Point `long:"point" description:"A x,y point" default:"1,2"` +} + +var options Options + +var parser = flags.NewParser(&options, flags.Default) + +func main() { + if _, err := parser.Parse(); err != nil { + os.Exit(1) + } +} diff --git a/github.com/jessevdk/go-flags/examples/rm.go b/github.com/jessevdk/go-flags/examples/rm.go new file mode 100644 index 000000000..c9c1dd03a --- /dev/null +++ b/github.com/jessevdk/go-flags/examples/rm.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" +) + +type RmCommand struct { + Force bool `short:"f" long:"force" description:"Force removal of files"` +} + +var rmCommand RmCommand + +func (x *RmCommand) Execute(args []string) error { + fmt.Printf("Removing (force=%v): %#v\n", x.Force, args) + return nil +} + +func init() { + parser.AddCommand("rm", + "Remove a file", + "The rm command removes a file to the repository. Use -f to force removal of files.", + &rmCommand) +} diff --git a/github.com/jessevdk/go-flags/flags.go b/github.com/jessevdk/go-flags/flags.go new file mode 100644 index 000000000..9b7728632 --- /dev/null +++ b/github.com/jessevdk/go-flags/flags.go @@ -0,0 +1,141 @@ +// Copyright 2012 Jesse van den Kieboom. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package flags provides an extensive command line option parser. +// The flags package is similar in functionality to the go builtin flag package +// but provides more options and uses reflection to provide a convenient and +// succinct way of specifying command line options. +// +// Supported features: +// Options with short names (-v) +// Options with long names (--verbose) +// Options with and without arguments (bool v.s. other type) +// Options with optional arguments and default values +// Multiple option groups each containing a set of options +// Generate and print well-formatted help message +// Passing remaining command line arguments after -- (optional) +// Ignoring unknown command line options (optional) +// Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification +// Supports multiple short options -aux +// Supports all primitive go types (string, int{8..64}, uint{8..64}, float) +// Supports same option multiple times (can store in slice or last option counts) +// Supports maps +// Supports function callbacks +// +// Additional features specific to Windows: +// Options with short names (/v) +// Options with long names (/verbose) +// Windows-style options with arguments use a colon as the delimiter +// Modify generated help message with Windows-style / options +// +// The flags package uses structs, reflection and struct field tags +// to allow users to specify command line options. This results in very simple +// and consise specification of your application options. For example: +// +// 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 +// will be appended to the Verbose field. e.g. when specifying -vvv, the +// resulting value of Verbose will be {[true, true, true]}. +// +// Slice options work exactly the same as primitive type options, except that +// whenever the option is encountered, a value is appended to the slice. +// +// Map options from string to primitive type are also supported. On the command +// line, you specify the value for such an option as key:value. For example +// +// type Options struct { +// AuthorInfo string[string] `short:"a"` +// } +// +// Then, the AuthorInfo map can be filled with something like +// -a name:Jesse -a "surname:van den Kieboom". +// +// Finally, for full control over the conversion between command line argument +// values and options, user defined types can choose to implement the Marshaler +// and Unmarshaler interfaces. +// +// Available field tags: +// short: the short name of the option (single character) +// long: the long name of the option +// description: the description of the option (optional) +// optional: whether an argument of the option is optional (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) +// default: the default value of an option. This tag can be specified +// multiple times in the case of slices or maps (optional). +// default-mask: when specified, this value will be displayed in the help +// instead of the actual default value. This is useful +// mostly for hiding otherwise sensitive information from +// showing up in the help. If default-mask takes the special +// value "-", then no default value will be shown at all +// (optional) +// required: whether an option is required to appear on the command +// line. If a required option is not present, the parser +// will return ErrRequired. +// base: a base (radix) used to convert strings to integer values, +// the default base is 10 (i.e. decimal) (optional) +// value-name: the name of the argument value (to be shown in the help, +// (optional) +// group: when specified on a struct field, makes the struct field +// a separate group with the given name (optional). +// command: when specified on a struct field, makes the struct field +// a (sub)command with the given name (optional). +// +// Either short: or long: must be specified to make the field eligible as an +// option. +// +// +// Option groups: +// +// Option groups are a simple way to semantically separate your options. The +// only real difference is in how your options will appear in the builtin +// generated help. All options in a particular group are shown together in the +// help under the name of the group. +// +// There are currently three ways to specify option groups. +// +// 1. Use NewNamedParser specifying the various option groups. +// 2. Use AddGroup to add a group to an existing parser. +// 3. Add a struct field to the toplevel options annotated with the +// group:"group-name" tag. +// +// +// +// Commands: +// +// The flags package also has basic support for commands. Commands are often +// used in monolithic applications that support various commands or actions. +// Take git for example, all of the add, commit, checkout, etc. are called +// commands. Using commands you can easily separate multiple functions of your +// application. +// +// There are currently two ways to specifiy a command. +// +// 1. Use AddCommand on an existing parser. +// 2. Add a struct field to your options struct annotated with the +// command:"command-name" tag. +// +// The most common, idiomatic way to implement commands is to define a global +// parser instance and implement each command in a separate file. These +// command files should define a go init function which calls AddCommand on +// the global parser. +// +// When parsing ends and there is an active command and that command implements +// 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 toplevel option "-v" and a command "add": +// +// Valid: ./app -v add +// Invalid: ./app add -v +// +package flags diff --git a/github.com/jessevdk/go-flags/group.go b/github.com/jessevdk/go-flags/group.go new file mode 100644 index 000000000..4e01736b2 --- /dev/null +++ b/github.com/jessevdk/go-flags/group.go @@ -0,0 +1,80 @@ +// Copyright 2012 Jesse van den Kieboom. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package flags + +import ( + "errors" + "strings" +) + +// ErrNotPointerToStruct indicates that a provided data container is not +// a pointer to a struct. Only pointers to structs are valid data containers +// for options. +var ErrNotPointerToStruct = errors.New("provided data is not a pointer to struct") + +// Group represents an option group. Option groups can be used to logically +// group options together under a description. Groups are only used to provide +// more structure to options both for the user (as displayed in the help message) +// and for you, since groups can be nested. +type Group struct { + // A short description of the group. The + // short description is primarily used in the builtin generated help + // message + ShortDescription string + + // A long description of the group. The long + // description is primarily used to present information on commands + // (Command embeds Group) in the builtin generated help and man pages. + LongDescription string + + // All the options in the group + options []*Option + + // All the subgroups + groups []*Group + + data interface{} +} + +// 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. +func (g *Group) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) { + group := newGroup(shortDescription, longDescription, data) + + if err := group.scan(); err != nil { + return nil, err + } + + g.groups = append(g.groups, group) + return group, nil +} + +// Groups returns the list of groups embedded in this group. +func (g *Group) Groups() []*Group { + return g.groups +} + +// Options returns the list of options in this group. +func (g *Group) Options() []*Option { + return g.options +} + +// Find locates the subgroup with the given short description and returns it. +// If no such group can be found Find will return nil. Note that the description +// is matched case insensitively. +func (g *Group) Find(shortDescription string) *Group { + lshortDescription := strings.ToLower(shortDescription) + + var ret *Group + + g.eachGroup(func(gg *Group) { + if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription { + ret = gg + } + }) + + return ret +} diff --git a/github.com/jessevdk/go-flags/group_private.go b/github.com/jessevdk/go-flags/group_private.go new file mode 100644 index 000000000..87c0f15ff --- /dev/null +++ b/github.com/jessevdk/go-flags/group_private.go @@ -0,0 +1,263 @@ +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.LongName && 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) storeDefaults() { + for _, option := range g.options { + // First. empty out the value + if len(option.Default) > 0 { + option.clear() + } + + for _, d := range option.Default { + option.set(&d) + } + + if !option.value.CanSet() { + continue + } + + option.defaultValue = reflect.ValueOf(option.value.Interface()) + } +} + +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, + OptionalArgument: optional, + OptionalValue: optionalValue, + Required: required, + ValueName: valueName, + DefaultMask: defaultMask, + + 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 != "" { + if otherOption, ok := longNames[option.LongName]; ok { + duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) + return + } + longNames[option.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") + + if _, err := g.AddGroup(subgroup, description, ptrval.Interface()); err != nil { + return true, err + } + + 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/github.com/jessevdk/go-flags/group_test.go b/github.com/jessevdk/go-flags/group_test.go new file mode 100644 index 000000000..281047a1c --- /dev/null +++ b/github.com/jessevdk/go-flags/group_test.go @@ -0,0 +1,160 @@ +package flags + +import ( + "testing" +) + +func TestGroupInline(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Group struct { + G bool `short:"g"` + } `group:"Grouped Options"` + }{} + + p, ret := assertParserSuccess(t, &opts, "-v", "-g") + + assertStringArray(t, ret, []string{}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Group.G { + t.Errorf("Expected Group.G to be true") + } + + if p.Command.Group.Find("Grouped Options") == nil { + t.Errorf("Expected to find group `Grouped Options'") + } +} + +func TestGroupAdd(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + }{} + + var grp = struct { + G bool `short:"g"` + }{} + + p := NewParser(&opts, Default) + g, err := p.AddGroup("Grouped Options", "", &grp) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + ret, err := p.ParseArgs([]string{"-v", "-g", "rest"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + assertStringArray(t, ret, []string{"rest"}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !grp.G { + t.Errorf("Expected Group.G to be true") + } + + if p.Command.Group.Find("Grouped Options") != g { + t.Errorf("Expected to find group `Grouped Options'") + } + + if p.Groups()[1] != g { + t.Errorf("Espected group #v, but got #v", g, p.Groups()[0]) + } + + if g.Options()[0].ShortName != 'g' { + t.Errorf("Expected short name `g' but got %v", g.Options()[0].ShortName) + } +} + +func TestGroupNestedInline(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + + Group struct { + G bool `short:"g"` + + Nested struct { + N string `long:"n"` + } `group:"Nested Options"` + } `group:"Grouped Options"` + }{} + + p, ret := assertParserSuccess(t, &opts, "-v", "-g", "--n", "n", "rest") + + assertStringArray(t, ret, []string{"rest"}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + if !opts.Group.G { + t.Errorf("Expected Group.G to be true") + } + + assertString(t, opts.Group.Nested.N, "n") + + if p.Command.Group.Find("Grouped Options") == nil { + t.Errorf("Expected to find group `Grouped Options'") + } + + if p.Command.Group.Find("Nested Options") == nil { + t.Errorf("Expected to find group `Nested Options'") + } +} + +func TestDuplicateShortFlags(t *testing.T) { + var opts struct { + Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` + Variables []string `short:"v" long:"variable" description:"Set a variable value."` + } + + args := []string{ + "--verbose", + "-v", "123", + "-v", "456", + } + + _, err := ParseArgs(&opts, args) + + if err == nil { + t.Errorf("Expected an error with type ErrDuplicatedFlag") + } else { + err2 := err.(*Error) + if err2.Type != ErrDuplicatedFlag { + t.Errorf("Expected an error with type ErrDuplicatedFlag") + } + } +} + +func TestDuplicateLongFlags(t *testing.T) { + var opts struct { + Test1 []bool `short:"a" long:"testing" description:"Test 1"` + Test2 []string `short:"b" long:"testing" description:"Test 2."` + } + + args := []string{ + "--testing", + } + + _, err := ParseArgs(&opts, args) + + if err == nil { + t.Errorf("Expected an error with type ErrDuplicatedFlag") + } else { + err2 := err.(*Error) + if err2.Type != ErrDuplicatedFlag { + t.Errorf("Expected an error with type ErrDuplicatedFlag") + } + } +} diff --git a/github.com/jessevdk/go-flags/help.go b/github.com/jessevdk/go-flags/help.go new file mode 100644 index 000000000..e1d9e5fc5 --- /dev/null +++ b/github.com/jessevdk/go-flags/help.go @@ -0,0 +1,275 @@ +// Copyright 2012 Jesse van den Kieboom. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package flags + +import ( + "bufio" + "bytes" + "fmt" + "io" + "reflect" + "strings" + "unicode/utf8" +) + +type alignmentInfo struct { + maxLongLen int + hasShort bool + hasValueName bool + terminalColumns int +} + +func (p *Parser) getAlignmentInfo() alignmentInfo { + ret := alignmentInfo{ + maxLongLen: 0, + hasShort: false, + hasValueName: false, + terminalColumns: getTerminalColumns(), + } + + if ret.terminalColumns <= 0 { + ret.terminalColumns = 80 + } + + p.eachActiveGroup(func(grp *Group) { + for _, info := range grp.options { + if info.ShortName != 0 { + ret.hasShort = true + } + + lv := utf8.RuneCountInString(info.ValueName) + + if lv != 0 { + ret.hasValueName = true + } + + l := utf8.RuneCountInString(info.LongName) + lv + + if l > ret.maxLongLen { + ret.maxLongLen = l + } + } + }) + + return ret +} + +func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) { + line := &bytes.Buffer{} + + distanceBetweenOptionAndDescription := 2 + paddingBeforeOption := 2 + + line.WriteString(strings.Repeat(" ", paddingBeforeOption)) + + if option.ShortName != 0 { + line.WriteRune(defaultShortOptDelimiter) + line.WriteRune(option.ShortName) + } else if info.hasShort { + line.WriteString(" ") + } + + descstart := info.maxLongLen + paddingBeforeOption + distanceBetweenOptionAndDescription + + if info.hasShort { + descstart += 2 + } + + if info.maxLongLen > 0 { + descstart += 4 + } + + if info.hasValueName { + descstart += 3 + } + + if len(option.LongName) > 0 { + if option.ShortName != 0 { + line.WriteString(", ") + } else if info.hasShort { + line.WriteString(" ") + } + + line.WriteString(defaultLongOptDelimiter) + line.WriteString(option.LongName) + } + + if option.canArgument() { + line.WriteRune(defaultNameArgDelimiter) + + if len(option.ValueName) > 0 { + line.WriteString(option.ValueName) + } + } + + written := line.Len() + line.WriteTo(writer) + + if option.Description != "" { + dw := descstart - written + writer.WriteString(strings.Repeat(" ", dw)) + + def := "" + defs := option.Default + + 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 { + def = strings.Join(defs, ", ") + } + + var desc string + + if def != "" { + desc = fmt.Sprintf("%s (%v)", option.Description, def) + } else { + desc = option.Description + } + + writer.WriteString(wrapText(desc, + info.terminalColumns-descstart, + strings.Repeat(" ", descstart))) + } + + writer.WriteString("\n") +} + +func maxCommandLength(s []*Command) int { + if len(s) == 0 { + return 0 + } + + ret := len(s[0].Name) + + for _, v := range s[1:] { + l := len(v.Name) + + if l > ret { + ret = l + } + } + + return ret +} + +// WriteHelp writes a help message containing all the possible options and +// their descriptions to the provided writer. Note that the HelpFlag parser +// option provides a convenient way to add a -h/--help option group to the +// command line parser which will automatically show the help messages using +// this method. +func (p *Parser) WriteHelp(writer io.Writer) { + if writer == nil { + return + } + + wr := bufio.NewWriter(writer) + aligninfo := p.getAlignmentInfo() + + cmd := p.Command + + for cmd.Active != nil { + cmd = cmd.Active + } + + if p.Name != "" { + wr.WriteString("Usage:\n") + wr.WriteString(" ") + + allcmd := p.Command + + for allcmd != nil { + var usage string + + if allcmd == p.Command { + if len(p.Usage) != 0 { + usage = p.Usage + } else { + usage = "[OPTIONS]" + } + } else if us, ok := allcmd.data.(Usage); ok { + usage = us.Usage() + } else { + usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name) + } + + if len(usage) != 0 { + fmt.Fprintf(wr, " %s %s", allcmd.Name, usage) + } else { + fmt.Fprintf(wr, " %s", allcmd.Name) + } + + allcmd = allcmd.Active + } + + fmt.Fprintln(wr) + + if len(cmd.LongDescription) != 0 { + fmt.Fprintln(wr) + + t := wrapText(cmd.LongDescription, + aligninfo.terminalColumns, + "") + + fmt.Fprintln(wr, t) + } + } + + p.eachActiveGroup(func(grp *Group) { + first := true + + for _, info := range grp.options { + if info.canCli() { + if first { + fmt.Fprintf(wr, "\n%s:\n", grp.ShortDescription) + first = false + } + + p.writeHelpOption(wr, info, aligninfo) + } + } + }) + + scommands := cmd.sortedCommands() + + if len(scommands) > 0 { + maxnamelen := maxCommandLength(scommands) + + fmt.Fprintln(wr) + fmt.Fprintln(wr, "Available commands:") + + for _, c := range scommands { + fmt.Fprintf(wr, " %s", c.Name) + + if len(c.ShortDescription) > 0 { + pad := strings.Repeat(" ", maxnamelen-len(c.Name)) + fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) + } + + fmt.Fprintln(wr) + } + } + + wr.Flush() +} diff --git a/github.com/jessevdk/go-flags/help_test.go b/github.com/jessevdk/go-flags/help_test.go new file mode 100644 index 000000000..c93195e0c --- /dev/null +++ b/github.com/jessevdk/go-flags/help_test.go @@ -0,0 +1,153 @@ +package flags + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "testing" + "time" +) + +func helpDiff(a, b string) (string, error) { + atmp, err := ioutil.TempFile("", "help-diff") + + if err != nil { + return "", err + } + + btmp, err := ioutil.TempFile("", "help-diff") + + if err != nil { + return "", err + } + + if _, err := io.WriteString(atmp, a); err != nil { + return "", err + } + + if _, err := io.WriteString(btmp, b); err != nil { + return "", err + } + + ret, err := exec.Command("diff", "-u", "-d", "--label", "got", atmp.Name(), "--label", "expected", btmp.Name()).Output() + + os.Remove(atmp.Name()) + os.Remove(btmp.Name()) + + return string(ret), nil +} + +type helpOptions struct { + Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information" ini-name:"verbose"` + Call func(string) `short:"c" description:"Call phone number" ini-name:"call"` + PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` + + OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"` + + Other struct { + StringSlice []string `short:"s" description:"A slice of strings"` + IntMap map[string]int `long:"intmap" description:"A map from string to int" ini-name:"int-map"` + } `group:"Other Options"` +} + +func TestHelp(t *testing.T) { + var opts helpOptions + + p := NewNamedParser("TestHelp", HelpFlag) + p.AddGroup("Application Options", "The application options", &opts) + + _, err := p.ParseArgs([]string{"--help"}) + + 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) + } + + expected := `Usage: + TestHelp [OPTIONS] + +Application Options: + -v, --verbose Show verbose debug information + -c= Call phone number + --ptrslice= A slice of pointers to string + +Other Options: + -s= A slice of strings + --intmap= A map from string to int + +Help Options: + -h, --help Show this help message +` + + if e.Message != expected { + ret, err := helpDiff(e.Message, expected) + + if err != nil { + t.Errorf("Unexpected diff error: %s", err) + t.Errorf("Unexpected help message, expected:\n\n%s\n\nbut got\n\n%s", expected, e.Message) + } else { + t.Errorf("Unexpected help message:\n\n%s", ret) + } + } + } +} + +func TestMan(t *testing.T) { + var opts helpOptions + + p := NewNamedParser("TestMan", HelpFlag) + p.ShortDescription = "Test manpage generation" + p.LongDescription = "This is a somewhat longer description of what this does" + p.AddGroup("Application Options", "The application options", &opts) + + var buf bytes.Buffer + p.WriteManPage(&buf) + + got := buf.String() + + tt := time.Now() + + expected := fmt.Sprintf(`.TH TestMan 1 "%s" +.SH NAME +TestMan \- Test manpage generation +.SH SYNOPSIS +\fBTestMan\fP [OPTIONS] +.SH DESCRIPTION +This is a somewhat longer description of what this does +.SH OPTIONS +.TP +\fB-v, --verbose\fP +Show verbose debug information +.TP +\fB-c\fP +Call phone number +.TP +\fB--ptrslice\fP +A slice of pointers to string +.TP +\fB-s\fP +A slice of strings +.TP +\fB--intmap\fP +A map from string to int +`, tt.Format("2 January 2006")) + + if got != expected { + ret, err := helpDiff(got, expected) + + if err != nil { + t.Errorf("Unexpected man page, expected:\n\n%s\n\nbut got\n\n%s", expected, got) + } else { + t.Errorf("Unexpected man page:\n\n%s", ret) + } + } +} diff --git a/github.com/jessevdk/go-flags/ini.go b/github.com/jessevdk/go-flags/ini.go new file mode 100644 index 000000000..59ccb3aff --- /dev/null +++ b/github.com/jessevdk/go-flags/ini.go @@ -0,0 +1,146 @@ +package flags + +import ( + "fmt" + "io" + "os" +) + +// IniError contains location information on where in the ini file an error +// occured. +type IniError struct { + // The error message. + Message string + + // The filename of the file in which the error occurred. + File string + + // The line number at which the error occurred. + LineNumber uint +} + +// Error provides a "file:line: message" formatted message of the ini error. +func (x *IniError) Error() string { + return fmt.Sprintf("%s:%d: %s", + x.File, + x.LineNumber, + x.Message) +} + +// IniOptions for writing ini files +type IniOptions uint + +const ( + // IniNone indicates no options. + IniNone IniOptions = 0 + + // IniIncludeDefaults indicates that default values should be written + // when writing options to an ini file. + IniIncludeDefaults = 1 << iota + + // IniIncludeComments indicates that comments containing the description + // of an option should be written when writing options to an ini file. + IniIncludeComments + + // IniDefault provides a default set of options. + IniDefault = IniIncludeComments +) + +// IniParser is a utility to read and write flags options from and to ini +// files. +type IniParser struct { + parser *Parser +} + +// NewIniParser creates a new ini parser for a given Parser. +func NewIniParser(p *Parser) *IniParser { + return &IniParser{ + parser: p, + } +} + +// IniParse is a convenience function to parse command line options with default +// settings from an ini file. The provided data is a pointer to a struct +// representing the default option group (named "Application Options"). For +// more control, use flags.NewParser. +func IniParse(filename string, data interface{}) error { + p := NewParser(data, Default) + return NewIniParser(p).ParseFile(filename) +} + +// ParseFile parses flags from an ini formatted file. See Parse for more +// information on the ini file foramt. The returned errors can be of the type +// flags.Error or flags.IniError. +func (i *IniParser) ParseFile(filename string) error { + i.parser.storeDefaults() + + ini, err := readIniFromFile(filename) + + if err != nil { + return err + } + + return i.parse(ini) +} + +// Parse parses flags from an ini format. You can use ParseFile as a +// convenience function to parse from a filename instead of a general +// io.Reader. +// +// The format of the ini file is as follows: +// +// [Option group name] +// option = value +// +// Each section in the ini file represents an option group or command in the +// flags parser. The default flags parser option group (i.e. when using +// flags.Parse) is named 'Application Options'. The ini option name is matched +// in the following order: +// +// 1. Compared to the ini-name tag on the option struct field (if present) +// 2. Compared to the struct field name +// 3. Compared to the option long name (if present) +// 4. Compared to the option short name (if present) +// +// Sections for nested groups and commands can be addressed using a dot `.' +// namespacing notation (i.e [subcommand.Options]). Group section names are +// matched case insensitive. +// +// The returned errors can be of the type flags.Error or +// flags.IniError. +func (i *IniParser) Parse(reader io.Reader) error { + i.parser.storeDefaults() + + ini, err := readIni(reader, "") + + if err != nil { + return err + } + + return i.parse(ini) +} + +// WriteFile writes the flags as ini format into a file. See WriteIni +// for more information. The returned error occurs when the specified file +// could not be opened for writing. +func (i *IniParser) WriteFile(filename string, options IniOptions) error { + file, err := os.Create(filename) + + if err != nil { + return err + } + + defer file.Close() + i.Write(file, options) + + return nil +} + +// Write writes the current values of all the flags to an ini format. +// See Parse for more information on the ini file format. You typically +// call this only after settings have been parsed since the default values of each +// option are stored just before parsing the flags (this is only relevant when +// IniIncludeDefaults is _not_ set in options). +func (i *IniParser) Write(writer io.Writer, options IniOptions) { + writeIni(i, writer, options) +} diff --git a/github.com/jessevdk/go-flags/ini_private.go b/github.com/jessevdk/go-flags/ini_private.go new file mode 100644 index 000000000..4ccda73ee --- /dev/null +++ b/github.com/jessevdk/go-flags/ini_private.go @@ -0,0 +1,333 @@ +package flags + +import ( + "bufio" + "fmt" + "io" + "os" + "reflect" + "strings" +) + +type iniValue struct { + Name string + Value string +} + +type iniSection []iniValue +type ini 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(group *Group, namespace string, writer io.Writer, options IniOptions) { + var sname string + + if len(namespace) != 0 { + sname = namespace + "." + group.ShortDescription + } else { + 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 && + reflect.DeepEqual(val, option.defaultValue) { + continue + } + + if !sectionwritten { + fmt.Fprintf(writer, "[%s]\n", sname) + sectionwritten = true + } + + if comments { + fmt.Fprintf(writer, "; %s\n", option.Description) + } + + oname := optionIniName(option) + + switch val.Type().Kind() { + case reflect.Slice: + for idx := 0; idx < val.Len(); idx++ { + v, _ := convertToString(val.Index(idx), option.tag) + fmt.Fprintf(writer, "%s = %s\n", oname, v) + } + + if val.Len() == 0 { + fmt.Fprintf(writer, "; %s =\n", oname) + } + case reflect.Map: + for _, key := range val.MapKeys() { + k, _ := convertToString(key, option.tag) + v, _ := convertToString(val.MapIndex(key), option.tag) + + fmt.Fprintf(writer, "%s = %s:%s\n", oname, k, v) + } + + if val.Len() == 0 { + fmt.Fprintf(writer, "; %s =\n", oname) + } + default: + v, _ := convertToString(val, option.tag) + + if len(v) != 0 { + fmt.Fprintf(writer, "%s = %s\n", oname, v) + } else { + fmt.Fprintf(writer, "%s =\n", oname) + } + } + + if comments { + fmt.Fprintln(writer) + } + } + + if sectionwritten && !comments { + fmt.Fprintln(writer) + } +} + +func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { + command.eachGroup(func(group *Group) { + writeGroupIni(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 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 := make(ini) + + reader := bufio.NewReader(contents) + + // Empty global section + section := make(iniSection, 0, 10) + sectionname := "" + + ret[sectionname] = section + + var lineno uint + + for { + line, err := readFullLine(reader) + + if err == io.EOF { + break + } + + 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] == ';' { + 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[name] + + if section == nil { + section = make(iniSection, 0, 10) + ret[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]) + + section = append(section, iniValue{ + Name: name, + Value: value, + }) + + ret[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 + + for name, section := range ini { + groups := i.matchingGroups(name) + + if len(groups) == 0 { + return newError(ErrUnknownGroup, + fmt.Sprintf("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 newError(ErrUnknownFlag, + fmt.Sprintf("unknown option: %s", inival.Name)) + } + + continue + } + + pval := &inival.Value + + if !opt.canArgument() && len(inival.Value) == 0 { + pval = nil + } + + if err := opt.set(pval); err != nil { + return wrapError(err) + } + + opt.tag.Set("_read-ini-name", inival.Name) + } + } + + return nil +} diff --git a/github.com/jessevdk/go-flags/ini_test.go b/github.com/jessevdk/go-flags/ini_test.go new file mode 100644 index 000000000..caff08c11 --- /dev/null +++ b/github.com/jessevdk/go-flags/ini_test.go @@ -0,0 +1,170 @@ +package flags + +import ( + "bytes" + "strings" + "testing" +) + +func TestWriteIni(t *testing.T) { + var opts helpOptions + + p := NewNamedParser("TestIni", Default) + p.AddGroup("Application Options", "The application options", &opts) + + p.ParseArgs([]string{"-vv", "--intmap=a:2", "--intmap", "b:3"}) + + inip := NewIniParser(p) + + var b bytes.Buffer + inip.Write(&b, IniDefault|IniIncludeDefaults) + + got := b.String() + expected := `[Application Options] +; Show verbose debug information +verbose = true +verbose = true + +; A slice of pointers to string +; PtrSlice = + +; Option only available in ini +only-ini = + +[Other Options] +; A slice of strings +; StringSlice = + +; A map from string to int +int-map = a:2 +int-map = b:3 + +` + + if got != expected { + ret, err := helpDiff(got, expected) + + if err != nil { + t.Errorf("Unexpected ini, expected:\n\n%s\n\nbut got\n\n%s", expected, got) + } else { + t.Errorf("Unexpected ini:\n\n%s", ret) + } + } +} + +func TestReadIni(t *testing.T) { + var opts helpOptions + + p := NewNamedParser("TestIni", Default) + p.AddGroup("Application Options", "The application options", &opts) + + inip := NewIniParser(p) + + inic := ` +; Show verbose debug information +verbose = true +verbose = true + +[Application Options] +; A slice of pointers to string +; PtrSlice = + +[Other Options] +; A slice of strings +; StringSlice = + +; A map from string to int +int-map = a:2 +int-map = b:3 + +` + + b := strings.NewReader(inic) + err := inip.Parse(b) + + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + assertBoolArray(t, opts.Verbose, []bool{true, true}) + + if v, ok := opts.Other.IntMap["a"]; !ok { + t.Errorf("Expected \"a\" in Other.IntMap") + } else if v != 2 { + t.Errorf("Expected Other.IntMap[\"a\"] = 2, but got %v", v) + } + + if v, ok := opts.Other.IntMap["b"]; !ok { + t.Errorf("Expected \"b\" in Other.IntMap") + } else if v != 3 { + t.Errorf("Expected Other.IntMap[\"b\"] = 3, but got %v", v) + } +} + +func TestIniCommands(t *testing.T) { + var opts struct { + Value string `short:"v" long:"value"` + + Add struct { + Name int `short:"n" long:"name" ini-name:"AliasName"` + + Other struct { + O string `short:"o" long:"other"` + } `group:"Other Options"` + } `command:"add"` + } + + p := NewNamedParser("TestIni", Default) + p.AddGroup("Application Options", "The application options", &opts) + + inip := NewIniParser(p) + + inic := `[Application Options] +value = some value + +[add] +AliasName = 5 + +[add.Other Options] +other = subgroup +` + + b := strings.NewReader(inic) + err := inip.Parse(b) + + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + assertString(t, opts.Value, "some value") + + if opts.Add.Name != 5 { + t.Errorf("Expected opts.Add.Name to be 5, but got %v", opts.Add.Name) + } + + assertString(t, opts.Add.Other.O, "subgroup") +} + +func TestIniNoIni(t *testing.T) { + var opts struct { + Value string `short:"v" long:"value" no-ini:"yes"` + } + + p := NewNamedParser("TestIni", Default) + p.AddGroup("Application Options", "The application options", &opts) + + inip := NewIniParser(p) + + inic := `[Application Options] +value = some value +` + + b := strings.NewReader(inic) + err := inip.Parse(b) + + if err == nil { + t.Fatalf("Expected error") + } + + assertError(t, err, ErrUnknownFlag, "unknown option: value") +} diff --git a/github.com/jessevdk/go-flags/long_test.go b/github.com/jessevdk/go-flags/long_test.go new file mode 100644 index 000000000..02fc8c701 --- /dev/null +++ b/github.com/jessevdk/go-flags/long_test.go @@ -0,0 +1,85 @@ +package flags + +import ( + "testing" +) + +func TestLong(t *testing.T) { + var opts = struct { + Value bool `long:"value"` + }{} + + ret := assertParseSuccess(t, &opts, "--value") + + assertStringArray(t, ret, []string{}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } +} + +func TestLongArg(t *testing.T) { + var opts = struct { + Value string `long:"value"` + }{} + + ret := assertParseSuccess(t, &opts, "--value", "value") + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "value") +} + +func TestLongArgEqual(t *testing.T) { + var opts = struct { + Value string `long:"value"` + }{} + + ret := assertParseSuccess(t, &opts, "--value=value") + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "value") +} + +func TestLongDefault(t *testing.T) { + var opts = struct { + Value string `long:"value" default:"value"` + }{} + + ret := assertParseSuccess(t, &opts) + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "value") +} + +func TestLongOptional(t *testing.T) { + var opts = struct { + Value string `long:"value" optional:"yes" optional-value:"value"` + }{} + + ret := assertParseSuccess(t, &opts, "--value") + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "value") +} + +func TestLongOptionalArg(t *testing.T) { + var opts = struct { + Value string `long:"value" optional:"yes" optional-value:"value"` + }{} + + ret := assertParseSuccess(t, &opts, "--value", "no") + + assertStringArray(t, ret, []string{"no"}) + assertString(t, opts.Value, "value") +} + +func TestLongOptionalArgEqual(t *testing.T) { + var opts = struct { + Value string `long:"value" optional:"yes" optional-value:"value"` + }{} + + ret := assertParseSuccess(t, &opts, "--value=value", "no") + + assertStringArray(t, ret, []string{"no"}) + assertString(t, opts.Value, "value") +} diff --git a/github.com/jessevdk/go-flags/man.go b/github.com/jessevdk/go-flags/man.go new file mode 100644 index 000000000..93d6149d6 --- /dev/null +++ b/github.com/jessevdk/go-flags/man.go @@ -0,0 +1,134 @@ +package flags + +import ( + "fmt" + "io" + "strings" + "time" +) + +func formatForMan(wr io.Writer, s string) { + for { + idx := strings.IndexRune(s, '`') + + if idx < 0 { + fmt.Fprintf(wr, "%s", s) + break + } + + fmt.Fprintf(wr, "%s", s[:idx]) + + s = s[idx+1:] + idx = strings.IndexRune(s, '\'') + + if idx < 0 { + fmt.Fprintf(wr, "%s", s) + break + } + + fmt.Fprintf(wr, "\\fB%s\\fP", s[:idx]) + s = s[idx+1:] + } +} + +func writeManPageOptions(wr io.Writer, grp *Group) { + grp.eachGroup(func(group *Group) { + for _, opt := range group.options { + if !opt.canCli() { + continue + } + + fmt.Fprintln(wr, ".TP") + fmt.Fprintf(wr, "\\fB") + + if opt.ShortName != 0 { + fmt.Fprintf(wr, "-%c", opt.ShortName) + } + + if len(opt.LongName) != 0 { + if opt.ShortName != 0 { + fmt.Fprintf(wr, ", ") + } + + fmt.Fprintf(wr, "--%s", opt.LongName) + } + + fmt.Fprintln(wr, "\\fP") + formatForMan(wr, opt.Description) + fmt.Fprintln(wr, "") + } + }) +} + +func writeManPageSubCommands(wr io.Writer, name string, root *Command) { + commands := root.sortedCommands() + + for _, c := range commands { + var nn string + + if len(name) != 0 { + nn = name + " " + c.Name + } else { + nn = c.Name + } + + writeManPageCommand(wr, nn, c) + } +} + +func writeManPageCommand(wr io.Writer, name string, command *Command) { + fmt.Fprintf(wr, ".SS %s\n", name) + fmt.Fprintln(wr, command.ShortDescription) + + if len(command.LongDescription) > 0 { + fmt.Fprintln(wr, "") + + cmdstart := fmt.Sprintf("The %s command", command.Name) + + if strings.HasPrefix(command.LongDescription, cmdstart) { + fmt.Fprintf(wr, "The \\fI%s\\fP command", command.Name) + + formatForMan(wr, command.LongDescription[len(cmdstart):]) + fmt.Fprintln(wr, "") + } else { + formatForMan(wr, command.LongDescription) + fmt.Fprintln(wr, "") + } + } + + writeManPageOptions(wr, command.Group) + writeManPageSubCommands(wr, name, command) +} + +// WriteManPage writes a basic man page in groff format to the specified +// writer. +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.Fprintln(wr, ".SH NAME") + fmt.Fprintf(wr, "%s \\- %s\n", p.Name, p.ShortDescription) + fmt.Fprintln(wr, ".SH SYNOPSIS") + + usage := p.Usage + + if len(usage) == 0 { + usage = "[OPTIONS]" + } + + fmt.Fprintf(wr, "\\fB%s\\fP %s\n", p.Name, usage) + fmt.Fprintln(wr, ".SH DESCRIPTION") + + formatForMan(wr, p.LongDescription) + fmt.Fprintln(wr, "") + + fmt.Fprintln(wr, ".SH OPTIONS") + + writeManPageOptions(wr, p.Command.Group) + + if len(p.commands) > 0 { + fmt.Fprintln(wr, ".SH COMMANDS") + + writeManPageSubCommands(wr, "", p.Command) + } +} diff --git a/github.com/jessevdk/go-flags/marshal_test.go b/github.com/jessevdk/go-flags/marshal_test.go new file mode 100644 index 000000000..9378db1bb --- /dev/null +++ b/github.com/jessevdk/go-flags/marshal_test.go @@ -0,0 +1,78 @@ +package flags + +import ( + "fmt" + "testing" +) + +type marshalled bool + +func (m *marshalled) UnmarshalFlag(value string) error { + if value == "yes" { + *m = true + } else if value == "no" { + *m = false + } else { + return fmt.Errorf("`%s' is not a valid value, please specify `yes' or `no'", value) + } + + return nil +} + +func (m marshalled) MarshalFlag() string { + if m { + return "yes" + } + + return "no" +} + +func TestMarshal(t *testing.T) { + var opts = struct { + Value marshalled `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v=yes") + + assertStringArray(t, ret, []string{}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } +} + +func TestMarshalDefault(t *testing.T) { + var opts = struct { + Value marshalled `short:"v" default:"yes"` + }{} + + ret := assertParseSuccess(t, &opts) + + assertStringArray(t, ret, []string{}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } +} + +func TestMarshalOptional(t *testing.T) { + var opts = struct { + Value marshalled `short:"v" optional:"yes" optional-value:"yes"` + }{} + + ret := assertParseSuccess(t, &opts, "-v") + + assertStringArray(t, ret, []string{}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } +} + +func TestMarshalError(t *testing.T) { + var opts = struct { + Value marshalled `short:"v"` + }{} + + assertParseFail(t, ErrMarshal, "invalid argument for flag `-v' (expected flags.marshalled): `invalid' is not a valid value, please specify `yes' or `no'", &opts, "-vinvalid") +} diff --git a/github.com/jessevdk/go-flags/multitag.go b/github.com/jessevdk/go-flags/multitag.go new file mode 100644 index 000000000..96bb1a31d --- /dev/null +++ b/github.com/jessevdk/go-flags/multitag.go @@ -0,0 +1,140 @@ +package flags + +import ( + "strconv" +) + +type multiTag struct { + value string + cache map[string][]string +} + +func newMultiTag(v string) multiTag { + return multiTag{ + value: v, + } +} + +func (x *multiTag) scan() (map[string][]string, error) { + v := x.value + + ret := make(map[string][]string) + + // This is mostly copied from reflect.StructTag.Get + for v != "" { + i := 0 + + // Skip whitespace + for i < len(v) && v[i] == ' ' { + i++ + } + + v = v[i:] + + if v == "" { + break + } + + // Scan to colon to find key + i = 0 + + for i < len(v) && v[i] != ' ' && v[i] != ':' && v[i] != '"' { + i++ + } + + if i >= len(v) { + return nil, newErrorf(ErrTag, "expected `:' after key name, but got end of tag (in `%v`)", x.value) + } + + if v[i] != ':' { + return nil, newErrorf(ErrTag, "expected `:' after key name, but got `%v' (in `%v`)", v[i], x.value) + } + + if i+1 >= len(v) { + return nil, newErrorf(ErrTag, "expected `\"' to start tag value at end of tag (in `%v`)", x.value) + } + + if v[i+1] != '"' { + return nil, newErrorf(ErrTag, "expected `\"' to start tag value, but got `%v' (in `%v`)", v[i+1], x.value) + } + + name := v[:i] + v = v[i+1:] + + // Scan quoted string to find value + i = 1 + + for i < len(v) && v[i] != '"' { + if v[i] == '\n' { + return nil, newErrorf(ErrTag, "unexpected newline in tag value `%v' (in `%v`)", name, x.value) + } + + if v[i] == '\\' { + i++ + } + i++ + } + + if i >= len(v) { + return nil, newErrorf(ErrTag, "expected end of tag value `\"' at end of tag (in `%v`)", x.value) + } + + val, err := strconv.Unquote(v[:i+1]) + + if err != nil { + return nil, newErrorf(ErrTag, "Malformed value of tag `%v:%v` => %v (in `%v`)", name, v[:i+1], err, x.value) + } + + v = v[i+1:] + + ret[name] = append(ret[name], val) + } + + return ret, nil +} + +func (x *multiTag) Parse() error { + vals, err := x.scan() + x.cache = vals + + return err +} + +func (x *multiTag) cached() map[string][]string { + if x.cache == nil { + cache, _ := x.scan() + + if cache == nil { + cache = make(map[string][]string) + } + + x.cache = cache + } + + return x.cache +} + +func (x *multiTag) Get(key string) string { + c := x.cached() + + if v, ok := c[key]; ok { + return v[len(v)-1] + } + + return "" +} + +func (x *multiTag) GetMany(key string) []string { + c := x.cached() + return c[key] +} + +func (x *multiTag) Set(key string, value string) { + c := x.cached() + c[key] = []string{value} +} + +func (x *multiTag) SetMany(key string, value []string) { + c := x.cached() + c[key] = value +} diff --git a/github.com/jessevdk/go-flags/option.go b/github.com/jessevdk/go-flags/option.go new file mode 100644 index 000000000..150a94f73 --- /dev/null +++ b/github.com/jessevdk/go-flags/option.go @@ -0,0 +1,95 @@ +package flags + +import ( + "fmt" + "reflect" + "unicode/utf8" +) + +// Option flag information. Contains a description of the option, short and +// long name as well as a default value and whether an argument for this +// flag is optional. +type Option struct { + // The description of the option flag. This description is shown + // automatically in the builtin help. + Description string + + // The short name of the option (a single character). If not 0, the + // option flag can be 'activated' using -. Either ShortName + // or LongName needs to be non-empty. + ShortName rune + + // The long name of the option. If not "", the option flag can be + // activated using --. Either ShortName or LongName needs + // to be non-empty. + LongName string + + // The default value of the option. + Default []string + + // 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. + // This is only valid for non-boolean options. + OptionalArgument bool + + // The optional value of the option. The optional value is used when + // the option flag is marked as having an OptionalArgument. This means + // that when the flag is specified, but no option argument is given, + // the value of the field this option represents will be set to + // OptionalValue. This is only valid for non-boolean options. + OptionalValue []string + + // If true, the option _must_ be specified on the command line. If the + // option is not specified, the parser will generate an ErrRequired type + // error. + Required bool + + // A name for the value of an option shown in the Help as --flag [ValueName] + ValueName string + + // A mask value to show in the help instead of the default value. This + // is useful for hiding sensitive information in the help, such as + // passwords. + DefaultMask string + + // The struct field which the option represents. + field reflect.StructField + + // The struct field value which the option represents. + value reflect.Value + + defaultValue reflect.Value + iniUsedName string + tag multiTag +} + +// String converts an option to a human friendly readable string describing the +// option. +func (option *Option) String() string { + var s string + var short string + + if option.ShortName != 0 { + data := make([]byte, utf8.RuneLen(option.ShortName)) + utf8.EncodeRune(data, option.ShortName) + short = string(data) + + if len(option.LongName) != 0 { + s = fmt.Sprintf("%s%s, %s%s", + string(defaultShortOptDelimiter), short, + defaultLongOptDelimiter, option.LongName) + } else { + s = fmt.Sprintf("%s%s", string(defaultShortOptDelimiter), short) + } + } else if len(option.LongName) != 0 { + s = fmt.Sprintf("%s%s", defaultLongOptDelimiter, option.LongName) + } + + return s +} + +// Value returns the option value as an interface{}. +func (option *Option) Value() interface{} { + return option.value.Interface() +} diff --git a/github.com/jessevdk/go-flags/option_private.go b/github.com/jessevdk/go-flags/option_private.go new file mode 100644 index 000000000..cba601134 --- /dev/null +++ b/github.com/jessevdk/go-flags/option_private.go @@ -0,0 +1,125 @@ +package flags + +import ( + "reflect" +) + +// 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 { + if option.isFunc() { + return option.call(value) + } else if value != nil { + return convert(*value, option.value, option.tag) + } else { + return convert("", option.value, option.tag) + } + + return nil +} + +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) clear() { + tp := option.value.Type() + + switch tp.Kind() { + case reflect.Func: + // Skip + case reflect.Map: + // Empty the map + option.value.Set(reflect.MakeMap(tp)) + default: + zeroval := reflect.Zero(tp) + option.value.Set(zeroval) + } +} + +func (option *Option) isUnmarshaler() Unmarshaler { + v := option.value + + for { + if !v.CanInterface() { + return nil + } + + i := v.Interface() + + if u, ok := i.(Unmarshaler); ok { + return u + } + + if !v.CanAddr() { + return nil + } + + 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 + } + } + + 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/github.com/jessevdk/go-flags/options_test.go b/github.com/jessevdk/go-flags/options_test.go new file mode 100644 index 000000000..b0fe9f456 --- /dev/null +++ b/github.com/jessevdk/go-flags/options_test.go @@ -0,0 +1,45 @@ +package flags + +import ( + "testing" +) + +func TestPassDoubleDash(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + }{} + + p := NewParser(&opts, PassDoubleDash) + ret, err := p.ParseArgs([]string{"-v", "--", "-v", "-g"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + assertStringArray(t, ret, []string{"-v", "-g"}) +} + +func TestPassAfterNonOption(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + }{} + + p := NewParser(&opts, PassAfterNonOption) + ret, err := p.ParseArgs([]string{"-v", "arg", "-v", "-g"}) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + return + } + + if !opts.Value { + t.Errorf("Expected Value to be true") + } + + assertStringArray(t, ret, []string{"arg", "-v", "-g"}) +} diff --git a/github.com/jessevdk/go-flags/optstyle_other.go b/github.com/jessevdk/go-flags/optstyle_other.go new file mode 100644 index 000000000..248996c5a --- /dev/null +++ b/github.com/jessevdk/go-flags/optstyle_other.go @@ -0,0 +1,54 @@ +// +build !windows + +package flags + +import ( + "strings" +) + +const ( + defaultShortOptDelimiter = '-' + defaultLongOptDelimiter = "--" + defaultNameArgDelimiter = '=' +) + +func argumentIsOption(arg string) bool { + return len(arg) > 0 && arg[0] == '-' +} + +// stripOptionPrefix returns the option without the prefix and whether or +// not the option is a long option or not. +func stripOptionPrefix(optname string) (prefix string, name string, islong bool) { + if strings.HasPrefix(optname, "--") { + return "--", optname[2:], true + } else if strings.HasPrefix(optname, "-") { + return "-", optname[1:], false + } + + return "", optname, false +} + +// splitOption attempts to split the passed option into a name and an argument. +// When there is no argument specified, nil will be returned for it. +func splitOption(prefix string, option string, islong bool) (string, *string) { + pos := strings.Index(option, "=") + + if (islong && pos >= 0) || (!islong && pos == 1) { + rest := option[pos+1:] + return option[:pos], &rest + } + + return option, nil +} + +// addHelpGroup adds a new group that contains default help parameters. +func (c *Command) addHelpGroup(showHelp func() error) *Group { + var help struct { + ShowHelp func() error `short:"h" long:"help" description:"Show this help message"` + } + + help.ShowHelp = showHelp + ret, _ := c.AddGroup("Help Options", "", &help) + + return ret +} diff --git a/github.com/jessevdk/go-flags/optstyle_windows.go b/github.com/jessevdk/go-flags/optstyle_windows.go new file mode 100644 index 000000000..8823c56fc --- /dev/null +++ b/github.com/jessevdk/go-flags/optstyle_windows.go @@ -0,0 +1,85 @@ +package flags + +import ( + "strings" +) + +// Windows uses a front slash for both short and long options. Also it uses +// a colon for name/argument delimter. +const ( + defaultShortOptDelimiter = '/' + defaultLongOptDelimiter = "/" + defaultNameArgDelimiter = ':' +) + +func argumentIsOption(arg string) bool { + // Windows-style options allow front slash for the option + // delimiter. + return len(arg) > 0 && (arg[0] == '-' || arg[0] == '/') +} + +// stripOptionPrefix returns the option without the prefix and whether or +// not the option is a long option or not. +func stripOptionPrefix(optname string) (prefix string, name string, islong bool) { + // Determine if the argument is a long option or not. Windows + // typically supports both long and short options with a single + // front slash as the option delimiter, so handle this situation + // nicely. + possplit := 0 + + if strings.HasPrefix(optname, "--") { + possplit = 2 + islong = true + } else if strings.HasPrefix(optname, "-") { + possplit = 1 + islong = false + } else if strings.HasPrefix(optname, "/") { + possplit = 1 + islong = len(optname) > 2 + } + + return optname[:possplit], optname[possplit:], islong +} + +// splitOption attempts to split the passed option into a name and an argument. +// When there is no argument specified, nil will be returned for it. +func splitOption(prefix string, option string, islong bool) (string, *string) { + if len(option) == 0 { + return option, nil + } + + // Windows typically uses a colon for the option name and argument + // delimiter while POSIX typically uses an equals. Support both styles, + // but don't allow the two to be mixed. That is to say /foo:bar and + // --foo=bar are acceptable, but /foo=bar and --foo:bar are not. + var pos int + + if prefix == "/" { + pos = strings.Index(option, ":") + } else if len(prefix) > 0 { + pos = strings.Index(option, "=") + } + + if (islong && pos >= 0) || (!islong && pos == 1) { + rest := option[pos+1:] + return option[:pos], &rest + } + + return option, nil +} + +// addHelpGroup adds a new group that contains default help parameters. +func (c *Command) addHelpGroup(showHelp func() error) *Group { + // Windows CLI applications typically use /? for help, so make both + // that available as well as the POSIX style h and help. + var help struct { + ShowHelpWindows func() error `short:"?" description:"Show this help message"` + ShowHelpPosix func() error `short:"h" long:"help" description:"Show this help message"` + } + + help.ShowHelpWindows = showHelp + help.ShowHelpPosix = showHelp + + ret, _ := c.AddGroup("Help Options", "", &help) + return ret +} diff --git a/github.com/jessevdk/go-flags/parser.go b/github.com/jessevdk/go-flags/parser.go new file mode 100644 index 000000000..481d35387 --- /dev/null +++ b/github.com/jessevdk/go-flags/parser.go @@ -0,0 +1,212 @@ +// Copyright 2012 Jesse van den Kieboom. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package flags + +import ( + "os" + "path" +) + +// A Parser provides command line option parsing. It can contain several +// option groups each with their own set of options. +type Parser struct { + // Embedded, see Command for more information + *Command + + // A usage string to be displayed in the help message. + Usage string + + // Option flags changing the behavior of the parser. + Options Options + + internalError error +} + +// Options provides parser options that change the behavior of the option +// parser. +type Options uint + +const ( + // None indicates no options. + None Options = 0 + + // HelpFlag adds a default Help Options group to the parser containing + // -h and --help options. When either -h or --help is specified on the + // command line, the parser will return the special error of type + // ErrHelp. When PrintErrors is also specified, then the help message + // will also be automatically printed to os.Stderr. + HelpFlag = 1 << iota + + // PassDoubleDash passes all arguments after a double dash, --, as + // remaining command line arguments (i.e. they will not be parsed for + // flags). + PassDoubleDash + + // IgnoreUnknown ignores any unknown options and passes them as + // remaining command line arguments instead of generating an error. + IgnoreUnknown + + // PrintErrors prints any errors which occurred during parsing to + // os.Stderr. + PrintErrors + + // PassAfterNonOption passes all arguments after the first non option + // as remaining command line arguments. This is equivalent to strict + // POSIX processing. + PassAfterNonOption + + // Default is a convenient default set of options which should cover + // most of the uses of the flags package. + Default = HelpFlag | PrintErrors | PassDoubleDash +) + +// 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 +// flags.NewParser. +func Parse(data interface{}) ([]string, error) { + return NewParser(data, Default).Parse() +} + +// ParseArgs 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"). The args argument is +// the list of command line arguments to parse. If you just want to parse the +// default program command line arguments (i.e. os.Args), then use flags.Parse +// instead. For more control, use flags.NewParser. +func ParseArgs(data interface{}, args []string) ([]string, error) { + return NewParser(data, Default).ParseArgs(args) +} + +// NewParser creates a new parser. It uses os.Args[0] as the application +// name and then calls Parser.NewNamedParser (see Parser.NewNamedParser for +// more details). The provided data is a pointer to a struct representing the +// default option group (named "Application Options"), or nil if the default +// group should not be added. The options parameter specifies a set of options +// for the parser. +func NewParser(data interface{}, options Options) *Parser { + ret := NewNamedParser(path.Base(os.Args[0]), options) + + if data != nil { + _, ret.internalError = ret.AddGroup("Application Options", "", data) + } + + return ret +} + +// NewNamedParser creates a new parser. The appname is used to display the +// executable name in the builtin help message. Option groups and commands can +// be added to this parser by using AddGroup and AddCommand. +func NewNamedParser(appname string, options Options) *Parser { + return &Parser{ + Command: newCommand(appname, "", "", nil), + Options: options, + } +} + +// Parse parses the command line arguments from os.Args using Parser.ParseArgs. +// For more detailed information see ParseArgs. +func (p *Parser) Parse() ([]string, error) { + return p.ParseArgs(os.Args[1:]) +} + +// ParseArgs parses the command line arguments according to the option groups that +// were added to the parser. On successful parsing of the arguments, the +// remaining, non-option, arguments (if any) are returned. The returned error +// indicates a parsing error and can be used with PrintError to display +// contextual information on where the error occurred exactly. +// +// 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. +// 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.eachCommand(func(c *Command) { + p.eachGroup(func(g *Group) { + g.storeDefaults() + }) + }, true) + + // Add builtin help group to all commands if necessary + if (p.Options & HelpFlag) != None { + p.addHelpGroups(p.showBuiltinHelp) + } + + s := &parseState{ + args: args, + retargs: make([]string, 0, len(args)), + command: p.Command, + lookup: p.makeLookup(), + } + + for !s.eof() { + arg := s.pop() + + // When PassDoubleDash is set and we encounter a --, then + // simply append all the rest as arguments and break out + if (p.Options&PassDoubleDash) != None && arg == "--" { + s.retargs = append(s.retargs, s.args...) + break + } + + if !argumentIsOption(arg) { + // Note: this also sets s.err, so we can just check for + // nil here and use s.err later + if p.parseNonOption(s) != nil { + break + } + + continue + } + + var err error + var option *Option + + prefix, optname, islong := stripOptionPrefix(arg) + optname, argument := splitOption(prefix, optname, islong) + + if islong { + option, err = p.parseLong(s, optname, argument) + } else { + option, err = p.parseShort(s, optname, argument) + } + + if err != nil { + ignoreUnknown := (p.Options & IgnoreUnknown) != None + parseErr := wrapError(err) + + if !(parseErr.Type == ErrUnknownFlag && ignoreUnknown) { + s.err = parseErr + break + } + + if ignoreUnknown { + s.retargs = append(s.retargs, arg) + } + } else { + delete(s.lookup.required, option) + } + } + + if s.err == nil { + s.checkRequired() + } + + if s.err != nil { + return nil, p.printError(s.err) + } + + if len(s.command.commands) != 0 { + return nil, p.printError(s.estimateCommand()) + } else if cmd, ok := s.command.data.(Commander); ok { + return nil, p.printError(cmd.Execute(s.retargs)) + } + + return s.retargs, nil +} diff --git a/github.com/jessevdk/go-flags/parser_private.go b/github.com/jessevdk/go-flags/parser_private.go new file mode 100644 index 000000000..21518ac6a --- /dev/null +++ b/github.com/jessevdk/go-flags/parser_private.go @@ -0,0 +1,243 @@ +package flags + +import ( + "bytes" + "fmt" + "os" + "strings" + "unicode/utf8" +) + +type parseState struct { + arg string + args []string + retargs []string + 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() error { + required := p.lookup.required + + if len(required) == 0 { + return nil + } + + names := make([]string, 0, len(required)) + + for k := range required { + names = append(names, "`"+k.String()+"'") + } + + 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 + + if len(p.retargs) != 0 { + c, l := closestChoice(p.retargs[0], cmdnames) + msg = fmt.Sprintf("Unknown command `%s'", p.retargs[0]) + + 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 { + 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(ErrRequired, msg) +} + +func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg bool, argument *string) (retoption *Option, err error) { + if !option.canArgument() { + if argument != nil { + msg := fmt.Sprintf("bool flag `%s' cannot have an argument", option) + return option, newError(ErrNoArgumentForBool, msg) + } + + err = option.set(nil) + } else if argument != nil { + err = option.set(argument) + } else if canarg && !s.eof() { + arg := s.pop() + err = option.set(&arg) + } else if option.OptionalArgument { + option.clear() + + for _, v := range option.OptionalValue { + err = option.set(&v) + + if err != nil { + break + } + } + } else { + msg := fmt.Sprintf("expected argument for flag `%s'", option) + err = newError(ErrExpectedArgument, msg) + } + + if err != nil { + if _, ok := err.(*Error); !ok { + msg := fmt.Sprintf("invalid argument for flag `%s' (expected %s): %s", + option, + option.value.Type(), + err.Error()) + + err = newError(ErrMarshal, msg) + } + } + + return option, err +} + +func (p *Parser) parseLong(s *parseState, name string, argument *string) (option *Option, err 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 nil, newError(ErrUnknownFlag, fmt.Sprintf("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) (option *Option, err 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 option, err + } + } else { + return nil, newError(ErrUnknownFlag, fmt.Sprintf("unknown flag `%s'", shortname)) + } + + // Only the first option can have a concatted argument, so just + // clear argument here + argument = nil + } + + return option, nil +} + +func (p *Parser) parseNonOption(s *parseState) error { + if cmd := s.lookup.commands[s.arg]; cmd != nil { + if err := s.checkRequired(); err != nil { + return err + } + + s.command.Active = cmd + + s.command = cmd + s.lookup = cmd.makeLookup() + } else if (p.Options & PassAfterNonOption) != None { + // If PassAfterNonOption is set then all remaining arguments + // are considered positional + s.retargs = append(append(s.retargs, s.arg), s.args...) + s.args = []string{} + } else { + s.retargs = append(s.retargs, 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 +} diff --git a/github.com/jessevdk/go-flags/pointer_test.go b/github.com/jessevdk/go-flags/pointer_test.go new file mode 100644 index 000000000..e17445f69 --- /dev/null +++ b/github.com/jessevdk/go-flags/pointer_test.go @@ -0,0 +1,81 @@ +package flags + +import ( + "testing" +) + +func TestPointerBool(t *testing.T) { + var opts = struct { + Value *bool `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v") + + assertStringArray(t, ret, []string{}) + + if !*opts.Value { + t.Errorf("Expected Value to be true") + } +} + +func TestPointerString(t *testing.T) { + var opts = struct { + Value *string `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v", "value") + + assertStringArray(t, ret, []string{}) + assertString(t, *opts.Value, "value") +} + +func TestPointerSlice(t *testing.T) { + var opts = struct { + Value *[]string `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v", "value1", "-v", "value2") + + assertStringArray(t, ret, []string{}) + assertStringArray(t, *opts.Value, []string{"value1", "value2"}) +} + +func TestPointerMap(t *testing.T) { + var opts = struct { + Value *map[string]int `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v", "k1:2", "-v", "k2:-5") + + assertStringArray(t, ret, []string{}) + + if v, ok := (*opts.Value)["k1"]; !ok { + t.Errorf("Expected key \"k1\" to exist") + } else if v != 2 { + t.Errorf("Expected \"k1\" to be 2, but got %#v", v) + } + + if v, ok := (*opts.Value)["k2"]; !ok { + t.Errorf("Expected key \"k2\" to exist") + } else if v != -5 { + t.Errorf("Expected \"k2\" to be -5, but got %#v", v) + } +} + +type PointerGroup struct { + Value bool `short:"v"` +} + +func TestPointerGroup(t *testing.T) { + var opts = struct { + Group *PointerGroup `group:"Group Options"` + }{} + + ret := assertParseSuccess(t, &opts, "-v") + + assertStringArray(t, ret, []string{}) + + if !opts.Group.Value { + t.Errorf("Expected Group.Value to be true") + } +} diff --git a/github.com/jessevdk/go-flags/short_test.go b/github.com/jessevdk/go-flags/short_test.go new file mode 100644 index 000000000..20a7a38db --- /dev/null +++ b/github.com/jessevdk/go-flags/short_test.go @@ -0,0 +1,169 @@ +package flags + +import ( + "testing" +) + +func TestShort(t *testing.T) { + var opts = struct { + Value bool `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v") + + assertStringArray(t, ret, []string{}) + + if !opts.Value { + t.Errorf("Expected Value to be true") + } +} + +func TestShortTooLong(t *testing.T) { + var opts = struct { + Value bool `short:"vv"` + }{} + + assertParseFail(t, ErrShortNameTooLong, "short names can only be 1 character long, not `vv'", &opts) +} + +func TestShortRequired(t *testing.T) { + var opts = struct { + Value bool `short:"v" required:"true"` + }{} + + assertParseFail(t, ErrRequired, "the required flag `-v' was not specified", &opts) +} + +func TestShortMultiConcat(t *testing.T) { + var opts = struct { + V bool `short:"v"` + O bool `short:"o"` + F bool `short:"f"` + }{} + + ret := assertParseSuccess(t, &opts, "-vo", "-f") + + assertStringArray(t, ret, []string{}) + + if !opts.V { + t.Errorf("Expected V to be true") + } + + if !opts.O { + t.Errorf("Expected O to be true") + } + + if !opts.F { + t.Errorf("Expected F to be true") + } +} + +func TestShortMultiSlice(t *testing.T) { + var opts = struct { + Values []bool `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v", "-v") + + assertStringArray(t, ret, []string{}) + assertBoolArray(t, opts.Values, []bool{true, true}) +} + +func TestShortMultiSliceConcat(t *testing.T) { + var opts = struct { + Values []bool `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-vvv") + + assertStringArray(t, ret, []string{}) + assertBoolArray(t, opts.Values, []bool{true, true, true}) +} + +func TestShortWithEqualArg(t *testing.T) { + var opts = struct { + Value string `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v=value") + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "value") +} + +func TestShortWithArg(t *testing.T) { + var opts = struct { + Value string `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-vvalue") + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "value") +} + +func TestShortArg(t *testing.T) { + var opts = struct { + Value string `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-v", "value") + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "value") +} + +func TestShortMultiWithEqualArg(t *testing.T) { + var opts = struct { + F []bool `short:"f"` + Value string `short:"v"` + }{} + + assertParseFail(t, ErrExpectedArgument, "expected argument for flag `-v'", &opts, "-ffv=value") +} + +func TestShortMultiArg(t *testing.T) { + var opts = struct { + F []bool `short:"f"` + Value string `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-ffv", "value") + + assertStringArray(t, ret, []string{}) + assertBoolArray(t, opts.F, []bool{true, true}) + assertString(t, opts.Value, "value") +} + +func TestShortMultiArgConcatFail(t *testing.T) { + var opts = struct { + F []bool `short:"f"` + Value string `short:"v"` + }{} + + assertParseFail(t, ErrExpectedArgument, "expected argument for flag `-v'", &opts, "-ffvvalue") +} + +func TestShortMultiArgConcat(t *testing.T) { + var opts = struct { + F []bool `short:"f"` + Value string `short:"v"` + }{} + + ret := assertParseSuccess(t, &opts, "-vff") + + assertStringArray(t, ret, []string{}) + assertString(t, opts.Value, "ff") +} + +func TestShortOptional(t *testing.T) { + var opts = struct { + F []bool `short:"f"` + Value string `short:"v" optional:"yes" optional-value:"value"` + }{} + + ret := assertParseSuccess(t, &opts, "-fv", "f") + + assertStringArray(t, ret, []string{"f"}) + assertString(t, opts.Value, "value") +} diff --git a/github.com/jessevdk/go-flags/tag_test.go b/github.com/jessevdk/go-flags/tag_test.go new file mode 100644 index 000000000..a549b4e5a --- /dev/null +++ b/github.com/jessevdk/go-flags/tag_test.go @@ -0,0 +1,39 @@ +package flags + +import ( + "testing" +) + +func TestTagMissingColon(t *testing.T) { + var opts = struct { + Value bool `short` + }{} + + assertParseFail(t, ErrTag, "expected `:' after key name, but got end of tag (in `short`)", &opts, "") +} + +func TestTagMissingValue(t *testing.T) { + var opts = struct { + Value bool `short:` + }{} + + assertParseFail(t, ErrTag, "expected `\"' to start tag value at end of tag (in `short:`)", &opts, "") +} + +func TestTagMissingQuote(t *testing.T) { + var opts = struct { + Value bool `short:"v` + }{} + + assertParseFail(t, ErrTag, "expected end of tag value `\"' at end of tag (in `short:\"v`)", &opts, "") +} + +func TestTagNewline(t *testing.T) { + var opts = struct { + Value bool `long:"verbose" description:"verbose +something"` + }{} + + assertParseFail(t, ErrTag, "unexpected newline in tag value `description' (in `long:\"verbose\" description:\"verbose\nsomething\"`)", &opts, "") +} + diff --git a/github.com/jessevdk/go-flags/termsize.go b/github.com/jessevdk/go-flags/termsize.go new file mode 100644 index 000000000..7583a3da6 --- /dev/null +++ b/github.com/jessevdk/go-flags/termsize.go @@ -0,0 +1,5 @@ +package flags + +func getTerminalColumns() int { + return 80 +} diff --git a/github.com/jessevdk/go-flags/unknown_test.go b/github.com/jessevdk/go-flags/unknown_test.go new file mode 100644 index 000000000..858be4588 --- /dev/null +++ b/github.com/jessevdk/go-flags/unknown_test.go @@ -0,0 +1,66 @@ +package flags + +import ( + "testing" +) + +func TestUnknownFlags(t *testing.T) { + var opts = struct { + Verbose []bool `short:"v" long:"verbose" description:"Verbose output"` + }{} + + args := []string{ + "-f", + } + + p := NewParser(&opts, 0) + args, err := p.ParseArgs(args) + + if err == nil { + t.Fatal("Expected error for unknown argument") + } +} + +func TestIgnoreUnknownFlags(t *testing.T) { + var opts = struct { + Verbose []bool `short:"v" long:"verbose" description:"Verbose output"` + }{} + + args := []string{ + "hello", + "world", + "-v", + "--foo=bar", + "--verbose", + "-f", + } + + p := NewParser(&opts, IgnoreUnknown) + args, err := p.ParseArgs(args) + + if err != nil { + t.Fatal(err) + } + + exargs := []string{ + "hello", + "world", + "--foo=bar", + "-f", + } + + issame := (len(args) == len(exargs)) + + if issame { + for i := 0; i < len(args); i++ { + if args[i] != exargs[i] { + issame = false + break + } + } + } + + if !issame { + t.Fatalf("Expected %v but got %v", exargs, args) + } +} diff --git a/main.go b/main.go index 549b61a3d..9c2f34329 100644 --- a/main.go +++ b/main.go @@ -16,8 +16,8 @@ import ( "github.com/calmh/ini" "github.com/calmh/syncthing/discover" + flags "github.com/calmh/syncthing/github.com/jessevdk/go-flags" "github.com/calmh/syncthing/protocol" - flags "github.com/jessevdk/go-flags" ) type Options struct {