=Start=
缘由:
在实际编程过程中,不论是底层的C语言还是上层的Java/Python,在实现某些功能的时候,少不了调用外部命令。所以这里整理总结一下在Go 语言中借助os/exec包执行外部命令的方法,以及一个实现了超时退出功能的命令执行函数。
正文:
参考解答:
Go 语言中使用 exec.Cmd 结构类型来抽象用户要执行的外部命令;该类型下的 Command 方法,接收给定参数并返回 Cmd 结构体,但它只设置返回Cmd结构中的Path和Args。
func Command(name string, arg ...string) *Cmd type Cmd struct { Path string Args []string Env []string Dir string Stdin io.Reader Stdout io.Writer Stderr io.Writer // ExtraFiles specifies additional open files to be inherited by the // new process. It does not include standard input, standard output, or // standard error. If non-nil, entry i becomes file descriptor 3+i. ExtraFiles []*os.File // SysProcAttr holds optional, operating system-specific attributes. // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. SysProcAttr *syscall.SysProcAttr // Process is the underlying process, once started. Process *os.Process // ProcessState contains information about an exited process, // available after a call to Wait or Run. ProcessState *os.ProcessState // contains filtered or unexported fields }
如果名称 name 中不包含路径分隔符,命令将使用LookPath函数将名称解析为完整路径。否则,它直接使用名称作为路径。LookPath函数用于在PATH环境变量指定的目录中搜索一个名为file的可执行二进制文件。如果文件包含斜杠,则直接尝试使用该文件,并且不查看路径。查询结果可能是绝对路径或相对于当前目录的相对路径。
返回的Cmd中的Args字段由 命令名name 跟着 参数arg 一起组成,因此 arg 本身不应该包含命令名。例如,命令(“echo”,”hello”)。Args[0]始终是名称,而不是可能解析的路径。
下面是一个简单的示例(包含 tr
命令和 echo
命令的调用,还有对LookPath函数的测试):
package main import ( "bytes" "fmt" "log" "os/exec" "strings" ) func main() { cmd := exec.Command("tr", "a-z", "A-Z") cmd.Stdin = strings.NewReader("some input") var out bytes.Buffer cmd.Stdout = &out err := cmd.Run() if err != nil { log.Fatal(err) } fmt.Printf("in all caps: %q\n", out.String()) // log.Fatal() is equivalent to fmt.Print() followed by a call to os.Exit(1). // cmd = exec.Command("sleep", "5") // need 5 seconds to finish cmd = exec.Command("echo", "hello") // finish immediately err = cmd.Start() if err != nil { log.Fatal(err) } log.Printf("Waiting for command to finish...") err = cmd.Wait() log.Printf("Command finished with error: %v", err) path, err := exec.LookPath("/bin/w") if err != nil { log.Println("installing `/bin/w` is in your future") } else { fmt.Printf("`/bin/w` is available at %q\n", path) } path, err = exec.LookPath("bin/w") if err != nil { log.Println("installing `bin/w` is in your future") } else { fmt.Printf("`bin/w` is available at %q\n", path) } path, err = exec.LookPath("w") if err != nil { log.Println("installing w is in your future") } else { fmt.Printf("`w` is available at %q\n", path) } }
但是在实际环境中,各种各样的情况都有可能出现,比如一个本地以及测试环境中执行只需要1、2秒的命令或脚本到了线上可能10秒钟也执行不完,但是,我们的流程又不能一直卡在这里等待完成,需要有一个超时退出的机制。这时就可以参考下面的示例:
package main import ( "bytes" "fmt" "log" "os/exec" "strings" "syscall" "time" ) // https://linkscue.com/2018/06/03/2018-06-03-golang-exec-command-timeout-trace/ // 删除输出内容中的\x00和多余的空格 func trimOutput(buffer bytes.Buffer) string { return strings.TrimSpace(string(bytes.TrimRight(buffer.Bytes(), "\x00"))) } // 实时打印输出(利用Ticker每隔一秒从out缓冲区中取出一部分内容进行打印) func traceOutput(out *bytes.Buffer) { offset := 0 t := time.NewTicker(time.Second) defer t.Stop() for { <-t.C result := bytes.TrimRight((*out).Bytes(), "\x00") // 每次读取的是所有的输出内容 size := len(result) rows := bytes.Split(bytes.TrimSpace(result), []byte{'\n'}) nRows := len(rows) newRows := rows[offset:nRows] if result[size-1] != '\n' { newRows = rows[offset : nRows-1] } if len(newRows) < offset { // 判断是否满足打印条件(之前是否已打印过) continue } for _, row := range newRows { // log.Println(string(row)) // 会在每行日志开头添加日期时间信息 fmt.Println(string(row)) } offset += len(newRows) // offset记录的是已打印行数的偏移值 } } // 运行Shell命令,设定超时时间(秒) func ExecCmdWithTimeout(timeout int, cmd string, args ...string) (stdout, stderr string, e error) { if len(cmd) == 0 { e = fmt.Errorf("cannot run a empty command") return } var out, err bytes.Buffer command := exec.Command(cmd, args...) // 根据输入的参数生成 Cmd 类型的结构体 command.Stdout = &out command.Stderr = &err command.Start() // 开始执行命令 // 下面2行用于启动一个单独的goroutine等待命令执行结束 done := make(chan error) // 创建一个 channel 用于承载命令执行状态的通讯 go func() { done <- command.Wait() }() // 启动routine持续打印输出(需要时再打开,不需要实时打印的话可以在最后一次性打印出来) // go traceOutput(&out) // 设定超时时间,并select它 after := time.After(time.Duration(timeout) * time.Second) // 可以理解为启动一个定时器 select { case <-after: // 当到了设定的超时时间后,进入这个 case 语句 command.Process.Signal(syscall.SIGINT) // 先发送SIGINT再去kill掉命令,以确保命令的输出完整 time.Sleep(time.Second) // 等待1秒后再去kill command.Process.Kill() // if err2 := command.Process.Kill(); err2 != nil { // "os: process already finished" // log.Printf("failed to kill: %s, error: %q\n", command.Path, err2) // } go func() { <-done // 当主动kill进程时,其实就不太关心done中的结果了,所以主动消费,且用go以避免这里卡住 }() log.Printf("运行命令(%s)超时,超时设定:%v 秒。", fmt.Sprintf(`%s %s`, cmd, strings.Join(args, " ")), timeout) case <-done: // 从 channel 中取出命令的执行状态并存入变量 e 中 } stdout = trimOutput(out) stderr = trimOutput(err) return } func main() { // _, _, e := ExecCmdWithTimeout(5, "ping", "114.114.114.114") stdout, stderr, e := ExecCmdWithTimeout(5, "ping", "114.114.114.114") log.Println("stdout:\n", stdout) log.Println("stderr:\n", stderr) log.Println("error:\n", e) }
参考链接:
- GO语言执行命令超时的设置#nice
- 使用Golang为脚本执行设置超时时间
- Go: Timeout Commands with os/exec CommandContext
https://www.cnblogs.com/shhnwangjian/p/9039742.html - Go 语言中如何终止一个用os/exec启动的进程
=END=
《 “Go语言学习#13-借助exec执行外部命令” 》 有 2 条评论
[译]使用os/exec执行命令
https://colobu.com/2017/06/19/advanced-command-execution-in-Go-with-os-exec/
Executing Commands In Go
http://www.darrencoxall.com/golang/executing-commands-in-go/
用Go编写的快速开启HTTP文件浏览服务的小工具,能够执行shell命令
https://github.com/tinyibird/HTTPServerGO
`
这是一个用Go编写的红队内网环境中一个能快速开启HTTP文件浏览服务的小工具,能够执行shell命令。它支持以下功能:
-提供指定目录中的文件
-能够使用指定的查询参数执行shell命令
-可自定义外壳路径和查询参数
-可自定义的IP地址和端口
-支持PHP、Java和.NET shell(目前仅支持转储PHP shell)
-在后台运行服务器而不向控制台打印任何内容的选项
-转储shell并在服务器上执行的选项(目前仅支持转储PHP shell)
`