コマンドライン引数を受け取る

RustBeginner
オンラインで実践に進む

はじめに

コマンドラインプログラムは、実行ファイル名の後に指定された値を受け取り、通常の結果を標準出力に書き込みます。エラーは標準エラー出力とゼロ以外の終了ステータスで通知します。これらのプロセス境界により、人とスクリプトは成功と失敗を区別できます。

小さなテキスト検索コマンドを完成させます。検索関数はパッケージのライブラリソースにすでに用意されています。そのため、各ステップでは引数の解析、ファイルの読み込み、該当結果がない場合の動作、そして最終的なプロセス間通信に集中できます。

コマンドライン引数を解析する

このステップでは、プロセスの引数から検索クエリとファイルパスを検証して取り出します。

プロジェクトは /home/labex/project/mini-search にあります。src/main.rs はコマンドラインバイナリで、src/lib.rs には後のステップで使う、あらかじめ用意された再利用可能な検索ロジックがあります。プロジェクトに移動して、バイナリのソースを開きます。

cd /home/labex/project/mini-search
nano src/main.rs

env::args() は、インデックス 0 の実行ファイルパスを含め、すべての引数を String として生成します。用意されている main は、それらをベクターに集めます。その後、&args をパラメーター args: &[String] に渡します。

&[String]String 値のスライスです。これは、ベクターの要素を読み取り専用で借用する範囲です。&str と同じ借用の考え方に基づいていますが、テキストのバイト列ではなく、String 要素の並びを参照します。run が要素を読み取っている間も、ベクターの所有権は main が保持します。

データの流れは次のとおりです。

shell words → env::args() → Vec<String> in main → borrowed &[String] in run

run 内のプレースホルダー Err(...) を、次のコードに置き換えます。

    if args.len() != 3 {
        return Err(String::from("usage: mini-search <query> <file>"));
    }
    let query = args[1].clone();
    let path = args[2].clone();
    Ok(vec![format!("Query: {query}"), format!("File: {path}")])

要素がちょうど 3 つある場合は、実行ファイル名と 2 つのユーザー引数が渡されています。引数の形が正しくない場合、明示的な return Err(...) によって run はすぐに終了します。これは 早期リターンです。関数の Lab で使った最終式による戻り値とは異なり、この後のコードは引数の数が正しい場合にだけ実行されます。

借用されたスライスをインデックスで参照すると、借用された文字列が得られます。2 回の clone は、クエリとパスだけを所有済みの値として複製するために意図的に使っています。これにより、run の残りの処理でローカルの所有済み値として扱えます。これは、ムーブエラーを一般的に解決する方法ではなく、意図した所有権の選択です。次に、vec![first, second] マクロで 2 要素のベクターを作成し、Ok がその 2 行の確認結果を一時的に返します。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。2 つの引数を指定してコマンドを実行します。

cargo run --quiet -- rust data/notes.txt

最初の --quiet は Cargo 自身のメッセージを減らします。独立した -- は、Cargo にオプションの解釈を停止させる区切りです。-- より後のすべての値は、プログラムに渡されます。

Query: rust
File: data/notes.txt

この結果から、引数が想定した位置に渡されたことを確認できます。

ファイルを読み込み、ライブラリのロジックを呼び出す

このステップでは、一時的な確認結果を実際のファイル読み込みと検索結果に置き換えます。

バイナリのソースを開きます。

nano src/main.rs

use std::env; の下に、次のインポートを追加します。

use std::fs;
use mini_search::find_lines;

パッケージ名 mini-search は、Rust のクレート名では mini_search になります。find_lines に付いている pub により、この関数をライブラリクレートの外部から利用できます。これをインポートすると、バイナリから src/lib.rs の公開関数を呼び出せます。検索ロジックをそこに置くことで、再利用可能なデータ処理と、プロセス固有の引数処理を分離できます。これは、後の modules Lab で詳しく学ぶライブラリとバイナリの構成、および可視性モデルの小さな例です。

一時的な Ok(vec![...]) の行を、次のコードに置き換えます。

    let contents = fs::read_to_string(&path)
        .map_err(|error| format!("could not read {path}: {error}"))?;
    Ok(find_lines(&query, &contents))

読み込みエラーには要求されたパスが含まれ、そのエラーは ? で呼び出し元に伝播します。読み込みに成功すると、バイナリはクエリとファイル内容を借用し、ライブラリは一致した行を所有済みの値として返します。

保存して終了し、確認してから実行します。

cargo check
cargo run --quiet -- rust data/notes.txt
Rust makes ownership explicit.
Cargo builds Rust packages.
Rust tools help beginners.

通常の検索結果は、main に用意された Ok アームによって標準出力に表示されます。

検索結果が空の場合に分かりやすいエラーを返す

このステップでは、結果が見つかった検索の成功と、検索自体は有効でも何も見つからなかった状態を区別します。

ソースを開きます。

nano src/main.rs

Ok(find_lines(&query, &contents)) を、次のコードに置き換えます。

    let matches = find_lines(&query, &contents);
    if matches.is_empty() {
        return Err(format!("no lines matched '{query}'"));
    }
    Ok(matches)

空のベクターでも結果がないことは確認できますが、その状態を説明する方がコマンドとして便利です。保存して終了し、存在しない検索語を試します。

cargo run --quiet -- python data/notes.txt
no lines matched 'python'

この中間段階では、用意されている Err アームはまだ標準出力にメッセージを表示し、正常終了します。次のステップで、エラーを正しいプロセス動作に変更します。

エラーを stderr に送り、ゼロ以外のステータスで終了する

このステップでは、通常の出力とエラーを分離して、コマンドラインの境界処理を完成させます。

標準出力(stdout)は、要求された結果を伝えます。標準エラー出力(stderr)は、診断メッセージを独立して伝えます。終了ステータスが 0 の場合は成功、0 以外の場合は失敗を表します。バイナリのソースを開きます。

nano src/main.rs

既存の標準ライブラリのインポートの下に、次のインポートを追加します。

use std::process;

1 行で書かれた Err(error) アームを、次のコードに置き換えます。

        Err(error) => {
            eprintln!("{error}");
            process::exit(1);
        }

eprintln! は 1 行を stderr に書き込みます。process::exit(1) は、ステータス 1 でプロセスを直ちに終了させます。保存して終了し、Cargo 自身のエラーメッセージを追加せずにバイナリを実行できるよう、まず一度ビルドします。

cargo build --quiet
./target/debug/mini-search python data/notes.txt

診断メッセージは次のとおりです。

no lines matched 'python'

直前に実行したコマンドのステータスをすぐに表示します。

echo $?

echo は引数を表示し、シェルは $? を直前に実行したコマンドの終了ステータスに展開します。

1

これで、無効な使い方や存在しないファイルにも同じ境界処理が適用されます。./target/debug/mini-search を引数なしで実行すると使用方法のメッセージが表示され、data/missing.txt のようなパスを指定すると読み込みエラーが表示されます。どちらも stderr にだけ書き込み、ステータス 1 で終了します。一方、一致する検索では結果を stdout に書き込み、ステータス 0 で終了します。

まとめ

コマンドライン引数を収集して検証し、Cargo の -- 区切り文字を使用しました。また、検索ロジックを用意されたライブラリ境界に分離し、指定されたファイルを読み込みました。さらに、成功時と 3 種類の失敗時について、stdout、stderr、終了ステータスを使った標準的な動作を完成させました。