Skip to content

Commit

Permalink
Merge pull request #231 from alexflint/subcommand-aliases
Browse files Browse the repository at this point in the history
add subcommand aliases
  • Loading branch information
alexflint committed Oct 10, 2023
2 parents 5ec29ce + e7a4f77 commit bf629a1
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 112 deletions.
30 changes: 21 additions & 9 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type spec struct {
// command represents a named subcommand, or the top-level command
type command struct {
name string
aliases []string
help string
dest path
specs []*spec
Expand Down Expand Up @@ -153,7 +154,7 @@ type Parser struct {
epilogue string

// the following field changes during processing of command line arguments
lastCmd *command
subcommand []string
}

// Versioned is the interface that the destination struct should implement to
Expand Down Expand Up @@ -384,18 +385,24 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
}
case key == "subcommand":
// decide on a name for the subcommand
cmdname := value
if cmdname == "" {
cmdname = strings.ToLower(field.Name)
var cmdnames []string
if value == "" {
cmdnames = []string{strings.ToLower(field.Name)}
} else {
cmdnames = strings.Split(value, "|")
}
for i := range cmdnames {
cmdnames[i] = strings.TrimSpace(cmdnames[i])
}

// parse the subcommand recursively
subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type)
if err != nil {
errs = append(errs, err.Error())
return false
}

subcmd.aliases = cmdnames[1:]
subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")

Expand Down Expand Up @@ -514,13 +521,13 @@ func (p *Parser) MustParse(args []string) {
err := p.Parse(args)
switch {
case err == ErrHelp:
p.writeHelpForSubcommand(p.config.Out, p.lastCmd)
p.WriteHelpForSubcommand(p.config.Out, p.subcommand...)
p.config.Exit(0)
case err == ErrVersion:
fmt.Fprintln(p.config.Out, p.version)
p.config.Exit(0)
case err != nil:
p.failWithSubcommand(err.Error(), p.lastCmd)
p.FailSubcommand(err.Error(), p.subcommand...)
}
}

Expand Down Expand Up @@ -577,7 +584,7 @@ func (p *Parser) process(args []string) error {

// union of specs for the chain of subcommands encountered so far
curCmd := p.cmd
p.lastCmd = curCmd
p.subcommand = nil

// make a copy of the specs because we will add to this list each time we expand a subcommand
specs := make([]*spec, len(curCmd.specs))
Expand Down Expand Up @@ -648,7 +655,7 @@ func (p *Parser) process(args []string) error {
}

curCmd = subcmd
p.lastCmd = curCmd
p.subcommand = append(p.subcommand, arg)
continue
}

Expand Down Expand Up @@ -842,6 +849,11 @@ func findSubcommand(cmds []*command, name string) *command {
if cmd.name == name {
return cmd
}
for _, alias := range cmd.aliases {
if alias == name {
return cmd
}
}
}
return nil
}
6 changes: 4 additions & 2 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,8 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
require.NoError(t, err)

err = p.Parse([]string{"sub"})
assert.NoError(t, err)
require.NoError(t, err)
require.NotNil(t, args.Sub)
assert.Equal(t, "", args.Sub.Foo)
}

Expand Down Expand Up @@ -1731,7 +1732,8 @@ func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
require.NoError(t, err)

err = p.Parse([]string{"sub", "-g"})
assert.NoError(t, err)
require.NoError(t, err)
assert.False(t, args.Global)
require.NotNil(t, args.Sub)
assert.True(t, args.Sub.Guard)
}
42 changes: 24 additions & 18 deletions subcommand.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
package arg

import "fmt"

// Subcommand returns the user struct for the subcommand selected by
// the command line arguments most recently processed by the parser.
// The return value is always a pointer to a struct. If no subcommand
// was specified then it returns the top-level arguments struct. If
// no command line arguments have been processed by this parser then it
// returns nil.
func (p *Parser) Subcommand() interface{} {
if p.lastCmd == nil || p.lastCmd.parent == nil {
if len(p.subcommand) == 0 {
return nil
}
cmd, err := p.lookupCommand(p.subcommand...)
if err != nil {
return nil
}
return p.val(p.lastCmd.dest).Interface()
return p.val(cmd.dest).Interface()
}

// SubcommandNames returns the sequence of subcommands specified by the
// user. If no subcommands were given then it returns an empty slice.
func (p *Parser) SubcommandNames() []string {
if p.lastCmd == nil {
return nil
}

// make a list of ancestor commands
var ancestors []string
cur := p.lastCmd
for cur.parent != nil { // we want to exclude the root
ancestors = append(ancestors, cur.name)
cur = cur.parent
}
return p.subcommand
}

// reverse the list
out := make([]string, len(ancestors))
for i := 0; i < len(ancestors); i++ {
out[i] = ancestors[len(ancestors)-i-1]
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
// first string should be a top-level subcommand, the next should be a child
// subcommand of that subcommand, and so on. If no strings are given then the
// root command is returned. If no such subcommand exists then an error is
// returned.
func (p *Parser) lookupCommand(path ...string) (*command, error) {
cmd := p.cmd
for _, name := range path {
found := findSubcommand(cmd.subcommands, name)
if found == nil {
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
}
cmd = found
}
return out
return cmd, nil
}
95 changes: 95 additions & 0 deletions subcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ func TestNamedSubcommand(t *testing.T) {
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}

func TestSubcommandAliases(t *testing.T) {
type listCmd struct {
}
var args struct {
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}

func TestEmptySubcommand(t *testing.T) {
type listCmd struct {
}
Expand Down Expand Up @@ -113,6 +126,23 @@ func TestTwoSubcommands(t *testing.T) {
assert.Equal(t, []string{"list"}, p.SubcommandNames())
}

func TestTwoSubcommandsWithAliases(t *testing.T) {
type getCmd struct {
}
type listCmd struct {
}
var args struct {
Get *getCmd `arg:"subcommand:get|g"`
List *listCmd `arg:"subcommand:list|ls"`
}
p, err := pparse("ls", &args)
require.NoError(t, err)
assert.Nil(t, args.Get)
assert.NotNil(t, args.List)
assert.Equal(t, args.List, p.Subcommand())
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}

func TestSubcommandsWithOptions(t *testing.T) {
type getCmd struct {
Name string
Expand Down Expand Up @@ -275,6 +305,60 @@ func TestNestedSubcommands(t *testing.T) {
}
}

func TestNestedSubcommandsWithAliases(t *testing.T) {
type child struct{}
type parent struct {
Child *child `arg:"subcommand:child|ch"`
}
type grandparent struct {
Parent *parent `arg:"subcommand:parent|pa"`
}
type root struct {
Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
}

{
var args root
p, err := pparse("gp parent child", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.NotNil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
}

{
var args root
p, err := pparse("grandparent pa", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.NotNil(t, args.Grandparent.Parent)
require.Nil(t, args.Grandparent.Parent.Child)
assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
}

{
var args root
p, err := pparse("grandparent", &args)
require.NoError(t, err)
require.NotNil(t, args.Grandparent)
require.Nil(t, args.Grandparent.Parent)
assert.Equal(t, args.Grandparent, p.Subcommand())
assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
}

{
var args root
p, err := pparse("", &args)
require.NoError(t, err)
require.Nil(t, args.Grandparent)
assert.Nil(t, p.Subcommand())
assert.Empty(t, p.SubcommandNames())
}
}

func TestSubcommandsWithPositionals(t *testing.T) {
type listCmd struct {
Pattern string `arg:"positional"`
Expand Down Expand Up @@ -411,3 +495,14 @@ func TestValForNilStruct(t *testing.T) {
v := p.val(path{fields: []reflect.StructField{subField, subField}})
assert.False(t, v.IsValid())
}

func TestSubcommandInvalidInternal(t *testing.T) {
// this situation should never arise in practice but still good to test for it
var cmd struct{}
p, err := NewParser(Config{}, &cmd)
require.NoError(t, err)

p.subcommand = []string{"should", "never", "happen"}
sub := p.Subcommand()
assert.Nil(t, sub)
}
Loading

0 comments on commit bf629a1

Please sign in to comment.