Go os/exec使用方式實踐
os/exec 是 Go 提供的內(nèi)置包,可以用來執(zhí)行外部命令或程序。比如,我們的主機上安裝了 redis-server 二進制文件,那么就可以使用 os/exec 在 Go 程序中啟動 redis-server 提供服務。當然,我們也可以使用 os/exec 執(zhí)行 ls、pwd 等操作系統(tǒng)內(nèi)置命令。本文不求內(nèi)容多么深入,旨在帶大家極速入門 os/exec 的常規(guī)使用。
os/exec 包結構體與方法
func LookPath(file string) (string, error)
type Cmd
func Command(name string, arg ...string) *Cmd
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd
func (c *Cmd) CombinedOutput() ([]byte, error)
func (c *Cmd) Environ() []string
func (c *Cmd) Output() ([]byte, error)
func (c *Cmd) Run() error
func (c *Cmd) Start() error
func (c *Cmd) StderrPipe() (io.ReadCloser, error)
func (c *Cmd) StdinPipe() (io.WriteCloser, error)
func (c *Cmd) StdoutPipe() (io.ReadCloser, error)
func (c *Cmd) String() string
func (c *Cmd) Wait() errorCmd 結構體表示一個準備或正在執(zhí)行的外部命令。
- 調(diào)用函數(shù)
Command或CommandContext可以構造一個*Cmd對象。 - 調(diào)用
Run、Start、Output、CombinedOutput方法可以運行*Cmd對象所代表的命令。 - 調(diào)用
Environ方法可以獲取命令執(zhí)行時的環(huán)境變量。 - 調(diào)用
StdinPipe、StdoutPipe、StderrPipe方法用于獲取管道對象。 - 調(diào)用
Wait方法可以阻塞等待命令執(zhí)行完成。 - 調(diào)用
String方法返回命令的字符串形式。LookPath函數(shù)用于搜索可執(zhí)行文件。
使用方法
package main
import (
"log"
"os/exec"
)
func main() {
// 創(chuàng)建一個命令
cmd := exec.Command("echo", "Hello, World!")
// 執(zhí)行命令并等待命令完成
err := cmd.Run() // 執(zhí)行后控制臺不會有任何輸出
if err != nil {
log.Fatalf("Command failed: %v", err)
}
}exec.Command函數(shù)用于創(chuàng)建一個命令,函數(shù)第一個參數(shù)是命令的名稱,后面跟一個不定常參數(shù)作為這個命令的參數(shù),最終會傳遞給這個命令。*Cmd.Run方法會阻塞等待命令執(zhí)行完成,默認情況下命令執(zhí)行后控制臺不會有任何輸出:
# 執(zhí)行程序 $ go run main.go # 執(zhí)行完成后沒有任何輸出
可以在后臺運行一個命令:
func main() {
cmd := exec.Command("sleep", "3")
// 執(zhí)行命令(非阻塞,不會等待命令執(zhí)行完成)
if err := cmd.Start(); err != nil {
log.Fatalf("Command start failed: %v", err)
return
}
fmt.Println("Command running in the background...")
// 阻塞等待命令完成
if err := cmd.Wait(); err != nil {
log.Fatalf("Command wait failed: %v", err)
return
}
log.Println("Command finished")
}
實際上 Run 方法就等于 Start + Wait 方法,如下是 Run 方法源碼的實現(xiàn):
func (c *Cmd) Run() error {
if err := c.Start(); err != nil {
return err
}
return c.Wait()
}創(chuàng)建帶有 context 的命令
os/exec 還提供了一個 exec.CommandContext 構造函數(shù)可以創(chuàng)建一個帶有 context 的命令。那么我們就可以利用 context 的特性來控制命令的執(zhí)行了。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "sleep", "5")
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v\n", err) // signal: killed
}
}
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go 2025/01/14 23:54:20 Command failed: signal: killed exit status 1
當命令執(zhí)行超時會收到 killed 信號自動取消。
獲取命令的輸出
無論是調(diào)用 *Cmd.Run 還是 *Cmd.Start 方法,默認情況下執(zhí)行命令后控制臺不會得到任何輸出。
可以使用 *Cmd.Output 方法來執(zhí)行命令,以此來獲取命令的標準輸出:
func main() {
// 創(chuàng)建一個命令
cmd := exec.Command("echo", "Hello, World!")
// 執(zhí)行命令,并獲取命令的輸出,Output 內(nèi)部會調(diào)用 Run 方法
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // Hello, World!
}
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go Hello, World!
獲取組合的標準輸出和錯誤輸出
*Cmd.CombinedOutput 方法能夠在運行命令后,返回其組合的標準輸出和標準錯誤輸出:
func main() {
// 使用一個命令,既產(chǎn)生標準輸出,也產(chǎn)生標準錯誤輸出
cmd := exec.Command("sh", "-c", "echo 'This is stdout'; echo 'This is stderr' >&2")
// 獲取 標準輸出 + 標準錯誤輸出 組合內(nèi)容
output, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("Command execution failed: %v", err)
}
// 打印組合輸出
fmt.Printf("Combined Output:\n%s", string(output))
}
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go Combined Output: This is stdout This is stderr
設置標準輸出和錯誤輸出
可以利用 *Cmd 對象的 Stdout 和 Stderr 屬性,重定向標準輸出和標準錯誤輸出到當前進程:
func main() {
cmd := exec.Command("ls", "-l")
// 設置標準輸出和標準錯誤輸出到當前進程,執(zhí)行后可以在控制臺看到命令執(zhí)行的輸出
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
這樣,使用 *Cmd.Run 執(zhí)行命令后控制臺就能看到命令執(zhí)行的輸出了。
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go total 4824 -rw-r--r-- 1 jianghushinian staff 12 Jan 4 10:37 demo.log drwxr-xr-x 3 jianghushinian staff 96 Jan 13 09:41 examples -rwxr-xr-x 1 jianghushinian staff 2453778 Jan 1 15:09 main -rw-r--r-- 1 jianghushinian staff 6179 Jan 15 09:13 main.go
使用標準輸入傳遞數(shù)據(jù)
可以使用 grep 命令接收 stdin 的數(shù)據(jù),然后在其中搜索包含指定模式的文本行:
func main() {
cmd := exec.Command("grep", "hello")
// 通過標準輸入傳遞數(shù)據(jù)給命令
cmd.Stdin = bytes.NewBufferString("hello world!\nhi there\n")
// 獲取標準輸出
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
return
}
fmt.Println(string(output)) // hello world!
}可以將一個 io.Reader 對象賦值給 *Cmd.Stdin 屬性,來實現(xiàn)將數(shù)據(jù)通過 stdin 傳遞給外部命令。
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go hello world!
還可以將打開的文件描述符傳給 *Cmd.Stdin 屬性:
func main() {
file, err := os.Open("demo.log") // 打開一個文件
if err != nil {
log.Fatalf("Open file failed: %v\n", err)
return
}
defer file.Close()
cmd := exec.Command("cat")
cmd.Stdin = file // 將文件作為 cat 的標準輸入
cmd.Stdout = os.Stdout // 獲取標準輸出
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
只要是 io.Reader 對象即可。
設置和使用環(huán)境變量
*Cmd 的 Environ 方法可以獲取環(huán)境變量,Env 屬性則可以設置環(huán)境變量:
func main() {
cmd := exec.Command("printenv", "ENV_VAR")
log.Printf("ENV: %+v\n", cmd.Environ())
// 設置環(huán)境變量
cmd.Env = append(cmd.Environ(), "ENV_VAR=HelloWorld")
log.Printf("ENV: %+v\n", cmd.Environ())
// 獲取輸出
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // HelloWorld
}
這段代碼輸出結果與執(zhí)行環(huán)境相關,此處不演示執(zhí)行結果了,你可以自行嘗試。
不過最終的 output 輸出結果一定是 HelloWorld。
使用管道
os/exec 支持管道功能,*Cmd 對象提供的 StdinPipe、StdoutPipe、StderrPipe 三個方法用于獲取管道對象。故名思義,三者分別對應標準輸入、標準輸出、標準錯誤輸出的管道對象。
使用示例如下:
func main() {
// 命令中使用了管道
cmdEcho := exec.Command("echo", "hello world\nhi there")
outPipe, err := cmdEcho.StdoutPipe()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
// 注意,這里不能使用 Run 方法阻塞等待,應該使用非阻塞的 Start 方法
if err := cmdEcho.Start(); err != nil {
log.Fatalf("Command failed: %v", err)
}
cmdGrep := exec.Command("grep", "hello")
cmdGrep.Stdin = outPipe
output, err := cmdGrep.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // hello world
}首先創(chuàng)建一個用于執(zhí)行 echo 命令的 *Cmd 對象 cmdEcho,并調(diào)用它的 StdoutPipe 方法獲得標準輸出管道對象 outPipe;
然后調(diào)用 Start 方法非阻塞的方式執(zhí)行 echo 命令;
接著創(chuàng)建一個用于執(zhí)行 grep 命令的 *Cmd 對象 cmdGrep,將 cmdEcho 的標準輸出管道對象賦值給 cmdGrep.Stdin 作為標準輸入,這樣,兩個命令就通過管道串聯(lián)起來了;
最終通過 cmdGrep.Output 方法拿到 cmdGrep 命令的標準輸出。
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go hello world
使用bash -c執(zhí)行復雜命令
如果你不想使用 os/exec 提供的管道功能,那么在命令中直接使用管道符 |,也可以實現(xiàn)同樣功能。
不過此時就需要使用 sh -c 或者 bash -c 等 Shell 命令來解析執(zhí)行更復雜的命令了:
func main() {
// 命令中使用了管道
cmd := exec.Command("bash", "-c", "echo 'hello world\nhi there' | grep hello")
output, err := cmd.Output()
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output)) // hello world
}
這段代碼中的管道功能同樣生效。
指定工作目錄
可以通過指定 *Cmd 對象的的 Dir 屬性來指定工作目錄:
func main() {
cmd := exec.Command("cat", "demo.log")
cmd.Stdout = os.Stdout // 獲取標準輸出
cmd.Stderr = os.Stderr // 獲取錯誤輸出
// cmd.Dir = "/tmp" // 指定絕對目錄
cmd.Dir = "." // 指定相對目錄
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}捕獲退出狀態(tài)
上面講解了很多執(zhí)行命令相關操作,但其實還有一個很重要的點沒有講到,就是如何捕獲外部命令執(zhí)行后的退出狀態(tài)碼:
func main() {
// 查看一個不存在的目錄
cmd := exec.Command("ls", "/nonexistent")
// 運行命令
err := cmd.Run()
// 檢查退出狀態(tài)
var exitError *exec.ExitError
if errors.As(err, &exitError) {
log.Fatalf("Process PID: %d exit code: %d", exitError.Pid(), exitError.ExitCode()) // 打印 pid 和退出碼
}
}
這里執(zhí)行 ls 命令來查看一個不存在的目錄 /nonexistent,程序退出狀態(tài)碼必然不為 0。
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go 2025/01/15 23:31:44 Process PID: 78328 exit code: 1 exit status 1
搜索可執(zhí)行文件
最后要介紹的函數(shù)就只剩一個 LookPath 了,它用來搜索可執(zhí)行文件。
搜索一個存在的命令:
func main() {
path, err := exec.LookPath("ls")
if err != nil {
log.Fatal("installing ls is in your future")
}
fmt.Printf("ls is available at %s\n", path)
}
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go ls is available at /bin/ls
搜索一個不存在的命令:
func main() {
path, err := exec.LookPath("lsx")
if err != nil {
log.Fatal(err)
}
fmt.Printf("ls is available at %s\n", path)
}
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go 2025/01/15 23:37:45 exec: "lsx": executable file not found in $PATH exit status 1
功能練習
介紹完了 os/exec 常用的方法和函數(shù),我們現(xiàn)在來做一個小練習,使用 os/exec 來執(zhí)行外部命令 ls -l /var/log/*.log。
示例如下:
func main() {
cmd := exec.Command("ls", "-l", "/var/log/*.log")
output, err := cmd.CombinedOutput() // 獲取標準輸出和錯誤輸出
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output))
}
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go 2025/01/16 09:15:52 Command failed: exit status 1 exit status 1
執(zhí)行報錯了,這里的錯誤碼為 1,但錯誤信息并不明確。
這個報錯其實是因為 os/exec 默認不支持通配符參數(shù)導致的,exec.Command 不支持直接在參數(shù)中使用 Shell 通配符(如 *),因為它不會通過 Shell 來解析命令,而是直接調(diào)用底層的程序。
要解決這個問題,可以通過顯式調(diào)用 Shell(例如 bash 或 sh),讓 Shell 來解析通配符。
比如使用 bash -c 執(zhí)行通配符命令 ls -l /var/log/*.log:
func main() {
// 使用 bash -c 來解析通配符
cmd := exec.Command("bash", "-c", "ls -l /var/log/*.log")
output, err := cmd.CombinedOutput() // 獲取標準輸出和錯誤輸出
if err != nil {
log.Fatalf("Command failed: %v", err)
}
fmt.Println(string(output))
}
執(zhí)行示例代碼,得到輸出如下:
$ go run main.go -rw-r--r-- 1 root wheel 0 Oct 7 21:20 /var/log/alf.log -rw-r--r-- 1 root wheel 11936 Jan 13 11:36 /var/log/fsck_apfs.log -rw-r--r-- 1 root wheel 334 Jan 13 11:36 /var/log/fsck_apfs_error.log -rw-r--r-- 1 root wheel 19506 Jan 11 18:04 /var/log/fsck_hfs.log -rw-r--r--@ 1 root wheel 21015342 Jan 16 09:02 /var/log/install.log -rw-r--r-- 1 root wheel 1502 Nov 5 09:44 /var/log/shutdown_monitor.log -rw-r-----@ 1 root admin 3779 Jan 16 08:59 /var/log/system.log -rw-r----- 1 root admin 187332 Jan 16 09:05 /var/log/wifi.log
此外,我們還可以用 Go 標準庫提供的 filepath.Glob 來手動解析通配符:
func main() {
// 匹配通配符路徑
files, err := filepath.Glob("/var/log/*.log")
if err != nil {
log.Fatalf("Glob failed: %v", err)
}
if len(files) == 0 {
log.Println("No matching files found")
return
}
// 將匹配到的文件傳給 ls 命令
args := append([]string{"-l"}, files...)
cmd := exec.Command("ls", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
filepath.Glob 函數(shù)會返回模式匹配的文件名列表,如果不匹配則返回 nil。這樣,我們就可以先解析文件名列表,再交給 exec.Command 來執(zhí)行 ls 命令了。
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
利用systemd部署golang項目的實現(xiàn)方法
這篇文章主要介紹了利用systemd部署golang項目的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-11-11
go語言搬磚之go jmespath實現(xiàn)查詢json數(shù)據(jù)
這篇文章主要為大家介紹了go語言搬磚之go jmespath實現(xiàn)查詢json數(shù)據(jù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06

