From e98403428e3ba55bafdea231daef2227887e3411 Mon Sep 17 00:00:00 2001 From: Sung Date: Sat, 11 Mar 2023 18:07:58 +1100 Subject: [PATCH 1/3] Remove autoclose of inactive issue --- .github/workflows/ci.yml | 49 ---------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index d953aedb..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: CI -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - build: - runs-on: ubuntu-22.04 - - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: dnote_test - POSTGRES_PORT: 5432 - # Wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - # Expose port to the host - ports: - - 5432:5432 - - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - with: - go-version: '1.20.0' - - - name: Install dependencies - run: | - make install - - - name: Test cli - run: | - make test-cli - - - name: Test app - run: | - make test-api From 37b137cbe7d27cf43651af1d9cd44cb3de0ef388 Mon Sep 17 00:00:00 2001 From: Sung Date: Sat, 11 Mar 2023 12:22:35 +1100 Subject: [PATCH 2/3] Add command package --- pkg/cli/cmd/add/add.go | 18 ++-- pkg/cli/cmd/cat/cat.go | 10 +- pkg/cli/cmd/edit/edit.go | 19 ++-- pkg/cli/cmd/find/find.go | 13 +-- pkg/cli/cmd/login/login.go | 15 +-- pkg/cli/cmd/logout/logout.go | 8 +- pkg/cli/cmd/ls/ls.go | 10 +- pkg/cli/cmd/remove/remove.go | 18 ++-- pkg/cli/cmd/root/root.go | 9 +- pkg/cli/cmd/sync/sync.go | 11 ++- pkg/cli/cmd/version/version.go | 8 +- pkg/cli/cmd/view/view.go | 15 +-- pkg/cli/command/command.go | 159 ++++++++++++++++++++++++++++++++ pkg/cli/command/command_test.go | 104 +++++++++++++++++++++ pkg/cli/infra/init.go | 4 +- 15 files changed, 347 insertions(+), 74 deletions(-) create mode 100644 pkg/cli/command/command.go create mode 100644 pkg/cli/command/command_test.go diff --git a/pkg/cli/cmd/add/add.go b/pkg/cli/cmd/add/add.go index 22d4a1ad..bb04306e 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"}, @@ -72,7 +73,8 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } f := cmd.Flags() - f.StringVarP(&contentFlag, "content", "c", "", "The new content for the note") + f.StringVar(&contentFlag, "c", "", "Shorthand for --content") + f.StringVar(&contentFlag, "content", "", "The new content for the note") return cmd } @@ -84,7 +86,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 +108,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..2af46532 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"}, @@ -60,14 +60,17 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } f := cmd.Flags() - f.StringVarP(&contentFlag, "content", "c", "", "a new content for the note") - f.StringVarP(&bookFlag, "book", "b", "", "the name of the book to move the note to") - f.StringVarP(&nameFlag, "name", "n", "", "a new name for a book") + f.StringVar(&contentFlag, "content", "", "a new content for the note") + f.StringVar(&contentFlag, "c", "", "a new content for the note") + f.StringVar(&bookFlag, "book", "", "the name of the book to move the note to") + f.StringVar(&bookFlag, "b", "", "the name of the book to move the note to") + f.StringVar(&nameFlag, "name", "", "a new name for a book") + f.StringVar(&nameFlag, "n", "", "a new name for a book") 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 +79,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..2c23adc6 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"}, @@ -63,7 +63,8 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } f := cmd.Flags() - f.StringVarP(&bookName, "book", "b", "", "book name to find notes in") + f.StringVar(&bookName, "book", "", "book name to find notes in") + f.StringVar(&bookName, "b", "", "Shorthand for --book") return cmd } @@ -154,7 +155,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..a9e4ca7e 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, @@ -49,8 +49,11 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } f := cmd.Flags() - f.StringVarP(&usernameFlag, "username", "u", "", "email address for authentication") - f.StringVarP(&passwordFlag, "password", "p", "", "password for authentication") + f.String("username", "", "email address for authentication") + f.StringVar(&usernameFlag, "u", "", "email address for authentication") + + f.String("password", "", "password for authentication") + f.StringVar(&passwordFlag, "p", "", "password for authentication") return cmd } @@ -150,7 +153,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..0a83db04 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"}, @@ -56,15 +56,17 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } f := cmd.Flags() - f.StringVarP(&bookFlag, "book", "b", "", "The book name to delete") - f.BoolVarP(&yesFlag, "yes", "y", false, "Assume yes to the prompts and run in non-interactive mode") + f.StringVar(&bookFlag, "book", "", "The book name to delete") + f.StringVar(&bookFlag, "b", "", "Alias for --book") + f.BoolVar(&yesFlag, "yes", false, "Assume yes to the prompts and run in non-interactive mode") + f.BoolVar(&yesFlag, "y", false, "Assume yes to the prompts and run in non-interactive mode") - f.MarkDeprecated("book", "Pass the book name as an argument. e.g. `dnote rm book_name`") + // f.MarkDeprecated("book", "Pass the book name as an argument. e.g. `dnote rm book_name`") 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 +83,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..58275241 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", @@ -55,7 +55,8 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } f := cmd.Flags() - f.BoolVarP(&isFullSync, "full", "f", false, "perform a full sync instead of incrementally syncing only the changed data.") + f.BoolVar(&isFullSync, "full", false, "perform a full sync instead of incrementally syncing only the changed data.") + f.BoolVar(&isFullSync, "f", false, "Alias for --full") return cmd } @@ -886,7 +887,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..a7b4c8c6 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,9 @@ 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{ + Name: "view", Use: "view ", Aliases: []string{"v"}, Short: "List books, notes or view a content", @@ -63,14 +64,14 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { } f := cmd.Flags() - f.BoolVarP(&nameOnly, "name-only", "", false, "print book names only") - f.BoolVarP(&contentOnly, "content-only", "", false, "print the note content only") + f.BoolVar(&nameOnly, "name-only", false, "print book names only") + f.BoolVar(&contentOnly, "content-only", false, "print the note content only") return cmd } 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..254f650c --- /dev/null +++ b/pkg/cli/command/command.go @@ -0,0 +1,159 @@ +package command + +import ( + "os" + + "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 +} + +// 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 := c.findSubCommand(args[0]) + if cmd == nil { + // not found. show suggestion + return nil + } + + cmd.execute(args[1:]) + + 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(name string) *Command { + log.Debug("sub-command: '%s'\n", name) + + for _, cmd := range c.commands { + if cmd.Name == name || cmd.hasAlias(name) { + return cmd + } + } + + return nil +} + +// 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..46bb8966 --- /dev/null +++ b/pkg/cli/command/command_test.go @@ -0,0 +1,104 @@ +package command + +import ( + "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, + }, + } + + assert.Equal(t, cmd.findSubCommand("bar"), &subCommand1, "Subcommand 'bar' mismatch") + assert.Equal(t, cmd.findSubCommand("baz"), &subCommand2, "Subcommand 'baz' mismatch") + + // Should match an alias + assert.Equal(t, cmd.findSubCommand("quz"), &subCommand1, "Subcommand 'quz' mismatch") + // Should not match if not exists + assert.Equal(t, cmd.findSubCommand("qux"), (*Command)(nil), "Subcommand 'qux' mismatch") + // Should not match itself + assert.Equal(t, subCommand1.findSubCommand("bar"), (*Command)(nil), "Subcommand 'bar' mismatch") +} + +func executeCommand(root *Command, args ...string) error { + return root.Execute() +} 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) From ff7e3a4cee5b63d5ef70acf676f165d6169fe704 Mon Sep 17 00:00:00 2001 From: Sung Date: Sun, 12 Mar 2023 13:46:28 +1100 Subject: [PATCH 3/3] wip --- pkg/cli/cmd/add/add.go | 3 +-- pkg/cli/cmd/edit/edit.go | 9 +++------ pkg/cli/cmd/find/find.go | 3 +-- pkg/cli/cmd/remove/remove.go | 6 ++---- pkg/cli/cmd/sync/sync.go | 3 +-- pkg/cli/cmd/view/view.go | 4 ++-- pkg/cli/command/command.go | 36 +++++++++++++++++++++++++++------ pkg/cli/command/command_test.go | 5 +++-- 8 files changed, 43 insertions(+), 26 deletions(-) diff --git a/pkg/cli/cmd/add/add.go b/pkg/cli/cmd/add/add.go index bb04306e..52c848e9 100644 --- a/pkg/cli/cmd/add/add.go +++ b/pkg/cli/cmd/add/add.go @@ -73,8 +73,7 @@ func NewCmd(ctx context.DnoteCtx) *command.Command { } f := cmd.Flags() - f.StringVar(&contentFlag, "c", "", "Shorthand for --content") - f.StringVar(&contentFlag, "content", "", "The new content for the note") + f.StringVarP(&contentFlag, "content", "c", "", "The new content for the note") return cmd } diff --git a/pkg/cli/cmd/edit/edit.go b/pkg/cli/cmd/edit/edit.go index 2af46532..7d53ce94 100644 --- a/pkg/cli/cmd/edit/edit.go +++ b/pkg/cli/cmd/edit/edit.go @@ -60,12 +60,9 @@ func NewCmd(ctx context.DnoteCtx) *command.Command { } f := cmd.Flags() - f.StringVar(&contentFlag, "content", "", "a new content for the note") - f.StringVar(&contentFlag, "c", "", "a new content for the note") - f.StringVar(&bookFlag, "book", "", "the name of the book to move the note to") - f.StringVar(&bookFlag, "b", "", "the name of the book to move the note to") - f.StringVar(&nameFlag, "name", "", "a new name for a book") - f.StringVar(&nameFlag, "n", "", "a new name for a book") + f.StringVarP(&contentFlag, "content", "c", "", "a new content for the note") + f.StringVarP(&bookFlag, "book", "b", "", "the name of the book to move the note to") + f.StringVarP(&nameFlag, "name", "n", "", "a new name for a book") return cmd } diff --git a/pkg/cli/cmd/find/find.go b/pkg/cli/cmd/find/find.go index 2c23adc6..930ae07f 100644 --- a/pkg/cli/cmd/find/find.go +++ b/pkg/cli/cmd/find/find.go @@ -63,8 +63,7 @@ func NewCmd(ctx context.DnoteCtx) *command.Command { } f := cmd.Flags() - f.StringVar(&bookName, "book", "", "book name to find notes in") - f.StringVar(&bookName, "b", "", "Shorthand for --book") + f.StringVarP(&bookName, "book", "b", "", "book name to find notes in") return cmd } diff --git a/pkg/cli/cmd/remove/remove.go b/pkg/cli/cmd/remove/remove.go index 0a83db04..9611079b 100644 --- a/pkg/cli/cmd/remove/remove.go +++ b/pkg/cli/cmd/remove/remove.go @@ -56,10 +56,8 @@ func NewCmd(ctx context.DnoteCtx) *command.Command { } f := cmd.Flags() - f.StringVar(&bookFlag, "book", "", "The book name to delete") - f.StringVar(&bookFlag, "b", "", "Alias for --book") - f.BoolVar(&yesFlag, "yes", false, "Assume yes to the prompts and run in non-interactive mode") - f.BoolVar(&yesFlag, "y", false, "Assume yes to the prompts and run in non-interactive mode") + f.StringVarP(&bookFlag, "book", "b", "", "The book name to delete") + f.BoolVarP(&yesFlag, "yes", "y", false, "Assume yes to the prompts and run in non-interactive mode") // f.MarkDeprecated("book", "Pass the book name as an argument. e.g. `dnote rm book_name`") diff --git a/pkg/cli/cmd/sync/sync.go b/pkg/cli/cmd/sync/sync.go index 58275241..2f29a386 100644 --- a/pkg/cli/cmd/sync/sync.go +++ b/pkg/cli/cmd/sync/sync.go @@ -55,8 +55,7 @@ func NewCmd(ctx context.DnoteCtx) *command.Command { } f := cmd.Flags() - f.BoolVar(&isFullSync, "full", false, "perform a full sync instead of incrementally syncing only the changed data.") - f.BoolVar(&isFullSync, "f", false, "Alias for --full") + f.BoolVarP(&isFullSync, "full", "f", false, "perform a full sync instead of incrementally syncing only the changed data.") return cmd } diff --git a/pkg/cli/cmd/view/view.go b/pkg/cli/cmd/view/view.go index a7b4c8c6..faaaa3af 100644 --- a/pkg/cli/cmd/view/view.go +++ b/pkg/cli/cmd/view/view.go @@ -64,8 +64,8 @@ func NewCmd(ctx context.DnoteCtx) *command.Command { } f := cmd.Flags() - f.BoolVar(&nameOnly, "name-only", false, "print book names only") - f.BoolVar(&contentOnly, "content-only", false, "print the note content only") + f.BoolVarP(&nameOnly, "name-only", "", false, "print book names only") + f.BoolVarP(&contentOnly, "content-only", "", false, "print the note content only") return cmd } diff --git a/pkg/cli/command/command.go b/pkg/cli/command/command.go index 254f650c..aef0147f 100644 --- a/pkg/cli/command/command.go +++ b/pkg/cli/command/command.go @@ -1,6 +1,7 @@ package command import ( + "fmt" "os" "github.com/dnote/dnote/pkg/cli/log" @@ -41,6 +42,13 @@ type Command struct { Run func(cmd *Command, args []string) } +func (c *Command) HelpFunc() { + err := tmpl(os.Stdout, c.HelpTemplate(), c) + if err != nil { + fmt.Println(err) + } +} + // 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 { @@ -96,17 +104,24 @@ func (c *Command) Execute() error { args := c.Args() log.Debug("root command received arguments: %s\n", args) - cmd := c.findSubCommand(args[0]) + cmd, args := c.findCommand(args) if cmd == nil { // not found. show suggestion return nil } - cmd.execute(args[1:]) + if err := cmd.execute(args); err != nil { + if errors.Cause(err) == errNotRunnable { + cmd.Help() + } + + } return nil } +var errNotRunnable = errors.New("Command is not runnable.") + // execute runs the command. func (c *Command) execute(args []string) error { log.Debug("command '%s' called with arguments: %s\n", c.Name, args) @@ -118,6 +133,10 @@ func (c *Command) execute(args []string) error { nonFlagArgs := c.Flags().Args() log.Debug("command '%s' called with non-flag arguments: %s\n", c.Name, nonFlagArgs) + if c.RunE == nil { + return errNotRunnable + } + if err := c.RunE(c, nonFlagArgs); err != nil { return err } @@ -136,19 +155,24 @@ func (c *Command) hasAlias(targetAlias string) bool { return false } -// findSubCommand finds and returns an appropriate subcommand to be called, based +// findCommand 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(name string) *Command { +func (c *Command) findCommand(args []string) (*Command, []string) { + if len(args) == 0 { + return c, args + } + + name := args[0] log.Debug("sub-command: '%s'\n", name) for _, cmd := range c.commands { if cmd.Name == name || cmd.hasAlias(name) { - return cmd + return cmd, args[1:] } } - return nil + return c, args } // AddCommand adds the given command as a subcommand. diff --git a/pkg/cli/command/command_test.go b/pkg/cli/command/command_test.go index 46bb8966..f9dfc34f 100644 --- a/pkg/cli/command/command_test.go +++ b/pkg/cli/command/command_test.go @@ -99,6 +99,7 @@ func TestFindSubcommand(t *testing.T) { assert.Equal(t, subCommand1.findSubCommand("bar"), (*Command)(nil), "Subcommand 'bar' mismatch") } -func executeCommand(root *Command, args ...string) error { - return root.Execute() +func executeCommand(cmd *Command, args ...string) error { + cmd.setArgs(args) + return cmd.Execute() }