Skip to content
Branch: master
Find file History
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
..
Failed to load latest commit information.
testcmd
README.md
main.go

README.md

#Go学习笔记(八) | 使用 os/exec 执行命令

用Go去调用一些外部的命令其实很愉快的,这遍文章就总结一下我自己日常用的比较多的几种方法。

关于Unix标准输入输出

在具体聊os/exec的使用前,了解一下shell的标准输出是很有必要的。

我们平常会用到或看到这样的命令:

$ ls xxx 1>out.txt 2>&1
$ nohup xxx 2>&1 &

你知道这里1,2含义么?

其实这里的1,2指的就是Unix文件描述符。文件描述符其实就一数字,每一个文件描述符代表的都是一个文件。如果你打开100个文件,你就会获取到100个文件描述符。

这里需要注意的一点就是,在Unix中一切皆文件。当然,这里我们不必去深究,我们需要知道的是1,2代表的是标准输出stdout与标准错误输出stderr。还有0代表标准输入stdin

os/exec中就用到了Stdin,Stdout,Stderr,这些基本Unix知识或能帮助我们更好理解这些参数。

os/exec

os/exec包内容并不多,我们大概过一下。

  1. LookPath(file string) (string, error)

    寻找可执行文件路径,如果你指定的可执行文件在$PATH中,就会返回这个可执行文件的相对/绝对路径;如果你指定的是一个文件路径,他就是去判断文件是否可读取/执行,返回的是一样的路径。

    在我们需要使用一些外部命令/可执行文件的时候,我们可以先使用该函数判断一下该命令/可执行文件是否有效。

  2. Command(name string, arg ...string) *Cmd

    使用你输入的参数,返回Cmd指针,可用于执行Cmd的方法。

    这里name就是我们的命令/可执行文件,后面的参数可以一个一个输入。

  3. CommandContext(ctx context.Context, name string, arg ...string) *Cmd

    和上面功能一样,不过我们可以用上下文做一些超时等控制。

  4. 之后几个就是Cmd的一些方法。

其实读完,结合官方的一些example,使用很简单,下面具体写几个场景。

  1. 本文全部的Demo在这里

  2. ./testcmd/testcmd是我用Go写的一个简单的可执行文件,可以根据指定的参数 输出/延时输出/输出错误,方便我们演示。如下

func main() {

	var (
		start bool
		e     bool
	)

	flag.BoolVar(&start, "s", false, "start output")
	flag.BoolVar(&e, "e", false, "output err")
	flag.Parse()

	if start {
		for i := 5; i > 0; i-- {
			fmt.Fprintln(os.Stdout, "test cmd output", i)
			time.Sleep(1 * time.Second)
		}
	}

	if e {
		fmt.Fprintln(os.Stderr, "a err occur")
		os.Exit(1)
	}

	fmt.Fprintln(os.Stdout, "test cmd stdout")
}

简单执行

// 简单执行
func test1() {
	cmd := exec.Command("./testcmd/testcmd", "-s")

	// 使用CombinedOutput 将stdout stderr合并输出
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Printf("test1 failed %s\n", err)
	}
	log.Println("test1 output ", string(out))
}

输出:

Run Test 1
2019/06/06 18:02:39 test1 output  test cmd output 5
test cmd output 4
test cmd output 3
test cmd output 2
test cmd output 1
done

整个过程等待5秒,所有结果一次输出。

分离标准输出与错误输出

将错误分开输出,同时开了两个协成,同步的接收命令的输出内容。

// 	stdout & stderr 分开输出
func test2() {
	cmd := exec.Command("./testcmd/testcmd", "-s", "-e")
	stdout, _ := cmd.StdoutPipe()
	stderr, _ := cmd.StderrPipe()
	cmd.Start()

	go func() {
		for {
			buf := make([]byte, 1024)
			n, err := stderr.Read(buf)

			if n > 0 {
				log.Printf("read err %s", string(buf[:n]))
			}

			if n == 0 {
				break
			}

			if err != nil {
				log.Printf("read err %v", err)
				return
			}
		}
	}()

	go func() {
		for {
			buf := make([]byte, 1024)
			n, err := stdout.Read(buf)

			if n == 0 {
				break
			}

			if n > 0 {
				log.Printf("read out %s", string(buf[:n]))

			}

			if n == 0 {
				break
			}

			if err != nil {
				log.Printf("read out %v", err)
				return
			}

		}
	}()

	err := cmd.Wait()
	if err != nil {
		log.Printf("cmd wait %v", err)
		return
	}
}

输出:

Run Test 2
2019/06/06 18:02:39 read out test cmd output 5
2019/06/06 18:02:40 read out test cmd output 4
2019/06/06 18:02:41 read out test cmd output 3
2019/06/06 18:02:42 read out test cmd output 2
2019/06/06 18:02:43 read out test cmd output 1
2019/06/06 18:02:44 read err a err occur
2019/06/06 18:02:44 cmd wait exit status 1

按行读取输出内容

使用bufio按行读取输出内容。

// 按行读输出的内容
func test3() {
	cmd := exec.Command("./testcmd/testcmd", "-s", "-e")
	stdout, _ := cmd.StdoutPipe()
	stderr, _ := cmd.StderrPipe()
	oReader := bufio.NewReader(stdout)
	eReader := bufio.NewReader(stderr)

	cmd.Start()

	go func() {
		for {
			line, err := oReader.ReadString('\n')

			if line != "" {
				log.Printf("read line %s", line)
			}

			if err != nil || line == "" {
				log.Printf("read line err %v", err)
				return
			}

		}
	}()

	go func() {
		for {
			line, err := eReader.ReadString('\n')

			if line != "" {
				log.Printf("read err %s", line)
			}

			if err != nil || line == "" {
				log.Printf("read err %v", err)
				return
			}

		}
	}()

	err := cmd.Wait()
	if err != nil {
		log.Printf("cmd wait %v", err)
		return
	}
}

输出:

Run Test 3
2019/06/06 18:06:44 read line test cmd output 5
2019/06/06 18:06:45 read line test cmd output 4
2019/06/06 18:06:46 read line test cmd output 3
2019/06/06 18:06:47 read line test cmd output 2
2019/06/06 18:06:48 read line test cmd output 1
2019/06/06 18:06:49 read err a err occur
2019/06/06 18:06:49 cmd wait exit status 1

设置执行超时时间

有时候我们要控制命令的执行时间,这是就可以使用上下文去控制了。

// 通过上下文控制超时
func test4() {

	ctx, calcel := context.WithTimeout(context.Background(), 2*time.Second)
	defer calcel()

	cmd := exec.CommandContext(ctx, "./testcmd/testcmd", "-s", "-e")

	stdout, _ := cmd.StdoutPipe()
	stderr, _ := cmd.StderrPipe()
	oReader := bufio.NewReader(stdout)
	eReader := bufio.NewReader(stderr)

	cmd.Start()

	go func() {
		for {
			line, err := oReader.ReadString('\n')

			if line != "" {
				log.Printf("read line %s", line)
			}

			if err != nil || line == "" {
				log.Printf("read line err %v", err)
				return
			}

		}
	}()

	go func() {
		for {
			line, err := eReader.ReadString('\n')

			if line != "" {
				log.Printf("read err %s", line)
			}

			if err != nil || line == "" {
				log.Printf("read err %v", err)
				return
			}

		}
	}()

	err := cmd.Wait()
	if err != nil {
		log.Printf("cmd wait %v", err)
		return
	}
}

输出:

Run Test 4
2019/06/06 18:06:49 read line err EOF
2019/06/06 18:06:49 read err EOF
2019/06/06 18:06:49 read line test cmd output 5
2019/06/06 18:06:50 read line test cmd output 4
2019/06/06 18:06:51 read line err EOF
2019/06/06 18:06:51 read err EOF
2019/06/06 18:06:51 cmd wait signal: killed

持续输入指令,交互模式

有很多命令支持交互模式,我们进入之后就可以持续的输入一些命令,同时获取输出。如openssl命令。

下面我们需要进入交换模式,执行输入三个命令,并获取输出。

// 持续输入
func test5() {
	cmd := exec.Command("openssl")

	stdout, _ := cmd.StdoutPipe()
	stderr, _ := cmd.StderrPipe()

	stdin, _ := cmd.StdinPipe()

	cmd.Start()

	//
	var wg sync.WaitGroup
	wg.Add(3)
	go func() {
		defer wg.Done()
		for {
			buf := make([]byte, 1024)
			n, err := stderr.Read(buf)

			if n > 0 {
				fmt.Println(string(buf[:n]))
			}

			if n == 0 {
				break
			}

			if err != nil {
				log.Printf("read err %v", err)
				return
			}
		}
	}()

	go func() {
		defer wg.Done()
		for {
			buf := make([]byte, 1024)
			n, err := stdout.Read(buf)

			if n == 0 {
				break
			}

			if n > 0 {
				fmt.Println(string(buf[:n]))
			}

			if n == 0 {
				break
			}

			if err != nil {
				log.Printf("read out %v", err)
				return
			}

		}
	}()

	//
	go func() {
		stdin.Write([]byte("version\n\n"))
		stdin.Write([]byte("ciphers -v\n\n"))
		stdin.Write([]byte("s_client -connect razeencheng.com:443"))
		stdin.Close()
		wg.Done()
	}()

	wg.Wait()
	err := cmd.Wait()
	if err != nil {
		log.Printf("cmd wait %v", err)
		return
	}

}

这里,我们就用到了stdin标准输入了。输出如下:

Run Test 5
OpenSSL> LibreSSL 2.6.5
OpenSSL> OpenSSL>
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AESGCM(256) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AESGCM(256) Mac=AEAD
ECDHE-RSA-AES256-SHA384 TLSv1.2 Kx=ECDH     Au=RSA  Enc=AES(256)  Mac=SHA384
ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=AES(256)  Mac=SHA384
ECDHE-RSA-AES256-SHA    SSLv3 Kx=ECDH     Au=RSA  Enc=AES(256)  Mac=SHA1
ECDHE-ECDSA-AES256-SHA  SSLv3 Kx=ECDH     Au=ECDSA Enc=AES(256)  Mac=SHA1
DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH       Au=RSA  Enc=AESGCM(256) Mac=AEAD
DHE-RSA-AES256-SHA256   TLSv1.2 Kx=DH       Au=RSA  Enc=AES(256)  Mac=SHA256
DES-CBC-SHA             SSLv3 Kx=RSA      Au=RSA  Enc=DES(56)   Mac=SHA1
...
OpenSSL> OpenSSL>
4466583148:error:14004410:SSL routines:CONNECT_CR_SRVR_HELLO:sslv3 alert handshake failure:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.260.1/libressl-2.6/ssl/ssl_pkt.c:1205:SSL alert number 40
4466583148:error:140040E5:SSL routines:CONNECT_CR_SRVR_HELLO:ssl handshake failure:/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-22.260.1/libressl-2.6/ssl/ssl_pkt.c:585:

CONNECTED(00000005)
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 0 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Start Time: 1559815613
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

—END---

You can’t perform that action at this time.