Skip to content

Commit

Permalink
implement group namespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
zimmski committed May 13, 2014
1 parent 5b8d63e commit 7762684
Show file tree
Hide file tree
Showing 12 changed files with 128 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -25,6 +25,7 @@ Supported features:
* 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
Expand Down
2 changes: 1 addition & 1 deletion command_private.go
Expand Up @@ -120,7 +120,7 @@ func (c *Command) makeLookup() lookup {
}

if len(option.LongName) > 0 {
ret.longNames[option.LongName] = option
ret.longNames[option.LongNameWithNamespace()] = option
}
}
})
Expand Down
13 changes: 9 additions & 4 deletions flags.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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.
//
Expand Down
3 changes: 3 additions & 0 deletions group.go
Expand Up @@ -29,6 +29,9 @@ type Group struct {
// (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

Expand Down
13 changes: 9 additions & 4 deletions group_private.go
Expand Up @@ -32,7 +32,7 @@ func (g *Group) optionByName(name string, namematch func(*Option, string) bool)
prio = 3
}

if name == opt.LongName && prio < 2 {
if name == opt.LongNameWithNamespace() && prio < 2 {
retopt = opt
prio = 2
}
Expand Down Expand Up @@ -191,11 +191,13 @@ func (g *Group) checkForDuplicateFlags() *Error {
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 {
Expand Down Expand Up @@ -223,10 +225,13 @@ func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.Struc
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
}

Expand Down
27 changes: 27 additions & 0 deletions group_test.go
Expand Up @@ -113,6 +113,33 @@ func TestGroupNestedInline(t *testing.T) {
}
}

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"`
Expand Down
4 changes: 2 additions & 2 deletions help.go
Expand Up @@ -50,7 +50,7 @@ func (p *Parser) getAlignmentInfo() alignmentInfo {
ret.hasValueName = true
}

l := utf8.RuneCountInString(info.LongName) + lv
l := utf8.RuneCountInString(info.LongNameWithNamespace()) + lv

if c != p.Command {
// for indenting
Expand Down Expand Up @@ -109,7 +109,7 @@ func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alig
}

line.WriteString(defaultLongOptDelimiter)
line.WriteString(option.LongName)
line.WriteString(option.LongNameWithNamespace())
}

if option.canArgument() {
Expand Down
20 changes: 20 additions & 0 deletions help_test.go
Expand Up @@ -57,6 +57,14 @@ type helpOptions struct {
IntMap map[string]int `long:"intmap" default:"a:1" description:"A map from string to int" ini-name:"int-map"`
} `group:"Other Options"`

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"`
Expand Down Expand Up @@ -97,6 +105,12 @@ Other Options:
-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
Expand Down Expand Up @@ -168,6 +182,12 @@ A slice of strings
.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
Expand Down
24 changes: 24 additions & 0 deletions ini_test.go
Expand Up @@ -63,6 +63,14 @@ StringSlice = value
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 =
Expand Down Expand Up @@ -103,6 +111,14 @@ int-map = b:3
; 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 =
Expand Down Expand Up @@ -141,6 +157,14 @@ DefaultMap = new:value
; 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 =
Expand Down
2 changes: 1 addition & 1 deletion man.go
Expand Up @@ -50,7 +50,7 @@ func writeManPageOptions(wr io.Writer, grp *Group) {
fmt.Fprintf(wr, ", ")
}

fmt.Fprintf(wr, "--%s", opt.LongName)
fmt.Fprintf(wr, "--%s", opt.LongNameWithNamespace())
}

fmt.Fprintln(wr, "\\fP")
Expand Down
27 changes: 25 additions & 2 deletions option.go
Expand Up @@ -67,6 +67,29 @@ type Option struct {
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 {
Expand All @@ -81,12 +104,12 @@ func (option *Option) String() string {
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
Expand Down
6 changes: 6 additions & 0 deletions optstyle.go
@@ -0,0 +1,6 @@
package flags

var (
// NamespaceDelimiter separates group namespaces and option long names
NamespaceDelimiter = "."
)

0 comments on commit 7762684

Please sign in to comment.