diff --git a/pkg/cli/cmd/add/add.go b/pkg/cli/cmd/add/add.go index 22d4a1ad..52c848e9 100644 --- a/pkg/cli/cmd/add/add.go +++ b/pkg/cli/cmd/add/add.go @@ -20,9 +20,10 @@ package add import ( "database/sql" - "time" "os" + "time" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/database" "github.com/dnote/dnote/pkg/cli/infra" @@ -33,7 +34,6 @@ import ( "github.com/dnote/dnote/pkg/cli/utils" "github.com/dnote/dnote/pkg/cli/validate" "github.com/pkg/errors" - "github.com/spf13/cobra" ) var contentFlag string @@ -52,7 +52,7 @@ var example = ` pull is fetch with a merge EOF` -func preRun(cmd *cobra.Command, args []string) error { +func preRun(cmd *command.Command, args []string) error { if len(args) != 1 { return errors.New("Incorrect number of argument") } @@ -61,8 +61,9 @@ func preRun(cmd *cobra.Command, args []string) error { } // NewCmd returns a new add command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ + Name: "add", Use: "add ", Short: "Add a new note", Aliases: []string{"a", "n", "new"}, @@ -84,7 +85,7 @@ func getContent(ctx context.DnoteCtx) (string, error) { // check for piped content fInfo, _ := os.Stdin.Stat() - if fInfo.Mode() & os.ModeCharDevice == 0 { + if fInfo.Mode()&os.ModeCharDevice == 0 { c, err := ui.ReadStdInput() if err != nil { return "", errors.Wrap(err, "Failed to get piped input") @@ -106,7 +107,7 @@ func getContent(ctx context.DnoteCtx) (string, error) { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { bookName := args[0] if err := validate.BookName(bookName); err != nil { return errors.Wrap(err, "invalid book name") diff --git a/pkg/cli/cmd/cat/cat.go b/pkg/cli/cmd/cat/cat.go index a82d0635..adb9753c 100644 --- a/pkg/cli/cmd/cat/cat.go +++ b/pkg/cli/cmd/cat/cat.go @@ -21,13 +21,13 @@ package cat import ( "strconv" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/database" "github.com/dnote/dnote/pkg/cli/infra" "github.com/dnote/dnote/pkg/cli/log" "github.com/dnote/dnote/pkg/cli/output" "github.com/pkg/errors" - "github.com/spf13/cobra" ) var example = ` @@ -40,7 +40,7 @@ var deprecationWarning = `and "view" will replace it in the future version. Run "dnote view --help" for more information. ` -func preRun(cmd *cobra.Command, args []string) error { +func preRun(cmd *command.Command, args []string) error { if len(args) != 2 { return errors.New("Incorrect number of arguments") } @@ -49,8 +49,8 @@ func preRun(cmd *cobra.Command, args []string) error { } // NewCmd returns a new cat command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "cat ", Aliases: []string{"c"}, Short: "See a note", @@ -65,7 +65,7 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { // NewRun returns a new run function func NewRun(ctx context.DnoteCtx, contentOnly bool) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { var noteRowIDArg string if len(args) == 2 { diff --git a/pkg/cli/cmd/edit/edit.go b/pkg/cli/cmd/edit/edit.go index c2b3c5cd..7d53ce94 100644 --- a/pkg/cli/cmd/edit/edit.go +++ b/pkg/cli/cmd/edit/edit.go @@ -19,12 +19,12 @@ package edit import ( + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/infra" "github.com/dnote/dnote/pkg/cli/log" "github.com/dnote/dnote/pkg/cli/utils" "github.com/pkg/errors" - "github.com/spf13/cobra" ) var contentFlag string @@ -49,8 +49,8 @@ var example = ` ` // NewCmd returns a new edit command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "edit ", Short: "Edit a note or a book", Aliases: []string{"e"}, @@ -67,7 +67,7 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { return cmd } -func preRun(cmd *cobra.Command, args []string) error { +func preRun(cmd *command.Command, args []string) error { if len(args) != 1 && len(args) != 2 { return errors.New("Incorrect number of argument") } @@ -76,7 +76,7 @@ func preRun(cmd *cobra.Command, args []string) error { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { // DEPRECATED: Remove in 1.0.0 if len(args) == 2 { log.Plain(log.ColorYellow.Sprintf("DEPRECATED: you no longer need to pass book name to the view command. e.g. `dnote view 123`.\n\n")) diff --git a/pkg/cli/cmd/find/find.go b/pkg/cli/cmd/find/find.go index dce3ad57..930ae07f 100644 --- a/pkg/cli/cmd/find/find.go +++ b/pkg/cli/cmd/find/find.go @@ -23,11 +23,11 @@ import ( "fmt" "strings" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/infra" "github.com/dnote/dnote/pkg/cli/log" "github.com/pkg/errors" - "github.com/spf13/cobra" ) var example = ` @@ -43,7 +43,7 @@ var example = ` var bookName string -func preRun(cmd *cobra.Command, args []string) error { +func preRun(cmd *command.Command, args []string) error { if len(args) != 1 { return errors.New("Incorrect number of argument") } @@ -52,8 +52,8 @@ func preRun(cmd *cobra.Command, args []string) error { } // NewCmd returns a new remove command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "find", Short: "Find notes by keywords", Aliases: []string{"f"}, @@ -154,7 +154,7 @@ func doQuery(ctx context.DnoteCtx, query, bookName string) (*sql.Rows, error) { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { phrase, err := escapePhrase(args[0]) if err != nil { return errors.Wrap(err, "escaping phrase") diff --git a/pkg/cli/cmd/login/login.go b/pkg/cli/cmd/login/login.go index a383bd6a..7549c61e 100644 --- a/pkg/cli/cmd/login/login.go +++ b/pkg/cli/cmd/login/login.go @@ -24,6 +24,7 @@ import ( "strconv" "github.com/dnote/dnote/pkg/cli/client" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/consts" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/database" @@ -31,7 +32,6 @@ import ( "github.com/dnote/dnote/pkg/cli/log" "github.com/dnote/dnote/pkg/cli/ui" "github.com/pkg/errors" - "github.com/spf13/cobra" ) var example = ` @@ -40,8 +40,8 @@ var example = ` var usernameFlag, passwordFlag string // NewCmd returns a new login command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "login", Short: "Login to dnote server", Example: example, @@ -150,7 +150,7 @@ func getGreeting(ctx context.DnoteCtx) string { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { greeting := getGreeting(ctx) log.Plain(greeting) diff --git a/pkg/cli/cmd/logout/logout.go b/pkg/cli/cmd/logout/logout.go index e1800ee2..2f23b802 100644 --- a/pkg/cli/cmd/logout/logout.go +++ b/pkg/cli/cmd/logout/logout.go @@ -22,13 +22,13 @@ import ( "database/sql" "github.com/dnote/dnote/pkg/cli/client" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/consts" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/database" "github.com/dnote/dnote/pkg/cli/infra" "github.com/dnote/dnote/pkg/cli/log" "github.com/pkg/errors" - "github.com/spf13/cobra" ) // ErrNotLoggedIn is an error for logging out when not logged in @@ -38,8 +38,8 @@ var example = ` dnote logout` // NewCmd returns a new logout command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "logout", Short: "Logout from the server", Example: example, @@ -83,7 +83,7 @@ func Do(ctx context.DnoteCtx) error { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { err := Do(ctx) if err == ErrNotLoggedIn { log.Error("not logged in\n") diff --git a/pkg/cli/cmd/ls/ls.go b/pkg/cli/cmd/ls/ls.go index 9f22ab4e..94e485a9 100644 --- a/pkg/cli/cmd/ls/ls.go +++ b/pkg/cli/cmd/ls/ls.go @@ -23,11 +23,11 @@ import ( "fmt" "strings" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/infra" "github.com/dnote/dnote/pkg/cli/log" "github.com/pkg/errors" - "github.com/spf13/cobra" ) var example = ` @@ -43,7 +43,7 @@ var deprecationWarning = `and "view" will replace it in the future version. Run "dnote view --help" for more information. ` -func preRun(cmd *cobra.Command, args []string) error { +func preRun(cmd *command.Command, args []string) error { if len(args) > 1 { return errors.New("Incorrect number of argument") } @@ -52,8 +52,8 @@ func preRun(cmd *cobra.Command, args []string) error { } // NewCmd returns a new ls command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "ls ", Aliases: []string{"l", "notes"}, Short: "List all notes", @@ -68,7 +68,7 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { // NewRun returns a new run function for ls func NewRun(ctx context.DnoteCtx, nameOnly bool) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { if len(args) == 0 { if err := printBooks(ctx, nameOnly); err != nil { return errors.Wrap(err, "viewing books") diff --git a/pkg/cli/cmd/remove/remove.go b/pkg/cli/cmd/remove/remove.go index 82394db4..90daa828 100644 --- a/pkg/cli/cmd/remove/remove.go +++ b/pkg/cli/cmd/remove/remove.go @@ -22,6 +22,7 @@ import ( "fmt" "strconv" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/database" "github.com/dnote/dnote/pkg/cli/infra" @@ -30,7 +31,6 @@ import ( "github.com/dnote/dnote/pkg/cli/ui" "github.com/dnote/dnote/pkg/cli/utils" "github.com/pkg/errors" - "github.com/spf13/cobra" ) var bookFlag string @@ -45,8 +45,8 @@ var example = ` ` // NewCmd returns a new remove command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "remove ", Short: "Remove a note or a book", Aliases: []string{"rm", "d", "delete"}, @@ -64,7 +64,7 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { return cmd } -func preRun(cmd *cobra.Command, args []string) error { +func preRun(cmd *command.Command, args []string) error { if len(args) != 1 && len(args) != 2 { return errors.New("Incorrect number of argument") } @@ -81,7 +81,7 @@ func maybeConfirm(message string, defaultValue bool) (bool, error) { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { // DEPRECATED: Remove in 1.0.0 if bookFlag != "" { if err := runBook(ctx, bookFlag); err != nil { diff --git a/pkg/cli/cmd/root/root.go b/pkg/cli/cmd/root/root.go index 854a6b53..2ae475e6 100644 --- a/pkg/cli/cmd/root/root.go +++ b/pkg/cli/cmd/root/root.go @@ -19,21 +19,18 @@ package root import ( - "github.com/spf13/cobra" + "github.com/dnote/dnote/pkg/cli/command" ) -var root = &cobra.Command{ +var root = &command.Command{ Use: "dnote", Short: "Dnote - a simple command line notebook", SilenceErrors: true, SilenceUsage: true, - CompletionOptions: cobra.CompletionOptions{ - DisableDefaultCmd: true, - }, } // Register adds a new command -func Register(cmd *cobra.Command) { +func Register(cmd *command.Command) { root.AddCommand(cmd) } diff --git a/pkg/cli/cmd/sync/sync.go b/pkg/cli/cmd/sync/sync.go index c6b0d698..2f29a386 100644 --- a/pkg/cli/cmd/sync/sync.go +++ b/pkg/cli/cmd/sync/sync.go @@ -23,6 +23,7 @@ import ( "fmt" "github.com/dnote/dnote/pkg/cli/client" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/consts" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/database" @@ -31,7 +32,6 @@ import ( "github.com/dnote/dnote/pkg/cli/migrate" "github.com/dnote/dnote/pkg/cli/upgrade" "github.com/pkg/errors" - "github.com/spf13/cobra" ) const ( @@ -45,8 +45,8 @@ var example = ` var isFullSync bool // NewCmd returns a new sync command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "sync", Aliases: []string{"s"}, Short: "Sync data with the server", @@ -886,7 +886,7 @@ func saveSyncState(tx *database.DB, serverTime int64, serverMaxUSN int) error { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { if ctx.SessionKey == "" { return errors.New("not logged in") } diff --git a/pkg/cli/cmd/version/version.go b/pkg/cli/cmd/version/version.go index bb050fa3..906dd753 100644 --- a/pkg/cli/cmd/version/version.go +++ b/pkg/cli/cmd/version/version.go @@ -21,17 +21,17 @@ package version import ( "fmt" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" - "github.com/spf13/cobra" ) // NewCmd returns a new version command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "version", Short: "Print the version number of Dnote", Long: "Print the version number of Dnote", - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *command.Command, args []string) { fmt.Printf("dnote %s\n", ctx.Version) }, } diff --git a/pkg/cli/cmd/view/view.go b/pkg/cli/cmd/view/view.go index cc39a451..8c0e9fdf 100644 --- a/pkg/cli/cmd/view/view.go +++ b/pkg/cli/cmd/view/view.go @@ -19,10 +19,10 @@ package view import ( + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/infra" "github.com/pkg/errors" - "github.com/spf13/cobra" "github.com/dnote/dnote/pkg/cli/cmd/cat" "github.com/dnote/dnote/pkg/cli/cmd/ls" @@ -43,7 +43,7 @@ var example = ` var nameOnly bool var contentOnly bool -func preRun(cmd *cobra.Command, args []string) error { +func preRun(cmd *command.Command, args []string) error { if len(args) > 2 { return errors.New("Incorrect number of argument") } @@ -52,8 +52,8 @@ func preRun(cmd *cobra.Command, args []string) error { } // NewCmd returns a new view command -func NewCmd(ctx context.DnoteCtx) *cobra.Command { - cmd := &cobra.Command{ +func NewCmd(ctx context.DnoteCtx) *command.Command { + cmd := &command.Command{ Use: "view ", Aliases: []string{"v"}, Short: "List books, notes or view a content", @@ -70,7 +70,7 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } func newRun(ctx context.DnoteCtx) infra.RunEFunc { - return func(cmd *cobra.Command, args []string) error { + return func(cmd *command.Command, args []string) error { var run infra.RunEFunc if len(args) == 0 { diff --git a/pkg/cli/command/command.go b/pkg/cli/command/command.go new file mode 100644 index 00000000..c8e1c96d --- /dev/null +++ b/pkg/cli/command/command.go @@ -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) +} diff --git a/pkg/cli/command/command_test.go b/pkg/cli/command/command_test.go new file mode 100644 index 00000000..628005c0 --- /dev/null +++ b/pkg/cli/command/command_test.go @@ -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)) + } +} diff --git a/pkg/cli/infra/init.go b/pkg/cli/infra/init.go index fc767e6b..d81df19d 100644 --- a/pkg/cli/infra/init.go +++ b/pkg/cli/infra/init.go @@ -28,6 +28,7 @@ import ( "strconv" "time" + "github.com/dnote/dnote/pkg/cli/command" "github.com/dnote/dnote/pkg/cli/config" "github.com/dnote/dnote/pkg/cli/consts" "github.com/dnote/dnote/pkg/cli/context" @@ -38,11 +39,10 @@ import ( "github.com/dnote/dnote/pkg/cli/utils" "github.com/dnote/dnote/pkg/clock" "github.com/pkg/errors" - "github.com/spf13/cobra" ) // RunEFunc is a function type of dnote commands -type RunEFunc func(*cobra.Command, []string) error +type RunEFunc func(*command.Command, []string) error func checkLegacyDBPath() (string, bool) { legacyDnoteDir := getLegacyDnotePath(dirs.Home)