Golang 调用系统命令

Golang 调用系统命令

在开发中我们经常会遇到需要调用系统的命令,我的 ECTS 项目中也用到了 Golang 的 exec 包。

创建系统命令调用

Command

1
cmd := exec.Command("/bin/bash", "-c", "command")

方法原型

1
func Command(name string, arg ...string) *Cmd {}

CommandContext

1
2
ctx, cancelFun := context.WithTimeout(context.Background, 5 * time.Second)
cmd := exec.CommandContext(ctx, "/bin/bash", "-c", "command")

方法原型

1
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd {}

这种方式,可以通过设置 context 来启用强行结束运行的命令。

你可能会感觉奇怪,为什么 name 参数是 /bin/bash 而不是直接输入命令?其实我们在终端输入命令按下回车键之后,是需要 Shell 去解析,而如今大多数的运行环境中都是 bash,也就是说当你在终端命令中输入 ls -la 时,系统真正执行的时 /bin/bash -c “ls -la”。

运行方式

Run

Run 方法时同步的,也就是说当代码执行到 Run 方法时会阻塞等待命令执行完成并返回。

1
2
3
4
5
6
7
8
9
10
command := exec.Command("ls", "-la")
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
command.Stdout = stdout
command.Stderr = stderr
if err := command.Run(); err != nil {
log.Println(err)
}

log.Println(stdout.String(), stderr.String())

Start

Start 方法是异步的,会立即返回,可以调用 Wait 方法”等待”命令执行完成。Run 方法就是一个例子:

1
2
3
4
5
6
func (c *Cmd) Run() error {
if err := c.Start(); err != nil {
return err
}
return c.Wait()
}

Output

这个方法只返回命令执行后的 stdout 也就是标准输出的内容。

1
stdout, err := cmd.Output()

CombinedOutput

这个方法会将 stdoutstderr 合并到一起,并返回。

1
output, err := cmd.CombinedOutput()

使用管道链接多个命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ps := exec.Command("/bin/bash", "-c",  "ps -ef")
grep := exec.Command("/bin/bash", "-c", "grep -i ssh")
// 创建管道
r, w := io.Pipe()
defer r.Close()
defer w.Close()
ps.Stdout = w // ps向管道的一端写
grep.Stdin = r // grep从管道的一端读
var buffer bytes.Buffer
grep.Stdout = &buffer // grep的输出为buffer

_ = ps.Start()
_ = grep.Start()
ps.Wait()
w.Close()
grep.Wait()
io.Copy(os.Stdout, &buffer) // buffer拷贝到系统标准输出

通过上面的代码可以看到,其原理就是将一个命令的 stdout 输入到另一个命令的 stdin

使用指定用户运行命令

业务中我们可能需要以特定用户来运行一个命令,那么可以参照如下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import (
"os/user"
"strconv"
"syscall"
)

cmd := exec.CommandContext(ctx, "/bin/bash", "-c", "command")
cmd.Env = env
cmd.Dir = dir
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}

sysuser, err := user.Lookup(username) // 通过用户名来获取用户信息
if err != nil {
return err
}
uid, err := strconv.Atoi(sysuser.Uid) // 将UID的类型转换成 uint32
gid, err := strconv.Atoi(sysuser.Gid) // 将GID的类型转换成 uint32

// 设置 Credential
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
Groups: nil,
NoSetGroups: false,
}

如果用户存在则会以指定的用户运行该命令。

强行结束命令

1
2
3
4
cmd := exec.Command("/bin/bash", "-c", "sleep 5")
if err := cmd.Process.Kill(); err != nil {
return err
}

如果是在协程外部,则建议使用 CommandContext 方法来创建命令,这样可以通过 context 来控制命令的生命周期。

评论