Five postures for executing commands with os/exec in Go language

For a complete series of tutorials, see: http://golang.iswbm.com

In Golang, the libraries used to execute commands are os/exec, exec The command function returns a Cmd object. According to different requirements, the execution of the command can be divided into three cases

Execute the command without getting the result
Execute the command and get the result (stdout and stderr are not distinguished)
Execute the command and get the result (distinguish between stdout and stderr)
The first is to execute commands without obtaining results#
Directly call the Run function of the Cmd object, only success and failure are returned, and no output results are obtained.

CopyCopy
package main

import (

"log"
"os/exec"

)

func main() {

cmd := exec.Command("ls", "-l", "/var/log/")
err := cmd.Run()
if err != nil {
    log.Fatalf("cmd.Run() failed with %s\n", err)
}

}
Second: execute the command and get the result#
Sometimes when we execute a command to get the output result, you can call the CombinedOutput function of Cmd.

CopyCopy
package main

import (
"fmt"
"log"
"os/exec"
)

func main() {

cmd := exec.Command("ls", "-l", "/var/log/")
out, err := cmd.CombinedOutput()
if err != nil {
    fmt.Printf("combined out:\n%s\n", string(out))
    log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))

}
The CombinedOutput function only returns out and does not distinguish between stdout and stderr. If you want to distinguish them, you can look directly at the third method.

CopyCopy
$ go run demo.go
combined out:
total 11540876
-rw-r--r-- 2 root root 4096 Oct 29 2018 yum.log
drwx------ 2 root root 94 Nov 6 05:56 audit
-rw-r--r-- 1 root root 185249234 Nov 28 2019 message
-rw-r--r-- 2 root root 16374 Aug 28 10:13 boot.log
But before that, I found a small problem: sometimes, shell commands can be executed without code exec.

For example, I just want to view the files with the log suffix in the / var/log / directory? Students with a little Linux foundation will use this command

CopyCopy
$ ls -l /var/log/*.log
total 11540
-rw-r--r-- 2 root root 4096 Oct 29 2018 /var/log/yum.log
-rw-r--r-- 2 root root 16374 Aug 28 10:13 /var/log/boot.log
Put it in exec. Com in this way Command

CopyCopy
package main

import (
"fmt"
"log"
"os/exec"
)

func main() {

cmd := exec.Command("ls", "-l", "/var/log/*.log")
out, err := cmd.CombinedOutput()
if err != nil {
    fmt.Printf("combined out:\n%s\n", string(out))
    log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))

}
What happened? I can't. I made a mistake.

CopyCopy
$ go run demo.go
combined out:
ls: cannot access /var/log/*.log: No such file or directory

2020/11/11 19:46:00 cmd.Run() failed with exit status 2
exit status 1
Why do you report an error? There's no problem

Actually, it's very simple. Originally, LS - L / var / log / * Log is not equivalent to the following code.

CopyCopy
exec.Command("ls", "-l", "/var/log/*.log")
The Shell command corresponding to the above code should be as follows. If you write like this, ls will treat the contents in the parameters as specific file names and ignore wildcards*

CopyCopy
$ ls -l "/var/log/*.log"
ls: cannot access /var/log/*.log: No such file or directory
Third: execute the command and distinguish stdout from stderr#
The above writing method cannot distinguish between standard output and standard error. It can be realized as long as it is changed to the following writing method.

CopyCopy
package main

import (

"bytes"
"fmt"
"log"
"os/exec"

)

func main() {

cmd := exec.Command("ls", "-l", "/var/log/*.log")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout  // standard output 
cmd.Stderr = &stderr  // Standard error
err := cmd.Run()
outStr, errStr := string(stdout.Bytes()), string(stderr.Bytes())
fmt.Printf("out:\n%s\nerr:\n%s\n", outStr, errStr)
if err != nil {
    log.Fatalf("cmd.Run() failed with %s\n", err)
}

}
The output is as follows. You can see that the previous error messages are classified into standard errors

CopyCopy
$ go run demo.go
out:

err:
ls: cannot access /var/log/*.log: No such file or directory

2020/11/11 19:59:31 cmd.Run() failed with exit status 2
exit status 1
Fourth: multiple command combinations, please use pipes#
Take the execution output result of the previous command as the parameter of the next command. In the Shell, you can use the pipe symbol |.

For example, the following command counts the number of ERROR logs in the message log.

CopyCopy
$ grep ERROR /var/log/messages | wc -l
19
Similarly, there are similar implementations in Golang.

CopyCopy
package main
import (

"os"
"os/exec"

)
func main() {

c1 := exec.Command("grep", "ERROR", "/var/log/messages")
c2 := exec.Command("wc", "-l")
c2.Stdin, _ = c1.StdoutPipe()
c2.Stdout = os.Stdout
_ = c2.Start()
_ = c1.Run()
_ = c2.Wait()

}
The output is as follows

CopyCopy
$ go run demo.go
19
The fifth is to set environment variables at the command level#
The environment variable set by using the Setenv function of the os library acts on the life cycle of the whole process.

CopyCopy
package main
import (

"fmt"
"log"
"os"
"os/exec"

)
func main() {

os.Setenv("NAME", "wangbm")
cmd := exec.Command("echo", os.ExpandEnv("$NAME"))
out, err := cmd.CombinedOutput()
if err != nil {
    log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("%s", out)

}
As long as in this process, the value of the NAME variable will be wangbm, no matter how many times you execute the command

CopyCopy
$ go run demo.go
wangbm
There is also a way to narrow the scope of environment variables to the command level.

To facilitate verification, I create a new sh script as follows

CopyCopy
$ cat /home/wangbm/demo.sh
echo $NAME
$ bash /home/wangbm/demo.sh # since there is no NAME in the global environment variable, there is no output

In addition, demo The code in go is as follows

CopyCopy
package main
import (

"fmt"
"os"
"os/exec"

)

func ChangeYourCmdEnvironment(cmd * exec.Cmd) error {

env := os.Environ()
cmdEnv := []string{}

for _, e := range env {
    cmdEnv = append(cmdEnv, e)
}
cmdEnv = append(cmdEnv, "NAME=wangbm")
cmd.Env = cmdEnv

return nil

}

func main() {

cmd1 := exec.Command("bash", "/home/wangbm/demo.sh")

ChangeYourCmdEnvironment(cmd1) / / add environment variable to cmd1 command: NAME=wangbm

out1, _ := cmd1.CombinedOutput()
fmt.Printf("output: %s", out1)

cmd2 := exec.Command("bash", "/home/wangbm/demo.sh")
out2, _ := cmd2.CombinedOutput()
fmt.Printf("output: %s", out2)

}
After execution, you can see that the command executed for the second time does not output the variable value of NAME.

CopyCopy
$ go run demo.go
output: wangbm
output:

Keywords: Go

Added by fleymingmasc on Wed, 05 Jan 2022 22:49:58 +0200