由于业务需求,需要使用go语言来调用操作系统的命令行执行命令,有以下要求:

  • 能够指定用户执行
  • 能够执行多条命令

首先针对第一点,指定用户执行,使用如下代码即可实现:

cmd := exec.Command("sh", "-c", "whoami")    
osUser, err := user.Lookup("username")
if err == nil {
    uid, _ := strconv.Atoi(osUser.Uid)
    gid, _ := strconv.Atoi(osUser.Gid)
    cmd.SysProcAttr = &syscall.SysProcAttr{}
    cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}

对于第二点,一个Command执行多条命令,需要用到 io.Pipe()这个方法。

io.Pipe()这个方法的返回值是*PipeReader*PipeWriter,这里我们把它们分别叫做reader和writer。

通过,通过writer写入的数据可以直接从reader读取到,在这里,Command的实例按照正常执行的话可以看作程序向操作系统申请了一个命令行会话,当命令执行完成之后,这个会话就关闭了,当你执行另一个Command时,使用的就是另一个会话了,不能实现一些特定的需求,例如先刷新当前的环境变量再执行语句。

为了实现在同一个Command会话中执行多条指令的效果,就要求在执行了一条Command指令之后不能立即关闭当前的会话,查看Command的结构体之后可以看到以下两个属性:

Stdin io.ReaderStdout io.Writer

go语言的源代码看起来比较容易看懂,可以看到 Stdin 就是用来接受指令输入的,Stdout是用来输出命令执行后控制台输出的,当一条指令输入完成之后,Stdin会被关闭,随后等到Stdout读取完控制台输出这次Command会话就结束了。

知道了上面的信息,要实现一个 Command 的执行多条指令的话,就只需要不停的向Stdin写入数据,并且写入一条指令后输入一个换行模拟回车,直到所有命令都输入完成后再关闭Stdin。

有了理论基础,代码撸起来:

func ExecCommand(command ...string) (*os.ProcessState, error) {
    cmd := exec.Command("sh")
    // 定义一对输入输出流
    inReader, inWriter := io.Pipe()
    // 把输入流的给到命令行
    cmd.Stdin = inReader
    // 获取标准输入流和错误信息流
    stderr, _ := cmd.StderrPipe()
    stdout, _ := cmd.StdoutPipe()

    sizeIndex := len(command) - 1
    // 指定用户执行
    osUser, err := user.Lookup(command[sizeIndex])
    if err == nil {
        //log.Printf("uid=%s,gid=%s", osUser.Uid, osUser.Gid)
        uid, _ := strconv.Atoi(osUser.Uid)
        gid, _ := strconv.Atoi(osUser.Gid)
        cmd.SysProcAttr = &syscall.SysProcAttr{}
        cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
    }

    if err = cmd.Start(); err != nil {
        return nil, err
    }
    // 正常日志
    go func() {
        logScan := bufio.NewScanner(stdout)
        for logScan.Scan() {
            text := logScan.Text()
            fmt.Println("std:", text)
        }
    }()
    // 错误日志
    go func() {
        scan := bufio.NewScanner(stderr)
        for scan.Scan() {
            s := scan.Text()
            log.Println("build error: ", s)
        }
    }()
    // 写指令
    go func() {
        lines := command[:sizeIndex]
        for i, str := range lines {
            _, err := inWriter.Write([]byte(str))
            if err != nil {
                fmt.Println(err)
            }
            _, err = inWriter.Write([]byte("\n"))
            if err != nil {
                fmt.Println(err)
            }
            if i == len(lines)-1 {
                _ = inWriter.Close()
            }
        }
    }()

    err = cmd.Wait()
    state := cmd.ProcessState
    //执行失败,返回错误信息
    if !state.Success() {
        return state, err
    }
    return state, err
}

上面这个方法,实现了指定用户执行和多命令执行两个需求,方法的入参部分,使用了...string 的形式来接收多个字符串,接收到的多个字符串最后一个是要用来执行命令的用户,其它就是按照传入顺序执行的命令,假设需要在同一个会话中使用用户test执行以下两条命令时

source /etc/profile
java -version

则调用上述方法的方式为

ExecCommand("source /etc/profile", "java -version", "test")

上述列举的方法并不完善,如果你传入的参数只有一个,那就取不出正确的用户,这就会造成错误,可以增加一个判断来限制输入的参数个数以解决这个问题,此处不再赘述。

参考文章:golang的Command一次执行多条命令