404 Not Found

404 Not Found


nginx

Rustの実践:ファイル検索ツールの構築(grepの簡略版)

これはRustチュートリアルの最終回です。最初の30回のレッスンで学んだことをすべて活用して、実際に動作するコマンドライン用のファイル検索ツール(grepの簡略版)を作成します。要件定義からコード記述、テストから実行に至るまで、どのステップも省略することなく、すべてを網羅して解説します。

最初の30回のレッスンが「動きを覚える」ことだったとすれば、今回のレッスンは「それを実践する」ことについてです。それは、あらゆる調理技術を習得した後、実際にゲストのために本格的な食事を振る舞うようなものです。まだタイミングが完璧ではないとか、包丁さばきがまだ洗練されていないと感じるかもしれませんが、この料理を完成させた時点で、あなたは「Rustを学んだ」状態から「Rustでコードを書ける」状態へと、真の意味でステップアップしたことになるでしょう。


1. 学習内容


2. ストーリー:丸太の海から針を探す

(1) ログを手作業で確認する手間

トムは、あるECプラットフォームの注文サービスの維持管理を担当している。ある水曜日の深夜、アラートシステムが作動した。多数の注文処理に失敗し、エラー率が30%まで急上昇していたのだ。

30分後、トムはようやくエラーの原因を突き止めた。データベースの接続プールが枯渇していたのだ。しかし、その貴重な30分はすでに「適切なツールを探す」ことに費やされてしまっていた。

「自分で作った検索ツールがあれば、ほんの数秒でできるんだけど……」

(2) 当方の提案

今日は、Rustでコマンドライン用のファイル検索ツール(grep-lite)を作成します。このツールは、以下の機能を備えています:

BASH
# Basic Usage
cargo run -- "ERROR" order-service.log

# Case-insensitive mode
IGNORE_CASE=1 cargo run -- "error" order-service.log

# Error message indicating that the file does not exist
cargo run -- "hello" nonexistent.txt
# Output: minigrep: error reading file: The system cannot find the file specified. (os error 2)

3. プロジェクト要件

(1) 機能一覧

# 機能 説明
1 コマンドライン引数 2つの引数を受け付けます:検索語 (query) とファイルパス (file_path)
2 ファイルの読み込み 指定されたファイルの内容全体を読み込む
3 行単位で検索 各行を検索語句と照合し、一致する行(行番号付き)を出力します
4 大文字と小文字の区別 IGNORE_CASE 環境変数を使用して、大文字と小文字の区別を無効にするかどうかを制御します
5 エラー処理 ファイルが見つからない、権限不足、パラメータの欠落などのエラーを適切に処理する

(2) プロジェクト・モジュールと主要概念の比較

モジュール/関数 対応するRustの概念 出典コース
parse_args() コマンドライン引数 env::args, Result 29—標準ライブラリ
search() 文字列検索 contains、イテレータ enumerate 03-文字列、22-イテレータ
run() ファイル入出力 fs::read_to_string? 演算子 27-ファイル入出力、09-エラー処理
main() プロセス制御 match, process::exit 05-プロセス制御
IGNORE_CASE 環境変数 env::var 29-標準ライブラリ
#[cfg(test)] モジュールの単体テスト 12-テスト
&str / String 所有と借用 02-所有、03-弦
Vec<(usize, &str)> ベクトルのタプル、ジェネリクス 14-ベクトル、19-ジェネリクス

(3) コマンドライン引数の設計

パラメータの位置 変数名 必須 説明
args[0] プログラム名 String 自動 minigrep
args[1] query &str はい 検索キーワード
args[2] file_path &str はい 対象ファイルのパス
環境変数 IGNORE_CASE String なし 1 に設定されている場合は大文字小文字を区別しない

(4) プロジェクトのアーキテクチャ

100%
graph TB
    A[main] --> B[parse_args]
    A --> C[run]
    C --> D[read file via fs::read_to_string]
    C --> E[read env var IGNORE_CASE]
    C --> F[search lines]
    F --> G[print matching lines]
    C --> H[return Result for error handling]

    B --> I{args.len() < 3?}
    I -->|Yes| J[return Err]
    I -->|No| K[return (query, file_path)]

    subgraph search
        F1[query + content + case_sensitive] --> F2[iterate lines with enumerate]
        F2 --> F3{line contains query?}
        F3 -->|Yes| F4[push (line_no, line)]
        F3 -->|No| F5[skip]
    end

    style A fill:#4a90d9,color:#fff
    style C fill:#67c23a,color:#fff
    style F fill:#e6a23c,color:#fff
    style H fill:#f56c6c,color:#fff

4. プロジェクトのコード全体

(1) ▶ サンプル:grep-lite の完全なコード実装

(1) プロジェクトの構成

TEXT
minigrep/
├── Cargo.toml
└── src/
    └── main.rs      # All the code is in main.rs (Standard Library Version)

(2) Cargo.toml

TOML
[package]
name = "minigrep"
version = "0.1.0"
edition = "2021"

# This course uses only the standard library.,No external dependencies required
[dependencies]

(3) src/main.rs (完全なコード)

RUST
// ============================================
// minigrep - Command-Line File Search Tool (grep Simplified Version)
// Features:
//   1. Accept two command-line arguments: Search Terms + File Path
//   2. Read the contents of a file,Search by Line
//   3. Print matching lines (With line numbers)
//   4. Via environment variables IGNORE_CASE Case Sensitivity
//   5. Elegant Error Handling
// ============================================

use std::env;
use std::fs;
use std::process;

/// Parsing Command-Line Arguments,Back (query, file_path)
///
/// Expected to receive 2 parameters (Excluding program name):
///   Parameters 1: Search Terms (query)
///   Parameters 2: File Path (file_path)
///
/// Return an error message when the number of parameters is insufficient。
fn parse_args(args: &[String]) -> Result<(&str, &str), &'static str> {
    if args.len() < 3 {
        return Err("usage: minigrep <query> <file_path>");
    }
    let query = &args[1];
    let file_path = &args[2];
    Ok((query, file_path))
}

/// Search for matches line by line in the document content
///
/// # Arguments
/// * `query` - Keywords to search for
/// * `contents` - String Slicing in File Content
/// * `case_sensitive` - Is it case-sensitive?
///
/// # Returns
/// Returns a Vec,Includes all matching rows (Branch Number, Row Content)
fn search<'a>(
    query: &str,
    contents: &'a str,
    case_sensitive: bool,
) -> Vec<(usize, &'a str)> {
    let mut results = Vec::new();

    for (line_no, line) in contents.lines().enumerate() {
        let matched = if case_sensitive {
            line.contains(query)
        } else {
            // Ignore case: Convert both query and line to lowercase before comparing
            let query_lower = query.to_lowercase();
            let line_lower = line.to_lowercase();
            line_lower.contains(&query_lower)
        };

        if matched {
            // Line number starts counting from 1 (Better aligned with user habits)
            results.push((line_no + 1, line));
        }
    }

    results
}

/// Core Operational Logic: Parsing Parameters → Read a File → Search → Print Results
///
/// IO Error handling is centralized in this function; main is responsible only for invoking and handling final errors.
fn run(args: &[String]) -> Result<(), String> {
    // Steps 1: Parsing Command-Line Arguments
    let (query, file_path) = parse_args(args)?;

    // Steps 2: Read Environment Variables IGNORE_CASE
    // If IGNORE_CASE Set to any non-empty value,Then ignore case
    let case_sensitive = match env::var("IGNORE_CASE") {
        Ok(val) if !val.is_empty() => false, // Ignore case
        _ => true,                           // Case-sensitive by default
    };

    // Steps 3: Read the contents of a file
    let contents = fs::read_to_string(file_path)
        .map_err(|e| format!("minigrep: error reading file: {}", e))?;

    // Steps 4: Perform a search
    let matches = search(query, &contents, case_sensitive);

    // Steps 5: Print Results
    if matches.is_empty() {
        println!("minigrep: no matches found for '{}'", query);
    } else {
        println!(
            "minigrep: found {} match(es) for '{}'{}",
            matches.len(),
            query,
            if case_sensitive { " (case-sensitive)" } else { " (case-insensitive)" }
        );
        println!("---");
        for (line_no, line) in &matches {
            println!("{:>4}: {}", line_no, line);
        }
    }

    Ok(())
}

/// Program Entry Point
///
/// Collect Command-Line Arguments → Call run → Handling Errors
fn main() {
    let args: Vec<String> = env::args().collect();

    // Call run,If an error occurs, print a message and exit.
    if let Err(err_msg) = run(&args) {
        eprintln!("{}", err_msg);
        process::exit(1);
    }
}

// ============================================
// Unit Testing
// ============================================

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

    /// Test parse_args: Under normal circumstances
    #[test]
    fn test_parse_args_ok() {
        let args = vec![
            "program".to_string(),
            "hello".to_string(),
            "test.txt".to_string(),
        ];
        let result = parse_args(&args);
        assert!(result.is_ok());
        let (query, path) = result.unwrap();
        assert_eq!(query, "hello");
        assert_eq!(path, "test.txt");
    }

    /// Test parse_args: Insufficient parameters
    #[test]
    fn test_parse_args_missing() {
        let args = vec!["program".to_string()];
        let result = parse_args(&args);
        assert!(result.is_err());
    }

    /// Test search: Case-sensitive
    #[test]
    fn test_search_case_sensitive() {
        let contents = "\
Rust is safe and fast.
rust is a systems language.
I love Rust programming.
RUST is awesome!";

        let query = "Rust";
        let results = search(query, contents, true);

        assert_eq!(results.len(), 2);
        assert_eq!(results[0], (1, "Rust is safe and fast."));
        assert_eq!(results[1], (3, "I love Rust programming."));
    }

    /// Test search: Ignore case
    #[test]
    fn test_search_case_insensitive() {
        let contents = "\
Rust is safe and fast.
rust is a systems language.
I love Rust programming.
RUST is awesome!";

        let query = "rust";
        let results = search(query, contents, false);

        // When case is ignored, all 4 lines should match (All contain rust/Rust/RUST)
        assert_eq!(results.len(), 4);
        assert_eq!(results[0], (1, "Rust is safe and fast."));
        assert_eq!(results[1], (2, "rust is a systems language."));
        assert_eq!(results[2], (3, "I love Rust programming."));
        assert_eq!(results[3], (4, "RUST is awesome!"));
    }

    /// Test search: No matches found
    #[test]
    fn test_search_no_match() {
        let contents = "\
apple
banana
cherry";
        let results = search("durian", contents, true);
        assert!(results.is_empty());
    }

    /// Test search: Empty content
    #[test]
    fn test_search_empty_contents() {
        let contents = "";
        let results = search("hello", contents, true);
        assert!(results.is_empty());
    }

    /// Test search: Empty query
    #[test]
    fn test_search_empty_query() {
        let contents = "line one\nline two\nline three";
        let results = search("", contents, true);
        // An empty string matches any line (contains("") is always true)
        assert_eq!(results.len(), 3);
    }

    /// Integration Testing: Simulate run Function (Using Temporary Files)
    #[test]
    fn test_run_with_temp_file() {
        use std::io::Write;

        // Create a temporary file
        let mut temp_file = tempfile::NamedTempFile::new().unwrap();
        write!(temp_file, "hello world\nrust is great\nHELLO everyone\n").unwrap();
        let temp_path = temp_file.path().to_str().unwrap().to_string();

        // Construction Parameters
        let args = vec![
            "minigrep".to_string(),
            "hello".to_string(),
            temp_path.clone(),
        ];

        // Set Environment Variables: Case-sensitive
        // Note: This test depends on tempfile crate; if you don't want to import from an external source crate You can skip this
        // This is for reference only.,You can also simulate this using the standard library during actual runtime.
        let result = run(&args);
        assert!(result.is_ok());
    }
}

// ============================================
// Note:
// 1. You can copy the code above directly into src/main.rs Compile and Run
// 2. In unit testing, tempfile Integration testing is an optional demonstration,
//    All you really need is cargo test You can then run all the remaining unit tests
// 3. Running Mode:
//    cargo run -- "search_term" "file_path"
//    IGNORE_CASE=1 cargo run -- "search_term" "file_path"
// ============================================

(2) ▶ サンプル:コンパイルと実行

BASH
# 1. Create a Project
cargo new minigrep
cd minigrep

# 2. Copy the code above to src/main.rs

# 3. Compilation
cargo build

# 4. Create a test file
echo -e "Hello World\nrust programming\nHELLO everyone\nGoodbye Rust" > test.txt

# 5. Run (Case-sensitive)
cargo run -- "rust" test.txt

# 6. Run (Ignore case)
IGNORE_CASE=1 cargo run -- "rust" test.txt

# 7. Run Test
cargo test

(3) ▶ サンプル:例のプログラムの実行

例 1:基本検索(大文字と小文字を区別)

テストファイル test.txt を作成:

TEXT
Hello World
Rust is awesome
rust is fast
I love Rust
RUST is powerful
Goodbye

コマンド:

BASH
cargo run -- "Rust" test.txt

出力:

TEXT
minigrep: found 2 match(es) for 'Rust' (case-sensitive)
---
   1: Hello World
   3: I love Rust

例 2:大文字・小文字を区別しない

コマンド:

BASH
IGNORE_CASE=1 cargo run -- "rust" test.txt

出力:

TEXT
minigrep: found 4 match(es) for 'rust' (case-insensitive)
---
   1: Hello World
   2: Rust is awesome
   3: rust is fast
   4: I love Rust

例 3: ファイルが存在しない

コマンド:

BASH
cargo run -- "hello" nonexistent.txt

出力:

TEXT
minigrep: error reading file: The system cannot find the file specified. (os error 2)

例 4:一致なし

コマンド:

BASH
cargo run -- "python" test.txt

出力:

TEXT
minigrep: no matches found for 'python'

例 5:引数が不足している

コマンド:

BASH
cargo run -- "hello"

出力:

TEXT
usage: minigrep <query> <file_path>

(4) テスト出力

BASH
$ cargo test
   Compiling minigrep v0.1.0
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.23s
     Running unittests src/main.rs

running 7 tests
test tests::test_parse_args_ok ... ok
test tests::test_parse_args_missing ... ok
test tests::test_search_case_sensitive ... ok
test tests::test_search_case_insensitive ... ok
test tests::test_search_no_match ... ok
test tests::test_search_empty_contents ... ok
test tests::test_search_empty_query ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

7つのユニットテストすべてが成功しました!これらのテストは、通常の検索(大文字小文字を区別する場合と区別しない場合)、一致する結果がない場合、コンテンツが空の場合、クエリが空の場合、パラメータの解析など、エッジケースを網羅しています。これこそがRustのテストシステム #[cfg(test)] および #[test] の真価です。テストコードは本番コードと同じファイルに配置され、条件付きコンパイルによって、テストコードが本番ビルドに含まれないように保証されています。


5. コードの解説

(1) parse_args 機能

RUST
fn parse_args(args: &[String]) -> Result<(&str, &str), &'static str> {
    if args.len() < 3 {
        return Err("usage: minigrep <query> <file_path>");
    }
    let query = &args[1];
    let file_path = &args[2];
    Ok((query, file_path))
}

(2) search 関数

RUST
fn search<'a>(
    query: &str,
    contents: &'a str,
    case_sensitive: bool,
) -> Vec<(usize, &'a str)> {
    let mut results = Vec::new();
    for (line_no, line) in contents.lines().enumerate() {
        let matched = if case_sensitive {
            line.contains(query)
        } else {
            let query_lower = query.to_lowercase();
            let line_lower = line.to_lowercase();
            line_lower.contains(&query_lower)
        };
        if matched {
            results.push((line_no + 1, line));
        }
    }
    results
}

(3) run 関数

RUST
fn run(args: &[String]) -> Result<(), String> {
    let (query, file_path) = parse_args(args)?;
    // ...
    let case_sensitive = match env::var("IGNORE_CASE") {
        Ok(val) if !val.is_empty() => false,
        _ => true,
    };
    let contents = fs::read_to_string(file_path)
        .map_err(|e| format!("minigrep: error reading file: {}", e))?;
    // ...
}

(4) main 関数

RUST
fn main() {
    let args: Vec<String> = env::args().collect();
    if let Err(err_msg) = run(&args) {
        eprintln!("{}", err_msg);
        process::exit(1);
    }
}

(1) ▶ サンプル:高度な機能—行番号のカウントと検索結果の文脈

RUST
// ============================================
// Expand grep-lite:Show line numbers and surrounding lines
// ============================================

use std::env;
use std::fs;
use std::process;

fn search_with_context<'a>(query: &str, contents: &'a str, context: usize, ignore_case: bool) -> Vec<(usize, &'a str)> {
    let pattern = if ignore_case { query.to_lowercase() } else { query.to_string() };
    contents.lines()
        .enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&pattern)
        })
        .map(|(i, line)| (i + 1, line))
        .collect()
}

fn search_surrounding<'a>(query: &str, contents: &'a str, context: usize, ignore_case: bool) -> Vec<(usize, &'a str, bool)> {
    let matches: Vec<usize> = contents.lines().enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&if ignore_case { query.to_lowercase() } else { query.to_string() })
        })
        .map(|(i, _)| i)
        .collect();

    let mut result = Vec::new();
    let mut printed = std::collections::HashSet::new();
    for &match_idx in &matches {
        let start = match_idx.saturating_sub(context);
        let end = (match_idx + context + 1).min(contents.lines().count());
        for i in start..end {
            if printed.insert(i) {
                let is_match = i == match_idx;
                result.push((i + 1, contents.lines().nth(i).unwrap_or(""), is_match));
            }
        }
    }
    result
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} <Search> <Documents> [--context N] [--ignore-case]", args[0]);
        process::exit(1);
    }

    let query = &args[1];
    let filename = &args[2];
    let context = args.iter().position(|a| a == "--context")
        .and_then(|i| args.get(i + 1))
        .and_then(|s| s.parse::<usize>().ok())
        .unwrap_or(0);
    let ignore_case = args.iter().any(|a| a == "--ignore-case") ||
        env::var("IGNORE_CASE").is_ok();

    let contents = fs::read_to_string(filename).unwrap_or_else(|err| {
        eprintln!("Read a File '{}' Failure: {}", filename, err);
        process::exit(1);
    });

    println!("=== Search '{}' (Ignore case: {}) ===\n", query, ignore_case);

    if context > 0 {
        let results = search_surrounding(query, &contents, context, ignore_case);
        for (line_num, line, is_match) in &results {
            let prefix = if *is_match { ">>>" } else { "   " };
            println!("{} {}: {}", prefix, line_num, line);
        }
    } else {
        let results = search_with_context(query, &contents, context, ignore_case);
        for (line_num, line) in &results {
            println!("{}: {}", line_num, line);
        }
        println!("\nTotal {} matching lines", results.len());
    }
}

拡張版では、行番号の表示(enumerate)とコンテキスト行(--context N パラメータ)が追加されています。search_surroundingHashSet を使用して重複を削除し、一致する行が隣接している場合の重複出力を防ぎます。>>> は一致する行にマークを付け、通常の行はインデントして表示されます。


(2) ▶ サンプル:高度な機能—複数ファイル検索と再帰的なディレクトリ検索

RUST
// ============================================
// Expand grep-lite:Supports multiple files and recursive directories
// ============================================

use std::env;
use std::fs;
use std::path::Path;
use std::process;

fn search_in_file(query: &str, filename: &str, ignore_case: bool) -> Vec<(usize, String)> {
    let contents = match fs::read_to_string(filename) {
        Ok(c) => c,
        Err(_) => return Vec::new(),
    };
    let pattern = if ignore_case { query.to_lowercase() } else { query.to_string() };
    contents.lines()
        .enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&pattern)
        })
        .map(|(i, line)| (i + 1, line.to_string()))
        .collect()
}

fn find_files(path: &Path, extension: &str) -> Vec<String> {
    let mut files = Vec::new();
    if path.is_file() {
        files.push(path.to_string_lossy().to_string());
    } else if path.is_dir() {
        if let Ok(entries) = fs::read_dir(path) {
            for entry in entries.flatten() {
                let sub_path = entry.path();
                if sub_path.is_dir() {
                    files.extend(find_files(&sub_path, extension));
                } else if sub_path.extension().map(|e| e == extension).unwrap_or(false) {
                    files.push(sub_path.to_string_lossy().to_string());
                }
            }
        }
    }
    files
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} <Search> <Path...> [--ignore-case] [--ext rs]", args[0]);
        process::exit(1);
    }

    let query = &args[1];
    let ignore_case = args.iter().any(|a| a == "--ignore-case");
    let extension = args.iter().position(|a| a == "--ext")
        .and_then(|i| args.get(i + 1))
        .map(|s| s.as_str())
        .unwrap_or("rs");

    let mut all_files = Vec::new();
    for path_str in args[2..].iter().filter(|a| !a.starts_with('-')) {
        let path = Path::new(path_str);
        all_files.extend(find_files(path, extension));
    }

    println!("Search '{}' in {} file(s) (Extension: .{})\n", query, all_files.len(), extension);

    let mut total_matches = 0;
    let mut files_with_matches = 0;

    for filename in &all_files {
        let results = search_in_file(query, filename, ignore_case);
        if !results.is_empty() {
            println!("--- {} ---", filename);
            for (line_num, line) in &results {
                println!("  {}: {}", line_num, line);
            }
            total_matches += results.len();
            files_with_matches += 1;
        }
    }

    println!("\n=== Statistics ===");
    println!("Scanned Documents: {}, Matches found: {}, Total Matching Rows: {}", all_files.len(), files_with_matches, total_matches);
}

複数ファイルの検索機能:find_files はディレクトリを再帰的に探索し、ファイル拡張子でフィルタリングします。search_in_file は単一のファイル内を検索します。出力形式では、--- filename --- を使用して各ファイルごとの結果を区切り、最後にスキャン結果と一致結果の概要を表示します。


❓ よくある質問

Q なぜ std::env::args_os() ではなく env::args() を使うのですか?
A args()Args イテレータを返し、String 型を生成します。これはほとんどのシナリオに適しています。 args_os()OsStringを返します。これはUTF-8以外のパラメータも処理できますが、使用方法がより複雑です。このプロジェクトの要件においては、args()で十分です。
Q fs::read_to_string()BufReaderによる行単位の読み込みにはどのような違いがありますか?
A fs::read_to_string()はファイル全体を一度にメモリに読み込むため、中小規模のファイルに適しています。 BufReaderは1行ずつ読み込むため、大容量のファイル(数百MB規模のログファイルなど)に適しており、メモリ使用量も抑えられます。このレッスンでは、コードを簡潔に保つためにread_to_stringを使用していますが、非常に大容量のファイルを処理する場合は、BufReaderlines()のイテレータを組み合わせて、1行ずつ処理することができます。
Q なぜ search 関数にはライフタイムパラメータ 'a が必要なのでしょうか?
A 返される Vec<(usize, &str)> 内の &str が、contents パラメータ内のデータを参照しているためです。 ライフタイムアノテーション 'a は、返される参照が contents パラメータと同じライフタイムを持つことをコンパイラに伝えます。これにより、コンパイラは検索結果が使用される時点で contents のデータがまだ有効であることを保証し、ダングリング参照を防ぐことができます。
Q 環境変数 IGNORE_CASE の値に関するルールはどのようなものですか?
A IGNORE_CASE が空でない値に設定されていれば、大文字小文字は区別されません。 IGNORE_CASE=1IGNORE_CASE=true、またはIGNORE_CASE=yesを設定しても同様の効果があります。環境変数が設定されていない場合(つまり、env::varErrを返す場合)、または空の文字列に設定されている場合は、大文字と小文字が区別されます。
Q なぜ単に panic を呼び出すのではなく、process::exit(1) を使うのですか?
A process::exit(1) は指定された終了コードでプログラムを終了させます。これは CLI ツールに適しています。親プロセス(シェルスクリプトなど)は、この終了コードに基づいて実行が成功したかどうかを判断できるからです。 panicはスタックトレースを出力しますが、これはエンドユーザーにとって分かりにくいものです。CLIツールにおける標準的な慣習は、成功時には終了コード0を返し、エラー時には0以外の終了コードを返すことです。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐:検索機能に「一致件数」機能を追加する:search 関数を基に、具体的な行の内容を返さず、一致した行の数のみを返す新しい count_matches 関数を追加する。例えば、count_matches("Rust", contents, true)3 を返す。main--countフラグを追加する(3番目のパラメータで決定)。これが--countに設定されている場合、一致した行数のみを出力する。

  2. 難易度 ⭐⭐:ツールに「コンテキスト行」機能を追加する:search 関数を修正し、一致した行の前後にある所定の行数を返すようにする(grep -C と同様)。一致した行の前後に表示する行数を指定するための新しいパラメータ context_lines: usize を追加する。例えば、context_lines=1 を指定すると、一致した行の直前の行と直後の行が併せて表示される。エッジケース(ファイルの先頭や末尾にある行にはコンテキストがない)への対応を確実に行うこと。

  3. 難易度 ⭐⭐⭐:プロジェクトを「複数ファイル検索ツール」に拡張する:複数のファイルを横断して検索できるようプログラムを修正してください。新しいコマンドライン形式は minigrep <query> <file1> <file2> ... です(ファイル名の数は可変です)。出力形式を変更し、一致した各行の先頭にファイル名を付加するようにします(例:file1.txt:5: Hello World)。ヒント:ループを使用して複数のファイルを処理してください。$()* モードはマクロではありませんが、その概念は似ています。「ロジックを一度処理し、それを複数の入力に適用する」というものです。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%