From 7cbdea3c61fa36336c391ddd31122da97d4d7781 Mon Sep 17 00:00:00 2001 From: Tian Yi Date: Wed, 25 Feb 2026 01:22:14 +0900 Subject: [PATCH] feat: Add Interactive Shell mode for pikpakcli --- cmd/root.go | 12 +++++ go.mod | 1 + go.sum | 5 ++ internal/shell/shell.go | 112 ++++++++++++++++++++++++++++++++++++++++ main.go | 15 +++++- 5 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 internal/shell/shell.go diff --git a/cmd/root.go b/cmd/root.go index fca8214..df9bd65 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -62,3 +62,15 @@ func Execute() { os.Exit(1) } } + +// ExecuteShell: Interactive shell +func ExecuteShell(shellStarter func(*cobra.Command)) { + // Init Config + err := conf.InitConfig("config.yml") + if err != nil { + logrus.Errorln(err) + os.Exit(1) + } + // Exec shell + shellStarter(rootCmd) +} diff --git a/go.mod b/go.mod index 3071516..2bfee6f 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 7b42c75..95a0d76 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,10 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -51,6 +55,7 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/vbauerster/mpb/v8 v8.7.2 h1:SMJtxhNho1MV3OuFgS1DAzhANN1Ejc5Ct+0iSaIkB14= github.com/vbauerster/mpb/v8 v8.7.2/go.mod h1:ZFnrjzspgDHoxYLGvxIruiNk73GNTPG4YHgVNpR10VY= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= diff --git a/internal/shell/shell.go b/internal/shell/shell.go new file mode 100644 index 0000000..61cc26e --- /dev/null +++ b/internal/shell/shell.go @@ -0,0 +1,112 @@ +package shell + +import ( + "fmt" + "os" + "strings" + + "github.com/chzyer/readline" + "github.com/spf13/cobra" +) + +// Start starts the interactive shell +func Start(rootCmd *cobra.Command) { + fmt.Println("PikPak CLI Interactive Shell") + fmt.Println("Type 'help' for available commands, 'exit' to quit") + fmt.Println() + + // Create readline instance + // TODO: we can add path here: pikpak {path} >. + l, err := readline.New("pikpak > ") + if err != nil { + fmt.Fprintf(os.Stderr, "Error initializing readline: %v\n", err) + return + } + defer l.Close() + + for { + input, err := l.Readline() + + // Handle EOF (Ctrl+D) + if err == readline.ErrInterrupt { + continue + } + + if err != nil { + // This is EOF + fmt.Println("\nBye~!") + break + } + + input = strings.TrimSpace(input) + + if input == "" { + continue + } + + if input == "exit" || input == "quit" { + fmt.Println("Bye~!") + break + } + + if input == "help" { + rootCmd.Help() + continue + } + + // Parse the args and set them to rootCmd + args := parseShellArgs(input) + rootCmd.SetArgs(args) + + // Directly use pre-defined Execute function + // TODO: Need to be updated if cd command is supported. + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + } + rootCmd.SetArgs([]string{}) // Reset for next iteration + } +} + +// parseShellArgs parses shell-like arguments +func parseShellArgs(input string) []string { + var args []string + var current strings.Builder + inDoubleQuote := false + inSingleQuote := false + + for i := 0; i < len(input); i++ { + ch := input[i] + + switch ch { + case '"': + if inSingleQuote { + current.WriteByte(ch) + } else { + inDoubleQuote = !inDoubleQuote + } + case '\'': + if inDoubleQuote { + current.WriteByte(ch) + } else { + inSingleQuote = !inSingleQuote + } + case ' ', '\t': + if inDoubleQuote || inSingleQuote { + current.WriteByte(ch) + } else { + if current.Len() > 0 { + args = append(args, current.String()) + current.Reset() + } + } + default: + current.WriteByte(ch) + } + } + + if current.Len() > 0 { + args = append(args, current.String()) + } + + return args +} diff --git a/main.go b/main.go index 51afea3..52c1ab8 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,18 @@ package main -import "github.com/52funny/pikpakcli/cmd" +import ( + "os" + + "github.com/52funny/pikpakcli/cmd" + "github.com/52funny/pikpakcli/internal/shell" +) func main() { - cmd.Execute() + // Check if any args + if len(os.Args) == 1 { + cmd.ExecuteShell(shell.Start) + } else { + // If no arg, execute the command directly + cmd.Execute() + } }