mirror of
https://github.com/dnote/dnote
synced 2026-03-18 08:19:55 +01:00
Add command package
This commit is contained in:
parent
b47c792d5f
commit
dc2501af50
15 changed files with 520 additions and 61 deletions
274
pkg/cli/command/command.go
Normal file
274
pkg/cli/command/command.go
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dnote/dnote/pkg/cli/log"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
// Name is the name of the command. This is used to find the subcommand
|
||||
// and is case sensitive.
|
||||
Name string
|
||||
|
||||
// flags is a FlagSet which can parse and hold flags and their values.
|
||||
flags *flag.FlagSet
|
||||
|
||||
// args is a list of arments
|
||||
args []string
|
||||
|
||||
// RunE is a function that contains the logic for the command
|
||||
RunE func(cmd *Command, args []string) error
|
||||
|
||||
// commands is the list of subcommands that this command has.
|
||||
commands []*Command
|
||||
|
||||
// Parent is a pointer to the parent command, if any, of which the command
|
||||
// is a subcommand.
|
||||
Parent *Command
|
||||
|
||||
Use string
|
||||
Short string
|
||||
SilenceErrors bool
|
||||
SilenceUsage bool
|
||||
Example string
|
||||
Aliases []string
|
||||
PreRunE func(cmd *Command, args []string) error
|
||||
Deprecated string
|
||||
Long string
|
||||
Run func(cmd *Command, args []string)
|
||||
}
|
||||
|
||||
// Flags returns a flag set for the command. If not initialized yet, it initializes
|
||||
// one and returns the result.
|
||||
func (c *Command) Flags() *flag.FlagSet {
|
||||
if c.flags == nil {
|
||||
c.flags = flag.NewFlagSet(c.Name, flag.ContinueOnError)
|
||||
}
|
||||
|
||||
return c.flags
|
||||
}
|
||||
|
||||
// ParseFlags parses the given slice of arguments using the flag set of the command.
|
||||
func (c *Command) ParseFlags(args []string) error {
|
||||
err := c.Flags().Parse(args)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error parsing flags")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Root returns the root command of the given command.
|
||||
func (c *Command) Root() *Command {
|
||||
if c.Parent != nil {
|
||||
return c.Parent.Root()
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// flagHasDefaultOption checks whether the given flag has a default value
|
||||
// in the given flag set.
|
||||
func flagHasDefaultOption(flag *flag.Flag, flagSet *flag.FlagSet) bool {
|
||||
if flag == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return flag.NoOptDefVal != ""
|
||||
}
|
||||
|
||||
// isFlagWithAdditionalArg checks whether the given string represents a flag
|
||||
// which is followed by an argument. For instance, suppose that there is no
|
||||
// default value for a flag "--flag". Then, given "--flag", this function returns
|
||||
// true because "--flag" must be followed by an argument. Similar logic applies to
|
||||
// a short-hand flag using a single dash. (e.g. "-f")
|
||||
func isFlagWithAdditionalArg(flagStr string, flagSet *flag.FlagSet) bool {
|
||||
// --flag arg
|
||||
if strings.HasPrefix(flagStr, "--") && !strings.Contains(flagStr, "=") {
|
||||
flagKey := flagStr[2:]
|
||||
flag := flagSet.Lookup(flagKey)
|
||||
|
||||
return !flagHasDefaultOption(flag, flagSet)
|
||||
}
|
||||
|
||||
// -f arg
|
||||
if strings.HasPrefix(flagStr, "-") && !strings.Contains(flagStr, "=") && len(flagStr) == 2 {
|
||||
flagKey := flagStr[1:]
|
||||
flag := flagSet.ShorthandLookup(flagKey)
|
||||
|
||||
return !flagHasDefaultOption(flag, flagSet)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// filterFlags removes flags and their values from the given arguments. It returns
|
||||
// a filtered slice which contains only non-flag arguments.
|
||||
func filterFlags(args []string, c *Command) []string {
|
||||
ret := []string{}
|
||||
flags := c.Flags()
|
||||
|
||||
idx := 0
|
||||
|
||||
for idx < len(args) {
|
||||
currentArg := args[idx]
|
||||
|
||||
// "--" signifies the end of command line flags
|
||||
if currentArg == "--" {
|
||||
ret = append(ret, args[idx+1:]...)
|
||||
break
|
||||
}
|
||||
|
||||
if isFlagWithAdditionalArg(currentArg, flags) {
|
||||
idx = idx + 2
|
||||
continue
|
||||
}
|
||||
|
||||
if currentArg != "" && !strings.HasPrefix(currentArg, "-") {
|
||||
ret = append(ret, currentArg)
|
||||
}
|
||||
|
||||
idx = idx + 1
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// setArgs sets the arguments for the command. It is useful while writing tests.
|
||||
func (c *Command) setArgs(args []string) {
|
||||
c.args = args
|
||||
}
|
||||
|
||||
// Args returns the argument for the command. By default, os.Args[1:] is used.
|
||||
func (c *Command) Args() []string {
|
||||
args := c.args
|
||||
if c.args == nil {
|
||||
args = os.Args[1:]
|
||||
}
|
||||
|
||||
return args
|
||||
|
||||
}
|
||||
|
||||
// Execute runs the root command. It is meant to be called on the root command.
|
||||
func (c *Command) Execute() error {
|
||||
// Call Execute on the root command
|
||||
if c.Parent != nil {
|
||||
return c.Root().Execute()
|
||||
}
|
||||
|
||||
args := c.Args()
|
||||
log.Debug("root command received arguments: %s\n", args)
|
||||
|
||||
cmd, flags := c.findSubCommand(args)
|
||||
if cmd == nil {
|
||||
// not found. show suggestion
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.execute(flags)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// execute runs the command.
|
||||
func (c *Command) execute(args []string) error {
|
||||
log.Debug("command '%s' called with arguments: %s\n", c.Name, args)
|
||||
|
||||
if err := c.ParseFlags(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nonFlagArgs := c.Flags().Args()
|
||||
log.Debug("command '%s' called with non-flag arguments: %s\n", c.Name, nonFlagArgs)
|
||||
|
||||
if err := c.RunE(c, nonFlagArgs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasAlias checks whether the command has the given alias.
|
||||
func (c *Command) hasAlias(targetAlias string) bool {
|
||||
for _, alias := range c.Aliases {
|
||||
if alias == targetAlias {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// findSubCommand finds and returns an appropriate subcommand to be called, based
|
||||
// on the given slice of arguments. It also returns a slice of arguments with which
|
||||
// the subcommand should be called.
|
||||
func (c *Command) findSubCommand(args []string) (*Command, []string) {
|
||||
nonFlagArgs := filterFlags(args, c)
|
||||
log.Debug("non-flag arguments: %s\n", nonFlagArgs)
|
||||
|
||||
subCommand := nonFlagArgs[0]
|
||||
log.Debug("sub-command: '%s'\n", subCommand)
|
||||
|
||||
for _, cmd := range c.commands {
|
||||
if cmd.Name == subCommand || cmd.hasAlias(subCommand) {
|
||||
return cmd, c.argsWithout(args, subCommand)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, []string{}
|
||||
}
|
||||
|
||||
// argsWithout removes the fist non-flag occurrence of the given key in the
|
||||
// given slice of arguments. It does not modify the given slice.
|
||||
func (c *Command) argsWithout(args []string, key string) []string {
|
||||
flags := c.Flags()
|
||||
|
||||
idx := -1
|
||||
pos := 0
|
||||
|
||||
for pos < len(args) {
|
||||
currentArg := args[pos]
|
||||
|
||||
// "--" signifies the end of command line flags
|
||||
if currentArg == "--" {
|
||||
break
|
||||
}
|
||||
|
||||
if isFlagWithAdditionalArg(currentArg, flags) {
|
||||
pos = pos + 2
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(currentArg, "-") {
|
||||
if currentArg == key {
|
||||
idx = pos
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pos = pos + 1
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
return args
|
||||
}
|
||||
|
||||
ret := []string{}
|
||||
ret = append(ret, args[:pos]...)
|
||||
ret = append(ret, args[pos+1:]...)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// AddCommand adds the given command as a subcommand.
|
||||
func (c *Command) AddCommand(cmd *Command) {
|
||||
cmd.Parent = c
|
||||
|
||||
c.commands = append(c.commands, cmd)
|
||||
}
|
||||
187
pkg/cli/command/command_test.go
Normal file
187
pkg/cli/command/command_test.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
)
|
||||
|
||||
func TestAddCommand(t *testing.T) {
|
||||
cmd := Command{Name: "root command"}
|
||||
assert.Equal(t, len(cmd.commands), 0, "Commands length mismatch")
|
||||
|
||||
subCommand1 := Command{Name: "foo"}
|
||||
cmd.AddCommand(&subCommand1)
|
||||
assert.Equal(t, subCommand1.Parent, &cmd, "subCommand1 Parent mismatch")
|
||||
assert.Equal(t, len(cmd.commands), 1, "Commands length mismatch")
|
||||
assert.Equal(t, cmd.commands[0], &subCommand1, "commands[0] mismatch")
|
||||
|
||||
subCommand2 := Command{Name: "bar"}
|
||||
cmd.AddCommand(&subCommand2)
|
||||
assert.Equal(t, len(cmd.commands), 2, "Commands length mismatch")
|
||||
assert.Equal(t, subCommand2.Parent, &cmd, "subCommand2 Parent mismatch")
|
||||
assert.Equal(t, cmd.commands[0], &subCommand1, "commands[0] mismatch")
|
||||
assert.Equal(t, cmd.commands[1], &subCommand2, "commands[1] mismatch")
|
||||
}
|
||||
|
||||
func TestHasAlias(t *testing.T) {
|
||||
cmd := Command{
|
||||
Name: "foo",
|
||||
Aliases: []string{"f", "bar"},
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd.hasAlias("f"), true, "Command should have 'f' alias")
|
||||
assert.Equal(t, cmd.hasAlias("F"), false, "Command should not have 'F' alias")
|
||||
assert.Equal(t, cmd.hasAlias("bar"), true, "Command should have 'bar' alias")
|
||||
assert.Equal(t, cmd.hasAlias("BAR"), false, "Command should have 'BAR' alias")
|
||||
assert.Equal(t, cmd.hasAlias("baz"), false, "Command should not have 'baz' alias")
|
||||
assert.Equal(t, cmd.hasAlias(""), false, "Command should not have an empty alias")
|
||||
}
|
||||
|
||||
func TestHasAlias_withoutAlias(t *testing.T) {
|
||||
cmd := Command{
|
||||
Name: "foo",
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd.hasAlias("f"), false, "Command should not have any alias")
|
||||
assert.Equal(t, cmd.hasAlias(""), false, "Command should not have any alias")
|
||||
}
|
||||
|
||||
func TestCommandRoot(t *testing.T) {
|
||||
subCommand2 := Command{
|
||||
Name: "baz",
|
||||
}
|
||||
subCommand1 := Command{
|
||||
Name: "bar",
|
||||
commands: []*Command{
|
||||
&subCommand2,
|
||||
},
|
||||
}
|
||||
cmd := Command{
|
||||
Name: "foo",
|
||||
commands: []*Command{
|
||||
&subCommand1,
|
||||
},
|
||||
}
|
||||
|
||||
subCommand1.Parent = &cmd
|
||||
subCommand2.Parent = &subCommand1
|
||||
|
||||
assert.Equal(t, cmd.Root(), &cmd, "Command should already be a root")
|
||||
assert.Equal(t, subCommand1.Root(), &cmd, "subCommand1 root mismatch")
|
||||
assert.Equal(t, subCommand2.Root(), &cmd, "subCommand2 root mismatch")
|
||||
}
|
||||
|
||||
func TestFindSubcommand(t *testing.T) {
|
||||
subCommand1 := Command{
|
||||
Name: "bar",
|
||||
Aliases: []string{"quz"},
|
||||
}
|
||||
subCommand2 := Command{
|
||||
Name: "baz",
|
||||
}
|
||||
cmd := Command{
|
||||
Name: "foo",
|
||||
commands: []*Command{
|
||||
&subCommand1,
|
||||
&subCommand2,
|
||||
},
|
||||
}
|
||||
|
||||
subcommandResult1, subcommandArgResult1 := cmd.findSubCommand([]string{"bar"})
|
||||
assert.Equal(t, subcommandResult1, &subCommand1, "Subcommand 'bar' mismatch")
|
||||
assert.DeepEqual(t, subcommandArgResult1, []string{}, "Subcommand arg for 'bar' mismatch")
|
||||
|
||||
subcommandResult2, subcommandArgResult2 := cmd.findSubCommand([]string{"baz", "echo"})
|
||||
assert.Equal(t, subcommandResult2, &subCommand2, "Subcommand 'baz' mismatch")
|
||||
assert.DeepEqual(t, subcommandArgResult2, []string{"echo"}, "Subcommand arg for 'baz' mismatch")
|
||||
|
||||
// Should match an alias
|
||||
subcommandResult3, subcommandArgResult3 := cmd.findSubCommand([]string{"quz"})
|
||||
assert.Equal(t, subcommandResult3, &subCommand1, "Subcommand 'quz' mismatch")
|
||||
assert.DeepEqual(t, subcommandArgResult3, []string{}, "Subcommand arg for 'quz' mismatch")
|
||||
|
||||
// Should not match if not exists
|
||||
subcommandResult4, subcommandArgResult4 := cmd.findSubCommand([]string{"qux"})
|
||||
assert.Equal(t, subcommandResult4, (*Command)(nil), "Subcommand 'qux' mismatch")
|
||||
assert.DeepEqual(t, subcommandArgResult4, []string{}, "Subcommand arg for 'qux' mismatch")
|
||||
|
||||
// Should not match itself
|
||||
subcommandResult5, subcommandArgResult5 := subCommand1.findSubCommand([]string{"bar"})
|
||||
assert.Equal(t, subcommandResult5, (*Command)(nil), "Subcommand 'bar' should not exist on 'bar'")
|
||||
assert.DeepEqual(t, subcommandArgResult5, []string{}, "Subcommand arg for 'bar' should be empty when there is no match")
|
||||
}
|
||||
|
||||
func executeCommand(root *Command, args ...string) error {
|
||||
return root.Execute()
|
||||
}
|
||||
|
||||
func TestFilterFlags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
arguments []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
arguments: []string{"a", "b", "c"},
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"-b", "cmd"},
|
||||
expected: []string{"cmd"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"foo", "-b", "--str"},
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"foo", "-b", "baz"},
|
||||
expected: []string{"foo", "baz"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"foo", "-i", "123"},
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"-b", "bar", "-i", "1", "a"},
|
||||
expected: []string{"bar", "a"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"a", "-b", "bar", "-i", "1"},
|
||||
expected: []string{"a", "bar"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"--foo", "b", "-baz"},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
arguments: []string{"--s=hello", "foo"},
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"-shello", "foo"},
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"-shello", "foo", "-i1"},
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
arguments: []string{"-si", "foo"},
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
}
|
||||
|
||||
cmd := Command{}
|
||||
cmd.Flags().StringP("str", "s", "", "")
|
||||
cmd.Flags().IntP("int", "i", 1, "")
|
||||
cmd.Flags().BoolP("bool", "b", false, "")
|
||||
// Default 'bool' to "true"
|
||||
cmd.Flags().Lookup("bool").NoOptDefVal = "true"
|
||||
|
||||
for idx, testCase := range testCases {
|
||||
got := filterFlags(testCase.arguments, &cmd)
|
||||
|
||||
assert.DeepEqual(t, got, testCase.expected, fmt.Sprintf("result mismatch for test case %d", idx))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue