將錯誤訊息寫入標準錯誤而非標準輸出

目前我們使用 println! 巨集來將所有的輸出顯示到終端機。大多數的終端機都提供兩種輸出方式:用於通用資訊的標準輸出(standard output, stdout以及用於錯誤訊息的標準錯誤(standard error, stderr。這樣的區別讓使用者可以選擇將程式的成功輸出導向到一個檔案中,並仍能在螢幕上顯示錯誤訊息。

println! 巨集只能夠印出標準輸出,所以我們得用其他方式來印出標準錯誤。

檢查該在哪裡寫錯誤

首先,我們先來觀察 minigrep 目前寫到標準輸出的顯示內容,其中包含任何我們想改寫成標準錯誤的錯誤訊息。我們會將標準輸出重新導向至一個檔案並故意產生一個錯誤。因為我們不會重新導向標準錯誤,所以任何傳送至標準錯誤的內容會繼續顯示在螢幕上。

命令列程式應該要傳送錯誤訊息至標準錯誤,讓我們可以在重新導向標準輸出至檔案時,仍能在螢幕上看到錯誤訊息。所以我們的程式目前並不符合預期:我們會看到它儲存錯誤訊息輸出到檔案中!

要觀察此行為的方式的話,我們要透過 > 來執行程式並加上檔案名稱 output.txt,這是我們要重新導向標準輸出到的地方。我們不會傳遞任何引數,這樣就應該會造成錯誤:

$ cargo run > output.txt

> 語法告訴 shell 要將標準輸出的內容寫入 output.txt 而不是顯示在螢幕上。我們沒有看到應顯示在螢幕上的錯誤訊息,這代表它一定跑到檔案中了。以下是 output.txt 包含的內容:

解析引數時出現問題:引數不足

是的,我們的錯誤訊息印到了標準輸出。像這樣的錯誤訊息印到標準錯誤會比較好,這樣才能只讓成功執行的資料存至檔案中。讓我們來修正吧。

將錯誤印出至標準錯誤

我們會使用範例 12-24 的程式碼來改變錯誤訊息印出的方式。由於我們在本章前幾篇的重構,所有印出錯誤訊息的程式碼都位於 main 函式中。標準函式庫有提供 eprintln! 巨集來印到標準錯誤,所以讓我們變更兩個原本呼叫 println! 來印出錯誤的段落來改使用 eprintln!

檔案名稱:src/main.rs

use std::env;
use std::process;

use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("解析引數時出現問題:{err}");
        process::exit(1);
    });

    if let Err(e) = minigrep::run(config) {
        eprintln!("應用程式錯誤:{e}");
        process::exit(1);
    }
}

範例 12-24:使用 eprintln! 來將錯誤訊息印至標準錯誤而非標準輸出

讓我們以相同方式再執行程式一次,沒有任何引數並用 > 重新導向標準輸出:

$ cargo run > output.txt
解析引數時出現問題:引數不足

現在我們看到錯誤顯示在螢幕上而且 output.txt 裡什麼也沒只有,這正是命令列程式所預期的行為。

讓我們加上不會產生錯誤的引數來執行程式,並仍重新導向標準輸出至檔案中,如以下所示:

$ cargo run -- to poem.txt > output.txt

我們在終端機不會看到任何輸出,而 output.txt 會包含我們的結果:

檔案名稱:output.txt

Are you nobody, too?
How dreary to be somebody!

這說明我們現在有對成功的輸出使用標準輸出,而且有妥善地將錯誤輸出傳至標準錯誤。

總結

本章節回顧了你目前所學的一些重要概念,並介紹了如何在 Rust 中進行常見的 I/O 操作。透過使用命令列引數、檔案、環境變數與用來印出錯誤的 eprintln! 巨集,你現在已經準備好能寫出命令列應用程式了。結合前幾章的概念,你的程式的組織架構會非常穩固、資料都能有效率地儲存至適當的資料結構、完善地處理錯誤,並通過測試檢驗。

接下來,我們要探討些 Rust 受到函式語言啟發的功能:閉包與疊代器。