From 83190a578e51ed8e1309333e2af9429fd852f37d Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Sat, 20 Jan 2018 22:23:52 -0500 Subject: [PATCH] Change HandleShellCommand backend I'm trying to add more options for plugins that want to run shell commands. Also trying to add support for running shell commands in the terminal emulator from a plugin and return the output. More to come soon. Ref #979 --- cmd/micro/command.go | 97 +-------------------------- cmd/micro/shell.go | 131 +++++++++++++++++++++++++++++++++++++ cmd/micro/shell_posix.go | 18 +++++ cmd/micro/shell_windows.go | 9 +++ cmd/micro/terminal.go | 8 +++ cmd/micro/view.go | 5 +- 6 files changed, 172 insertions(+), 96 deletions(-) create mode 100644 cmd/micro/shell.go create mode 100644 cmd/micro/shell_posix.go create mode 100644 cmd/micro/shell_windows.go diff --git a/cmd/micro/command.go b/cmd/micro/command.go index 2607cdb8..ade999cd 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -1,11 +1,8 @@ package main import ( - "bytes" "fmt" "os" - "os/exec" - "os/signal" "path/filepath" "regexp" "runtime" @@ -714,105 +711,15 @@ func ReplaceAll(args []string) { func Term(args []string) { var err error if len(args) == 0 { - err = CurView().StartTerminal([]string{os.Getenv("SHELL"), "-i"}) + err = CurView().StartTerminal([]string{os.Getenv("SHELL"), "-i"}, true, false, "") } else { - err = CurView().StartTerminal(args) + err = CurView().StartTerminal(args, true, false, "") } if err != nil { messenger.Error(err) } } -// RunShellCommand executes a shell command and returns the output/error -func RunShellCommand(input string) (string, error) { - args, err := shellwords.Split(input) - if err != nil { - return "", err - } - inputCmd := args[0] - - cmd := exec.Command(inputCmd, args[1:]...) - outputBytes := &bytes.Buffer{} - cmd.Stdout = outputBytes - cmd.Stderr = outputBytes - cmd.Start() - err = cmd.Wait() // wait for command to finish - outstring := outputBytes.String() - return outstring, err -} - -// HandleShellCommand runs the shell command -// The openTerm argument specifies whether a terminal should be opened (for viewing output -// or interacting with stdin) -func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string { - args, err := shellwords.Split(input) - if err != nil { - return "" - } - inputCmd := args[0] - if !openTerm { - // Simply run the command in the background and notify the user when it's done - messenger.Message("Running...") - go func() { - output, err := RunShellCommand(input) - totalLines := strings.Split(output, "\n") - - if len(totalLines) < 3 { - if err == nil { - messenger.Message(inputCmd, " exited without error") - } else { - messenger.Message(inputCmd, " exited with error: ", err, ": ", output) - } - } else { - messenger.Message(output) - } - // We have to make sure to redraw - RedrawAll() - }() - } else { - // Shut down the screen because we're going to interact directly with the shell - screen.Fini() - screen = nil - - args := args[1:] - - // Set up everything for the command - var output string - cmd := exec.Command(inputCmd, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - // This is a trap for Ctrl-C so that it doesn't kill micro - // Instead we trap Ctrl-C to kill the program we're running - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - for range c { - cmd.Process.Kill() - } - }() - - cmd.Start() - err := cmd.Wait() - - if err != nil { - output = err.Error() - } - - if waitToFinish { - // This is just so we don't return right away and let the user press enter to return - TermMessage("") - } - - // Start the screen back up - InitScreen() - - return output - } - return "" -} - // HandleCommand handles input from the user func HandleCommand(input string) { args, err := shellwords.Split(input) diff --git a/cmd/micro/shell.go b/cmd/micro/shell.go new file mode 100644 index 00000000..94ed9569 --- /dev/null +++ b/cmd/micro/shell.go @@ -0,0 +1,131 @@ +package main + +import ( + "bytes" + "io" + "os" + "os/exec" + "os/signal" + "strings" + + "github.com/zyedidia/micro/cmd/micro/shellwords" +) + +// ExecCommand executes a command using exec +// It returns any output/errors +func ExecCommand(name string, arg ...string) (string, error) { + var err error + cmd := exec.Command(name, arg...) + outputBytes := &bytes.Buffer{} + cmd.Stdout = outputBytes + cmd.Stderr = outputBytes + err = cmd.Start() + if err != nil { + return "", err + } + err = cmd.Wait() // wait for command to finish + outstring := outputBytes.String() + return outstring, err +} + +// RunShellCommand executes a shell command and returns the output/error +func RunShellCommand(input string) (string, error) { + args, err := shellwords.Split(input) + if err != nil { + return "", err + } + inputCmd := args[0] + + return ExecCommand(inputCmd, args[1:]...) +} + +func RunBackgroundShell(input string) { + args, err := shellwords.Split(input) + if err != nil { + messenger.Error(err) + return + } + inputCmd := args[0] + messenger.Message("Running...") + go func() { + output, err := RunShellCommand(input) + totalLines := strings.Split(output, "\n") + + if len(totalLines) < 3 { + if err == nil { + messenger.Message(inputCmd, " exited without error") + } else { + messenger.Message(inputCmd, " exited with error: ", err, ": ", output) + } + } else { + messenger.Message(output) + } + // We have to make sure to redraw + RedrawAll() + }() +} + +func RunInteractiveShell(input string, wait bool, getOutput bool) string { + args, err := shellwords.Split(input) + if err != nil { + return "" + } + inputCmd := args[0] + + // Shut down the screen because we're going to interact directly with the shell + screen.Fini() + screen = nil + + args = args[1:] + + // Set up everything for the command + outputBytes := &bytes.Buffer{} + cmd := exec.Command(inputCmd, args...) + cmd.Stdin = os.Stdin + if getOutput { + cmd.Stdout = io.MultiWriter(os.Stdout, outputBytes) + } else { + cmd.Stdout = os.Stdout + } + cmd.Stderr = os.Stderr + + // This is a trap for Ctrl-C so that it doesn't kill micro + // Instead we trap Ctrl-C to kill the program we're running + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + cmd.Process.Kill() + } + }() + + cmd.Start() + err = cmd.Wait() + + output := outputBytes.String() + if err != nil { + output = err.Error() + } + + if wait { + // This is just so we don't return right away and let the user press enter to return + TermMessage("") + } + + // Start the screen back up + InitScreen() + + return output +} + +// HandleShellCommand runs the shell command +// The openTerm argument specifies whether a terminal should be opened (for viewing output +// or interacting with stdin) +func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string { + if !openTerm { + RunBackgroundShell(input) + return "" + } else { + return RunInteractiveShell(input, waitToFinish, false) + } +} diff --git a/cmd/micro/shell_posix.go b/cmd/micro/shell_posix.go new file mode 100644 index 00000000..6c19bf40 --- /dev/null +++ b/cmd/micro/shell_posix.go @@ -0,0 +1,18 @@ +// +build linux darwin dragonfly solaris openbsd netbsd freebsd + +package main + +import ( + "github.com/zyedidia/micro/cmd/micro/shellwords" +) + +const TermEmuSupported = true + +func RunTermEmulator(input string, wait bool, getOutput bool) error { + args, err := shellwords.Split(input) + if err != nil { + return err + } + err = CurView().StartTerminal(args, wait, false, "") + return err +} diff --git a/cmd/micro/shell_windows.go b/cmd/micro/shell_windows.go new file mode 100644 index 00000000..4ad4c415 --- /dev/null +++ b/cmd/micro/shell_windows.go @@ -0,0 +1,9 @@ +// +build plan9 nacl windows + +package main + +const TermEmuSupported = false + +func RunTermEmulator(input string, wait bool, getOutput bool) string { + return "Unsupported" +} diff --git a/cmd/micro/terminal.go b/cmd/micro/terminal.go index 06a24ce3..1defa61f 100644 --- a/cmd/micro/terminal.go +++ b/cmd/micro/terminal.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "strconv" + "strings" "github.com/zyedidia/clipboard" "github.com/zyedidia/tcell" @@ -27,6 +28,8 @@ type Terminal struct { status int selection [2]Loc wait bool + getOutput bool + callback string } // HasSelection returns whether this terminal has a valid selection @@ -159,6 +162,11 @@ func (t *Terminal) Stop() { // is ready for a new command to execute func (t *Terminal) Close() { t.status = VTIdle + // call the lua function that the user has given as a callback + _, err := Call(t.callback) + if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") { + TermMessage(err) + } } // WriteString writes a given string to this terminal's pty diff --git a/cmd/micro/view.go b/cmd/micro/view.go index fec32013..7969c274 100644 --- a/cmd/micro/view.go +++ b/cmd/micro/view.go @@ -165,8 +165,11 @@ func (v *View) ToggleStatusLine() { } // StartTerminal execs a command in this view -func (v *View) StartTerminal(execCmd []string) error { +func (v *View) StartTerminal(execCmd []string, wait bool, getOutput bool, luaCallback string) error { err := v.term.Start(execCmd, v) + v.term.wait = wait + v.term.getOutput = getOutput + v.term.callback = luaCallback if err == nil { v.term.Resize(v.Width, v.Height) v.Type = vtTerm