implement group namespaces
diff --git a/README.md b/README.md
index 0e31cb1..7daf91c 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@
* Supports same option multiple times (can store in slice or last option counts)
* Supports maps
* Supports function callbacks
+* Supports namespaces for (nested) option groups
The flags package uses structs, reflection and struct field tags
to allow users to specify command line options. This results in very simple
diff --git a/command_private.go b/command_private.go
index bd97cc7..2560324 100644
--- a/command_private.go
+++ b/command_private.go
@@ -120,7 +120,7 @@
}
if len(option.LongName) > 0 {
- ret.longNames[option.LongName] = option
+ ret.longNames[option.LongNameWithNamespace()] = option
}
}
})
diff --git a/flags.go b/flags.go
index 34adf1a..73da309 100644
--- a/flags.go
+++ b/flags.go
@@ -22,6 +22,7 @@
// Supports same option multiple times (can store in slice or last option counts)
// Supports maps
// Supports function callbacks
+// Supports namespaces for (nested) option groups
//
// Additional features specific to Windows:
// Options with short names (/v)
@@ -94,6 +95,10 @@
//
// group: when specified on a struct field, makes the struct
// field a separate group with the given name (optional)
+// namespace: when specified on a group struct field, the namespace
+// gets prepended to every option's long name and
+// subgroup's namespace of this group, separated by
+// the global namespace delimiter (optional)
// command: when specified on a struct field, makes the struct
// field a (sub)command with the given name (optional)
// subcommands-optional: when specified on a command struct field, makes
@@ -109,10 +114,10 @@
//
// 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 built-in
-// generated help. All options in a particular group are shown together in the
-// help under the name of the group.
+// Option groups are a simple way to semantically separate your options. All
+// options in a particular group are shown together in the help under the name
+// of the group. Namespaces can be used to specify option long names more
+// precisely and emphasize the options affiliation to their group.
//
// There are currently three ways to specify option groups.
//
diff --git a/group.go b/group.go
index b424f60..49f6621 100644
--- a/group.go
+++ b/group.go
@@ -29,6 +29,9 @@
// (Command embeds Group) in the built-in generated help and man pages.
LongDescription string
+ // The namespace of the group
+ Namespace string
+
// The parent of the group or nil if it has no parent
parent *Group
diff --git a/group_private.go b/group_private.go
index bbfef49..4ea7340 100644
--- a/group_private.go
+++ b/group_private.go
@@ -32,7 +32,7 @@
prio = 3
}
- if name == opt.LongName && prio < 2 {
+ if name == opt.LongNameWithNamespace() && prio < 2 {
retopt = opt
prio = 2
}
@@ -191,11 +191,13 @@
g.eachGroup(func(g *Group) {
for _, option := range g.options {
if option.LongName != "" {
- if otherOption, ok := longNames[option.LongName]; ok {
+ longName := option.LongNameWithNamespace()
+
+ if otherOption, ok := longNames[longName]; ok {
duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption)
return
}
- longNames[option.LongName] = option
+ longNames[longName] = option
}
if option.ShortName != 0 {
if otherOption, ok := shortNames[option.ShortName]; ok {
@@ -223,10 +225,13 @@
ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
description := mtag.Get("description")
- if _, err := g.AddGroup(subgroup, description, ptrval.Interface()); err != nil {
+ group, err := g.AddGroup(subgroup, description, ptrval.Interface())
+ if err != nil {
return true, err
}
+ group.Namespace = mtag.Get("namespace")
+
return true, nil
}
diff --git a/group_test.go b/group_test.go
index 35d0767..b5ed9d4 100644
--- a/group_test.go
+++ b/group_test.go
@@ -113,6 +113,33 @@
}
}
+func TestGroupNestedInlineNamespace(t *testing.T) {
+ var opts = struct {
+ Opt string `long:"opt"`
+
+ Group struct {
+ Opt string `long:"opt"`
+ Group struct {
+ Opt string `long:"opt"`
+ } `group:"Subsubgroup" namespace:"sap"`
+ } `group:"Subgroup" namespace:"sip"`
+ }{}
+
+ p, ret := assertParserSuccess(t, &opts, "--opt", "a", "--sip.opt", "b", "--sip.sap.opt", "c", "rest")
+
+ assertStringArray(t, ret, []string{"rest"})
+
+ assertString(t, opts.Opt, "a")
+ assertString(t, opts.Group.Opt, "b")
+ assertString(t, opts.Group.Group.Opt, "c")
+
+ for _, name := range []string{"Subgroup", "Subsubgroup"} {
+ if p.Command.Group.Find(name) == nil {
+ t.Errorf("Expected to find group '%s'", name)
+ }
+ }
+}
+
func TestDuplicateShortFlags(t *testing.T) {
var opts struct {
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
diff --git a/help.go b/help.go
index 4ded286..8283223 100644
--- a/help.go
+++ b/help.go
@@ -50,7 +50,7 @@
ret.hasValueName = true
}
- l := utf8.RuneCountInString(info.LongName) + lv
+ l := utf8.RuneCountInString(info.LongNameWithNamespace()) + lv
if c != p.Command {
// for indenting
@@ -109,7 +109,7 @@
}
line.WriteString(defaultLongOptDelimiter)
- line.WriteString(option.LongName)
+ line.WriteString(option.LongNameWithNamespace())
}
if option.canArgument() {
diff --git a/help_test.go b/help_test.go
index e3e84d8..0522ce7 100644
--- a/help_test.go
+++ b/help_test.go
@@ -57,6 +57,14 @@
IntMap map[string]int `long:"intmap" default:"a:1" description:"A map from string to int" ini-name:"int-map"`
} `group:"Other Options"`
+ Group struct {
+ Opt string `long:"opt" description:"This is a subgroup option"`
+
+ Group struct {
+ Opt string `long:"opt" description:"This is a subsubgroup option"`
+ } `group:"Subsubgroup" namespace:"sap"`
+ } `group:"Subgroup" namespace:"sip"`
+
Command struct {
ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"`
} `command:"command" alias:"cm" alias:"cmd" description:"A command"`
@@ -97,6 +105,12 @@
-s= A slice of strings (some, value)
--intmap= A map from string to int (a:1)
+Subgroup:
+ --sip.opt= This is a subgroup option
+
+Subsubgroup:
+ --sip.sap.opt= This is a subsubgroup option
+
Help Options:
-h, --help Show this help message
@@ -168,6 +182,12 @@
.TP
\fB--intmap\fP
A map from string to int
+.TP
+\fB--sip.opt\fP
+This is a subgroup option
+.TP
+\fB--sip.sap.opt\fP
+This is a subsubgroup option
.SH COMMANDS
.SS command
A command
diff --git a/ini_test.go b/ini_test.go
index 976c962..d25ac52 100644
--- a/ini_test.go
+++ b/ini_test.go
@@ -63,6 +63,14 @@
int-map = a:2
int-map = b:3
+[Subgroup]
+; This is a subgroup option
+Opt =
+
+[Subsubgroup]
+; This is a subsubgroup option
+Opt =
+
[command.A command]
; Use for extra verbosity
; ExtraVerbose =
@@ -103,6 +111,14 @@
; A map from string to int
; int-map = a:1
+[Subgroup]
+; This is a subgroup option
+; Opt =
+
+[Subsubgroup]
+; This is a subsubgroup option
+; Opt =
+
[command.A command]
; Use for extra verbosity
; ExtraVerbose =
@@ -141,6 +157,14 @@
; A map from string to int
; int-map = a:1
+[Subgroup]
+; This is a subgroup option
+; Opt =
+
+[Subsubgroup]
+; This is a subsubgroup option
+; Opt =
+
[command.A command]
; Use for extra verbosity
; ExtraVerbose =
diff --git a/man.go b/man.go
index 9e1f036..3097156 100644
--- a/man.go
+++ b/man.go
@@ -50,7 +50,7 @@
fmt.Fprintf(wr, ", ")
}
- fmt.Fprintf(wr, "--%s", opt.LongName)
+ fmt.Fprintf(wr, "--%s", opt.LongNameWithNamespace())
}
fmt.Fprintln(wr, "\\fP")
diff --git a/option.go b/option.go
index 17548a7..cb7ba75 100644
--- a/option.go
+++ b/option.go
@@ -67,6 +67,29 @@
tag multiTag
}
+// LongNameWithNamespace returns the option's long name with the group namespaces
+// prepended by walking up the option's group tree. Namespaces and the long name
+// itself are separated by the global namespace delimiter. If the long name is
+// empty an empty string is returned.
+func (option *Option) LongNameWithNamespace() string {
+ if len(option.LongName) == 0 {
+ return ""
+ }
+
+ longName := option.LongName
+ g := option.group
+
+ for g != nil {
+ if g.Namespace != "" {
+ longName = g.Namespace + NamespaceDelimiter + longName
+ }
+
+ g = g.parent
+ }
+
+ return longName
+}
+
// String converts an option to a human friendly readable string describing the
// option.
func (option *Option) String() string {
@@ -81,12 +104,12 @@
if len(option.LongName) != 0 {
s = fmt.Sprintf("%s%s, %s%s",
string(defaultShortOptDelimiter), short,
- defaultLongOptDelimiter, option.LongName)
+ defaultLongOptDelimiter, option.LongNameWithNamespace())
} else {
s = fmt.Sprintf("%s%s", string(defaultShortOptDelimiter), short)
}
} else if len(option.LongName) != 0 {
- s = fmt.Sprintf("%s%s", defaultLongOptDelimiter, option.LongName)
+ s = fmt.Sprintf("%s%s", defaultLongOptDelimiter, option.LongNameWithNamespace())
}
return s
diff --git a/optstyle.go b/optstyle.go
new file mode 100644
index 0000000..0eedda9
--- /dev/null
+++ b/optstyle.go
@@ -0,0 +1,6 @@
+package flags
+
+var (
+ // NamespaceDelimiter separates group namespaces and option long names
+ NamespaceDelimiter = "."
+)