Rust: Rust في التطبيق العملي

آخر تحديث: 2026-08-26

هذه هي الحلقة الأخيرة من سلسلة دروس لغة Rust — حيث سنستخدم كل ما تعلمته في الدروس الثلاثين الأولى لإنشاء أداة فعالة للبحث عن الملفات عبر سطر الأوامر (نسخة مبسطة من grep). وسنغطي كل شيء بدءًا من المتطلبات وصولاً إلى الكود، ومن الاختبار وصولاً إلى التنفيذ، دون تخطي أي خطوة.

إذا كانت الدروس الثلاثين الأولى تدور حول «تعلم الحركات»، فإن هذا الدرس يدور حول «تطبيقها عمليًّا». الأمر أشبه بإتقان جميع تقنيات الطهي ثم القيام فعليًّا بإعداد وجبة كاملة للضيوف. قد تجد أن توقيتك ليس دقيقًا تمامًا بعد أو أن مهاراتك في استخدام السكين لم تصل إلى الكمال بعد — ولكن بمجرد الانتهاء من إعداد هذا الطبق، ستكون قد انتقلت حقًّا من مرحلة «تعلم لغة Rust» إلى مرحلة «القدرة على كتابة كود بلغة Rust».


1. ما ستتعلمه



2. القصة: البحث عن إبرة في بحر من جذوع الأشجار

(1) عناء مراجعة السجلات يدويًّا

توم مسؤول عن صيانة خدمة معالجة الطلبات في إحدى منصات التجارة الإلكترونية. في وقت متأخر من ليلة الأربعاء، انطلق نظام الإنذار — فقد فشل عدد كبير من الطلبات، وارتفع معدل الأخطاء بشكل حاد إلى 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 معلمات سطر الأوامر تقبل معلمتين: مصطلح البحث (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. كود المشروع الكامل

▶ مثال:التنفيذ الكامل لبرنامج 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"
// ============================================

▶ مثال:الترجمة والتشغيل

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

▶ مثال:تشغيل المثال

المثال 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

اجتازت جميع الاختبارات الوحدوية السبعة! وهي تغطي الحالات الاستثنائية مثل عمليات البحث العادية (مع مراعاة الأحرف الكبيرة والصغيرة ودون مراعاة الأحرف الكبيرة والصغيرة)، وعدم وجود نتائج مطابقة، والمحتوى الفارغ، والاستعلامات الفارغة، وتحليل المعلمات. هذه هي قوة نظام الاختبار في لغة 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);
    }
}

▶ مثال:الميزات المتقدمة — حساب أرقام الأسطر والسياق في نتائج البحث

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_surrounding المعلمة HashSet لإزالة التكرارات ومنع تكرار الإخراج عندما تكون الأسطر المتطابقة متجاورة. وتقوم >>> بتمييز الأسطر المتطابقة، بينما تُعرض الأسطر العادية مع مسافة بادئة.


▶ مثال:الميزات المتقدمة — البحث في ملفات متعددة والبحث التنازلي في المجلدات

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 --- لفصل النتائج الخاصة بكل ملف؛ وأخيرًا، يقدم ملخصًا لنتائج الفحص والمطابقة.



❓ أسئلة شائعة

س لماذا نستخدم env::args() بدلاً من std::env::args_os()؟
ج تُرجع args() مُكررًا من نوع Args، مما ينتج النوع String، وهو مناسب لمعظم الحالات. تُرجع args_os() OsString، الذي يمكنه التعامل مع المعلمات غير المكتوبة بترميز UTF-8، لكن استخدامه أكثر تعقيدًا. وبالنسبة لمتطلبات هذا المشروع، فإن args() كافٍ.
س ما الفرق بين القراءة سطراً سطراً باستخدام fs::read_to_string() وBufReader؟
ج يقوم fs::read_to_string() بقراءة الملف بأكمله إلى الذاكرة دفعة واحدة، وهو مناسب للملفات الصغيرة والمتوسطة الحجم. أما BufReader فيقرأ سطراً سطراً وهو مناسب للملفات الكبيرة (مثل ملفات السجلات التي يبلغ حجمها عدة مئات من الميجابايت)، مع استهلاك أقل للذاكرة. يستخدم هذا الدرس read_to_string للحفاظ على إيجاز الكود؛ إذا كنت تعالج ملفات كبيرة جداً، فيمكنك استخدام المكررات BufReader + lines() لمعالجتها سطراً سطراً.
س لماذا تتطلب الدالة search معلمة مدة صلاحية 'a؟
ج لأن &str في Vec<(usize, &str)> المُرجعة تشير إلى البيانات الموجودة في المعلمة contents. تُعلم علامة العمر 'a المُترجم بأن المرجع المُرجع له نفس عمر المعلمة contents. وهذا يسمح للمُترجم بالتأكد من أن بيانات contents لا تزال صالحة عند استخدام نتائج البحث، مما يمنع حدوث المراجع المعلقة.
س ما هي القواعد المتعلقة بقيمة متغير البيئة IGNORE_CASE؟
ج طالما أن IGNORE_CASE مُعيَّن بقيمة غير فارغة، يتم تجاهل حالة الأحرف. ويكون لتعيين IGNORE_CASE=1 أو IGNORE_CASE=true أو IGNORE_CASE=yes نفس التأثير. أما إذا لم يتم تعيين متغير البيئة (أي إذا كانت env::var ترجع Err) أو تم تعيينه إلى سلسلة فارغة، فيتم تطبيق التمييز بين الأحرف الكبيرة والصغيرة.
س لماذا نستخدم process::exit(1) بدلاً من مجرد استدعاء panic؟
ج يقوم process::exit(1) بإنهاء البرنامج برمز خروج محدد، وهو ما يناسب أدوات واجهة سطر الأوامر (CLI) — حيث يمكن للعملية الأم (مثل برنامج نصي شل) تحديد ما إذا كان التنفيذ ناجحًا أم لا بناءً على رمز الخروج. أما panic فيقوم بطباعة تتبع المكدس، وهو أمر غير سهل الاستخدام بالنسبة للمستخدمين النهائيين. والممارسة المعتادة لأدوات واجهة سطر الأوامر (CLI) هي إرجاع رمز الخروج 0 في حالة النجاح، ورمز خروج غير صفري في حالة حدوث خطأ.

📖 ملخص


📝 تمارين

  1. الصعوبة ⭐: إضافة ميزة «عدد التطابقات» إلى وظيفة البحث: استنادًا إلى الدالة search، أضف دالة جديدة count_matches تعيد عدد الأسطر المطابقة فقط دون إعادة محتوى السطر المحدد. على سبيل المثال، تعيد الدالة count_matches("Rust", contents, true) القيمة 3. أضف علامة --count إلى main (يتم تحديدها بواسطة المعلمة الثالثة)؛ إذا تم تعيينها إلى --count، فاطبع عدد الأسطر المطابقة فقط.

  2. الصعوبة ⭐⭐: أضف ميزة «أسطر السياق» إلى الأداة: قم بتعديل الدالة search بحيث تُرجع عددًا معينًا من الأسطر قبل وبعد السطر المطابق (على غرار grep -C). أضف معلمة جديدة، context_lines: usize، لتحديد عدد الأسطر المراد عرضها قبل وبعد السطر المطابق. على سبيل المثال، باستخدام context_lines=1، سيصاحب السطر المطابق السطر الذي يسبقه مباشرةً والسطر الذي يليه مباشرةً. تأكد من معالجة الحالات الاستثنائية (الأسطر الموجودة في بداية الملف أو نهايته لا تحتوي على سياق).

  3. الصعوبة ⭐⭐⭐: توسيع نطاق المشروع ليصبح «أداة بحث متعددة الملفات»: قم بتعديل البرنامج ليتمكن من البحث عبر ملفات متعددة. تنسيق سطر الأوامر الجديد هو minigrep <query> <file1> <file2> ... (حيث يكون عدد أسماء الملفات متغيرًا). قم بتغيير تنسيق الإخراج بحيث يسبق كل سطر مطابق اسم الملف، مثل file1.txt:5: Hello World. تلميح: استخدم حلقة (loop) لمعالجة ملفات متعددة. على الرغم من أن الوضع $()* ليس ماكرو، إلا أن المفهوم مشابه — «معالجة المنطق مرة واحدة وتطبيقه على مدخلات متعددة».

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%