golang exec command: stream output to stdout *and* capture output in variable - go

I'm building a cli in go and have the following situation. I'm calling a bash command from go that prompts the user for a login, then prints a token after login. For example:
cmd := exec.Command("vault", "login", "-method=okta", "-format=json", "username=abc")
cmd.Stdin = os.Stdinout
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
This streams the output nicely, but I have no way to parse the token from the command's output after the user logs in. I've tried to wrap the cmd.Run() into piping functions like this this and this but in both cases the output returned is empty. Any ideas?
Thanks!

There are probably lots of packages to do this, but it's not hard to whip up your own:
package main
import (
"fmt"
"os"
"os/exec"
)
type saveOutput struct {
savedOutput []byte
}
func (so *saveOutput) Write(p []byte) (n int, err error) {
so.savedOutput = append(so.savedOutput, p...)
return os.Stdout.Write(p)
}
func main() {
var so saveOutput
cmd := exec.Command("factor", "999999")
cmd.Stdin = os.Stdin
cmd.Stdout = &so
cmd.Stderr = os.Stderr
_ = cmd.Run()
fmt.Printf("I got this output: %s\n", so.savedOutput)
}
Playground: https://go.dev/play/p/T-o3QvGOm5q

Don't make your structure for nothing.
Use bytes.Buffer
package main
import (
"bytes"
"log"
)
func main() {
var buffer bytes.Buffer
cmd := exec.Command("vault", "login", "-method=okta", "-format=json", "username=abc")
cmd.Stdout = &buffer
_ = cmd.Run()
log.Printf("Vault login output: %s", buffer.String())
}

Related

Spawn vim using golang

I am trying to run a simple program that spawns a vim process.
The user should be able (when the exec.Command starts) to switch to vim window and the process execution should halt there.
When user closes vim (wq!) the program execution should resume from that point.
The following simple attempt fails but I cannot figure out why
package main
import (
"log"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "lala")
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
▶ go run main.go
2022/11/25 09:16:44 exit status 1
exit status 1
Why the exit status 1?
You missed these two lines:
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
Thanks to these two lines the user is able to edit with vim the file in the terminal. The control is returned to the program when the user quit from the terminal (e.g., with the command :wq). Below, you can find the whole code:
package main
import (
"log"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "lala")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}
Hope this helps!
Because you should set Stdin and Stdoutfor cmd:
package main
import (
"log"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "lala")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
}

Executable file not found in %PATH% golang

package main
import (
"bytes"
"fmt"
//"log"
"os/exec"
)
func main() {
cmd := exec.Command("dir")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
fmt.Printf("cmd.Run: %s failed: %s\n", err, err)
}
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
if len(errStr) > 1 {
fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr)
}
fmt.Printf(outStr)
}
*Hi guys, whenever I try to run this file with go it shows me This error "cmd.Run: exec: "dir": executable file not found in %PATH% failed:". I have golang in my PATH but it still failed *
dir is not an executable file in Windows, rather it is an internal command of Command prompt. You need to pass dir to command prompt.
Your command would look like this:
cmd.exe /c dir
You can implement it like this:
args := strings.Split("/c dir"," ")
cmd := exec.Command("cmd.exe",args...)
Pass your command line arguments like this, strings.Split() will split "/c dir" into all substrings separated by " " and returns a slice of the substrings between those separators.
Also if you need to print dir of a specific location you can set the working directory of the command:
cmd.Dir = filepath.Join("C:","Windows")
filepath.Join joins any number of path elements into a single path, separating them with an OS specific Separator.
Add the following packages into your file
import (
"os"
"path/filepath"
"strings"
)
To print the result you can connect your output and error to the standard output, and standard error.
cmd.Stdout = os.Stdout
cmd.Stderr = &os.Stderr
Your overall code would be:
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
func main() {
args := strings.Split("/c dir"," ")
cmd := exec.Command("cmd.exe",args...)
cmd.Dir = filepath.Join("C:","Windows")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
fmt.Printf("cmd.Run: %s failed: %s\n", err, err)
}
}

Calling appcmd results in the wrong password being set

I'm trying to call appcmd from within Go. The code below shows success, but the password is set to the wrong thing. If I remove the inner quotes (on the second line of main) it works, but then it doesn't work when the password includes spaces! Now WITH the quotes, if I type in cmd.exe the command exactly as it outputs, it works! So what the heck! Why does it work with the quotes directly in cmd but not when called from Go?
I really don't want to be that guy who says you can't use spaces in passwords because I can't figure out why it doesn't work! UGH!
package main
import (
"bytes"
"fmt"
"os/exec"
"strconv"
"strings"
"syscall"
)
func main() {
iisPath := "C:\\WINDOWS\\sysWOW64\\inetsrv\\"
callAppcmd(iisPath, "-processModel.password:\"password\"")
}
func callAppcmd(iisPath string, param string) {
stdOut, _, _, exitCode := runCommand(
iisPath+"appcmd.exe",
"set",
"apppool",
"/apppool.name:DefaultAppPool",
param)
printOut(stdOut)
printOut(strconv.Itoa(exitCode))
}
func printOut(text string) {
fmt.Println(text)
}
func runCommand(commands ...string) (string, string, error, int) {
printOut(strings.Join(commands, " "))
cmd := exec.Command(commands[0], commands[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
exitCode := 0
if exitError, ok := err.(*exec.ExitError); ok {
exitCode = exitError.ExitCode()
}
return out.String(), stderr.String(), err, exitCode
}
Output:
C:\WINDOWS\sysWOW64\inetsrv\appcmd.exe set apppool /apppool.name:DefaultAppPool -processModel.password:"password"
APPPOOL object "DefaultAppPool" changed
0
It seems to format the string with backticks is a solution to this, which will not do automatic escaping and can process the quotes properly.
cmd := exec.Command(`find`)
cmd.SysProcAttr.CmdLine = `find "SomeText" test.txt`
Please refer to the below link.
exec with double quoted argument

return cmd stdout and stderr as string instead of printing to console in golang

I am executing bash commands from a golang application. Now the stdout and stderr go directly to console:
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
But I would like stdout and stderr to be returned as string variables from the runBashCommandAndKillIfTooSlow function without printing to the console immediately.
How to implement this?
The code:
package main
import (
"fmt"
"log"
"os"
"os/exec"
"time"
)
func main() {
ok, outString, errString := runBashCommandAndKillIfTooSlow("ls -la", 2000)
fmt.Println("ok")
fmt.Println(ok)
fmt.Println("outString")
fmt.Println(outString)
fmt.Println("errString")
fmt.Println(errString)
}
/*
run bash command and kill it if it works longer than "killInMilliSeconds" milliseconds
*/
func runBashCommandAndKillIfTooSlow(command string, killInMilliSeconds time.Duration) (okResult bool, stdout, stderr string) {
fmt.Println("running bash command...")
fmt.Println(command)
cmd := exec.Command("sh", "-c", command)
cmd.Stdout = os.Stdout // cmd.Stdout -> stdout
cmd.Stderr = os.Stderr // cmd.Stderr -> stderr
okResult = true
err := cmd.Start()
log.Printf("Waiting for command to finish...")
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-time.After(killInMilliSeconds * time.Millisecond):
if err := cmd.Process.Kill(); err != nil {
log.Fatal("failed to kill: ", err)
okResult = false
}
<-done // allow goroutine to exit
// log.Println("process killed")
case err := <-done:
if err != nil {
log.Printf("process done with error = %v", err)
okResult = false
}
}
if err != nil {
log.Fatal(err)
okResult = false
}
return
}
By the way, the program should keep its ability to kill the bash command if it was too slow (killInMilliSeconds parameter).
Set the output to a strings.Builder (in Go versions 1.10 or later) or a bytes.Buffer
var outbuf, errbuf strings.Builder // or bytes.Buffer
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf
After running the command, you can get the stdout and stderr as a string by calling the Builder.String() method:
stdout := outbuf.String()
stderr := errbuf.String()
You can simplify this quite a bit by using cmd.Run() instead of cmd.Start() to have it automatically wait for it to be finish, and use exec.CommandContext() to have it timeout. This will also output in the correct order, whereas the original program is out of order due to go routines.
Here's the exact same program simplified and using #Mello Marmot's answer:
package main
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"time"
"golang.org/x/net/context"
)
func main() {
ctx := context.Background()
ok, outString, errString := runBashCommandAndKillIfTooSlow(ctx, "ls -la", 2000*time.Millisecond)
fmt.Println("ok")
fmt.Println(ok)
fmt.Println("outString")
fmt.Println(outString)
fmt.Println("errString")
fmt.Println(errString)
}
/*
run bash command and kill it if it works longer than "killIn"
*/
func runBashCommandAndKillIfTooSlow(ctx context.Context, command string, killIn time.Duration) (okResult bool, stdout, stderr string) {
fmt.Println("running bash command...")
fmt.Println(command)
ctx, _ = context.WithTimeout(ctx, killIn)
cmd := exec.CommandContext(ctx, "sh", "-c", command)
// Set output to Byte Buffers
var outb, errb bytes.Buffer
cmd.Stdout = &outb
cmd.Stderr = &errb
okResult = true
err := cmd.Run()
stdout = outb.String()
stderr = errb.String()
if err != nil {
log.Fatal(err)
okResult = false
}
return
}
Another option is strings.Builder:
package main
import (
"os/exec"
"strings"
)
func main() {
b := new(strings.Builder)
c := exec.Command("go", "version")
c.Stdout = b
c.Run()
println(b.String() == "go version go1.16.3 windows/amd64\n")
}
https://golang.org/pkg/strings#Builder

How to start vim from go?

I've got a command line tool written in Golang and I need to start vim from it. However it's not working, and there's not any error or much else to work with. I've reduced the code to just this:
package main
import (
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "test.txt")
err := cmd.Run()
fmt.Println(err)
}
When I run this, I can see the vim process for a 2-3 seconds but the application doesn't actually open. Then the program simply exits (and the vim process closes) with an "exit status 1".
I've also tried this to capture stderr:
package main
import (
"bytes"
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "test.txt")
var stderr bytes.Buffer
cmd.Stderr = &stderr
err := cmd.Run()
fmt.Println(err)
fmt.Println(stderr)
}
But in this case, the program gets stuck indefinitely.
Any idea what could be the issue?
Pass on stdin and stdout from the calling program which, provided it was run from a terminal (likely for a command line program) will start vim for you and return control when the user has finished editing the file.
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("vim", "test.txt")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
err := cmd.Run()
fmt.Println(err)
}
VIM needs a proper terminal and detects the absence of one.
If you use the StderrPipe and read it while vim is running you will see this:
2014/02/02 20:25:49 Vim: Warning: Output is not to a terminal
2014/02/02 20:25:49 Vim: Warning: Input is not from a terminal
Example for reading stderr while executing (on play):
func logger(pipe io.ReadCloser) {
reader := bufio.NewReader(pipe)
for {
output, err := reader.ReadString('\n')
if err != nil {
log.Println(err)
return
}
log.Print(string(output))
}
}
pipe, err := cmd.StderrPipe()
go logger(pipe)
cmd.Run()
For vim to run you probably need to emulate a terminal.
Maybe goat (doc) can help you out:
tty := term.NewTTY(os.Stdin)
cmd := exec.Command("vim", "test.txt")
cmd.Stdin = t
cmd.Stdout = t
// ...

Resources