Skip to content

Commit

Permalink
feat: Allow a Delegator to nest another Delegator
Browse files Browse the repository at this point in the history
Removes the restriction on how deeply one can compose their CLI program.
Previously, a Delegator could not have other Delegators as a subcommand.
Attempting to select one of those resulted in an error:
        one does not simply create too many layers of delegators

These changes are an attempt to allow the developer to create an
arbitrary number of levels of delegators. Added some tests for one extra
level than before. This will probably cover most use cases. Haven't
tried creating more levels because it's not that immediately practical,
but will cross that bridge if it arrives.
  • Loading branch information
rafaelespinoza committed May 16, 2021
1 parent 572c8e0 commit ee1c366
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 60 deletions.
63 changes: 22 additions & 41 deletions alf.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package alf
import (
"context"
"errors"
"flag"
)

// Root is your main, top-level command.
Expand All @@ -20,56 +21,22 @@ type Root struct {

// Run parses the top-level flags, extracts the positional arguments and
// executes the command. Invoke this from main with args as os.Args[1:].
func (r *Root) Run(ctx context.Context, args []string) error {
if err := r.Flags.Parse(args); err != nil {
return err
func (r *Root) Run(ctx context.Context, args []string) (err error) {
if err = r.Flags.Parse(args); err != nil {
return
}
if r.PrePerform != nil {
err := r.PrePerform(ctx)
err = r.PrePerform(ctx)
if errors.Is(err, ErrShowUsage) {
r.Flags.Usage()
}
if err != nil {
return err
return
}
}
var directive Directive
err := r.Perform(ctx)
if r.Selected == nil {
// either asked for help or asked for unknown command.
r.Flags.Usage()
} else {
directive = r.Selected
}
if err != nil {
return err
}

if cmd, ok := directive.(*Command); ok {
ierr := cmd.Perform(ctx)
if errors.Is(ierr, ErrShowUsage) {
cmd.flags.Usage()
}
return ierr
}

delegator := directive.(*Delegator)
if err = delegator.Perform(ctx); err != nil {
delegator.Flags.Usage()
return err
}

subcmd, ok := delegator.Selected.(*Command)
if !ok {
// yeah, let's try not to create too deep of a command hierarchy.
err = errors.New("one does not simply create too many layers of delegators")
} else {
err = subcmd.Perform(ctx)
}
if errors.Is(err, ErrShowUsage) {
subcmd.flags.Usage()
}
return err
err = r.Perform(ctx)
return
}

// Directive is an abstraction for a parent or child command. A parent would
Expand All @@ -86,3 +53,17 @@ type Directive interface {
// specifically request it. It has no text so it doesn't mess up your error
// message if you're use error wrapping.
var ErrShowUsage = errors.New("")

func maybeCallUsage(err error, flags *flag.FlagSet) {
if err == nil {
return
}
for _, target := range []error{ErrShowUsage, flag.ErrHelp, errUnknownCommand} {
if errors.Is(err, target) {
flags.Usage()
return
}
}
}

var errUnknownCommand = errors.New("unknown command")
64 changes: 53 additions & 11 deletions alf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@ func newStubRoot(name string, usage *string) alf.Root {
}
}

india := alf.Delegator{
Description: "subcmd of a subcmd with more subs",
Flags: newMutedFlagSet("india", flag.ContinueOnError),
Subs: map[string]alf.Directive{
"foo": &alf.Command{
Description: "works OK",
Setup: func(p flag.FlagSet) *flag.FlagSet {
f := newMutedFlagSet("delta india foo", flag.ContinueOnError)
f.Usage = func() { firstUsage("root.delta.india.foo") }
return f
},
Run: func(ctx context.Context) error { return nil },
},
"bar": &alf.Command{
Description: "has an error",
Setup: func(p flag.FlagSet) *flag.FlagSet {
f := newMutedFlagSet("delta india bar", flag.ContinueOnError)
f.Usage = func() { firstUsage("root.delta.india.bar") }
return f
},
Run: func(ctx context.Context) error { return errStub },
},
},
}
india.Flags.Usage = func() { firstUsage("root.delta.india") }

delta := alf.Delegator{
Description: "subcmd with more subs",
Flags: newMutedFlagSet("delta", flag.ContinueOnError),
Expand Down Expand Up @@ -66,6 +92,7 @@ func newStubRoot(name string, usage *string) alf.Root {
},
Run: func(ctx context.Context) error { return alf.ErrShowUsage },
},
"india": &india,
},
}
delta.Flags.IntVar(&bar, "bar", 2, "bbb")
Expand Down Expand Up @@ -136,27 +163,33 @@ func TestRoot(t *testing.T) {
// Returns whatever the Command returns.
{args: []string{"alpha"}, expErr: false},
{args: []string{"bravo"}, expErr: true},
// Help on Command
// Calls correct Usage function (Command).
{args: []string{"charlie"}, expErr: true, expUsage: "root.charlie"},
// Works with flag values on Root's FlagSet.
{args: []string{"-foo", "fff", "alpha"}, expErr: false},
// Parent flags should be specified before the subcommand.
{args: []string{"alpha", "-foo", "fff"}, expErr: true, expUsage: "root.alpha"},
// Help on a Delegator.
// Calls correct Usage function (Delegator).
{args: []string{"delta"}, expErr: true, expUsage: "root.delta"},
{args: []string{"delta", "-h"}, expErr: true, expUsage: "root.delta"},
// Access Delegator -> Command.
{args: []string{"delta", "echo"}, expErr: false},
{args: []string{"delta", "foxtrot"}, expErr: false},
{args: []string{"delta", "golf"}, expErr: false},
// Help on Delegator -> Command.
// Calls correct Usage function (Delegator -> Command).
{args: []string{"delta", "echo", "-h"}, expErr: true, expUsage: "root.delta.echo"},
{args: []string{"delta", "foxtrot", "-h"}, expErr: true, expUsage: "root.delta.foxtrot"},
{args: []string{"delta", "golf", "-h"}, expErr: true, expUsage: "root.delta"},
{args: []string{"delta", "hotel"}, expErr: true, expUsage: "root.delta.hotel"},
// Access Delegator -> Delegator -> Command.
{args: []string{"delta", "india", "foo"}, expErr: false},
{args: []string{"delta", "india", "bar"}, expErr: true},
// Calls correct Usage function (Delegator -> Delegator -> Command).
{args: []string{"delta", "india", "foo", "-h"}, expErr: true, expUsage: "root.delta.india.foo"},
{args: []string{"delta", "india", "bar", "-h"}, expErr: true, expUsage: "root.delta.india.bar"},
// Unknown command.
{args: []string{"echo"}, expErr: true, expUsage: "root"},
{args: []string{"delta", "india"}, expErr: true, expUsage: "root.delta"},
{args: []string{"delta", "zulu"}, expErr: true, expUsage: "root.delta"},
// Optional PrePerform field.
{
args: []string{"alpha"},
Expand Down Expand Up @@ -302,15 +335,24 @@ func TestDelegator(t *testing.T) {
}

func TestCommand(t *testing.T) {
root := newStubRoot(t.Name(), nil)
var usage string
root := newStubRoot(t.Name(), &usage)

got := root.Subs["alpha"].Perform(context.TODO())
if got != nil {
t.Errorf("should return result of Run; got %v, expected %v", got, nil)
}

alpha := root.Subs["alpha"].Perform(context.TODO())
if alpha != nil {
t.Errorf("should return result of Run; got %v, expected %v", alpha, nil)
got = root.Subs["bravo"].Perform(context.TODO())
if got != errStub {
t.Errorf("should return result of Run; got %v, expected %v", got, errStub)
}

bravo := root.Subs["bravo"].Perform(context.TODO())
if bravo != errStub {
t.Errorf("should return result of Run; got %v, expected %v", bravo, errStub)
// simulate traversing down the tree to a known Command.
deleg := root.Subs["delta"].(*alf.Delegator)
nestedDeleg := deleg.Subs["india"].(*alf.Delegator)
got = nestedDeleg.Subs["bar"].Perform(context.TODO())
if got != errStub {
t.Errorf("should return result of Run; got %v, expected %v", got, errStub)
}
}
28 changes: 20 additions & 8 deletions delegator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ type Delegator struct {
Flags *flag.FlagSet
// Selected is the chosen transfer point of control.
Selected Directive
// Subs associates a name with another Directive. It's probably best to not
// create too deep of a hierarchy of Delegators pointing to Delegators. An
// exception to this recommendation is a Root command with some Delegators
// as direct childen, which in turn have just one more level of subcommands.
// Subs associates a name with another Directive. The name is what to
// specify from the command line.
Subs map[string]Directive
}

Expand All @@ -31,7 +29,9 @@ func (d *Delegator) Summary() string { return d.Description }
func (d *Delegator) Perform(ctx context.Context) error {
args := d.Flags.Args()
if len(args) < 1 {
return flag.ErrHelp
err := flag.ErrHelp
maybeCallUsage(err, d.Flags)
return err
}

var err error
Expand All @@ -40,21 +40,33 @@ func (d *Delegator) Perform(ctx context.Context) error {
err = flag.ErrHelp
default:
if cmd, ok := d.Subs[first]; !ok {
err = fmt.Errorf("unknown command %q", first)
err = fmt.Errorf("%w %q", errUnknownCommand, first)
} else {
d.Selected = cmd
}
}
if err != nil {
maybeCallUsage(err, d.Flags)
return err
}

switch selected := d.Selected.(type) {
case *Command:
selected.flags = selected.Setup(*d.Flags)
err = selected.flags.Parse(args[1:])
if err = selected.flags.Parse(args[1:]); err != nil {
return err
}
err = selected.Perform(ctx)
maybeCallUsage(err, selected.flags)
case *Delegator:
err = selected.Flags.Parse(args[1:])
f := selected.Flags
if f == nil {
return fmt.Errorf("selected Delegator %q requires Flags", args[0])
}
if err = f.Parse(args[1:]); err != nil {
return err
}
err = selected.Perform(ctx)
default:
err = fmt.Errorf("unsupported value of type %T", selected)
}
Expand Down
51 changes: 51 additions & 0 deletions examples/full/bar.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"strings"
Expand Down Expand Up @@ -133,5 +134,55 @@ Description:
},
}

// This demonstrates a subcommand that is both:
// - a child of a parent command
// - and a parent of some child commands
nested := alf.Delegator{
Description: "a subcommand (with its own commands) of a subcommand",
Subs: map[string]alf.Directive{
"alfa": &alf.Command{
Description: "terminal command of a nested subcommand",
Setup: func(inFlags flag.FlagSet) *flag.FlagSet {
inFlags.Init("nested alfa", flag.ContinueOnError)
inFlags.Usage = func() { fmt.Println("help for nested.alfa") }
return &inFlags
},
Run: func(ctx context.Context) error {
fmt.Println("called bar.moar.alfa")
return nil
},
},
"bravo": &alf.Command{
Description: "terminal command of a nested subcommand, (returns error)",
Setup: func(inFlags flag.FlagSet) *flag.FlagSet {
inFlags.Init("nested bravo", flag.ContinueOnError)
inFlags.Usage = func() { fmt.Println("help for nested.bravo") }
return &inFlags
},
Run: func(ctx context.Context) error {
fmt.Println("called bar.moar.bravo")
return errors.New("demo error")
},
},
},
}
nested.Flags = flag.NewFlagSet("nested", flag.ContinueOnError)
nested.Flags.Usage = func() {
fmt.Fprintf(nested.Flags.Output(), `Usage:
%s [flags]
Description:
Demo of nested Delegator (subcommand has a parent and its own subcommands).
Subcommands:
%v`, _Bin, strings.Join(nested.DescribeSubcommands(), "\n\t"))
fmt.Printf("\n\nFlags:\n\n")
nested.Flags.PrintDefaults()
}
del.Subs["nested"] = &nested

return del
}("bar")

0 comments on commit ee1c366

Please sign in to comment.