如果想在 C++ 的程式裡面、呼叫外部的程式的話,一般傳統的做法,應該有使用 system()(參考)或 C 風格的 popen() 這兩種方法(參考)。
不過,以 system() 來說,他並不能處理外部程式的輸出與輸入,在某些情況下不算實用。
popen() 的話,則是不存在於 MSVC 的環境中,需要使用替代的 _popen()(MSDN)才行;同時,popen() 也僅有提供單向、不能雙向(只能寫或讀、不能同時讀寫);真要雙向的使用,其實相當地麻煩(參考)…
而 Boost.Process 則是一個還在開發中、也還沒正式進到 Boost C++ Libraries 的函式庫,他基本上就是希望可以提供一個跨平台、同時有更多功能的外部程序處理函式庫。
它的原始碼在:
目前版本標記是 alpha06、需要搭配支援 C++11 的編譯器、以及 Boost 1.62.0 使用(試過 1.61 不行);文件則是在 http://klemens-morgenstern.github.io/process/。
不過,Heresy 自己試過,是覺得目前的版本(不管是 alpha06 或是 develop 分支) ,似乎都有些問題。
以 alpha06 版來說,感覺有內部的語法直接有問題,導致整個程式無法編譯;而 develop 分支雖然可以運作,但是卻還是有不少功能似乎有問題…所以,個人是覺得,除非有必要,否則還是先不要用、等他穩定一點再說吧。
簡易使用
要使用 Boost.Process 需要引入 boost/process.hpp 這個 header 檔,之後的東西大多會在 boost::process 這個 namespace 下。
由於 Boost.Process 本身是 header-only 的,所以不需要編譯,不過因為他有用到 Boost.System,所以還是要建置 Boost C++ Libraries 才行。
至於要怎麼透過 Boost.Process 呼叫外部的程式呢?基本上有三種方法。
child
在使用上,他主要是提供了 child 這個類別,來處理外部的程序;他最簡單的寫法,大致上可以寫成:
#include <iostream> #include <boost/process.hpp> int main(int argc, char** argv) { boost::process::child c("ping 127.0.0.1"); c.wait(); std::cout << c.exit_code() << std::endl; }
在上面的例子裡面,他會去建立一個 child 的物件 c 去執行「ping 127.0.0.1」這個外部命令;之後則是透過 wait() 來確定他結束了,才會輸出這個外部程式回傳的結果(c.exit_code())、並輸出。
system()
而如果中間沒有打算對這個外部程式做特殊處理的話,其實也可以用 Boost.Process 提供的 system() 來簡化,其寫法如下:
#include <iostream> #include <boost/process.hpp> int main(int argc, char** argv) { std::cout << boost::process::system("ping 127.0.0.1") << std::endl; }
這個寫法和上面的是相同的。
至於和標準函式庫的 system() 看起來似乎一樣?不過實際上 Boost.Process 提供的 system() 還有許多其他的功能,這點就之後再提了。
spawn()
上面的程式寫法,基本上都是會等到外部的指令執行完成後,在結束程式。而如果希望把外部的程式執行起來後、就不管它的話,則就可以使用 spawn() 這個函式。
#include <iostream> #include <boost/process.hpp> int main(int argc, char** argv) { boost::process::spawn("notepad"); }
像是以上面的程式來說,在他把 notepad 開啟後,自己就會先結束了。
而如果把 spawn() 改成 system() 的話,則會等到 notepad 被使用者關閉、程式才會結束。
執行屬性
上面基本上都是最基本的用法。不過如果只能這樣用的話,那其實用 Boost.Process 的用處就不大了。
在 Heresy 來看,Boost.Process 最大的好處,就是提供了許多執行時的屬性(properties),可以用來做進一步的控制~這些屬性包括了:cmd、args、exe、shell、env、std_in 等等…完整的列表可以參考官方文件(連結),下面 Heresy 就選一些做說明。
執行參數(cmd、exe、args)
在前面的例子裡面,不管是 child、system() 或是 spawn(),都是把指令以單一字串的形式傳進去。
而實際上,Boost.Process 在執行外部程式的時候,在參數的傳遞上給了相當大的自由度。
比如說,以「ping 127.0.0.1」這個指令,直接以 system() 來呼叫的話,可以寫成:
namespace bp = boost::process; bp::system( "ping 127.0.0.1" );
而這邊其實可以把執行指令「ping」和執行參數「127.0.0.1」分開、寫成:
namespace bp = boost::process; bp::system("ping", "127.0.0.1");
但是 Heresy 自己在測試的時候,卻發現這樣的寫法會出現找不到指定檔案的錯誤…而要讓他可以運作的話,則需要使用完整的路徑,也就是:
namespace bp = boost::process; bp::system("C:\Windows\System32\PING.EXE", "127.0.0.1");
而除了直接以字串傳入指令之外,他其實也還有提供許多屬性(properties),在執行時可以做設定,包括了:cmd、args、exe、shell、env 等等…完整的列表可以參考官方文件(連結)。
比如說,上面的例子實際上就是:
bp::system(bp::exe="C:\Windows\System32\PING.EXE", bp::args="127.0.0.1");
而最初的寫法,也等同於:
bp::system(bp::cmd="ping 127.0.0.1");
如果要執行的參數不只一個的話,也可以有幾種寫法;下面幾個寫法的效果基本上都是相同的:
bp::system("ping 127.0.0.1 -n 1"); bp::system(bp::cmd="ping 127.0.0.1 -n 1"); bp::system("C:\Windows\System32\PING.EXE", "127.0.0.1", "-n", "1"); bp::system( bp::exe = "C:\Windows\System32\PING.EXE", bp::args = { "127.0.0.1", "-n", "1" });
不過,個人覺得比較討厭的,是「-n 1」這種用空格隔開的參數不能寫成一個字串、而得分成兩個,算是比較不好用的點了。
使用系統殼層執行(shell)
另外,如果想直接使用 DOS 內建的命令(例如「dir」)的話,直接寫
bp::system("dir");
應該也會出現錯誤。
而要解決的話,就是再加上 shell,讓 Boost.Process 透過系統的 shell 去執行,這樣就可以了~他的寫法就是:
bp::system("dir", bp::shell);
而使用系統 shell 的另一個好處,就是前面有的必須要寫出執行檔完整路徑的情況,也可以不用了!例如:
bp::system("ping", "127.0.0.1", "-n", "1", bp::shell); bp::system(bp::exe="ping", bp::shell, bp::args = {"127.0.0.1","-n","1"});
所以目前看來,在使用 Boost.Process 的時候,全部都加上 shell 應該會是個比較直覺的做法。
指定起始位置(start_dir)
再來,如果想要指定程式運作時的路徑的話,則可以透過 start_dir 這個屬性來指定,比如說:
bp::system("dir", bp::shell, bp::start_dir="d:\");
就可以透過 dir 的指令、列出 d: 下的檔案了。
輸入輸出(std_in、std_out、std_err)
前面最初就有提到,Heresy 之所以不用 popen(),一個主要的原因就是他要同時操作 std input 和 std output 時會很麻煩…
而 Boost.Process 在這部分,就算是相對方便了!他提供了 std_in、std_out、std_err 這三個屬性,來對應標準輸入輸出、以及錯誤的處理。
在使用時,std_out、std_err 都是透過「>」來指定重新導向的目標,而 std_in 則是透過「<」來指定來源;這邊的目標或來源,可以直接給檔案名稱、或是使用 Boost.Process 的 pipe、pstream 來做處理。
最簡單的例子,就是如果不希望外部程式的輸出顯示在 console 上的話,可以透過把 std_out 導到 null 來做;這樣的程式會是:
bp::system("dir", bp::shell, bp::std_out > bp::null);
如此一來,程式執行時就看不到 dir 的結果了。
而官方也提供了一個範例:
boost::filesystem::path log = "my_log_file.txt"; boost::filesystem::path input = "input.txt"; boost::filesystem::path output = "output.txt"; bp::system("my_prog", bp::std_out>output, bp::std_in<input, bp::std_err>log);
在這個範例裡,會把 input.txt 這個檔案的內容、透過標準輸入(std::cin)的方式送給程式(my_prog),而程式輸出到標準輸出(std::cout)的內容、則是會被寫到 output.txt 這個檔案;另外,輸出到標準錯誤(std::cerr)的內容則是會被寫到 my_log_file.txt 裡。
而如果希望把輸出都以字串的形式記錄下來的話,也可以寫成:
std::string line; std::vector<std::string> outline; namespace bp = boost::process; bp::ipstream ps; bp::child c("ping 127.0.0.1", bp::std_out > ps); while (c.running() && std::getline(ps, line) && !line.empty()) { outline.push_back(line); }
如此一來,ping 的輸出結果都會逐行儲存在 outline 裡面, 也就可以拿來做後續的分析了。
而如果同時要輸出輸入的話,則可以寫成類似下面的樣子:
namespace bp = boost::process; bp::opstream ops; bp::ipstream ips; bp::child c("prog", bp::std_out > ips, bp::std_in < ops); ops << "1"; std::string str; ips >> str; c.wait();
不過如果要和外部程式互動的話,實際上應該會有很多繁瑣的東西要處理就是了。
一般比較可能會用到的功能,大概就是這些了吧?
除了上面的功能之外,其實 child 本身也還有一些其他的函式可以使用;像是他實際上有提供 terminate() 這個函式,理論上應該可以用來中斷所建立的外部程式。但是很遺憾的是,在 Heresy 這邊測試都是失敗的…看來在 Windows 10 的環境下,他並沒有辦法把外部程式真的中斷掉。
另外,他實際上也支援 asynchronous 的操作,或是透過設定 on_eixt、來指定當外部程式結束後、要做什麼動作;在某些其況下,這樣的架構應該也算很實用的。
最後,他也有提供一個 boost::this_process,可以用來取得一些目前 process 的資訊;其中,也可以透過 environment() 這個函式、來取得環境變數;不過,由於 Windows 和 Linux 的環境變數定義差很多,所以如果是寫跨平台的程式,那這邊應該還是得分開處理了。
另外,這邊也補充一下。
根據《Where is Boost.Process?》 這篇,可以看到,實際上 Julio M. Merino Vidal 在之前也有試著開發過 Boost.Process 這個函式庫,他的語法和目前的差很多;而其版本最後應該是 0.5,之後就沒開發了。
不過考量到 Klemens D. Morgenstern 這個版本的 Boost.Process 在 GitHub 上的紀錄是從 0.5 開始、第一個 release 是 0.6,而且在文件上方的 Copyright 還是有保留 Julio M. Merino Vidal 等人的名字,所以應該可以把目前這個版本的 Boost.Process 視為之前版本的大改造吧?