Added support for positional arguments
diff --git a/arg.go b/arg.go
new file mode 100644
index 0000000..f436278
--- /dev/null
+++ b/arg.go
@@ -0,0 +1,17 @@
+package flags
+
+import (
+ "reflect"
+)
+
+type Arg struct {
+ Name string
+ Description string
+
+ value reflect.Value
+ tag multiTag
+}
+
+func (a *Arg) isRemaining() bool {
+ return a.value.Type().Kind() == reflect.Slice
+}
diff --git a/arg_test.go b/arg_test.go
new file mode 100644
index 0000000..faea280
--- /dev/null
+++ b/arg_test.go
@@ -0,0 +1,53 @@
+package flags
+
+import (
+ "testing"
+)
+
+func TestPositional(t *testing.T) {
+ var opts = struct {
+ Value bool `short:"v"`
+
+ Positional struct {
+ Command int
+ Filename string
+ Rest []string
+ } `positional-args:"yes" required:"yes"`
+ }{}
+
+ p := NewParser(&opts, Default)
+ ret, err := p.ParseArgs([]string{"10", "arg_test.go", "a", "b"})
+
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ return
+ }
+
+ if opts.Positional.Command != 10 {
+ t.Fatalf("Expected opts.Positional.Command to be 10, but got %v", opts.Positional.Command)
+ }
+
+ if opts.Positional.Filename != "arg_test.go" {
+ t.Fatalf("Expected opts.Positional.Filename to be \"arg_test.go\", but got %v", opts.Positional.Filename)
+ }
+
+ assertStringArray(t, opts.Positional.Rest, []string{"a", "b"})
+ assertStringArray(t, ret, []string{})
+}
+
+func TestPositionalRequired(t *testing.T) {
+ var opts = struct {
+ Value bool `short:"v"`
+
+ Positional struct {
+ Command int
+ Filename string
+ Rest []string
+ } `positional-args:"yes" required:"yes"`
+ }{}
+
+ p := NewParser(&opts, None)
+ _, err := p.ParseArgs([]string{"10"})
+
+ assertError(t, err, ErrRequired, "the required argument `Filename` was not provided")
+}
diff --git a/command.go b/command.go
index 16369c4..d6dbc42 100644
--- a/command.go
+++ b/command.go
@@ -20,8 +20,12 @@
// Aliases for the command
Aliases []string
+ // Whether positional arguments are required
+ ArgsRequired bool
+
commands []*Command
hasBuiltinHelpGroup bool
+ args []*Arg
}
// Commander is an interface which can be implemented by any command added in
@@ -92,3 +96,11 @@
return nil
}
+
+// Args returns a list of positional arguments associated with this command
+func (c *Command) Args() []*Arg {
+ ret := make([]*Arg, len(c.args))
+ copy(ret, c.args)
+
+ return ret
+}
diff --git a/command_private.go b/command_private.go
index b5d60ab..36d564b 100644
--- a/command_private.go
+++ b/command_private.go
@@ -29,6 +29,44 @@
return true, err
}
+ positional := mtag.Get("positional-args")
+
+ if len(positional) != 0 {
+ stype := realval.Type()
+
+ for i := 0; i < stype.NumField(); i++ {
+ field := stype.Field(i)
+
+ m := newMultiTag((string(field.Tag)))
+
+ if err := m.Parse(); err != nil {
+ return true, err
+ }
+
+ name := m.Get("name")
+
+ if len(name) == 0 {
+ name = field.Name
+ }
+
+ arg := &Arg{
+ Name: name,
+ Description: m.Get("description"),
+
+ value: realval.Field(i),
+ tag: m,
+ }
+
+ c.args = append(c.args, arg)
+
+ if len(mtag.Get("required")) != 0 {
+ c.ArgsRequired = true
+ }
+ }
+
+ return true, nil
+ }
+
subcommand := mtag.Get("command")
if len(subcommand) != 0 {
diff --git a/example_test.go b/example_test.go
index b06dd27..b5bf67a 100644
--- a/example_test.go
+++ b/example_test.go
@@ -4,7 +4,6 @@
import (
"fmt"
"os/exec"
- "strings"
)
func Example() {
@@ -36,6 +35,13 @@
// Example of a map
IntMap map[string]int `long:"intmap" description:"A map from string to int"`
+
+ // Example of positional arguments
+ Args struct {
+ Id string
+ Num int
+ Rest []string
+ } `positional-args:"yes" required:"yes"`
}
// Callback which will invoke callto:<argument> to call a number.
@@ -59,15 +65,16 @@
"--ptrslice", "world",
"--intmap", "a:1",
"--intmap", "b:5",
- "arg1",
- "arg2",
- "arg3",
+ "id",
+ "10",
+ "remaining1",
+ "remaining2",
}
// 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)
+ _, err := ParseArgs(&opts, args)
if err != nil {
panic(err)
@@ -80,7 +87,9 @@
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, " "))
+ fmt.Printf("Args.Id: %s\n", opts.Args.Id)
+ fmt.Printf("Args.Num: %d\n", opts.Args.Num)
+ fmt.Printf("Args.Rest: %v\n", opts.Args.Rest)
// Output: Verbosity: [true true]
// Offset: 5
@@ -89,5 +98,7 @@
// StringSlice: [hello world]
// PtrSlice: [hello world]
// IntMap: [a:1 b:5]
- // Remaining args: arg1 arg2 arg3
+ // Args.Id: id
+ // Args.Num: 10
+ // Args.Rest: [remaining1 remaining2]
}
diff --git a/flags.go b/flags.go
index 13b0ee3..be007a5 100644
--- a/flags.go
+++ b/flags.go
@@ -107,6 +107,14 @@
// specified name as an alias for the command. Can be
// be specified multiple times to add more than one
// alias (optional)
+// positional-args: when specified on a field with a struct type,
+// uses the fields of that struct to parse remaining
+// positional command line arguments into (in order
+// of the fields). If a field has a slice type,
+// then all remaining arguments will be added to it.
+// Positional arguments are optional by default,
+// unless the "required" tag is specified together
+// with the "positional-args" tag (optional)
//
// Either short: or long: must be specified to make the field eligible as an
// option.
diff --git a/help.go b/help.go
index 8283223..0139215 100644
--- a/help.go
+++ b/help.go
@@ -22,6 +22,41 @@
indent bool
}
+const (
+ paddingBeforeOption = 2
+ distanceBetweenOptionAndDescription = 2
+)
+
+func (a *alignmentInfo) descriptionStart() int {
+ ret := a.maxLongLen + distanceBetweenOptionAndDescription
+
+ if a.hasShort {
+ ret += 2
+ }
+
+ if a.maxLongLen > 0 {
+ ret += 4
+ }
+
+ if a.hasValueName {
+ ret += 3
+ }
+
+ return ret
+}
+
+func (a *alignmentInfo) updateLen(name string, indent bool) {
+ l := utf8.RuneCountInString(name)
+
+ if indent {
+ l = l + 4
+ }
+
+ if l > a.maxLongLen {
+ a.maxLongLen = l
+ }
+}
+
func (p *Parser) getAlignmentInfo() alignmentInfo {
ret := alignmentInfo{
maxLongLen: 0,
@@ -34,7 +69,15 @@
ret.terminalColumns = 80
}
+ var prevcmd *Command
+
p.eachActiveGroup(func(c *Command, grp *Group) {
+ if c != prevcmd {
+ for _, arg := range c.args {
+ ret.updateLen(arg.Name, c != p.Command)
+ }
+ }
+
for _, info := range grp.options {
if !info.canCli() {
continue
@@ -44,22 +87,11 @@
ret.hasShort = true
}
- lv := utf8.RuneCountInString(info.ValueName)
-
- if lv != 0 {
+ if len(info.ValueName) > 0 {
ret.hasValueName = true
}
- l := utf8.RuneCountInString(info.LongNameWithNamespace()) + lv
-
- if c != p.Command {
- // for indenting
- l = l + 4
- }
-
- if l > ret.maxLongLen {
- ret.maxLongLen = l
- }
+ ret.updateLen(info.LongNameWithNamespace()+info.ValueName, c != p.Command)
}
})
@@ -69,9 +101,6 @@
func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) {
line := &bytes.Buffer{}
- distanceBetweenOptionAndDescription := 2
- paddingBeforeOption := 2
-
prefix := paddingBeforeOption
if info.indent {
@@ -87,19 +116,7 @@
line.WriteString(" ")
}
- descstart := info.maxLongLen + paddingBeforeOption + distanceBetweenOptionAndDescription
-
- if info.hasShort {
- descstart += 2
- }
-
- if info.maxLongLen > 0 {
- descstart += 4
- }
-
- if info.hasValueName {
- descstart += 3
- }
+ descstart := info.descriptionStart() + paddingBeforeOption
if len(option.LongName) > 0 {
if option.ShortName != 0 {
@@ -236,6 +253,28 @@
fmt.Fprintf(wr, " %s", allcmd.Name)
}
+ if len(allcmd.args) > 0 {
+ fmt.Fprintf(wr, " ")
+ }
+
+ for i, arg := range allcmd.args {
+ if i != 0 {
+ fmt.Fprintf(wr, " ")
+ }
+
+ name := arg.Name
+
+ if arg.isRemaining() {
+ name = name + "..."
+ }
+
+ if !allcmd.ArgsRequired {
+ fmt.Fprintf(wr, "[%s]", name)
+ } else {
+ fmt.Fprintf(wr, "%s", name)
+ }
+ }
+
if allcmd.Active == nil && len(allcmd.commands) > 0 {
var co, cc string
@@ -275,26 +314,32 @@
}
}
- prevcmd := p.Command
+ c := p.Command
- p.eachActiveGroup(func(c *Command, grp *Group) {
- first := true
+ for c != nil {
+ printcmd := c != p.Command
- // Skip built-in help group for all commands except the top-level
- // parser
- if grp.isBuiltinHelp && c != p.Command {
- return
- }
+ c.eachGroup(func(grp *Group) {
+ first := true
- for _, info := range grp.options {
- if info.canCli() {
- if prevcmd != c {
- fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
- prevcmd = c
- aligninfo.indent = true
+ // Skip built-in help group for all commands except the top-level
+ // parser
+ if grp.isBuiltinHelp && c != p.Command {
+ return
+ }
+
+ for _, info := range grp.options {
+ if !info.canCli() {
+ continue
}
- if first && prevcmd.Group != grp {
+ if printcmd {
+ fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
+ aligninfo.indent = true
+ printcmd = false
+ }
+
+ if first && cmd.Group != grp {
fmt.Fprintln(wr)
if aligninfo.indent {
@@ -307,8 +352,32 @@
p.writeHelpOption(wr, info, aligninfo)
}
+ })
+
+ if len(c.args) > 0 {
+ if c == p.Command {
+ fmt.Fprintf(wr, "\nArguments:\n")
+ } else {
+ fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
+ }
+
+ maxlen := aligninfo.descriptionStart()
+
+ for _, arg := range c.args {
+ prefix := strings.Repeat(" ", paddingBeforeOption)
+ fmt.Fprintf(wr, "%s%s", prefix, arg.Name)
+
+ if len(arg.Description) > 0 {
+ align := strings.Repeat(" ", maxlen-len(arg.Name)-1)
+ fmt.Fprintf(wr, ":%s%s", align, arg.Description)
+ }
+
+ fmt.Fprintln(wr)
+ }
}
- })
+
+ c = c.Active
+ }
scommands := cmd.sortedCommands()
diff --git a/help_test.go b/help_test.go
index a59cfbf..27de672 100644
--- a/help_test.go
+++ b/help_test.go
@@ -73,6 +73,11 @@
Command struct {
ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"`
} `command:"command" alias:"cm" alias:"cmd" description:"A command"`
+
+ Args struct {
+ Filename string `name:"filename" description:"A filename"`
+ Num int `name:"num" description:"A number"`
+ } `positional-args:"yes"`
}
func TestHelp(t *testing.T) {
@@ -98,7 +103,7 @@
if runtime.GOOS == "windows" {
expected = `Usage:
- TestHelp [OPTIONS] <command>
+ TestHelp [OPTIONS] [filename] [num] <command>
Application Options:
/v, /verbose Show verbose debug information
@@ -123,12 +128,16 @@
/? Show this help message
/h, /help Show this help message
+Arguments:
+ filename: A filename
+ num: A number
+
Available commands:
command A command (aliases: cm, cmd)
`
} else {
expected = `Usage:
- TestHelp [OPTIONS] <command>
+ TestHelp [OPTIONS] [filename] [num] <command>
Application Options:
-v, --verbose Show verbose debug information
@@ -152,6 +161,10 @@
Help Options:
-h, --help Show this help message
+Arguments:
+ filename: A filename
+ num: A number
+
Available commands:
command A command (aliases: cm, cmd)
`
diff --git a/ini_test.go b/ini_test.go
index 6d037bf..eb752e9 100644
--- a/ini_test.go
+++ b/ini_test.go
@@ -15,7 +15,7 @@
expected string
}{
{
- []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "command"},
+ []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"},
IniDefault,
`[Application Options]
; Show verbose debug information
@@ -30,7 +30,7 @@
`,
},
{
- []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "command"},
+ []string{"-vv", "--intmap=a:2", "--intmap", "b:3", "filename", "0", "command"},
IniDefault | IniIncludeDefaults,
`[Application Options]
; Show verbose debug information
@@ -80,7 +80,7 @@
`,
},
{
- []string{"command"},
+ []string{"filename", "0", "command"},
IniDefault | IniIncludeDefaults | IniCommentDefaults,
`[Application Options]
; Show verbose debug information
@@ -128,7 +128,7 @@
`,
},
{
- []string{"--default=New value", "--default-array=New value", "--default-map=new:value", "command"},
+ []string{"--default=New value", "--default-array=New value", "--default-map=new:value", "filename", "0", "command"},
IniDefault | IniIncludeDefaults | IniCommentDefaults,
`[Application Options]
; Show verbose debug information
diff --git a/parser.go b/parser.go
index 4ebf1aa..aa93450 100644
--- a/parser.go
+++ b/parser.go
@@ -146,11 +146,15 @@
p.addHelpGroups(p.showBuiltinHelp)
}
+ positional := make([]*Arg, len(p.args))
+ copy(positional, p.args)
+
s := &parseState{
- args: args,
- retargs: make([]string, 0, len(args)),
- command: p.Command,
- lookup: p.makeLookup(),
+ args: args,
+ retargs: make([]string, 0, len(args)),
+ positional: positional,
+ command: p.Command,
+ lookup: p.makeLookup(),
}
for !s.eof() {
@@ -159,7 +163,7 @@
// 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...)
+ s.addArgs(s.args...)
break
}
@@ -194,7 +198,7 @@
}
if ignoreUnknown {
- s.retargs = append(s.retargs, arg)
+ s.addArgs(arg)
}
}
}
diff --git a/parser_private.go b/parser_private.go
index df640d8..a6ba1ae 100644
--- a/parser_private.go
+++ b/parser_private.go
@@ -10,10 +10,11 @@
)
type parseState struct {
- arg string
- args []string
- retargs []string
- err error
+ arg string
+ args []string
+ retargs []string
+ positional []*Arg
+ err error
command *Command
lookup lookup
@@ -60,6 +61,34 @@
}
if len(required) == 0 {
+ if len(p.positional) > 0 && p.command.ArgsRequired {
+ reqnames := make([]string, 0)
+
+ for _, arg := range p.positional {
+ if arg.isRemaining() {
+ break
+ }
+
+ reqnames = append(reqnames, "`"+arg.Name+"`")
+ }
+
+ if len(reqnames) == 0 {
+ return nil
+ }
+
+ var msg string
+
+ if len(reqnames) == 1 {
+ msg = fmt.Sprintf("the required argument %s was not provided", reqnames[0])
+ } else {
+ msg = fmt.Sprintf("the required arguments %s and %s were not provided",
+ strings.Join(reqnames[:len(reqnames)-1], ", "), reqnames[len(reqnames)-1])
+ }
+
+ p.err = newError(ErrRequired, msg)
+ return p.err
+ }
+
return nil
}
@@ -226,19 +255,52 @@
return nil
}
+func (s *parseState) addArgs(args ...string) error {
+ for len(s.positional) > 0 && len(args) > 0 {
+ arg := s.positional[0]
+
+ if err := convert(args[0], arg.value, arg.tag); err != nil {
+ return err
+ }
+
+ if !arg.isRemaining() {
+ s.positional = s.positional[1:]
+ }
+
+ args = args[1:]
+ }
+
+ s.retargs = append(s.retargs, args...)
+ return nil
+}
+
func (p *Parser) parseNonOption(s *parseState) error {
+ if len(s.positional) > 0 {
+ return s.addArgs(s.arg)
+ }
+
if cmd := s.lookup.commands[s.arg]; cmd != nil {
s.command.Active = cmd
s.command = cmd
s.lookup = cmd.makeLookup()
+
+ s.positional = make([]*Arg, len(cmd.args))
+ copy(s.positional, cmd.args)
} 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...)
+ if err := s.addArgs(s.arg); err != nil {
+ return err
+ }
+
+ if err := s.addArgs(s.args...); err != nil {
+ return err
+ }
+
s.args = []string{}
} else {
- s.retargs = append(s.retargs, s.arg)
+ return s.addArgs(s.arg)
}
return nil