Get exit code - Go
GoGo Problem Overview
I'm using the package: os/exec http://golang.org/pkg/os/exec/ to execute a command in the operating system but I don't seem to find the way to get the exit code. I can read the output though
ie.
package main
import(
"os/exec"
"bytes"
"fmt"
"log"
)
func main() {
cmd := exec.Command("somecommand", "parameter")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run() ; err != nil {
//log.Fatal( cmd.ProcessState.Success() )
log.Fatal( err )
}
fmt.Printf("%q\n", out.String() )
}
Go Solutions
Solution 1 - Go
It's easy to determine if the exit code was 0 or something else. In the first case, cmd.Wait()
will return nil (unless there is another error while setting up the pipes).
Unfortunately, there is no platform independent way to get the exit code in the error case. That's also the reason why it isn't part of the API. The following snippet will work with Linux, but I haven't tested it on other platforms:
package main
import "os/exec"
import "log"
import "syscall"
func main() {
cmd := exec.Command("git", "blub")
if err := cmd.Start(); err != nil {
log.Fatalf("cmd.Start: %v", err)
}
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
log.Printf("Exit Status: %d", status.ExitStatus())
}
} else {
log.Fatalf("cmd.Wait: %v", err)
}
}
}
Solution 2 - Go
Since golang version 1.12, the exit code is available natively and in a cross-platform manner. See ExitError and ExitCode().
> ExitCode returns the exit code of the exited process, or -1 if the process hasn't exited or was terminated by a signal.
if err := cmd.Run() ; err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode()
}
}
Solution 3 - Go
Here's my enhanced version based on @tux21b 's answer
utils/cmd.go
package utils
import (
"bytes"
"log"
"os/exec"
"syscall"
)
const defaultFailedCode = 1
func RunCommand(name string, args ...string) (stdout string, stderr string, exitCode int) {
log.Println("run command:", name, args)
var outbuf, errbuf bytes.Buffer
cmd := exec.Command(name, args...)
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf
err := cmd.Run()
stdout = outbuf.String()
stderr = errbuf.String()
if err != nil {
// try to get the exit code
if exitError, ok := err.(*exec.ExitError); ok {
ws := exitError.Sys().(syscall.WaitStatus)
exitCode = ws.ExitStatus()
} else {
// This will happen (in OSX) if `name` is not available in $PATH,
// in this situation, exit code could not be get, and stderr will be
// empty string very likely, so we use the default fail code, and format err
// to string and set to stderr
log.Printf("Could not get exit code for failed program: %v, %v", name, args)
exitCode = defaultFailedCode
if stderr == "" {
stderr = err.Error()
}
}
} else {
// success, exitCode should be 0 if go is ok
ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
exitCode = ws.ExitStatus()
}
log.Printf("command result, stdout: %v, stderr: %v, exitCode: %v", stdout, stderr, exitCode)
return
}
I have tested it on OSX, if it's not working as expected on other platforms, please tell me so we can make it better.
Solution 4 - Go
September 2019, Go 1.13 introduced errors.As which supports error "unwrapping" - handy for finding precise errors in a nested call-chain.
So to extract and inspect the two most common errors when running an external command:
err := cmd.Run()
var (
ee *exec.ExitError
pe *os.PathError
)
if errors.As(err, &ee) {
log.Println("exit code error:", ee.ExitCode()) // ran, but non-zero exit code
} else if errors.As(err, &pe) {
log.Printf("os.PathError: %v", pe) // "no such file ...", "permission denied" etc.
} else if err != nil {
log.Printf("general error: %v", err) // something really bad happened!
} else {
log.Println("success!") // ran without error (exit code zero)
}
Solution 5 - Go
New package github.com/bitfield/script makes exec a LOT easier and has some really great added features to it as well. Check it out.
In this example I run two commands. One that errors and one that doesn't. Both have output and both show the exit value.
package main
import (
"fmt"
"github.com/bitfield/script"
)
func main() {
for _, c := range []string{"git blub", "git version"} {
fmt.Println("running", c)
p := script.Exec(c)
fmt.Println("Exit Status:", p.ExitStatus())
if err := p.Error(); err != nil {
p.SetError(nil)
out,_:=p.Stdout()
fmt.Println(out)
} else {
out,_:=p.Stdout()
fmt.Println(out)
}
fmt.Println("--")
}
}
Output:
running git blub
Exit Status: 1
git: 'blub' is not a git command. See 'git --help'.
The most similar command is
pull
--
running git version
Exit Status: 0
git version 2.24.3 (Apple Git-128)
--
Solution 6 - Go
Recently run into this when developing my helper package.
Base on example code here: https://go-review.googlesource.com/c/go/+/213337/1/src/os/exec/example_test.go
func ExampleExitError() {
cmd := exec.Command("sleep", "-u")
err := cmd.Run()
var exerr *exec.ExitError
if errors.As(err, &exerr) {
fmt.Printf("the command exited unsuccessfully: %d\n", exerr.ExitCode())
}
The Exit Code
I end up doing the following for my own exec cmd wrapper:
// Return exit code
func (self *MyCmd) ExitCode() int {
var exitErr *exec.ExitError
if errors.As(self.Err, &exitErr) {
return exitErr.ExitCode()
}
// No error
return 0
}
self.Err
is the return value from a exec.Command.Run()
. Complete listing is here: https://github.com/J-Siu/go-helper/blob/master/myCmd.go
Text Error Message
While @colm.anseo answer take consideration of os.patherror
, it doesn't give an error code(int), and IMHO should be handled separately. Instead text error message can be extract from execCmd.Stderr
like follow:
func (self *MyCmd) Run() error {
execCmd := exec.Command(self.CmdName, *self.ArgsP...)
execCmd.Stdout = &self.Stdout
execCmd.Stderr = &self.Stderr
execCmd.Dir = self.WorkDir
self.CmdLn = execCmd.String()
self.Err = execCmd.Run()
self.Ran = true
ReportDebug(&self, "myCmd:", false, false)
ReportDebug(self.Stderr.String(), "myCmd:Stderr", false, false)
ReportDebug(self.Stdout.String(), "myCmd:Stdout", false, false)
return self.Err
}
self.Stderr
is a bytes.Buffer
and pass into execCmd
before Run()
. After execCmd.Run()
, text err can be extracted with self.Stderr.String()
.