Skip to content

Commit

Permalink
Add support for sub-commands:
Browse files Browse the repository at this point in the history
- `cmd.ProcessArgs` is changed to contain only the arguments, skip the `argv[0]` process name
- Internal `getCompletionRequest` returns adjusted index, where 0 is valid; return -1 when no completion request pending
- Adjust all test feeding `cmd.ProcessArgs` to not include command name
- Add `Name` and `SubCommands` fields to `Command` definition.
- `Command.Run()` locates the requested sub-command, and invokes it in place of the root command.
  • Loading branch information
maargenton committed Nov 5, 2023
1 parent ddbac18 commit a11653e
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 17 deletions.
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
"--timestamp",
"/dev/tty.Bluetooth-Incoming-Port",
]
},
{
"name": "subcmd-demo",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/sample/subcmd-demo",
"args": [
"migrate",
]
}
]
}
2 changes: 1 addition & 1 deletion cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func Run(cmd *Command) {
cmd.ProcessName = fileutils.Base(os.Args[0])
}
if cmd.ProcessArgs == nil {
cmd.ProcessArgs = os.Args
cmd.ProcessArgs = os.Args[1:]
}
cmd.ConsoleWidth = consoleWidth()
cmd.SetProcessEnv(os.Environ())
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/maargenton/go-testpredicate v1.3.0
golang.org/x/term v0.12.0
golang.org/x/tools v0.13.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
40 changes: 37 additions & 3 deletions pkg/cli/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import (
// Command is the representation of a runnable command, with reference to a
// runnable command attached to an options struct
type Command struct {
Handler Handler
Description string
Name string // Sub-command name, ignored for root command
Description string // Command description for help display
Handler Handler // Options recipient and handler for the command
SubCommands []Command // Optional sub-commands

ProcessName string
ProcessArgs []string
Expand Down Expand Up @@ -78,6 +80,13 @@ func (cmd *Command) SetProcessEnv(env []string) {
// decodes the command-line and run the command.
func (cmd *Command) Run() error {

if len(cmd.SubCommands) > 0 {
var sub = cmd.MatchSubCommand()
if sub != nil {
return sub.Run()
}
}

if err := cmd.initialize(); err != nil {
return err
}
Expand Down Expand Up @@ -110,7 +119,7 @@ func (cmd *Command) Run() error {
if err := cmd.opts.ApplyEnv(cmd.ProcessEnv); err != nil {
return err
}
if err := cmd.opts.ApplyArgs(cmd.ProcessArgs[1:]); err != nil {
if err := cmd.opts.ApplyArgs(cmd.ProcessArgs); err != nil {
return err
}

Expand All @@ -121,6 +130,31 @@ func (cmd *Command) Run() error {
return nil
}

// MatchSubCommand looks at the command ProcessArgs first argument and returns a
// matching sub-command if any.
func (cmd *Command) MatchSubCommand() *Command {
if len(cmd.ProcessArgs) < 1 {
return nil
}

var cmdName = cmd.ProcessArgs[0]
var args = cmd.ProcessArgs[1:]

for _, sub := range cmd.SubCommands {
if sub.Name == cmdName {
sub.ProcessName = cmd.ProcessName
sub.ProcessArgs = args
sub.ProcessEnv = cmd.ProcessEnv
sub.ConsoleWidth = cmd.ConsoleWidth
sub.DisableCompletion = cmd.DisableCompletion

return &sub
}
}
return nil

}

// Usage returns a string containign the usage for the command. The display name
// for the command is expected as first argument.
func (cmd *Command) Usage() string {
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestCommandRun(t *testing.T) {
var c = cmd.Handler.(*myCmd)

t.Run("when calling run with valid arguments", func(t *testing.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--arg", "123"}
cmd.ProcessArgs = []string{"-v", "--arg", "123"}
err := cmd.Run()

t.Run("then the fields are set and the command is run", func(t *testing.T) {
Expand All @@ -75,7 +75,7 @@ func TestCommandRun(t *testing.T) {
})

t.Run("when the command handler returns an error", func(t *testing.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--arg", "123"}
cmd.ProcessArgs = []string{"-v", "--arg", "123"}
c.err = fmt.Errorf("myError")
err := cmd.Run()

Expand All @@ -85,15 +85,15 @@ func TestCommandRun(t *testing.T) {
})

t.Run("when calling run with invalid arguments", func(t *testing.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--args", "123"}
cmd.ProcessArgs = []string{"-v", "--args", "123"}
err := cmd.Run()

t.Run("then an error is returned", func(t *testing.T) {
require.That(t, err).IsNotNil()
})
})
t.Run("when calling run with invalid environment value", func(t *testing.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--arg", "123"}
cmd.ProcessArgs = []string{"-v", "--arg", "123"}
cmd.ProcessEnv = map[string]string{
"TEST_ARG": "argument",
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/cli/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ func MatchingFilenameCompletion(opt *option.T, pattern string, w string) (r []st

func (cmd *Command) handleCompletionRequest() bool {
i, w := cmd.getCompletionRequest()
if i == 0 {
if i < 0 {
return false
}

// Trim process arguments past the completion point
var args = cmd.ProcessArgs[:]
// i = i - 1 // adjust for cmd.ProcessArgs skipping argv[0]
if i > 0 && i < len(args) {
args = args[:i]
}
Expand Down Expand Up @@ -118,15 +119,16 @@ func (cmd *Command) handleCompletionRequest() bool {
func (cmd *Command) getCompletionRequest() (index int, word string) {
var env = cmd.ProcessEnv

index = -1
word = env["COMP_WORD"]
if ii, ok := env["COMP_INDEX"]; ok {
if ii, err := strconv.ParseInt(ii, 0, 0); err == nil {
index = int(ii)
index = int(ii) - 1
}
}

var args = cmd.ProcessArgs[:]
if index == 0 || index >= len(args) || !strings.HasPrefix(args[index], word) {
if index < 0 || index >= len(args) || !strings.HasPrefix(args[index], word) {
word = ""
}
return
Expand Down
8 changes: 4 additions & 4 deletions pkg/cli/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func TestCommandRunCompletion(t *testing.T) {
var c = cmd.Handler.(*compCmd)

t.When("calling Run() with completion request and partial option flag", func(t *bdd.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--o"}
cmd.ProcessArgs = []string{"-v", "--o"}
cmd.ProcessEnv = map[string]string{
"COMP_WORD": "--o",
"COMP_INDEX": "2",
Expand All @@ -135,7 +135,7 @@ func TestCommandRunCompletion(t *testing.T) {
})

t.When("calling Run() with a partial option argument", func(t *bdd.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--option", "co"}
cmd.ProcessArgs = []string{"-v", "--option", "co"}
cmd.ProcessEnv = map[string]string{
"COMP_WORD": "co",
"COMP_INDEX": "3",
Expand Down Expand Up @@ -215,7 +215,7 @@ func TestCommandRunCompletion(t *testing.T) {
})

t.When("calling Run() with missing option argument", func(t *bdd.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--option"}
cmd.ProcessArgs = []string{"-v", "--option"}
cmd.ProcessEnv = map[string]string{
"COMP_WORD": "",
"COMP_INDEX": "3",
Expand All @@ -240,7 +240,7 @@ func TestCommandRunCompletionDebuf(t *testing.T) {
t.Setenv("COMPLETION_DEBUG_OUTPUT", filename)

t.When("calling Run() with completion request", func(t *bdd.T) {
cmd.ProcessArgs = []string{"command-name", "-v", "--o"}
cmd.ProcessArgs = []string{"-v", "--o"}
cmd.ProcessEnv = map[string]string{
"COMP_WORD": "--o",
"COMP_INDEX": "2",
Expand Down
4 changes: 2 additions & 2 deletions sample/sercat-demo/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package main

import (
"encoding/json"
"fmt"

"github.com/maargenton/go-cli"
"github.com/maargenton/go-cli/pkg/option"
"gopkg.in/yaml.v3"
)

func main() {
Expand Down Expand Up @@ -66,7 +66,7 @@ func (options *sercatCmd) Run() error {
options.Format = &v
}

d, err := json.Marshal(options)
d, err := yaml.Marshal(options)
if err != nil {
return err
}
Expand Down
67 changes: 67 additions & 0 deletions sample/subcmd-demo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"fmt"

"github.com/maargenton/go-cli"
"gopkg.in/yaml.v3"
)

func main() {
cli.Run(&cli.Command{
Handler: &ServerCmd{},
Description: "API server for demo service",

SubCommands: []cli.Command{
{
Name: "migrate",
Description: "run DB migration",
Handler: &MigrateCmd{},
},
},
})
}

type BaseOpts struct {
Debug bool `opts:"-d,--debug" desc:"generating more diagnostic upon error"`
Verbose bool `opts:"-v,--verbose" desc:"display additional information on startup"`

DB string `opts:"--db" desc:"primary DB connection string"`
DBPasswd string `opts:"--db-passwd" desc:"primary DB password"`
}

func (options *BaseOpts) Version() string {
return "subcmd-demo v0.1.2"
}

// ---------------------------------------------------------------------------

type ServerCmd struct {
BaseOpts
}

func (options *ServerCmd) Run() error {
d, err := yaml.Marshal(options)
if err != nil {
return err
}
fmt.Printf("Running server with options:\n%v\n", string(d))
return nil
}

// ---------------------------------------------------------------------------

type MigrateCmd struct {
BaseOpts

SkipDups bool `opts:"--skip-duplicates" desc:"skip duplicates during migration"`
}

func (options *MigrateCmd) Run() error {
d, err := yaml.Marshal(options)
if err != nil {
return err
}
fmt.Printf("Running migration with options:\n%v\n", string(d))
return nil
}

0 comments on commit a11653e

Please sign in to comment.