diff --git a/cmd/list/list.go b/cmd/list/list.go index 9828ae2..85d3dfd 100644 --- a/cmd/list/list.go +++ b/cmd/list/list.go @@ -25,7 +25,11 @@ var ListCmd = &cobra.Command{ if err != nil { logrus.Errorln("Login Failed:", err) } - handle(&p, args) + long, _ := cmd.Flags().GetBool("long") + human, _ := cmd.Flags().GetBool("human") + path, _ := cmd.Flags().GetString("path") + parentId, _ := cmd.Flags().GetString("parent-id") + handle(&p, args, long, human, path, parentId) }, } @@ -36,8 +40,11 @@ func init() { ListCmd.Flags().StringVarP(&parentId, "parent-id", "P", "", "display the specified parent id") } -func handle(p *pikpak.PikPak, args []string) { +func handle(p *pikpak.PikPak, args []string, long, human bool, path, parentId string) { var err error + if len(args) > 0 { + path = args[0] + } if parentId == "" { parentId, err = p.GetPathFolderId(path) if err != nil { 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..1b329db --- /dev/null +++ b/internal/shell/shell.go @@ -0,0 +1,206 @@ +package shell + +import ( + "fmt" + "os" + "strings" + + "github.com/52funny/pikpakcli/conf" + "github.com/52funny/pikpakcli/internal/pikpak" + "github.com/chzyer/readline" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// 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() + + currentPath := "/" + globalPath := "/" + // globalPath := currentPath + + // Create readline instance + // TODO: we can add path here: pikpak {path} >. + l, err := readline.New(fmt.Sprintf("pikpak %s > ", currentPath)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error initializing readline: %v\n", err) + return + } + defer l.Close() + + for { + input, err := l.Readline() + + // l.SetPrompt(fmt.Sprintf("pikpak %s > ", currentPath)) + + // 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) + + // Handle cd command + if len(args) > 0 && args[0] == "cd" { + var path string + if len(args) > 1 { + path = args[1] + } + // Go back to root if target path is empty, ~ or / + switch path { + case "", "~", "/": + currentPath = "/" + globalPath = currentPath + case "..": + // Go back to parent directory + if currentPath != "/" { + currentPath = currentPath[:strings.LastIndex(currentPath, "/")] + globalPath = currentPath + "/" + if currentPath == "" { + currentPath = "/" + globalPath = currentPath + } + } + + // Update the prompt with the new path + l.SetPrompt(fmt.Sprintf("pikpak %s > ", globalPath)) + default: + // Handle relative and absolute paths + var targetPath string + if strings.HasPrefix(path, "/") { + // Absolute path + targetPath = path + } else { + // Relative path + if currentPath == "/" { + targetPath = "/" + path + } else { + targetPath = currentPath + "/" + path + } + } + // Normalize path: remove duplicate slashes and trailing slash + targetPath = strings.ReplaceAll(targetPath, "//", "/") + if targetPath != "/" { + targetPath = strings.TrimSuffix(targetPath, "/") + } + // Check if the path exists and is a directory + p := pikpak.NewPikPak(conf.Config.Username, conf.Config.Password) + err := p.Login() + if err != nil { + fmt.Fprintf(os.Stderr, "Login failed: %v\n", err) + continue + } + _, err = p.GetPathFolderId(targetPath) + if err != nil { + fmt.Fprintf(os.Stderr, "cd: %s: No such directory\n", targetPath) + continue + } + currentPath = targetPath + globalPath = currentPath + "/" + // Update the prompt with the new path + l.SetPrompt(fmt.Sprintf("pikpak %s > ", globalPath)) + } + + continue + } + + // For ls command, if no path specified, add current path + if len(args) == 1 && args[0] == "ls" { + args = append(args, "-p", currentPath) + } + + 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 + + // Reset flags to default values to prevent state retention between commands + resetFlags(rootCmd) + } +} + +// 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 +} + +// resetFlags recursively resets all flags in the command tree to their default values +func resetFlags(cmd *cobra.Command) { + cmd.Flags().VisitAll(func(f *pflag.Flag) { + f.Value.Set(f.DefValue) + }) + cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { + f.Value.Set(f.DefValue) + }) + for _, subCmd := range cmd.Commands() { + resetFlags(subCmd) + } +} 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() + } }