Rust: Rust في التطبيق العملي
آخر تحديث: 2026-08-26
هذه هي الحلقة الأخيرة من سلسلة دروس لغة Rust — حيث سنستخدم كل ما تعلمته في الدروس الثلاثين الأولى لإنشاء أداة فعالة للبحث عن الملفات عبر سطر الأوامر (نسخة مبسطة من
grep). وسنغطي كل شيء بدءًا من المتطلبات وصولاً إلى الكود، ومن الاختبار وصولاً إلى التنفيذ، دون تخطي أي خطوة.
إذا كانت الدروس الثلاثين الأولى تدور حول «تعلم الحركات»، فإن هذا الدرس يدور حول «تطبيقها عمليًّا». الأمر أشبه بإتقان جميع تقنيات الطهي ثم القيام فعليًّا بإعداد وجبة كاملة للضيوف. قد تجد أن توقيتك ليس دقيقًا تمامًا بعد أو أن مهاراتك في استخدام السكين لم تصل إلى الكمال بعد — ولكن بمجرد الانتهاء من إعداد هذا الطبق، ستكون قد انتقلت حقًّا من مرحلة «تعلم لغة Rust» إلى مرحلة «القدرة على كتابة كود بلغة Rust».
1. ما ستتعلمه
- التطبيق الشامل لأساسيات لغة Rust: الملكية، ومعالجة السلاسل، وVec، ومعالجة الأخطاء باستخدام Result
- تصميم بنية وظيفية لأداة تعمل عبر واجهة الأوامر (CLI): تحليل المعلمات → قراءة الملفات → معالجة البيانات → إخراج النتائج
- استخدم
std::env::args()لتحليل معلمات سطر الأوامر - استخدم
std::fs::read_to_string()وBufReadلقراءة الملف سطراً سطراً - كتابة اختبارات الوحدة للتحقق من صحة منطق البحث
2. القصة: البحث عن إبرة في بحر من جذوع الأشجار
(1) عناء مراجعة السجلات يدويًّا
توم مسؤول عن صيانة خدمة معالجة الطلبات في إحدى منصات التجارة الإلكترونية. في وقت متأخر من ليلة الأربعاء، انطلق نظام الإنذار — فقد فشل عدد كبير من الطلبات، وارتفع معدل الأخطاء بشكل حاد إلى 30%.
- أرسلت إدارة العمليات ملف سجل بحجم 200 ميغابايت
order-service.log - ابحث عن جميع الأسطر التي تحتوي على الكلمة الرئيسية
ERROR - فتح توم المحرر للبحث — فتعطل المحرر (كان حجم الملف كبيرًا جدًّا).
- استخدم
grepالمدمج في النظام — لكن توم لا يمتلكه على جهاز ويندوز الخاص به - باستخدام
Select-Stringفي PowerShell — لم أستطع تذكر صيغة الأمر، لذا قضيت وقتًا طويلاً في البحث عنها في الوثائق
بعد ثلاثين دقيقة، توصل توم أخيرًا إلى سبب الخطأ: فقد نفد مخزون اتصالات قاعدة البيانات. لكن تلك الثلاثين دقيقة الثمينة كانت قد ضاعت بالفعل في «البحث عن الأداة المناسبة».
«لو كان لدي أداة بحث قمت ببرمجتها بنفسي، لكان بإمكاني القيام بذلك في غضون ثوانٍ...»
(2) اقتراحنا
اليوم، سأقوم بكتابة أداة بحث عن الملفات عبر سطر الأوامر بلغة Rust (grep-lite)، والتي:
- قبول مصطلحات البحث ومسارات الملفات من سطر الأوامر
- البحث سطراً سطراً، وطباعة الأسطر المطابقة وأرقامها
- يدعم كلاً من الوضع الذي يراعي التمييز بين الأحرف الكبيرة والصغيرة والوضع الذي لا يراعي ذلك
- يتميز بمعالجة أخطاء فعالة
# 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) بنية المشروع
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) هيكل المشروع
minigrep/
├── Cargo.toml
└── src/
└── main.rs # All the code is in main.rs (Standard Library Version)
(2) Cargo.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 (الكود الكامل)
// ============================================
// 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"
// ============================================
▶ مثال:الترجمة والتشغيل
# 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:
Hello World
Rust is awesome
rust is fast
I love Rust
RUST is powerful
Goodbye
الأمر:
cargo run -- "Rust" test.txt
الناتج:
minigrep: found 2 match(es) for 'Rust' (case-sensitive)
---
1: Hello World
3: I love Rust
المثال 2: تجاهل حالة الأحرف
الأمر:
IGNORE_CASE=1 cargo run -- "rust" test.txt
الناتج:
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: الملف غير موجود
الأمر:
cargo run -- "hello" nonexistent.txt
الناتج:
minigrep: error reading file: The system cannot find the file specified. (os error 2)
المثال 4: لا توجد مطابقة
الأمر:
cargo run -- "python" test.txt
الناتج:
minigrep: no matches found for 'python'
المثال 5: عدم كفاية الحجج
الأمر:
cargo run -- "hello"
الناتج:
usage: minigrep <query> <file_path>
(4) ناتج الاختبار
$ 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 الوظيفة
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))
}
- استلمت الشريحة
&[String](لا أملكها؛ بل استعرتها فحسب) - تحقق من عدد المعلمات؛ إذا لم يكن العدد كافيًا، فقم بإرجاع
Err(&'static str) - تُرجع
Ok((&str, &str))في حالة النجاح — شريحتين من السلسلة - استخدم النوع
Resultمن أجل معالجة موحدة للأخطاء
(2) search الوظيفة
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
}
- يشير استخدام تعليق دورة الحياة
'aإلى أن القيمة&strالتي يتم إرجاعها لها نفس دورة الحياة التي تتمتع بها المعلمةcontents. contents.lines()تُرجع مُكررًا لكل سطر (باستثناء أحرف نهاية السطر).enumerate()أضف رقمًا ترتيبيًّا لكل سطر (بدءًا من 0؛ وعند الإخراج، أضف 1 ليبدأ من 1)- عند تجاهل التمييز بين الأحرف الكبيرة والصغيرة، قم بتحويل كل من
queryوlineإلى أحرف صغيرة قبل مقارنتهما
(3) run الوظيفة
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))?;
// ...
}
?المشغل: إذا أعادparse_argsالقيمةErr، فسيتم ترحيل الخطأ تلقائيًاenv::var("IGNORE_CASE"): قراءة متغيرات البيئةfs::read_to_string: قراءة الملف إلىStringفي عملية واحدة.map_err(): تحويل أنواع الأخطاء الواردة في المكتبة القياسية إلى سلاسل نصية سهلة الفهم
(4) main الوظيفة
fn main() {
let args: Vec<String> = env::args().collect();
if let Err(err_msg) = run(&args) {
eprintln!("{}", err_msg);
process::exit(1);
}
}
env::args().collect(): جمع معلمات سطر الأوامر إلىVec<String>if let Err(...): يعالج النتيجة التي تُرجعهاrunباستخدام مطابقة الأنماطeprintln!: يتم إخراج رسائل الخطأ إلى stderr (الخطأ القياسي) بدلاً من stdoutprocess::exit(1): إنهاء البرنامج برمز خروج غير صفر (مما يشير إلى إنهاء غير طبيعي)
▶ مثال:الميزات المتقدمة — حساب أرقام الأسطر والسياق في نتائج البحث
// ============================================
// 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لإزالة التكرارات ومنع تكرار الإخراج عندما تكون الأسطر المتطابقة متجاورة. وتقوم>>>بتمييز الأسطر المتطابقة، بينما تُعرض الأسطر العادية مع مسافة بادئة.
▶ مثال:الميزات المتقدمة — البحث في ملفات متعددة والبحث التنازلي في المجلدات
// ============================================
// 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 في حالة النجاح، ورمز خروج غير صفري في حالة حدوث خطأ.📖 ملخص
- بنية المشروع: تعتمد أداة CLI على بنية
main → run → searchثلاثية المستويات، حيث تعملmainكنقطة دخول فقط وتتعامل مع معالجة الأخطاء، بينما تتولىrunمسؤولية منطق التنسيق، وتتألفsearchمن دوال بحتة. - معلمات سطر الأوامر: استخدم
std::env::args()لجمع المعلمات، وقم بتخصيص الدالةparse_argsللتحقق من صحة عدد المعلمات، واستخدمResultلمعالجة الأخطاء بشكل موحد - قراءة الملفات: يقوم
std::fs::read_to_string()بقراءة الملفات الصغيرة في مسار واحد؛ أماBufReader+lines()فهما مناسبان لمعالجة الملفات الكبيرة سطراً سطراً - التحكم في متغيرات البيئة: يقوم
std::env::var("IGNORE_CASE")بقراءة متغيرات البيئة ويستخدم مطابقة الأنماطmatchلتحديد ما إذا كانت هذه المتغيرات قد تم تعيينها أم لا - معالجة الأخطاء: استخدم
Result<(), String>كنوع القيمة المرجعة؛?ينقل الأخطاء؛eprintln!يخرج النتائج إلى stderr؛process::exit(1)يحدد رمز الخروج - اختبار الوحدات: اكتب اختبارات باستخدام
#[cfg(test)]و#[test]لتغطية ثلاثة سيناريوهات — العادي، والحدودي، والخطأ — وقم بتشغيلها بنقرة واحدة باستخدامcargo test
📝 تمارين
-
الصعوبة ⭐: إضافة ميزة «عدد التطابقات» إلى وظيفة البحث: استنادًا إلى الدالة
search، أضف دالة جديدةcount_matchesتعيد عدد الأسطر المطابقة فقط دون إعادة محتوى السطر المحدد. على سبيل المثال، تعيد الدالةcount_matches("Rust", contents, true)القيمة3. أضف علامة--countإلىmain(يتم تحديدها بواسطة المعلمة الثالثة)؛ إذا تم تعيينها إلى--count، فاطبع عدد الأسطر المطابقة فقط. -
الصعوبة ⭐⭐: أضف ميزة «أسطر السياق» إلى الأداة: قم بتعديل الدالة
searchبحيث تُرجع عددًا معينًا من الأسطر قبل وبعد السطر المطابق (على غرارgrep -C). أضف معلمة جديدة،context_lines: usize، لتحديد عدد الأسطر المراد عرضها قبل وبعد السطر المطابق. على سبيل المثال، باستخدامcontext_lines=1، سيصاحب السطر المطابق السطر الذي يسبقه مباشرةً والسطر الذي يليه مباشرةً. تأكد من معالجة الحالات الاستثنائية (الأسطر الموجودة في بداية الملف أو نهايته لا تحتوي على سياق). -
الصعوبة ⭐⭐⭐: توسيع نطاق المشروع ليصبح «أداة بحث متعددة الملفات»: قم بتعديل البرنامج ليتمكن من البحث عبر ملفات متعددة. تنسيق سطر الأوامر الجديد هو
minigrep <query> <file1> <file2> ...(حيث يكون عدد أسماء الملفات متغيرًا). قم بتغيير تنسيق الإخراج بحيث يسبق كل سطر مطابق اسم الملف، مثلfile1.txt:5: Hello World. تلميح: استخدم حلقة (loop) لمعالجة ملفات متعددة. على الرغم من أن الوضع$()*ليس ماكرو، إلا أن المفهوم مشابه — «معالجة المنطق مرة واحدة وتطبيقه على مدخلات متعددة».