在設計程式的時候偶爾會需要依靠系統指令或是其它的程式來取得一些相關的訊息,或是進行一些特殊的處理,如查看系統的網卡介面,或是呼叫FFmpeg來實現影音編碼的格式轉換。雖然這些工作基本上都可以靠Rust原生程式來自行實作,或是引用其它C/C++函式庫,但既然有已存在的指令和執行檔可以用,能省下許多開發時間,何不去用呢?



使用Rust去執行系統指令或是其它可執行檔案,可以用標準函式庫的process模組提供的Command結構體來實體化執行行程(process)的指令。Command結構實體提供的status方法可以執行指令,並在在執行指令之後,取得行程的Exit Status;output方法可以執行指令,並抓取標準輸出(stdout)和標準錯誤(stdout)的資料,當然也一樣能取得行程的Exit Status。

例如:

use std::process::Command;

fn main() {
    let mut command = Command::new("ping");
    command.arg("-c").arg("5").arg("magiclen.org");

    let output = command.output().unwrap();

    match output.status.code() {
        Some(code) => {
            println!("Exit Status: {}", code);

            if code == 0 {
                println!("{}", String::from_utf8(output.stdout).unwrap());
            }
        }
        None => {
            println!("Process terminated.");
        }
    }
}

以上指令,會建立出執行ping指令的Command結構實體。在Command結構實體執行指令時,指令所建立的行程有可能在執行的過程中被中止,此時Command結構實體返回的Exit Status就會是None

預設情況下,若是使用Command結構實體的output方法來執行指令,原先指令輸出到標準輸出和標準錯誤的資料就會被目前的Rust程式抓取,所以在螢幕上並不會直接看到ping指令的輸出資料。不過若是使用Command結構實體status方法來執行ping指令,ping指令的輸出資料就可以直接在螢幕上被看到。

Command結構實體的stdinstdoutstderr方法可以設定執行指令時產生的行程之標準輸入來源,以及標準輸出和標準錯誤的目的。來源和目的可以是一個File結構實體,或者是其它的透過Command結構實體建立出來的行程之標準輸入、標準輸出或是標準錯誤,就是管線(pipeline)啦!

以下是使用File結構實體當作標準輸出目的的例子:

use std::fs::File;
use std::process::Command;

fn main() {
    let mut command = Command::new("ping");
    command.arg("-c").arg("5").arg("magiclen.org");

    command.stdout(File::create("ping_result.txt").unwrap());

    match command.status().unwrap().code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}

另外還有三種比較特殊的來源或目的,可以靠process模組提供的Stdio結構體來設定。Stdio結構體的inherit關聯函數的回傳值可以使標準輸入、標準輸出或是標準錯誤就是基本的樣子,也就是會直接從螢幕輸入或是直接輸出到螢幕的那種;piped關聯函數的回傳值可以讓Rust程式抓取行程的標準輸出或是標準錯誤;null關聯函數的回傳值可以將行程的標準輸入、標準輸出或是標準錯誤的來源或目的設定為像是Linux作業系統上的/dev/null

以下程式利用nslookup指令確認網域是否可以被「正解」,但不輸出任何nslookup指令的資料:

use std::fs::File;
use std::process::{Command, Stdio};

fn main() {
    let mut command = Command::new("nslookup");
    command.arg("magiclen.org");

    command.stdout(Stdio::null());
    command.stderr(Stdio::null());

    match command.status().unwrap().code() {
        Some(code) => {
            if code == 0 {
                println!("This domain is available.");
            } else {
                println!("This domain is unavailable.");
            }
        }
        None => {
            println!("Process terminated.");
        }
    }
}

以下是將第一個Command結構實體的標準輸出導到第二個Command結構實體的標準輸入的例子:

use std::fs::File;
use std::process::{Command, Stdio};

fn main() {
    let mut command1 = Command::new("ping");
    command1.arg("-c").arg("5").arg("magiclen.org");

    let mut command2 = Command::new("grep");
    command2.arg("ttl");

    command1.stdout(Stdio::piped());

    command2.stdin(command1.spawn().unwrap().stdout.unwrap());

    let output = command2.output().unwrap();

    match output.status.code() {
        Some(code) => {
            println!("Exit Status: {}", code);

            if code == 0 {
                println!("{}", String::from_utf8(output.stdout).unwrap());
            }
        }
        None => {
            println!("Process terminated.");
        }
    }
}

以上程式中,有使用到Command結構實體的spawn方法,這個方法類似statusoutput方法,可以執行指令並產生行程,但它不會等待並返回結果,取而代之的是返回一個代表新行程的Child結構實體。在預設情況(即不使用Command結構實體的stdinstdoutstderr方法來設定行程的輸入來源和輸出目的)下,使用spawn執行指令,會等同於將來源或目的設定為Stdio::inherit()

筆者覺得Rust標準函式庫提供的Command結構體之API容易讓人混亂,不太容易閱讀,撰寫起來冗長也很容易出錯。再加上要產生一個Command結構實體實在很麻煩,幾乎不能夠用一行程式敘述來解決,所有指令的引數一定要分開提供,實在是難以閱讀及維護。還有就是雖然行程的輸出可以輕易地存成Vec<u8>,但輸入卻不能夠直接從Vec<u8>來提供,更別說是直接把任意有實作Read特性的物件當作行程的資料輸入來源了。

Execute

「Execute」是筆者開發的套件,用來擴展Command結構體的API,使它更容易使用。

Crates.io

Cargo.toml

execute = "*"

巨集的使用

execute這個crate提供了commandcommand_args這兩個方便的巨集,可以用來快速產生Command結構實體。

command!

command巨集可以直接傳入一個指令字串,不需將程式名稱和引數分開。這個指令字串,會在程式編譯階段被解析,不用擔心會對執行階段的效能造成影響。使用起來就像是在終端機打指令一樣,非常容易閱讀和維護。

fn main() {
    let mut command = execute::command!("ping -c 5 magiclen.org");

    match command.status().unwrap().code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}
command_args!

command_args巨集可以直接傳入一個程式路徑和多個分開的引數。程式路徑和每個引數可以是不同的型別。

fn main() {
    let mut command = execute::command_args!("ping", "-c", "5", "magiclen.org");

    match command.status().unwrap().code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}

commandshell函數

execute這個crate也提供了commandshell這兩個方便的函數,可以用來快速產生Command結構實體。

command

command巨集可以直接傳入一個指令字串,不需將程式名稱和引數分開。這個指令字串,會在程式執行階段被解析,所以可以用程式來組成指令,不過因為會對執行階段的效能造成影響,不是很建議使用這樣的方式來產生Command結構實體。

fn main() {
    let mut command = execute::command("ping -c 5 magiclen.org");

    match command.status().unwrap().code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}
shell

shell巨集可以直接傳入一個指令字串,不需將程式名稱和引數分開。這個指令字串,並不會被Rust程式解析,而是會透過作業系統環境預設的shell直譯器來解析並執行。

fn main() {
    let mut command = execute::shell("ping -c 5 magiclen.org");

    match command.status().unwrap().code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}

在Linux作業系統下,甚至可以直接使用|來進行管線處理。

fn main() {
    let mut command = execute::shell("ping -c 5 magiclen.org | grep ttl");

    match command.status().unwrap().code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}

不過如果很在乎程式執行效能的話,也是不建議用shell函數來產生Command結構實體。

Execute特性的使用

execute這個crate提供的Execute特性,可以擴充Command結構實體,使其擁有多個以execute為名稱前綴的方法。

驗證程式

由於Command結構體是用來產生一個行程來執行某個指令,也就是某個外部的程式。也就是說,這個程式可能不存在於當前的執行環境中,或者當前的執行環境中找到的這個程式,並不是我們的Rust程式所要的。因此,我們通常會需要在程式執行階段,開始正式做事情前,先去驗證這個外部程式,避免工作失敗。

execute_check_exit_status_code方法可以執行指令並且驗證行程回傳的Exit Status是不是我們預期的。

例如:

use execute::{command, Execute};

fn main() {
    if command!("ping -c 1 127.0.0.1").execute_check_exit_status_code(0).is_err() {
        println!("Is your `ping` correct?");
        return;
    }

    let mut command = execute::shell("ping -c 5 magiclen.org | grep ttl");

    match command.status().unwrap().code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}
執行程式並僅取得Exit Status
use execute::{command, Execute};

fn main() {
    match command!("ping -c 5 magiclen.org").execute().unwrap() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}
執行程式並讓行程輸出
輸出到螢幕
use execute::{command, Execute};

fn main() {
    let output = command!("ping -c 5 magiclen.org").execute_output().unwrap();

    match output.status.code() {
        Some(code) => {
            println!("Exit Status: {}", code);
        }
        None => {
            println!("Process terminated.");
        }
    }
}
輸出到記憶體(抓取)
use std::process::Stdio;

use execute::{command, Execute};

fn main() {
    let mut command = command!("ping -c 5 magiclen.org");

    command.stdout(Stdio::piped());
    command.stderr(Stdio::null());

    let output = command.execute_output().unwrap();

    match output.status.code() {
        Some(code) => {
            println!("Exit Status: {}", code);
            
            if code == 0 {
                println!("{}", String::from_utf8(output.stdout).unwrap());
            }
        }
        None => {
            println!("Process terminated.");
        }
    }
}
執行程式並輸入資料到行程
輸入記憶體中的資料
use std::process::Stdio;

use execute::{command, Execute};

fn main() {
    let mut command = command!("bc");

    command.stdout(Stdio::piped());

    let output = command.execute_input_output("2^99\n").unwrap();

    println!("Answer: {}", String::from_utf8(output.stdout).unwrap().trim_end());
}
輸入Reader中的資料
use std::fs::File;
use std::process::Stdio;

use execute::{command, Execute};

fn main() {
    let mut command = command!("cat");

    command.stdout(Stdio::piped());

    let mut file = File::open("Cargo.toml").unwrap();

    let output = command.execute_input_reader_output(&mut file).unwrap();

    println!("{}", String::from_utf8(output.stdout).unwrap());
}

以上程式只是為了舉例而舉例,實際上要用檔案作為標準輸入的話可以直接將File結構實體傳給Command結構實體的stdin方法。

在預設的情況下,讀取Reader使用的緩衝空間大小為256個位元組。如果想要更改緩衝空間大小,就要使用以_reader2或是_reader_output2為名稱結尾的方法,並明確定義要使用的大小。

例如,將緩衝空間大小改為4096個位元組:

use std::fs::File;
use std::process::Stdio;

use execute::generic_array::typenum::U4096;
use execute::{command, Execute};

fn main() {
    let mut command = command!("cat");

    command.stdout(Stdio::piped());

    let mut file = File::open("Cargo.toml").unwrap();

    let output = command.execute_input_reader_output2::<U4096>(&mut file).unwrap();

    println!("{}", String::from_utf8(output.stdout).unwrap());
}
執行多個Command結構實體,將它們的標準輸入和標準輸出透過管線來引導

execute_multipl為名稱前綴的方法可以執行多個Command結構實體,並將它們的標準輸入和標準輸出依序連接在一起。

use std::process::Stdio;

use execute::{command, Execute};

fn main() {
    let mut command1 = command!("echo 'HELLO WORLD'");
    let mut command2 = command!("cut -d ' ' -f 1");
    let mut command3 = command!("tr A-Z a-z");

    command3.stdout(Stdio::piped());

    let output = command1.execute_multiple_output(&mut [&mut command2, &mut command3]).unwrap();

    assert_eq!(b"hello\n", output.stdout.as_slice());
}