Go标准库os/exec使用指南
點擊上方“朱小廝的博客”,選擇“設為星標”
回復”1024“獲取獨家整理的學習資料
歡迎跳轉到本文的原文鏈接:https://honeypps.com/golang/go-standard-lib-os-exec-guide/
?
有時我們在寫程序的時候會需要調用系統的某個命令來完成一些任務。Go語言os/exec標準庫就提供這種調用外部命令的功能。
如下面的代碼調用ls命令來查看指定目錄下面的文件:
import?("os""os/exec" )func?ls(path?string)?error?{cmd?:=?exec.Command("ls",?path)cmd.Stdout?=?os.Stdoutreturn?cmd.Run() }func?main()?{if?err?:=?ls("/");?err?!=?nil?{panic(err)} }再舉一個例子,將小寫轉成大寫:
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.Buffercmd.Stdout?=?&outif?err?:=?cmd.Run();?err?!=?nil?{log.Fatal(err)}fmt.Printf("in?all?caps:?%q\n",?out.String()) }概述
golang下的os/exec包執行外部命令,它將os.StartProcess進行包裝使得它更容易映射到stdin/stdout、管道I/O。
與C語言或者其他語言中的“系統”庫調用不同,os/exec包并不調用系統shell,也不展開任何glob模式,也不處理通常由shell完成的其他擴展、管道或重定向。這個包的行為更像C語言的“exec”函數家族。要展開glob模式,可以直接調用shell,注意避免任何危險的輸入,或者使用path/filepath包的glob函數。如果要展開環境變量,請使用package os的ExpandEnv。
所謂的 glob 模式是指 shell 所使用的簡化了的正則表達式。星號(*)匹配零個或多個任意字符;[abc]匹配任何一個列在方括號中的字符(這個例子要么匹配一個 a,要么匹配一個 b,要么匹配一個 c);問號(?)只匹配一個任意字符;如果在方括號中使用短劃線分隔兩個字符,表示所有在這兩個字符范圍內的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的數字)。
注意,這個包中的示例假設是Unix系統。它們不能在Windows上運行,也不能在golang.org和godoc.org使用的Go Playground上運行。
相關函數定義如下:
Variablesfunc?LookPath(file?string)?(string,?error) type?Cmd//方法返回一個*Cmd,?用于執行name指定的程序(攜帶arg參數)func?Command(name?string,?arg?...string)?*Cmd?//func?CommandContext(ctx?context.Context,?name?string,?arg?...string)?*Cmd//執行Cmd中包含的命令,并返回標準輸出與標準錯誤合并后的切片func?(c?*Cmd)?CombinedOutput()?([]byte,?error)//執行Cmd中包含的命令,并返回標準輸出的切片func?(c?*Cmd)?Output()?([]byte,?error)//執行Cmd中包含的命令,阻塞直到命令執行完成func?(c?*Cmd)?Run()?error//執行Cmd中包含的命令,該方法立即返回,并不等待命令執行完成func?(c?*Cmd)?Start()?error//返回一個管道,該管道會在Cmd中的命令被啟動后連接到其標準輸入func?(c?*Cmd)?StderrPipe()?(io.ReadCloser,?error)//返回一個管道,該管道會在Cmd中的命令被啟動后連接到其標準輸出func?(c?*Cmd)?StdinPipe()?(io.WriteCloser,?error)//返回一個管道,該管道會在Cmd中的命令被啟動后連接到其標準錯誤func?(c?*Cmd)?StdoutPipe()?(io.ReadCloser,?error)//func?(c?*Cmd)?String()?string//該方法會阻塞直到Cmd中的命令執行完成,但該命令必須是被Start方法開始執行的func?(c?*Cmd)?Wait()?error type?Errorfunc?(e?*Error)?Error()?stringfunc?(e?*Error)?Unwrap()?error type?ExitErrorfunc?(e?*ExitError)?Error()?string各個函數詳解
func LookPath
函數定義:
func?LookPath(file?string)?(string,?error)在環境變量PATH指定的目錄中搜索可執行文件,如過file中有文件分隔符(斜杠),則只在當前目錄搜索。即:默認在系統的環境變量里查找給定的可執行命令文件,如果查找到返回路徑,否則報錯??是PATH$。可提供相對路徑下進行查找,并返回相對路徑。
示例:
import?("fmt""os/exec" )func?main()?{f,?err?:=?exec.LookPath("ls")if?err?!=?nil?{fmt.Println(err)}fmt.Println(f)?//輸出?/bin/ls }struct Cmd
Cmd代表一個正在準備或者在執行中的外部命令。
type?Cmd?struct?{// Path是將要執行的命令的路徑。//?該字段不能為空,如為相對路徑會相對于Dir字段。Path?string// Args保管命令的參數,包括命令名作為第一個參數;如果為空切片或者nil,相當于無參數命令。//?典型用法下,Path和Args都應被Command函數設定。Args?[]string// Env指定進程的環境,如為nil,則是在當前進程的環境下執行。Env?[]string// Dir指定命令的工作目錄。如為空字符串,會在調用者的進程當前目錄下執行。Dir?string//?Stdin指定進程的標準輸入,如為nil,進程會從空設備讀取(os.DevNull)Stdin?io.Reader// Stdout和Stderr指定進程的標準輸出和標準錯誤輸出。//?如果任一個為nil,Run方法會將對應的文件描述符關聯到空設備(os.DevNull)//?如果兩個字段相同,同一時間最多有一個線程可以寫入。Stdout?io.WriterStderr?io.Writer// ExtraFiles指定額外被新進程繼承的已打開文件流,不包括標準輸入、標準輸出、標準錯誤輸出。//?如果本字段非nil,entry i會變成文件描述符3+i。////?BUG:?在OS X 10.6系統中,子進程可能會繼承不期望的文件描述符。//?http://golang.org/issue/2603ExtraFiles?[]*os.File// SysProcAttr保管可選的、各操作系統特定的sys執行屬性。// Run方法會將它作為os.ProcAttr的Sys字段傳遞給os.StartProcess函數。SysProcAttr?*syscall.SysProcAttr// Process是底層的,只執行一次的進程。Process?*os.Process// ProcessState包含一個已經存在的進程的信息,只有在調用Wait或Run后才可用。ProcessState?*os.ProcessState//?下面是隱藏或非導出字段ctx?????????????context.Context?//?nil?means?nonelookPathErr?????error???????????//?LookPath?error,?if?any.finished????????bool????????????//?when?Wait?was?calledchildFiles??????[]*os.FilecloseAfterStart?[]io.ClosercloseAfterWait??[]io.Closergoroutine???????[]func()?errorerrch???????????chan?error?//?one?send?per?goroutinewaitDone????????chan?struct{} }注:exec在執行調用系統命令時,會先對需要執行的操作進行一次封裝,然后在執行。封裝后的命令對象具有以上struct屬性。而封裝方式即使用下邊的Command函數。
func Command
函數定義:
func?Command(name?string,?arg?...string)?*Cmd函數返回一個*Cmd,用于使用給出的參數執行name指定的程序。返回值只設定了Path和Args兩個參數。
如果name不含路徑分隔符(如果不是相對路徑),將使用LookPath獲取完整路徑(就是用默認的全局變量路徑);否則直接使用name。參數arg不應包含命令名。
示例:
func?main()?{cmd?:=?exec.Command("go",?"version")fmt.Println(cmd.Path,?cmd.Args)//輸出:?/usr/local/go/bin/go [go version] }注:在調用命令執行封裝時,如果不提供相對路徑,系統會使用LookPath獲取完整路徑;即這里可以給一個相對路徑。
以上操作只會將命令進行封裝,相當于告訴系統將進行哪些操作,但是執行時無法獲取相關信息。
func Run
函數定義:
func?(c?*Cmd)?Run()?errorRun執行c包含的命令,并阻塞直到完成。
如果命令成功執行,stdin、stdout、stderr的轉交沒有問題,并且返回狀態碼為0,方法的返回值為nil(執行Run函數的返回狀態,正確執行Run函數,并不代表正確執行了命令);如果函數沒有執行或者執行失敗,會返回*ExitError類型的錯誤;否則返回的error可能是表示I/O問題。
即:該命令只會執行且阻塞到執行結束,如果執行函數有錯則返回報錯信息,沒錯則返回nil,并不會返回執行結果。
func Start
函數定義:
func?(c?*Cmd)?Start()?errorStart開始執行c包含的命令,但并不會等待該命令完成即返回。
func Wait
函數定義:
func?(c?*Cmd)?Wait()?errorWait會阻塞直到該命令執行完成,該命令必須是被Start方法開始執行的。
如果命令成功執行,stdin、stdout、stderr的轉交沒有問題,并且返回狀態碼為0,方法的返回值為nil;如果命令沒有執行或者執行失敗,會返回*ExitError類型的錯誤;否則返回的error,可能是表示I/O問題。Wait方法會在命令返回后釋放相關的資源。
Start和Run的區別
示例:
func?main()?{cmd?:=?exec.Command("sleep",?"5")//?如果用Run,?執行到該步會阻塞等待5s//err?:=?cmd.Run()err?:=?cmd.Start()if?err?!=?nil?{fmt.Println(err)}fmt.Printf("Waiting?for?command?to?finish...")//?Start,?上面的內容會先輸出,然后這里會阻塞等待5serr?=?cmd.Wait()fmt.Printf("Command?finished?with?error:?%v",?err) }注:一個命令只能使用Start()或者Run()中的一個啟動命令,不能兩個同時使用。
func CombinedOutput
函數定義:
func?(c?*Cmd)?CombinedOutput()?([]byte,?error)執行Cmd中包含的命令,并返回標準輸出與標準錯誤合并后的切片。
示例:
func?main()?{cmd?:=?exec.Command("ls",?"-a",?"-l")out,?err?:=?cmd.CombinedOutput()if?err?!=?nil?{fmt.Println(err)}fmt.Println(string(out)) }func Output
函數定義:
func?(c?*Cmd)?Output()?([]byte,?error)執行Cmd中包含的命令,并返回標準輸出的切片。
示例:
import?("fmt""os/exec" )func?main()?{cmd?:=?exec.Command("ls",?"-a",?"-l")out,?err?:=?cmd.Output()if?err?!=?nil?{fmt.Println(err)}fmt.Println(string(out)) }注:Output()和CombinedOutput()不能夠同時使用,因為command的標準輸出只能有一個,同時使用的話便會定義了兩個,便會報錯。
我們還可以通過指定一個對象連接到對應的管道進行傳輸參數(stdinpipe),獲取輸出(stdoutpipe),獲取錯誤(stderrpipe)
func StdinPipe
函數定義:
func?(c?*Cmd)?StdinPipe()?(io.WriteCloser,?error)err 返回的是執行函數時的錯誤。StdinPipe方法返回一個在命令Start后與命令標準輸入關聯的管道。當命令退出時,Wait方法將關閉這個管道。必要時調用者可以調用Close方法來強行關閉管道,例如命令在輸入關閉后才會執行返回時需要顯式關閉管道。
示例:
func?main()?{cmd?:=?exec.Command("cat")stdin,?err?:=?cmd.StdinPipe()if?err?!=?nil?{fmt.Println(err)}_,?err?=?stdin.Write([]byte("tmp.txt"))if?err?!=?nil?{fmt.Println(err)}stdin.Close()cmd.Stdout?=?os.Stdoutcmd.Start() }func StdoutPipe
函數定義:
func?(c?*Cmd)?StdoutPipe()?(io.ReadCloser,?error)StdoutPipe方法返回一個在命令Start后與命令標準輸出關聯的管道。當命令退出時,Wait方法將關閉這個管道。但是在從管道讀取完全部數據之前調用Wait是錯誤的;同樣使用StdoutPipe方法時調用Run函數也是錯誤的。
示例:
func?main()?{//執行系統命令,?第一個參數是命令名稱,?后面參數可以有多個,命令參數cmd?:=?exec.Command("ls",?"-a",?"-l")//獲取輸出對象,可以從該對象中讀取輸出結果stdout,?err?:=?cmd.StdoutPipe()if?err?!=?nil?{fmt.Println(err)}//保證關閉輸出流defer?stdout.Close()//運行命令if?err?:=?cmd.Start();?err?!=?nil?{fmt.Println(err)}//讀取輸出結果content,?err?:=?ioutil.ReadAll(stdout)if?err?!=?nil?{fmt.Println(err)}fmt.Println(string(content)) }輸出重定向到文件:
import?("fmt""os""os/exec" )func?main()?{cmd?:=?exec.Command("ls",?"-a",?"-l")//重定向標準輸出到文件stdout,?err?:=?os.OpenFile("stdout.log",?os.O_CREATE|os.O_WRONLY,?0600)if?err?!=?nil?{fmt.Println(err)}defer?stdout.Close()cmd.Stdout?=?stdout//執行命令if?err?:=?cmd.Start();?err?!=?nil?{fmt.Println(err)} }func StderrPipe
函數定義:
func?(c?*Cmd)?StderrPipe()?(io.ReadCloser,?error)StderrPipe方法返回一個在命令Start后與命令標準錯誤輸出關聯的管道。當命令退出時,Wait方法將關閉這個管道,一般不需要顯式的關閉該管道。但是在從管道讀取完全部數據之前調用Wait是錯誤的;同樣使用StderrPipe方法時調用Run函數也是錯誤的。
示例:
func?main()?{cmd?:=?exec.Command("mv",?"hello")i,?err?:=?cmd.StderrPipe()if?err?!=?nil?{fmt.Printf("Error:%s\n",?err)}b,?_?:=?ioutil.ReadAll(i)if?err?:=?cmd.Wait();?err?!=?nil?{fmt.Printf("Error:?%s\n",?err)}fmt.Println(string(b)) }應用示例
項目中需要執行shell命令,雖然exec包提供了CombinedOutput()方法,在shell運行結束會返回shell執行的輸出,但是用戶在發起一次任務時,可能在不停的刷新log,想達到同步查看log的目的,但是CombinedOutput()方法只能在命令完全執行結束才返回整個shell的輸出,所以肯定達不到效果,所以,需要尋找其它方法達到命令一邊執行log一邊輸出的目的。
使用重定向
如果你的shell比較簡單,并且log的文件路徑也很容易確定,那么直接對shell執行的命令添加重定向最簡單不過了,程序參考如下:
import?("fmt""os/exec" )func?main()?{cmdStr?:=?` #!/bin/bash for?var?in?{1..10} dosleep?1echo?"Hello,?Welcome?${var}?times?" done`cmd?:=?exec.Command("bash",?"-c",?cmdStr+"?>>?file.log")if?err?:=?cmd.Start();?err?!=?nil?{fmt.Println(err)}if?err?:=?cmd.Wait();?err?!=?nil?{fmt.Println(err)} }上面程序定義了一個每秒1次的shell,但是在shell執行前,對shell進行了拼接,使用了重定向,所以我們可以在另外一個終端中實時的看到 log 的變化
指定shell執行時的輸出
使用exec.Command創建一個Shell后,就具有了兩個變量:Stdout io.Writer和Stderr io.Writer。
這兩個變量是用來指定程序的標準輸出和標準錯誤輸出的位置,所以我們就利用這兩個變量,直接打開文件,然后將打開的文件指針賦值給這兩個變量即可將程序的輸出直接輸出到文件中,也能達到相同的效果,參考程序如下:
import?("fmt""os""os/exec" )func?main()?{cmdStr?:=?` #!/bin/bash for?var?in?{1..10} dosleep?1echo?"Hello,?Welcome?${var}?times?" done`cmd?:=?exec.Command("bash",?"-c",?cmdStr)file,?_?:=?os.OpenFile("file.log",?os.O_RDWR|os.O_CREATE|os.O_APPEND,?0666)defer?file.Close()//指定輸出位置cmd.Stderr?=?filecmd.Stdout?=?fileif?err?:=?cmd.Start();?err?!=?nil?{fmt.Println(err)}if?err?:=?cmd.Wait();?err?!=?nil?{fmt.Println(err)} }從shell執行結果的管道中獲取輸出
我們可以通過管道獲取命令執行過程中的輸出,參考程序如下:
import?("fmt""io""os""os/exec""strings""time" )//通過管道同步獲取日志的函數 func?syncLog(reader?io.ReadCloser)?{file,?_?:=?os.OpenFile("file.log",?os.O_RDWR|os.O_CREATE|os.O_APPEND,?0666)defer?file.Close()buf?:=?make([]byte,?1024,?1024)for?{strNum,?err?:=?reader.Read(buf)if?strNum?>?0?{outputByte?:=?buf[:strNum]file.WriteString(string(outputByte))}if?err?!=?nil?{//讀到結尾if?err?==?io.EOF?||?strings.Contains(err.Error(),?"file?already?closed")?{err?=?nil}?}} }func?main()?{cmdStr?:=?` #!/bin/bash for?var?in?{1..10} dosleep?1echo?"Hello,?Welcome?${var}?times?" done`cmd?:=?exec.Command("bash",?"-c",?cmdStr)//這里得到標準輸出和標準錯誤輸出的兩個管道,此處獲取了錯誤處理cmdStdoutPipe,?_?:=?cmd.StdoutPipe()cmdStderrPipe,?_?:=?cmd.StderrPipe()if?err?:=?cmd.Start();?err?!=?nil?{fmt.Println(err)}go?syncLog(cmdStdoutPipe)go?syncLog(cmdStderrPipe)if?err?:=?cmd.Wait();?err?!=?nil?{fmt.Println(err)}time.Sleep(time.Second?*?5) }擴展 - 解決log格式亂的問題
上面第三種方式,我們直接是通過打開文件,然后將讀取到的程序輸出寫入文件,但是實際上可能別人又已經封裝好了一個logger,讓你往logger里面寫。比如,我這里就使用log包提供的log然后將程序的執行結果寫入,但是因為使用了log包,所以寫入的格式和log本身的格式造成格式會有部分的錯亂,參考我的解決辦法,解釋都在注釋里,如下:
import?("fmt""io""log""os""os/exec""strings" )//通過管道同步獲取日志的函數 func?syncLog(logger?*log.Logger,?reader?io.ReadCloser)?{//因為logger的print方法會自動添加一個換行,所以我們需要一個cache暫存不滿一行的logcache?:=?""buf?:=?make([]byte,?1024,?1024)for?{strNum,?err?:=?reader.Read(buf)if?strNum?>?0?{outputByte?:=?buf[:strNum]outputSlice?:=?strings.Split(string(outputByte),?"\n")logText?:=?strings.Join(outputSlice[:len(outputSlice)-1],?"\n")logger.Printf("%s%s",?cache,?logText)cache?=?outputSlice[len(outputSlice)-1]}if?err?!=?nil?{//讀到結尾if?err?==?io.EOF?||?strings.Contains(err.Error(),?"file?already?closed")?{err?=?nil}}} }func?main()?{cmdStr?:=?` #!/bin/bash for?var?in?{1..10} dosleep?1echo?"Hello,?Welcome?${var}?times?" done`cmd?:=?exec.Command("bash",?"-c",?cmdStr)//這里得到標準輸出和標準錯誤輸出的兩個管道,此處獲取了錯誤處理cmdStdoutPipe,?_?:=?cmd.StdoutPipe()cmdStderrPipe,?_?:=?cmd.StderrPipe()if?err?:=?cmd.Start();?err?!=?nil?{fmt.Println(err)}//打開一個文件,用作log封裝輸出file,?_?:=?os.OpenFile("file.log",?os.O_RDWR|os.O_CREATE|os.O_APPEND,?0666)defer?file.Close()//創建封裝的log,第三個參數設置log輸出的格式logger?:=?log.New(file,?"",?log.LstdFlags)logger.Print("start?print?log:")oldFlags?:=?logger.Flags()//為了保證shell的輸出和標準的log格式不沖突,并且為了整齊,關閉logger自身的格式logger.SetFlags(0)go?syncLog(logger,?cmdStdoutPipe)go?syncLog(logger,?cmdStderrPipe)err?:=?cmd.Wait()//執行完后再打開log輸出的格式logger.SetFlags(oldFlags)logger.Print("log?print?done")if?err?!=?nil?{fmt.Println(err)} }程序執行結果如下:
2019/08/11?21:30:51?start?print?log: Hello,?Welcome?1?times? Hello,?Welcome?2?times? Hello,?Welcome?3?times? Hello,?Welcome?4?times? Hello,?Welcome?5?times? Hello,?Welcome?6?times? Hello,?Welcome?7?times? Hello,?Welcome?8?times? Hello,?Welcome?9?times? Hello,?Welcome?10?times? 2019/08/11?21:31:01?log?print?done參考資料及衍生讀物
Go基礎篇5:內置模塊
golang os/exec 執行外部命令
https://gowalker.org/os/exec
[譯]使用os/exec執行命令 GO語言
golang os/exec包用法之Kill進程及其子進程
?
歡迎跳轉到本文的原文鏈接:https://honeypps.com/golang/go-standard-lib-os-exec-guide/
想知道更多?掃描下面的二維碼關注我
精彩推薦:
-
《遲來的干貨|Kafka權限管理實戰》
-
《Go命令行Cobra的使用》
-
《史上最難的10道Java面試題》
-
《7102-2019年技術文全套整理,建議收藏》
?
?
加技術群入口(備注:技術):>>>Learn More<<
免費資料入口(備注:1024):>>>Learn More<<
免費星球入口:>>>Free<<<
?點個"在看"唄^_^
總結
以上是生活随笔為你收集整理的Go标准库os/exec使用指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HR 的那些黑话大全,太扎心了!(漫画)
- 下一篇: 干货 | SpringBoot注解大全,