通过Golang执行Shell脚本后找回错误

I have a simple shell script (named copy.sh) which looks like below:-

#! /bin/sh

cp $1 $2

I did chmod 777 copy.sh.

I have a golang code which executes the above shell code:-

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    _, err := exec.Command("/Users/debraj/copy.sh", "/Users/debraj/temp.txt", "/Users/debraj/gotest/").Output()
    if err != nil {
        fmt.Println("Failed to execute command " + err.Error())
        return
    }
    fmt.Printf("
Copy Successful - %v")
}

The above code is showing be the below output:-

jabongs-MacBook-Pro-4:src debraj$ go run copyerr.go 
Failed to execute command exit status 1
jabongs-MacBook-Pro-4:src debraj$ 

But the error I receive from shell script looks like below:-

jabongs-MacBook-Pro-4:~ debraj$ ./copy.sh /Users/debraj/temp.txt /Users/debraj/gotest/
cp: /Users/debraj/gotest/temp.txt: Permission denied     

Can someone let me know how how can I get the same error message that is returned by the shell script?

If I don;t do chmod 777 copy.sh and the file has permission as below:-

jabongs-MacBook-Pro-4:~ debraj$ ls -la copy.sh 
-rw-r--r--  1 debraj  staff  21 Sep 29 13:28 copy.sh

Then the golang code gives the output as given by the shell script. Can some also let me know why this is behaving like this?

I am on

  • Golang 1.7
  • Mac OS X 10.11.4

Alrighty! So the problem at its core is that you have two different places that you can get an error:

  1. When exec.Command().Output() can't run your shell script at all (because it doesn't have permissions in this case), and
  2. When your shell script itself runs, but returns an error code (because the last line run in the script is an error).

In the first case, you should get something reasonable in err because Go itself got a well-defined system error when trying to do something (which failed immediately). In the second case, Go was able to run your script, but it had output (that you're ignoring) and an return code (in this case, the exit code of the last thing executed by your script). Since the output could have literally been anything at all (including megabytes and megabytes of nonsense text), Go takes the lazy way out and just returns the status code. This, IMHO, is the right thing to do, given the rampant abuse of stderr by basically everyone.

Plus, your script (if modified slightly) could actually return an error code without displaying an error message at all!

That said, if you want to see the actual error information from your script, do one of the following:

// Display everything we got if error.
output, err := exec.Command(...).CombinedOutput()
if err != nil {
    fmt.Println("Error when running command.  Output:")
    fmt.Println(string(output))
    fmt.Printf("Got command status: %s
", err.Error())
    return
}

or:

// Display just the stderr if an error occurs
cmd := exec.Command(...)
stderr := &bytes.Buffer{}    // make sure to import bytes
cmd.Stderr = stderr
err := cmd.Run()
if err != nil {
    fmt.Println("Error when running command.  Error log:")
    fmt.Println(stderr.String())
    fmt.Printf("Got command status: %s
", err.Error())
    return
}

...or you can go fancier and use StderrPipe() and just read the last line or similar. Really, there are a bunch of ways to skin this particular cat (since cmd.Stderr can be any io.Writer, you could easily write nearly any handler -- including one that parses the stream looking for root causes, etc etc).

Something to consider is that Output() and CombinedOutput() as used will display stuff only when the script is completely done running. This can be a serious UI problem with long-running scripts, as it means that any user monitoring what's going on will see a big, fat load of nothing while processing is happening. Using StdoutPipe() and StderrPipe() is likely preferable, as you can parse/interpret (or just display) output as it happens. Of course, if you can guarantee that your script won't run for more than a few seconds or that it won't be monitored by a human, who cares?

If you want to explore how to display output as the script runs, the example for StdoutPipe() is a pretty good place to start.