95

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() )
}
OscarRyz
  • 196,001
  • 113
  • 385
  • 569

6 Answers6

112

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 {
            log.Printf("Exit Status: %d", exiterr.ExitCode())
        } else {
            log.Fatalf("cmd.Wait: %v", err)
        }
    }
}

Just follow the api docs to find out more :)

Sergii Getman
  • 3,845
  • 5
  • 34
  • 50
tux21b
  • 90,183
  • 16
  • 117
  • 101
  • 1
    I looked into the docs but never found the way to go from `ProcessState` to `WaitStatus` how did you do that?. Also I've just found in the mailing list the solution ( just like the one you've described) https://groups.google.com/forum/?fromgroups#!searchin/golang-nuts/exit$20code/golang-nuts/dKbL1oOiCIY/Bz_haQYmMrcJ I'm trying it now in windows and let you know :) – OscarRyz Apr 30 '12 at 15:12
  • It's definitely not that easy to get the exit code, but the docs provide at least some hints where you have to look next. But they do not mention Windows at all, so I am looking forward to your answer. – tux21b Apr 30 '12 at 15:16
  • The mailing list says something about Windows specific, but I've just run this code and works perfect, this is on go1. – OscarRyz Apr 30 '12 at 15:18
  • Offtopi c- is there a way to get that `if/if` in a single if ( using an `&&` or something? ) – OscarRyz Apr 30 '12 at 15:20
  • I don't think so. Also, if there is an error while retrieving the exit status only the first if will be evaluated. – tux21b Apr 30 '12 at 15:22
  • 2
    @OscarRyz From `ProcessState` to `WaitStatus`: you can either use `fmt.Printf("%T\n", exiterr.Sys())` to print out the type of the value, or you can examine the sources at http://golang.org/src/pkg. –  Apr 30 '12 at 15:23
  • @Atom: you can also read the docs. That's what I have done :) http://golang.org/pkg/os/#ProcessState.Sys – tux21b Apr 30 '12 at 15:25
  • 1
    Atom and tux21b thank you both :) I read the docs, but got stuck in places like _Convert it to the appropriate underlying type_ :P So, my strategy was _Ask on StackOverflow_ and worked \o/ – OscarRyz Apr 30 '12 at 15:56
  • A function called `ExitStatus()` on the returned `ExitError` type would be a lovely addition to os/exec... – Michael Whatcott Mar 10 '15 at 05:04
  • I've found that on Windows, it does not (at least not always) return an error with the command exits with a non-zero code. However, if it does not return an error, you can do cmd.ProcessState.Sys().(syscall.WaitStatus) and use a similar technique as the one in this answer. – Adrian Apr 08 '16 at 14:59
  • 1
    I don't know if it is a recent addition, but now instead of `status, ok := exiterr.Sys().(syscall.WaitStatus)` you can do just `status := exiterr.ExitCode()` – Sergii Getman Sep 02 '22 at 13:46
96

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()
    }
}
David
  • 9,635
  • 5
  • 62
  • 68
27

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.

Reorx
  • 2,801
  • 2
  • 24
  • 29
16

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)
}
colm.anseo
  • 19,337
  • 4
  • 43
  • 52
2

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().

John Siu
  • 5,056
  • 2
  • 26
  • 47
0

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)

--
Rich
  • 57
  • 3