ファイルの読み書き

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

はじめに

実用的なプログラムの多くはファイルシステムとの境界をまたぎます。既存のデータを読み取り、処理し、結果を保存するためです。これらの操作は失敗する可能性があるため、Rust のファイル API は成功が保証されているかのように扱わず、Result 値を返します。

この実験では、用意されたサービスログを確認し、Path でファイルの場所を表し、std::fs でテキストを読み取り、小さな要約を作成します。その後、別のファイルに書き込み、? を使って I/O エラーを伝播させます。

Path を使ってテキストファイルを読み取る

このステップでは、用意されたログを確認し、Rust の String に読み込みます。

プロジェクトは /home/labex/project/log-summary にあります。入力ファイルとして data/events.log、後でレポートを保存する空の output ディレクトリ、プログラムのソースである src/main.rs が含まれています。プロジェクトに移動します。

cd /home/labex/project/log-summary

nano で入力ファイルを開き、プログラムが読み取る内容を確認します。

nano data/events.log

ファイルには短いレコードが 3 つ含まれています。内容は変更せず、Ctrl+X を押して終了します。続けて Rust のソースを開きます。

nano src/main.rs

用意されている 2 行の use によって、std::fs のファイル関数と Path 型を短い名前で使えるようになっています。これはインポートの準備であり、この実験の主題ではありません。後のモジュールの実験で、パスと use 宣言について詳しく説明します。戻り値の型 std::io::Result<()> は、main が空の成功値 () で完了するか、I/O エラーを返すことを示します。最後の Ok(()) が成功時の結果です。

Step 1 のコメントを次のコードに置き換えます。

    let input_path = Path::new("data/events.log");
    let contents = fs::read_to_string(input_path)?;
    println!("Input: {}", input_path.display());
    println!("Contents:\n{}", contents.trim_end());

Path::new は、ファイルを開かずに借用されたファイルシステムパスのビューを作成します。相対パスは、Cargo を実行するプロジェクトディレクトリを基準に解決されます。fs::read_to_string はファイルを開き、UTF-8 テキストを所有権付きの String に読み込み、Result を返します。成功すると ? は String を取り出し、失敗すると I/O エラーを main から返します。display() は人間が読みやすい形式でパスを表示し、trim_end() はファイル末尾の改行によって出力に余分な空行が入るのを防ぎます。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。確認して実行します。

cargo check
cargo run --quiet
Input: data/events.log
Contents:
INFO service started
WARN disk nearing limit
INFO backup complete

表示されたパスと 3 つすべてのレコードによって、意図したファイルを読み取れたことを確認できます。

読み込んだテキストのレコード数を数える

このステップでは、メモリ上に保持されている String の論理行数を数えます。

lines() メソッドは、元の文字列を変更せずにテキスト行のイテレーターを作成します。そのイテレーターに対する count() は、イテレーターが生成した項目数を返します。ソースを開きます。

nano src/main.rs

Step 2 のコメントを次のコードに置き換えます。

    let line_count = contents.lines().count();
    println!("Line count: {line_count}");

保存して終了し、実行します。

cargo run --quiet

最後に次の行が表示されます。

Line count: 3

lines() は改行バイトの数ではなく論理レコードを識別するため、入力の末尾に改行があるかどうかに関係なく動作します。

安定した要約文字列を作成する

このステップでは、警告レコード数を数え、2 つの数値を保存できるテキスト形式に整えます。

文字列の starts_with メソッドは、指定したプレフィックスで始まるかどうかを判定します。これを、すでに学んだイテレーターアダプターと組み合わせると、WARN で始まる行だけを数えられます。ソースを開きます。

nano src/main.rs

Step 3 のコメントを次のコードに置き換えます。

    let warning_count = contents
        .lines()
        .filter(|line| line.starts_with("WARN"))
        .count();
    let summary = format!("lines: {line_count}\nwarnings: {warning_count}\n");
    println!("Summary ready:\n{}", summary.trim_end());

フィルターの述語によって警告行だけが残り、count() がそのフィルター済みイテレーターを消費します。format! は内容を表示するのではなく新しい String を返します。明示的な \n によって 2 行が作られ、テキストファイルに適した末尾の改行も追加されます。

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

cargo check
cargo run --quiet

最後のセクションは次のようになります。

Summary ready:
lines: 3
warnings: 1

要約はメモリ上のデータになり、ファイルシステムとの境界へ渡す準備ができました。

要約ファイルを書き込み、再度読み取る

このステップでは、用意された出力ディレクトリに要約を書き込み、実際に保存された内容を読み戻して確認します。

fs::write(path, data) はファイルを作成するか、指定されたバイト列で既存の内容を完全に置き換えます。追記は行いません。読み取りと同じく書き込みも失敗する可能性があり、I/O Result を返すため、? でエラーを伝播させます。ソースを開きます。

nano src/main.rs

Step 4 のコメントを次のコードに置き換えます。

    let output_path = Path::new("output/summary.txt");
    fs::write(output_path, &summary)?;
    let saved = fs::read_to_string(output_path)?;
    println!("Saved {}:\n{}", output_path.display(), saved.trim_end());

&summary は、write がそのバイト列を読み取る間だけ要約を借用します。そのため、String の所有権はプログラムが保持したままになります。ファイルを読み戻すことで、書き込みが成功したと仮定するのではなく、実際に保存されたファイルを確認できます。

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

cargo check
cargo run --quiet

最後のセクションは次のようになります。

Saved output/summary.txt:
lines: 3
warnings: 1

最後に、作成されたファイルを確認します。

nano output/summary.txt

同じ 2 行の要約が表示されます。内容を変更せず、Ctrl+X を押して終了します。

まとめ

Path でファイルの場所を表し、fs::read_to_string で UTF-8 テキストを読み取り、? で I/O エラーを伝播させました。また、読み込んだレコードを要約し、fs::write で結果を書き込み、保存されたファイルを再度読み取って内容を確認しました。