エラーを標準エラーにリダイレクトする
現時点では、すべての出力を println! マクロを使ってターミナルに書き込んでいます。ほとんどのターミナルには 2 種類の出力があります。一般的な情報のための 標準出力 (stdout) と、エラーメッセージのための 標準エラー (stderr) です。この区別によって、ユーザーはプログラムの正常な出力をファイルに送る一方で、エラーメッセージは引き続き画面に表示することを選べます。
println! マクロは標準出力にしか出力できないため、標準エラーに出力するには別のものを使う必要があります。
エラーがどこに書き込まれているかを確認する
まず、minigrep が出力している内容が現在どのように標準出力に書き込まれているのかを確認しましょう。これには、本来は標準エラーに書き込みたいエラーメッセージも含まれています。これを確認するために、意図的にエラーを発生させつつ、標準出力ストリームをファイルにリダイレクトします。標準エラーストリームはリダイレクトしないので、標準エラーに送られた内容は引き続き画面に表示されます。
コマンドラインプログラムは、標準出力ストリームをファイルにリダイレクトした場合でも画面上でエラーメッセージを確認できるよう、エラーメッセージを標準エラーストリームに送ることが期待されます。現在の私たちのプログラムはそのように適切には振る舞っていません。エラーメッセージの出力を代わりにファイルへ保存してしまうことを、これから確認します。
この挙動を示すために、> と、標準出力ストリームのリダイレクト先にしたいファイルパス output.txt を指定してプログラムを実行します。引数は何も渡しません。これによりエラーが発生するはずです。
$ cargo run > output.txt
> という構文は、標準出力の内容を画面ではなく output.txt に書き込むようシェルに指示します。予想していたエラーメッセージは画面に表示されなかったので、これはそのメッセージがファイルに入ったことを意味します。output.txt の中身は次のとおりです。
Problem parsing arguments: not enough arguments
そのとおり、エラーメッセージは標準出力に出力されています。このようなエラーメッセージは、正常実行時のデータだけがファイルに入るように、標準エラーに出力されるほうがずっと有用です。これを変更しましょう。
エラーを標準エラーに出力する
リスト 12-24 のコードを使って、エラーメッセージの出力方法を変更します。この章の前半で行ったリファクタリングのおかげで、エラーメッセージを出力するコードはすべて 1 つの関数 main にあります。標準ライブラリには標準エラーストリームに出力する eprintln! マクロが用意されているので、エラーを出力するために println! を呼び出していた 2 か所を、eprintln! を使うように変更しましょう。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
それでは、先ほどと同じように、引数を渡さず、> で標準出力をリダイレクトして、もう一度プログラムを実行してみましょう。
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
今度は画面上にエラーが表示され、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 の機能、すなわちクロージャとイテレータを見ていきます。