From d887da32f20feee8bfbebf7b2db63f1a42fd7176 Mon Sep 17 00:00:00 2001 From: Jeff Carr Date: Fri, 8 Nov 2024 06:43:04 -0600 Subject: [PATCH] use go-cmd/cmd and purge old code --- chomp.go | 98 ------------------ cmd.go | 192 ++++++++++++++++++++++++++++++++++ int.go | 31 ------ shell.go => old.go | 88 +++------------- run.go | 253 --------------------------------------------- wget.go | 3 +- xterm.go | 4 +- 7 files changed, 212 insertions(+), 457 deletions(-) delete mode 100644 chomp.go create mode 100644 cmd.go delete mode 100644 int.go rename shell.go => old.go (67%) delete mode 100644 run.go diff --git a/chomp.go b/chomp.go deleted file mode 100644 index 19acaf7..0000000 --- a/chomp.go +++ /dev/null @@ -1,98 +0,0 @@ -package shell - -/* - perl 'chomp' - - send it anything, always get back a string -*/ - -import ( - "bytes" - "fmt" - "reflect" - "strings" - - "go.wit.com/log" -) - -// import "github.com/davecgh/go-spew/spew" - -func chompBytesBuffer(buf *bytes.Buffer) string { - var bytesSplice []byte - bytesSplice = buf.Bytes() - - return Chomp(string(bytesSplice)) -} - -// TODO: obviously this is stupidly wrong -// TODO: fix this to trim fucking everything -// really world? 8 fucking years of this language -// and I'm fucking writing this? jesus. how the -// hell is everyone else doing this? Why isn't -// this already in the strings package? -func perlChomp(s string) string { - // lots of stuff in go moves around the whole block of whatever it is so lots of things are padded with NULL values - s = strings.Trim(s, "\x00") // removes NULL (needed!) - - // TODO: christ. make some fucking regex that takes out every NULL, \t, ' ", \n, and \r - s = strings.Trim(s, "\n") - s = strings.Trim(s, "\n") - s = strings.TrimSuffix(s, "\r") - s = strings.TrimSuffix(s, "\n") - - s = strings.TrimSpace(s) // this is like 'chomp' in perl - s = strings.TrimSuffix(s, "\n") // this is like 'chomp' in perl - return s -} - -// TODO: fix this to chomp \n \r NULL \t and ' ' -func Chomp(a interface{}) string { - // switch reflect.TypeOf(a) { - switch t := a.(type) { - case string: - var s string - s = a.(string) - return perlChomp(s) - case []uint8: - // log.Printf("shell.Chomp() FOUND []uint8") - var tmp []uint8 - tmp = a.([]uint8) - - s := string(tmp) - return perlChomp(s) - case uint64: - // log.Printf("shell.Chomp() FOUND []uint64") - s := fmt.Sprintf("%d", a.(uint64)) - return perlChomp(s) - case int64: - // log.Printf("shell.Chomp() FOUND []int64") - s := fmt.Sprintf("%d", a.(int64)) - return perlChomp(s) - case *bytes.Buffer: - // log.Printf("shell.Chomp() FOUND *bytes.Buffer") - var tmp *bytes.Buffer - tmp = a.(*bytes.Buffer) - if tmp == nil { - return "" - } - - var bytesSplice []byte - bytesSplice = tmp.Bytes() - return Chomp(string(bytesSplice)) - default: - tmp := fmt.Sprint("shell.Chomp() NO HANDLER FOR TYPE: %T", a) - handleError(fmt.Errorf(tmp), -1) - log.Warn("shell.Chomp() NEED TO MAKE CONVERTER FOR type =", reflect.TypeOf(t)) - } - tmp := "shell.Chomp() THIS SHOULD NEVER HAPPEN" - handleError(fmt.Errorf(tmp), -1) - return "" -} - -// this is stuff from a long time ago that there must be a replacement for -func RemoveFirstElement(slice []string) (string, []string) { - if len(slice) == 0 { - return "", slice // Return the original slice if it's empty - } - return slice[0], slice[1:] // Return the slice without the first element -} diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..cfaafd1 --- /dev/null +++ b/cmd.go @@ -0,0 +1,192 @@ +package shell + +import ( + "errors" + "fmt" + "time" + + "github.com/go-cmd/cmd" + "go.wit.com/log" +) + +// this is a simplified interaction with the excellent +// go-cmd/cmd package to work 'shell' like. + +// in all cases here, STDERR -> STDOUT +// If you want the output from whatever you run +// to be captured like it appears when you see it +// on the command line, this is what this tries to do + +/* + if r := shell.Run([]{"ping", "-c", "3", "localhost"}); r.Error == nil { + if r.Exit == 0 { + log.Println("ran ok") + } else { + log.Println("ran") + } + // all stdout/stderr captured in r.Stdout + } +*/ + +// shortcut, sends a blank value for pwd +// which means the exec Dir is not set +// echos output (otherwise use RunQuiet) +func Run(args []string) cmd.Status { + return PathRun("", args) +} + +// exec the cmd at a filepath. this does not change the working directory +// sets the exec dir if it's not an empty string +// combines stdout and stderr +// echo's output (otherwise use PathRunQuiet() +// this is basically the exact example from the go-cmd/cmd devs +// where the have rocked out a proper smart read on both filehandles +// https://dave.cheney.net/2013/04/30/curious-channels +func PathRun(path string, args []string) cmd.Status { + var save []string // combined stdout & stderr + var arg0 string + var argx []string + // Check if the slice has at least one element (the command name) + if len(args) == 0 { + var s cmd.Status + s.Error = errors.New("Error: Command slice is empty.") + return s + } + if len(args) == 1 { + // Pass the first element as the command, and the rest as variadic arguments + arg0 = args[0] + } else { + arg0 = args[0] + argx = args[1:] + } + + // Disable output buffering, enable streaming + cmdOptions := cmd.Options{ + Buffered: false, + Streaming: true, + } + + // Create Cmd with options + envCmd := cmd.NewCmdOptions(cmdOptions, arg0, argx...) + if path != "" { + // set the path for exec + envCmd.Dir = path + } + + // Print STDOUT and STDERR lines streaming from Cmd + doneChan := make(chan struct{}) + go func() { + defer close(doneChan) + // Done when both channels have been closed + // https://dave.cheney.net/2013/04/30/curious-channels + for envCmd.Stdout != nil || envCmd.Stderr != nil { + select { + case line, open := <-envCmd.Stdout: + if !open { + envCmd.Stdout = nil + continue + } + save = append(save, line) + fmt.Println(line) + case line, open := <-envCmd.Stderr: + if !open { + envCmd.Stderr = nil + continue + } + save = append(save, line) + fmt.Println(line) + } + } + }() + + // Run and wait for Cmd to return, discard Status + <-envCmd.Start() + + // Wait for goroutine to print everything + <-doneChan + + s := envCmd.Status() + s.Stdout = save + return s +} + +// absolutely doesn't echo anything +func PathRunQuiet(pwd string, args []string) cmd.Status { + // Check if the slice has at least one element (the command name) + if len(args) == 0 { + var s cmd.Status + s.Error = errors.New("Error: Command slice is empty.") + return s + } + + // Start a long-running process, capture stdout and stderr + a, b := RemoveFirstElement(args) + findCmd := cmd.NewCmd(a, b...) + if pwd != "" { + findCmd.Dir = pwd + } + statusChan := findCmd.Start() // non-blocking + + ticker := time.NewTicker(2 * time.Second) + + // this is interesting, maybe useful, but wierd, but neat. interesting even + // Print last line of stdout every 2s + go func() { + for range ticker.C { + status := findCmd.Status() + n := len(status.Stdout) + if n != 0 { + fmt.Println("todo:removethisecho", status.Stdout[n-1]) + } + } + }() + + // Stop command after 1 hour + go func() { + <-time.After(1 * time.Hour) + findCmd.Stop() + }() + + // Check if command is done + select { + case finalStatus := <-statusChan: + log.Info("finalStatus =", finalStatus.Exit, finalStatus.Error) + return finalStatus + // done + default: + // no, still running + } + + // Block waiting for command to exit, be stopped, or be killed + finalStatus := <-statusChan + return finalStatus +} + +func blah(cmd []string) { + r := Run(cmd) + log.Info("cmd =", r.Cmd) + log.Info("complete =", r.Complete) + log.Info("exit =", r.Exit) + log.Info("err =", r.Error) + log.Info("len(stdout+stderr) =", len(r.Stdout)) +} + +// run these to see confirm the sytem behaves as expected +func RunTest() { + blah([]string{"ping", "-c", "3", "localhost"}) + blah([]string{"exit", "0"}) + blah([]string{"exit", "-1"}) + blah([]string{"true"}) + blah([]string{"false"}) + blah([]string{"grep", "root", "/etc/", "/proc/cmdline", "/usr/bin/chmod"}) + blah([]string{"grep", "root", "/proc/cmdline"}) + fmt.Sprint("blahdone") +} + +// this is stuff from a long time ago that there must be a replacement for +func RemoveFirstElement(slice []string) (string, []string) { + if len(slice) == 0 { + return "", slice // Return the original slice if it's empty + } + return slice[0], slice[1:] // Return the slice without the first element +} diff --git a/int.go b/int.go deleted file mode 100644 index bf78209..0000000 --- a/int.go +++ /dev/null @@ -1,31 +0,0 @@ -package shell - -/* - send it anything, always get back an int -*/ - -// import "log" -// import "reflect" -// import "strings" -// import "bytes" -import "strconv" - -func Int(s string) int { - s = Chomp(s) - i, err := strconv.Atoi(s) - if err != nil { - handleError(err, -1) - return 0 - } - return i -} - -func Int64(s string) int64 { - s = Chomp(s) - i, err := strconv.Atoi(s) - if err != nil { - handleError(err, -1) - return 0 - } - return int64(i) -} diff --git a/shell.go b/old.go similarity index 67% rename from shell.go rename to old.go index 9ef2dfb..b4133fd 100644 --- a/shell.go +++ b/old.go @@ -1,12 +1,12 @@ package shell +// old code and probably junk + import ( "io/ioutil" "net/http" "os" "os/exec" - "strings" - "time" "go.wit.com/log" ) @@ -47,6 +47,7 @@ func Quiet(q bool) { quiet = q } +/* func Script(cmds string) int { // split on new lines (while we are at it, handle stupid windows text files lines := strings.Split(strings.Replace(cmds, "\r\n", "\n", -1), "\n") @@ -59,22 +60,7 @@ func Script(cmds string) int { } return 0 } - -func SpewOn() { - spewOn = true -} - -func SetDelayInMsec(msecs int) { - msecDelay = msecs -} - -func SetStdout(newout *os.File) { - shellStdout = newout -} - -func SetStderr(newerr *os.File) { - shellStderr = newerr -} +*/ func Unlink(filename string) bool { if err := os.Remove(filename); err != nil { @@ -84,40 +70,16 @@ func Unlink(filename string) bool { } } -func RM(filename string) { - os.Remove(Path(filename)) -} - -func Daemon(cmdline string, timeout time.Duration) int { - for { - RunString(cmdline) - time.Sleep(timeout) - } -} - -// run something and never return from it -// TODO: pass STDOUT, STDERR, STDIN correctly -// TODO: figure out how to nohup the process and exit -func Exec(cmdline string) { - log.Log(INFO, "shell.Run() START "+cmdline) - - cmd := Chomp(cmdline) // this is like 'chomp' in perl - cmdArgs := strings.Fields(cmd) - - process := exec.Command(cmdArgs[0], cmdArgs[1:len(cmdArgs)]...) - process.Stderr = os.Stderr - process.Stdin = os.Stdin - process.Stdout = os.Stdout - process.Start() - err := process.Wait() - log.Log(INFO, "shell.Exec() err =", err) - os.Exit(0) -} - // run interactively. output from the cmd is in real time // shows all the output. For example, 'ping -n localhost' // shows the output like you would expect to see -func NewRun(workingpath string, cmd []string) error { +func RunSimple(cmd []string) error { + log.Log(INFO, "NewRun() ", cmd) + + return PathRunSimple("", cmd) +} + +func PathRunSimple(workingpath string, cmd []string) error { log.Log(INFO, "NewRun() ", cmd) process := exec.Command(cmd[0], cmd[1:len(cmd)]...) @@ -128,10 +90,14 @@ func NewRun(workingpath string, cmd []string) error { process.Stdout = os.Stdout process.Start() err := process.Wait() - log.Log(INFO, "shell.Exec() err =", err) + if err != nil { + log.Log(INFO, "shell.Exec() err =", err) + } return err } +// return true if the filename exists (cross-platform) + // return true if the filename exists (cross-platform) func Exists(filename string) bool { _, err := os.Stat(Path(filename)) @@ -175,29 +141,9 @@ func Cat(filename string) string { if err != nil { return "" } - return Chomp(buffer) + return string(buffer) } -/* -// run interactively. output from the cmd is in real time -// shows all the output. For example, 'ping -n localhost' -// shows the output like you would expect to see -func RunPathHttpOut(workingpath string, cmd []string, w http.ResponseWriter, r *http.Request) error { - log.Log(INFO, "NewRun() ", cmd) - - process := exec.Command(cmd[0], cmd[1:len(cmd)]...) - // Set the working directory - process.Dir = workingpath - process.Stderr = os.Stderr - process.Stdin = os.Stdin - process.Stdout = os.Stdout - process.Start() - err := process.Wait() - log.Log(INFO, "shell.Exec() err =", err) - return err -} -*/ - func RunPathHttpOut(path string, cmd []string, w http.ResponseWriter, r *http.Request) error { log.Warn("Run(): ", cmd) diff --git a/run.go b/run.go deleted file mode 100644 index ca6bf23..0000000 --- a/run.go +++ /dev/null @@ -1,253 +0,0 @@ -package shell - -import ( - "errors" - "os" - "os/exec" - "strings" - "syscall" - - "go.wit.com/log" -) - -var msecDelay int = 20 // check every 20 milliseconds - -// TODO: look at https://github.com/go-cmd/cmd/issues/20 -// use go-cmd instead here? -// exiterr.Sys().(syscall.WaitStatus) - -// run command and return it's output -/* -func RunCapture(cmdline string) string { - test := New() - test.Exec(cmdline) - return Chomp(test.Buffer) -} - -func RunWait(args []string) *OldShell { - test := New() - cmdline := strings.Join(args, " ") - test.Exec(cmdline) - return test -} -*/ - -// var newfile *shell.File -func RunString(args string) bool { - // return false - parts := strings.Split(args, " ") - return Run(parts) -} - -func Run(args []string) bool { - dir, err := os.Getwd() - if err != nil { - return false - } - - r := RunPath(dir, args) - if r.Ok { - return true - } - return false -} - -var ErrorArgvEmpty error = errors.New("command was empty") - -type RunResult struct { - Ok bool - Argv []string - Path string - Output []byte - Err error - Outerr error -} - -// run, but set the working path -func RunPath(path string, args []string) *RunResult { - r := new(RunResult) - r.Path = path - r.Argv = args - if len(args) == 0 { - r.Ok = true - r.Err = ErrorArgvEmpty - return r - } - if args[0] == "" { - r.Ok = false - r.Err = ErrorArgvEmpty - return r - } - thing := args[0] - parts := args[1:] - cmd := exec.Command(thing, parts...) - cmd.Dir = path - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - log.Info("path =", path, "cmd =", strings.Join(args, " ")) - if err := cmd.Run(); err != nil { - // Handle error if the command execution fails - // log.Info("RunPath() failed") - // log.Info("cmd.Enviorn =", cmd.Environ()) - out, outerr := cmd.Output() - // log.Info("cmd.output =", out) - // log.Info("cmd.output err=", outerr) - // log.Info("path =", path) - // log.Info("args =", args) - // log.Info("err =", err.Error()) - r.Ok = false - r.Err = err - r.Output = out - r.Outerr = outerr - return r - } - out, outerr := cmd.Output() - r.Output = out - r.Outerr = outerr - r.Ok = true - return r -} - -// send the path and the command -// captures the output so you can not see the command run in real time -func RunCmd(workingpath string, parts []string) (error, bool, string) { - if len(parts) == 0 { - log.Warn("command line was empty") - return errors.New("empty"), false, "" - } - if parts[0] == "" { - log.Warn("command line was empty") - return errors.New("empty"), false, "" - } - thing := parts[0] - parts = parts[1:] - log.Log(INFO, "working path =", workingpath, "thing =", thing, "cmdline =", parts) - - // Create the command - cmd := exec.Command(thing, parts...) - - // Set the working directory - cmd.Dir = workingpath - - // Execute the command - output, err := cmd.CombinedOutput() - if err != nil { - if thing == "git" { - log.Log(INFO, "git ERROR. maybe okay", workingpath, "thing =", thing, "cmdline =", parts) - log.Log(INFO, "git ERROR. maybe okay err =", err) - if err.Error() == "exit status 1" { - log.Log(INFO, "git ERROR. normal exit status 1") - if parts[0] == "diff-index" { - log.Log(INFO, "git normal diff-index when repo dirty") - return nil, false, "git diff-index exit status 1" - } - } - } - - log.Warn("ERROR working path =", workingpath, "thing =", thing, "cmdline =", parts) - log.Warn("ERROR working path =", workingpath, "thing =", thing, "cmdline =", parts) - log.Warn("ERROR working path =", workingpath, "thing =", thing, "cmdline =", parts) - log.Error(err) - log.Warn("output was", string(output)) - log.Warn("cmd exited with error", err) - // panic("fucknuts") - - // The command failed (non-zero exit status) - if exitErr, ok := err.(*exec.ExitError); ok { - // Assert that it is an exec.ExitError and get the exit code - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - log.Warn("Exit Status: %d\n", status.ExitStatus()) - } - } else { - log.Warn("cmd.Run() failed with %s\n", err) - } - return err, false, string(output) - } - - tmp := string(output) - tmp = strings.TrimSpace(tmp) - - // Print the output - return nil, true, tmp -} - -// send the path and the command -// also does not seem to show the output in realtime -func RunCmdRun(workingpath string, parts []string) error { - if len(parts) == 0 { - log.Warn("command line was empty") - return errors.New("empty") - } - if parts[0] == "" { - log.Warn("command line was empty") - return errors.New("empty") - } - thing := parts[0] - parts = parts[1:] - log.Log(INFO, "working path =", workingpath, "thing =", thing, "cmdline =", parts) - - // Create the command - cmd := exec.Command(thing, parts...) - - // Set the working directory - cmd.Dir = workingpath - - // Execute the command - err := cmd.Run() - if err != nil { - log.Warn("ERROR working path =", workingpath, "thing =", thing, "cmdline =", parts) - log.Error(err) - log.Warn("cmd exited with error", err) - // panic("fucknuts") - - // The command failed (non-zero exit status) - if exitErr, ok := err.(*exec.ExitError); ok { - // Assert that it is an exec.ExitError and get the exit code - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - log.Warn("Exit Status: %d\n", status.ExitStatus()) - } - } else { - log.Warn("cmd.Run() failed with %s\n", err) - } - return err - } - return nil -} - -func (r *RunResult) Stdout() string { - return string(r.Output) -} - -// run, but set the working path -func Output(path string, args []string) *RunResult { - r := new(RunResult) - r.Path = path - r.Argv = args - if len(args) == 0 { - r.Ok = true - r.Err = ErrorArgvEmpty - return r - } - if args[0] == "" { - r.Ok = false - r.Err = ErrorArgvEmpty - return r - } - thing := args[0] - parts := args[1:] - cmd := exec.Command(thing, parts...) - cmd.Dir = path - output, err := cmd.CombinedOutput() - - if err := cmd.Run(); err != nil { - r.Ok = false - r.Err = err - r.Output = output - return r - } - r.Output = output - r.Err = err - r.Ok = true - return r -} diff --git a/wget.go b/wget.go index bd767d6..a09550b 100644 --- a/wget.go +++ b/wget.go @@ -74,8 +74,7 @@ func WgetToFile(filepath string, url string) error { // BUGS: The author's idea of friendly may differ to that of many other people. func Write(filepath string, data string) bool { // TODO: this isn't working for some reason and is making two '\n' chars - // probably because Chomp() isn't fixed yet - data = Chomp(data) + "\n" + data = strings.TrimSpace(data) + "\n" // Create the file ospath := Path(filepath) log.Log(INFO, "shell.Write() START ospath =", ospath, "filepath =", filepath) diff --git a/xterm.go b/xterm.go index b06ef33..051e956 100644 --- a/xterm.go +++ b/xterm.go @@ -123,7 +123,7 @@ func XtermCmdWait(path string, cmd []string) { // keeps git diff from exiting on small diffs os.Setenv("LESS", "-+F -+X -R") - RunCmdRun(path, argsXterm) + PathRun(path, argsXterm) } // spawns an xterm with something you can run at a command line @@ -136,5 +136,5 @@ func XtermCmdBash(path string, cmd []string) { bash += "'; bash\"" tmp = append(argsXterm, "bash", bash) log.Info("XtermCmd() path =", path, "cmd =", tmp) - go RunCmd(path, tmp) + go PathRun(path, tmp) }