課題24: 完全なCLIアプリケーション

マンダトリー要件

この課題では、これまで学んだすべての知識を統合し、実用的なCLIアプリケーションを構築します。

選択肢

以下の3つから1つを選択して実装してください:

  • rgrep: ファイル検索ツール(推奨)
  • rcat: 高機能なcatコマンド
  • 独自ツール: 自分で設計したCLIツール(要事前相談)

---

オプション1: rgrep(ファイル検索ツール)

問題1: 基本実装(40点)

1.1 プロジェクト構造(10点)

rgrep/
├── Cargo.toml
├── README.md
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── cli/
│   │   └── mod.rs
│   ├── search/
│   │   ├── mod.rs
│   │   └── matcher.rs
│   ├── output/
│   │   └── mod.rs
│   └── error.rs
├── tests/
│   ├── integration_tests.rs
│   └── fixtures/
│       ├── sample.txt
│       └── test_dir/
└── benches/
    └── search_bench.rs

要件:

  • 適切なモジュール分割
  • ドキュメント完備
  • README.mdに使用例

1.2 CLI設計(10点)

// src/cli/mod.rs

use clap::Parser;

#[derive(Parser)]
#[command(name = "rgrep")]
#[command(about = "A fast grep implementation")]
pub struct Cli {
    /// Search pattern
    pub pattern: String,

    /// Files or directories to search
    #[arg(default_value = ".")]
    pub paths: Vec<PathBuf>,

    /// Case-insensitive search
    #[arg(short, long)]
    pub ignore_case: bool,

    /// Recursive search
    #[arg(short, long)]
    pub recursive: bool,

    /// Show line numbers
    #[arg(short = 'n', long)]
    pub line_number: bool,

    // 他のオプションを追加
}

要件:

  • 最低5つのオプション
  • ヘルプメッセージ
  • バージョン情報

1.3 検索機能(15点)

// src/search/matcher.rs

pub struct Matcher {
    pattern: String,
    ignore_case: bool,
}

impl Matcher {
    pub fn new(pattern: String, ignore_case: bool) -> Self {
        // 実装
    }

    pub fn is_match(&self, line: &str) -> bool {
        // 実装
    }

    pub fn find_matches(&self, line: &str) -> Vec<(usize, usize)> {
        // マッチ位置を返す
    }
}

pub fn search_file(path: &Path, matcher: &Matcher) -> Result<Vec<Match>> {
    // ファイルを検索
}

pub fn search_directory(dir: &Path, matcher: &Matcher, recursive: bool) -> Result<Vec<Match>> {
    // ディレクトリを検索
}

要件:

  • ファイル検索
  • ディレクトリ検索(再帰/非再帰)
  • マッチ位置の特定

1.4 出力(5点)

// src/output/mod.rs

pub struct Formatter {
    show_line_numbers: bool,
    color_enabled: bool,
}

impl Formatter {
    pub fn print_match(&self, m: &Match) {
        // マッチ結果を出力
    }
}

要件:

  • ファイル名と行番号の表示
  • 読みやすいフォーマット

問題2: エラー処理(20点)

// src/error.rs

use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("Pattern error: {0}")]
    Pattern(String),

    #[error("Permission denied: {0}")]
    PermissionDenied(PathBuf),

    // 他のエラー型を追加
}

pub type Result<T> = std::result::Result<T, Error>;

要件:

  • すべてのエラーケースをカバー
  • 適切なエラーメッセージ
  • エラーの伝播(?演算子)

問題3: テスト(20点)

3.1 単体テスト(10点)

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_matcher_basic() {
        let matcher = Matcher::new("hello".to_string(), false);
        assert!(matcher.is_match("hello world"));
        assert!(!matcher.is_match("Hello world"));
    }

    #[test]
    fn test_matcher_case_insensitive() {
        // 実装
    }

    #[test]
    fn test_find_matches() {
        // 実装
    }

    // 他のテストを追加
}

要件:

  • 各モジュールに対するテスト
  • カバレッジ70%以上

3.2 統合テスト(10点)

// tests/integration_tests.rs

use assert_cmd::Command;
use predicates::prelude::*;

#[test]
fn test_search_basic() {
    let mut cmd = Command::cargo_bin("rgrep").unwrap();
    cmd.arg("pattern")
        .arg("tests/fixtures/sample.txt")
        .assert()
        .success()
        .stdout(predicate::str::contains("pattern"));
}

#[test]
fn test_case_insensitive() {
    // 実装
}

#[test]
fn test_recursive_search() {
    // 実装
}

// 他のテストを追加

要件:

  • 主要な機能のテスト
  • エッジケースのテスト
  • エラーケースのテスト

---

オプション2: rcat(高機能catコマンド)

基本機能(40点)

// src/main.rs

#[derive(Parser)]
struct Cli {
    /// Files to display
    files: Vec<PathBuf>,

    /// Number all output lines
    #[arg(short = 'n', long)]
    number: bool,

    /// Number non-empty lines only
    #[arg(short = 'b', long)]
    number_nonblank: bool,

    /// Show tabs as ^I
    #[arg(short = 'T', long)]
    show_tabs: bool,

    /// Show line ends as $
    #[arg(short = 'E', long)]
    show_ends: bool,

    // 他のオプション
}

実装要件:

  • ファイル連結
  • 行番号表示
  • 特殊文字の可視化
  • 標準入力からの読み込み

---

ボーナス課題

ボーナス1: 正規表現サポート(10点)

use regex::Regex;

pub enum Matcher {
    Literal {
        pattern: String,
        ignore_case: bool,
    },
    Regex {
        regex: Regex,
    },
}

impl Matcher {
    pub fn new(pattern: &str, use_regex: bool, ignore_case: bool) -> Result<Self> {
        // 実装
    }

    pub fn is_match(&self, line: &str) -> bool {
        match self {
            Matcher::Literal { pattern, ignore_case } => {
                // リテラルマッチ
            }
            Matcher::Regex { regex } => {
                regex.is_match(line)
            }
        }
    }
}

要件:

  • --regexフラグでの切り替え
  • 正規表現のエラーハンドリング

ボーナス2: カラー出力(10点)

use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};

pub struct ColoredFormatter {
    stdout: StandardStream,
}

impl ColoredFormatter {
    pub fn print_match(&mut self, file: &Path, line_num: usize, line: &str, matches: &[(usize, usize)]) {
        // ファイル名を緑で表示
        self.set_color(Color::Green);
        write!(self.stdout, "{}", file.display()).unwrap();

        // 行番号を黄色で表示
        self.set_color(Color::Yellow);
        write!(self.stdout, ":{}", line_num).unwrap();

        // マッチ部分を赤で強調
        self.highlight_matches(line, matches);
    }

    fn highlight_matches(&mut self, line: &str, matches: &[(usize, usize)]) {
        // 実装
    }
}

要件:

  • --colorオプション(auto/always/never)
  • マッチ部分のハイライト
  • TTY検出

ボーナス3: 並列検索(10点)

use rayon::prelude::*;

pub fn search_parallel(files: &[PathBuf], matcher: &Matcher) -> Vec<Match> {
    files.par_iter()
        .filter_map(|file| search_file(file, matcher).ok())
        .flatten()
        .collect()
}

要件:

  • Rayonを使った並列化
  • --threadsオプション
  • ベンチマークで高速化を証明

ボーナス4: パフォーマンス最適化(10点)

// benches/search_bench.rs

use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};

fn bench_search(c: &mut Criterion) {
    let mut group = c.benchmark_group("search");

    for size in [100, 1000, 10000].iter() {
        group.bench_with_input(
            BenchmarkId::new("serial", size),
            size,
            |b, &size| {
                // ベンチマーク
            },
        );

        group.bench_with_input(
            BenchmarkId::new("parallel", size),
            size,
            |b, &size| {
                // ベンチマーク
            },
        );
    }

    group.finish();
}

要件:

  • Criterionベンチマーク
  • 並列版とシリアル版の比較
  • プロファイリング(flamegraph)
  • 最適化レポート

---

評価基準

マンダトリー部分(80点)

項目 配点 評価ポイント
問題1: 基本実装 40点 機能の完全性
問題2: エラー処理 20点 堅牢性
問題3: テスト 20点 カバレッジと品質

ボーナス部分(20点)

項目 配点 評価ポイント
ボーナス1: 正規表現 10点 正しい実装
ボーナス2: カラー出力 10点 美しい出力
ボーナス3: 並列化 10点 パフォーマンス向上
ボーナス4: 最適化 10点 詳細な分析

: ボーナスは最大20点まで加算されます。

---

提出方法

ファイル構成

rust-foundations-24/
├── Cargo.toml
├── README.md
├── LICENSE
├── CHANGELOG.md
├── src/
├── tests/
├── benches/
├── docs/
│   ├── architecture.md
│   └── performance.md
└── examples/
    └── basic_usage.rs

ドキュメント要件

README.md:

# rgrep

A fast grep implementation in Rust.

## Installation

bash cargo install --path .

## Usage

bash

Basic search

rgrep "pattern" file.txt

Case-insensitive

rgrep -i "pattern" file.txt

Recursive

rgrep -r "pattern" directory/

## Features

- Fast parallel search
- Regex support
- Colored output
- ...

## Benchmarks

...

## License

MIT

architecture.md:

  • アーキテクチャ図
  • モジュール設計の説明
  • 設計決定の理由

performance.md:

  • ベンチマーク結果
  • プロファイリング結果
  • 最適化の詳細
  • ---

    提出前チェックリスト

  • [ ] すべてのテストが通る(cargo test
  • [ ] Clippyの警告がない(cargo clippy
  • [ ] フォーマットされている(cargo fmt
  • [ ] ドキュメントが生成できる(cargo doc
  • [ ] ベンチマークが実行できる(cargo bench
  • [ ] README.mdが完備
  • [ ] ライセンスファイルがある
  • [ ] 例が動作する
  • ---

    ヒント

    エラー処理のヒント

    // mainでのエラー処理
    fn main() -> anyhow::Result<()> {
        let cli = Cli::parse();
    
        let matcher = Matcher::new(&cli.pattern, cli.ignore_case)
            .context("Failed to create matcher")?;
    
        let results = search(&cli.paths, &matcher)
            .context("Search failed")?;
    
        print_results(&results);
    
        Ok(())
    }
    

    テストのヒント

    // テストヘルパー
    #[cfg(test)]
    mod test_helpers {
        use tempfile::TempDir;
    
        pub fn create_test_file(content: &str) -> (TempDir, PathBuf) {
            let dir = TempDir::new().unwrap();
            let file_path = dir.path().join("test.txt");
            std::fs::write(&file_path, content).unwrap();
            (dir, file_path)
        }
    }
    

    ---

    学習の確認

    この課題を通じて、以下をマスターできたか確認してください:

  • [ ] アーキテクチャ設計
  • [ ] エラー処理パターン
  • [ ] CLI設計
  • [ ] テスト戦略
  • [ ] パフォーマンス最適化
  • [ ] ドキュメント作成
  • [ ] 実用的なツール開発

おめでとうございます!Rust Foundationsコースを完了しました!