مقدمه

تاکنون، برنامه minigrep ما یک قابلیت اصلی دارد: جستجوی یک رشته در یک فایل. حالا می‌خواهیم یک قابلیت جدید به آن اضافه کنیم: امکان انجام جستجوی غیرحساس به حروف بزرگ و کوچک (case-insensitive). ما می‌توانیم این قابلیت را از طریق یک آرگومان خط فرمان دیگر کنترل کنیم، اما یک روش رایج دیگر برای تنظیم رفتار برنامه‌های خط فرمان، استفاده از «متغیرهای محیطی» (Environment Variables) است.

یک متغیر محیطی، یک متغیر است که در خارج از برنامه ما و توسط پوسته (shell) سیستم عامل تنظیم می‌شود. استفاده از آنها برای گزینه‌هایی که ممکن است در هر بار اجرای برنامه تغییر نکنند، بسیار مناسب است. در این درس، ما یک متغیر محیطی به نام IGNORE_CASE را پیاده‌سازی خواهیم کرد که اگر مقدار آن 1 باشد، جستجو به صورت غیرحساس به حروف انجام خواهد شد.

نوشتن تست شکست‌خورده برای قابلیت جدید

ما با رویکرد TDD ادامه می‌دهیم و ابتدا تست‌های خود را برای منطق جدید می‌نویسیم. ما به دو تابع جستجوی جدید نیاز خواهیم داشت: یکی برای جستجوی حساس به حروف (رفتار فعلی) و دیگری برای جستجوی غیرحساس به حروف.

Copy Icon src/lib.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

تست اول، رفتار فعلی تابع search را تأیید می‌کند. تست دوم، برای تابع جدید search_case_insensitive است که هنوز آن را ننوشته‌ایم. اجرای cargo test در این مرحله با شکست مواجه خواهد شد.

پیاده‌سازی جستجوی غیرحساس به حروف

حالا تابع search_case_insensitive را پیاده‌سازی می‌کنیم. منطق آن بسیار شبیه به تابع search است، با این تفاوت که قبل از مقایسه، هم query و هم هر خط از محتوا را به حروف کوچک تبدیل می‌کنیم.

Copy Icon src/lib.rs
pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

با افزودن این تابع، هر دو تست ما با موفقیت پاس خواهند شد.

خواندن متغیر محیطی

اکنون باید منطق برنامه اصلی را طوری تغییر دهیم که بر اساس متغیر محیطی IGNORE_CASE، یکی از دو تابع جستجو را فراخوانی کند. برای خواندن متغیرهای محیطی، از ماژول std::env و تابع var استفاده می‌کنیم.

تابع env::var() یک Result برمی‌گرداند. اگر متغیر محیطی تنظیم شده باشد، یک Ok حاوی مقدار آن را برمی‌گرداند. اگر تنظیم نشده باشد، یک Err برمی‌گرداند. ما این منطق را به Config خود اضافه می‌کنیم.

Copy Icon src/main.rs
// In Config struct
pub struct Config {
    // ... other fields
    pub ignore_case: bool,
}

// In Config::build function
impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        // ... argument parsing ...
        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config { query, file_path, ignore_case })
    }
}

// In run function
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    // ... read file contents ...
    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    // ... print results ...
    Ok(())
}

ما یک فیلد جدید به نام ignore_case به Config اضافه کرده‌ایم. در تابع build، با استفاده از env::var("IGNORE_CASE").is_ok() بررسی می‌کنیم که آیا این متغیر محیطی تنظیم شده است یا خیر (مقدار آن مهم نیست، صرفاً وجود داشتنش کافی است). سپس در تابع run، بر اساس مقدار این فیلد بولی، تابع جستجوی مناسب را فراخوانی می‌کنیم.

حالا می‌توانید برنامه را به دو صورت اجرا کرده و نتایج متفاوت را مشاهده کنید:

# Case-sensitive search (default)
$ cargo run -- to poem.txt

# Case-insensitive search
$ IGNORE_CASE=1 cargo run -- to poem.txt
                    

در این درس با استفاده از متغیرهای محیطی به عنوان روشی برای پیکربندی رفتار برنامه آشنا شدیم و با رویکرد TDD یک قابلیت جدید به ابزار خود اضافه کردیم. این کار به ما اجازه می‌دهد تا بدون تغییر آرگومان‌های خط فرمان، رفتار برنامه را کنترل کنیم. در درس نهایی این فصل، به بررسی نحوه «ارسال پیام‌های خطا به StdErr» خواهیم پرداخت تا ابزار خط فرمان خود را حرفه‌ای‌تر کنیم.