課題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.txtCase-insensitive
rgrep -i "pattern" file.txtRecursive
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)
}
}
---
学習の確認
この課題を通じて、以下をマスターできたか確認してください:
おめでとうございます!Rust Foundationsコースを完了しました!