【前篇】 Rustでダミーファイル生成CLIツールを作りながら基礎を学ぶ

【前篇】 Rustでダミーファイル生成CLIツールを作りながら基礎を学ぶ

Rustに興味あるけど難しそう?ダミーファイル生成CLIツールを作りながら、Option型、Result型、パターンマッチなど、Rustの基礎を体験していきます。実際に手を動かして学んでいきましょう。
2026.02.14

こんにちは!製造ビジネステクノロジー部の石井です。

Rustかけるとかっこよくないですか?「メモリ安全」「並行処理」「速度はC++並み」って聞くと惹かれますよね。
Stack Overflowでは「最も愛されている言語」連続1位ですし、TikTokではインターン生がGoのサービスの一部をRustで書き直したら年間30万ドル削減できたとか話題になりました。

とはいえ「所有権がー」「ライフタイムがー」って聞くとちょっとビビっちゃいますよね。
でも実際に物作りながら手を動かしてみるのが理解への近道だったりします。
思い切ってやってみることが大事ってわけです。

ということで今回は、ダミーファイルを生成するCLIツールを作りながら、Rustの基礎を体験していきます。
完成すると cargo run new dummy.txt 5GB みたいなコマンドで好きなサイズのファイルが作れるようになります。

このツールを作ろうと思ったきっかけ

業務で大容量ファイルを使ったテストをしてて、任意サイズのファイルをサクッと作りたくてこのツールを考えました。
あと、久しぶりにRust触りたかったっていうのが一番大きいです。

この記事で作るもの

テンプレートベースのファイル生成CLIツールを作ります。
使い道としては:

  • 大容量ダミーデータ生成
  • 有効なフォーマットを保持したまま大容量化

完成すると、こんなコマンドが使えるようになります:

# 新規ダミーファイル作成(前編で実装)
cargo run new dummy.txt 5GB

# テンプレートから10GBのファイル生成(後編で実装予定)
cargo run replicate template.txt large.txt 10GB

前編では new コマンドを実装して、後編で replicate コマンドと仕上げを行っていきます。

完成版のコードはこちらで公開しています:
https://github.com/yuta-ishii-cm/dummy-file-creator

わからない部分があれば、公式ドキュメント「The Rust Programming Language」の日本語訳を読みながら進めると理解が深まりますよ:
https://doc.rust-jp.rs/book-ja/

前提情報

想定読者

この記事は以下のような方を想定してます:

  • Rustに興味があるけど、まだ触ったことがない人
  • 他の言語(TypeScript、Python、Javaなど)で開発経験がある人
  • 「作りながら学ぶ」スタイルが好きな人

必要な前提知識

  • 基本的なプログラミング概念(変数、関数、条件分岐など)
  • コマンドラインの基本操作(cdls など)

動作環境

筆者の環境はこんな感じです:

  • macOS
  • Rust 1.93.0(このバージョンで検証)

それでは環境構築から始めていきましょう!

環境構築

既にRustをインストール済みの方は、バージョンを確認しておきましょう:

cargo --version

まだインストールしてない方は、公式の rustup を使うのが一番簡単です:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

それでは、早速プロジェクトを作ってみます:

cargo new fgen
cd fgen

CleanShot 2026-02-12 at 23.03.56@2x

ディレクトリ構成はこんな感じになってるはずです:

fgen/
├── Cargo.toml    # プロジェクトの設定ファイル(Node.js の package.json みたいなもの)
└── src/
    └── main.rs   # エントリーポイント

まずは動かしてみましょう:

cargo run

Hello, world! が表示されたら成功です!

CleanShot 2026-02-12 at 23.04.56@2x

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

CLIツールなので、コマンドライン引数を受け取れるようにする必要がありますね。Rustでは std::env::args() を使います。

src/main.rs を以下のように書き換えてみてください:

fn main() {
    // コマンドライン引数を取得して Vec<String> に変換
    let args: Vec<String> = std::env::args().collect();

    // 引数の数をチェック(4未満ならエラー)
    if args.len() < 4 {
        eprintln!("Usage: fgen new <output> <size>");
        std::process::exit(1);
    }

    // 引数を表示
    println!("Command: {}, Output: {}, Size: {}", args[1], args[2], args[3]);
}

動かしてみます:

cargo run new test.txt 100MB

CleanShot 2026-02-12 at 23.14.55@2x

ここで学んだこと:

  • std::env::args() で引数を取得できる
  • Vec<String> は文字列の配列(TypeScriptの string[] みたいなもの)
  • Rustの主な型:
    • i32, u64 などの整数型(32や64はビット数。u はunsigned、符号なし)
    • f64 などの浮動小数点型
    • String は変更可能な文字列、&str は読み取り専用の文字列参照
    • Vec<T> は可変長配列
    • bool は真偽値
  • eprintln! は標準エラー出力に出す(println! は標準出力)
  • std::process::exit(1) でエラー終了

サイズ指定をパースする

"5GB" みたいな文字列を実際のバイト数に変換する必要がありますね。
関数を追加していきましょう:

fn parse_size(s: &str) -> u64 {
    // "GB" で終わる場合
    if let Some(n) = s.strip_suffix("GB") {
        n.parse::<u64>().unwrap() * 1024 * 1024 * 1024
    // "MB" で終わる場合
    } else if let Some(n) = s.strip_suffix("MB") {
        n.parse::<u64>().unwrap() * 1024 * 1024
    // "KB" で終わる場合
    } else if let Some(n) = s.strip_suffix("KB") {
        n.parse::<u64>().unwrap() * 1024
    // すべて None だった場合(単位なしの数値として扱う)
    } else {
        s.parse::<u64>().unwrap()
    }
}

バイト数を人間が読みやすい形式に戻す関数も作っておきます:

fn format_size(bytes: u64) -> String {
    // 各単位のバイト数を定数で定義
    const GB: u64 = 1024 * 1024 * 1024;
    const MB: u64 = 1024 * 1024;
    const KB: u64 = 1024;

    // バイト数に応じて適切な単位で表示
    if bytes >= GB {
        format!("{:.2}GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2}MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2}KB", bytes as f64 / KB as f64)
    } else {
        format!("{}B", bytes)
    }
}

ここで学んだこと:

  • fn 関数名(引数: 型) -> 戻り値の型 で関数を定義
  • 最後の式(セミコロンなし)が戻り値になる
  • strip_suffix() で文字列の末尾を切り取れる(末尾が一致すれば Some、しなければ None を返す)
  • Option<T> は値がある(Some(値))か、ない(None)かを表す型
  • if let Some(n) = ... で値がある場合だけ処理、None なら次の分岐へ
  • parse::<u64>() で文字列を数値に変換できる
  • unwrap() は Result や Option から値を取り出す(エラー処理は後述)
  • as で型変換(キャスト)ができる
  • format! マクロで文字列フォーマット

ファイルに書き込む

いよいよファイルを生成する処理を書いていきます。
一度にメモリに全部載せるとヤバいので、チャンク単位で書き込んでいきましょう:

use std::fs::File;
use std::io::Write;

fn generate_file(path: &str, size: u64) {
    // ファイルを作成
    let mut file = File::create(path).unwrap();
    // 8KBのゼロ埋めチャンクを用意
    let chunk = vec![0u8; 8192];
    // 残りの書き込みバイト数
    let mut remaining = size;

    // 残りがなくなるまでループ
    while remaining > 0 {
        // 書き込むサイズを決定(残りとチャンクサイズの小さい方)
        let write_size = std::cmp::min(remaining, chunk.len() as u64) as usize;
        // ファイルに書き込み
        file.write_all(&chunk[..write_size]).unwrap();
        // 残りを減らす
        remaining -= write_size as u64;
    }
}

ここで学んだこと:

  • std::fs::File::create() でファイルを作成
  • Write トレイトの write_all() でバイト列を書き込む
  • vec![0u8; 8192] でゼロ埋めの配列を作成(8KBチャンク)
  • ! が付いてる(println!, vec! など)のはマクロ(可変長引数を受け取れる)
  • std::cmp::min() で最小値を取得
  • &chunk[..write_size] でスライスを作る(配列の先頭からwrite_size個の要素を参照)

エラー処理を導入する

ここまでのコードを見返すと、unwrap() ってのをたくさん使ってますよね。
他の言語だと、エラーが起きたときに例外(Exception)を投げて try-catch で受け取るのが一般的です。

でもRustでは違うアプローチを取ります。
Result<T, E> という型で「成功時の値」か「エラー」のどちらかを値として返すんです。
unwrap() はこの Result から強制的に値を取り出すんですが、エラーだった場合はプログラムがクラッシュしちゃいます。

? 演算子を使うと、エラーが起きたときに自動的に呼び出し元にエラーを返してくれて便利なんですよね。

まず parse_size 関数をエラー処理対応に書き換えてみます:

fn parse_size(s: &str) -> Result<u64, String> {
    // "GB" で終わる場合
    if let Some(n) = s.strip_suffix("GB") {
        n.parse::<u64>()
            .map(|v| v * 1024 * 1024 * 1024)  // 成功時はバイト数に変換
            .map_err(|e| format!("Invalid size: {}", e))  // エラー時はメッセージを整形
    // "MB" で終わる場合
    } else if let Some(n) = s.strip_suffix("MB") {
        n.parse::<u64>()
            .map(|v| v * 1024 * 1024)
            .map_err(|e| format!("Invalid size: {}", e))
    // "KB" で終わる場合
    } else if let Some(n) = s.strip_suffix("KB") {
        n.parse::<u64>()
            .map(|v| v * 1024)
            .map_err(|e| format!("Invalid size: {}", e))
    // すべて None だった場合(単位なしの数値として扱う)
    } else {
        s.parse::<u64>()
            .map_err(|e| format!("Invalid size: {}", e))
    }
}

generate_file 関数も修正します:

use std::io;

fn generate_file(path: &str, size: u64) -> io::Result<()> {
    // ファイルを作成(エラー時は ? で即座に返す)
    let mut file = File::create(path)?;
    // 8KBのゼロ埋めチャンクを用意
    let chunk = vec![0u8; 8192];
    // 残りの書き込みバイト数
    let mut remaining = size;

    // 残りがなくなるまでループ
    while remaining > 0 {
        // 書き込むサイズを決定
        let write_size = std::cmp::min(remaining, chunk.len() as u64) as usize;
        // ファイルに書き込み(エラー時は ? で即座に返す)
        file.write_all(&chunk[..write_size])?;
        // 残りを減らす
        remaining -= write_size as u64;
    }

    // 成功を返す
    Ok(())
}

ここで学んだこと:

  • Result<T, E> は成功時の値 T かエラー E を返す型
  • ? 演算子でエラーを簡潔に伝播できる
  • map() で Result の成功時の値を変換できる
  • map_err() でエラーの型を変換できる
  • |引数| 処理 でクロージャ(無名関数)を定義(JavaScriptの (引数) => 処理 と同じ)
  • 関数が Result を返す場合、最後に Ok(()) を返す

統合して完成させる

全部まとめて、最終的な main.rs はこんな感じになります:

use std::fs::File;
use std::io::{self, Write};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // コマンドライン引数を取得
    let args: Vec<String> = std::env::args().collect();

    // 第1引数(サブコマンド)で分岐
    match args.get(1).map(|s| s.as_str()) {
        Some("new") => {
            // 引数の数をチェック
            if args.len() < 4 {
                eprintln!("Usage: fgen new <output> <size>");
                std::process::exit(1);
            }

            // 出力ファイル名とサイズを取得
            let output = &args[2];
            let size = parse_size(&args[3])?;

            // ファイル生成処理
            println!("Generating {} file...", format_size(size));
            generate_file(output, size)?;
            println!("Done!");
        }
        _ => {
            // 未知のサブコマンドまたは引数なし
            eprintln!("Usage:");
            eprintln!("  fgen new <output> <size>");
            std::process::exit(1);
        }
    }

    Ok(())
}

fn parse_size(s: &str) -> Result<u64, String> {
    // "GB" で終わる場合
    if let Some(n) = s.strip_suffix("GB") {
        n.parse::<u64>()
            .map(|v| v * 1024 * 1024 * 1024)
            .map_err(|e| format!("Invalid size: {}", e))
    // "MB" で終わる場合
    } else if let Some(n) = s.strip_suffix("MB") {
        n.parse::<u64>()
            .map(|v| v * 1024 * 1024)
            .map_err(|e| format!("Invalid size: {}", e))
    // "KB" で終わる場合
    } else if let Some(n) = s.strip_suffix("KB") {
        n.parse::<u64>()
            .map(|v| v * 1024)
            .map_err(|e| format!("Invalid size: {}", e))
    // すべて None だった場合(単位なしの数値として扱う)
    } else {
        s.parse::<u64>()
            .map_err(|e| format!("Invalid size: {}", e))
    }
}

fn format_size(bytes: u64) -> String {
    // 各単位のバイト数を定数で定義
    const GB: u64 = 1024 * 1024 * 1024;
    const MB: u64 = 1024 * 1024;
    const KB: u64 = 1024;

    // バイト数に応じて適切な単位で表示
    if bytes >= GB {
        format!("{:.2}GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2}MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2}KB", bytes as f64 / KB as f64)
    } else {
        format!("{}B", bytes)
    }
}

fn generate_file(path: &str, size: u64) -> io::Result<()> {
    // ファイルを作成
    let mut file = File::create(path)?;
    // 8KBのゼロ埋めチャンクを用意
    let chunk = vec![0u8; 8192];
    // 残りの書き込みバイト数
    let mut remaining = size;

    // 残りがなくなるまでループ
    while remaining > 0 {
        // 書き込むサイズを決定
        let write_size = std::cmp::min(remaining, chunk.len() as u64) as usize;
        // ファイルに書き込み
        file.write_all(&chunk[..write_size])?;
        // 残りを減らす
        remaining -= write_size as u64;
    }

    // 成功を返す
    Ok(())
}

動かしてみましょう:

cargo run new test.txt 100MB
ls -lh test.txt  # → 100MB のファイルができてる!

CleanShot 2026-02-12 at 23.56.57@2x

まとめ

前編では、ダミーファイル生成CLIツールの基本機能を作りながら、Rustの基礎を体験しました。

学んだ重要な概念:

  • 型システム: i32, u64, String, &str, Vec<T> などの基本型
  • Option型: 値の有無を表す SomeNone(null の代わり)
  • Result型: 成功/失敗を表す型(例外の代わり)
  • パターンマッチ: matchif let で分岐処理
  • エラー処理: ? 演算子でエラーを簡潔に伝播
  • クロージャ: |引数| 処理 で無名関数を定義
  • マクロ: ! が付く関数(println!, vec! など)
  • 所有権の基礎: & で参照を渡す、mut で可変性を指定

この記事では簡潔に書くことを優先したので、それぞれの概念についてはもっと深い部分があります。
実際に手を動かしながら、ここで出てきた処理を自分で調べてみると面白い発見があると思います。
もっと良い書き方とか、効率的なパターンとかも見つかるはずなので、ぜひ学びを深めてみてください!

特に所有権エラーハンドリングは、Rustの特徴的な部分で詳しく知ると面白いですよ。
所有権はメモリ安全性を保証する仕組みで、エラーハンドリングは型で安全性を担保するアプローチです。
どちらもRustらしさが詰まってるので、余裕があれば深掘りしてみることをおすすめします。

わからない部分があれば、公式ドキュメント「The Rust Programming Language」の日本語訳を読みながら進めると理解が深まります:
https://doc.rust-jp.rs/book-ja/

後編では、テンプレート複製機能、進捗表示、ツールの仕上げを行っていく予定です。
お楽しみに!

コードはこちら:
https://github.com/yuta-ishii-cm/dummy-file-creator

この記事をシェアする

FacebookHatena blogX

関連記事