کار با متغیرهای محیطی

ما قصد داریم برنامه minigrep را با افزودن یک ویژگی جدید بهبود دهیم: گزینه‌ای برای جستجوی حساس به حروف کوچک و بزرگ که کاربر می‌تواند آن را از طریق یک متغیر محیطی فعال کند. ما می‌توانیم این ویژگی را به عنوان یک گزینه خط فرمان قرار دهیم و کاربران را ملزم کنیم که هر بار که می‌خواهند این ویژگی اعمال شود آن را وارد کنند، اما با استفاده از یک متغیر محیطی به جای آن، به کاربران اجازه می‌دهیم که فقط یک بار متغیر محیطی را تنظیم کنند و همه جستجوهایشان در همان نشست ترمینال به صورت غیرحساس به حروف کوچک و بزرگ باشد.

نوشتن یک تست شکست‌خورده برای تابع search_case_insensitive

ابتدا یک تابع جدید به نام search_case_insensitive اضافه می‌کنیم که زمانی که متغیر محیطی دارای مقدار باشد، فراخوانی خواهد شد. ما همچنان از فرآیند TDD پیروی می‌کنیم، بنابراین اولین گام، نوشتن یک تست شکست‌خورده است. یک تست جدید برای تابع search_case_insensitive اضافه می‌کنیم و تست قدیمی خود را از one_result به case_sensitive تغییر نام می‌دهیم تا تفاوت بین این دو تست مشخص شود، همان‌طور که در لیستینگ 12-20 نشان داده شده است.

Filename: src/lib.rs
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

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

    results
}

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

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

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

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

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-20: افزودن یک تست شکست‌خورده جدید برای تابع غیرحساس به حروف کوچک و بزرگ که قصد داریم اضافه کنیم

توجه کنید که ما متن تست قدیمی را نیز ویرایش کرده‌ایم. ما یک خط جدید با متن "Duct tape." با حرف بزرگ D اضافه کرده‌ایم که نباید با عبارت جستجو "duct" در حالت حساس به حروف کوچک و بزرگ مطابقت داشته باشد. تغییر دادن تست قدیمی به این صورت کمک می‌کند که مطمئن شویم عملکرد جستجوی حساس به حروف کوچک و بزرگ که قبلاً پیاده‌سازی کرده‌ایم به طور تصادفی شکسته نمی‌شود. این تست باید اکنون عبور کند و همچنان باید عبور کند در حالی که ما روی جستجوی غیرحساس به حروف کار می‌کنیم.

تست جدید برای جستجوی غیرحساس به حروف کوچک و بزرگ از "rUsT" به عنوان عبارت جستجو استفاده می‌کند. در تابع search_case_insensitive که قصد داریم اضافه کنیم، عبارت جستجوی "rUsT" باید با خط حاوی "Rust:" با حرف بزرگ R و خط "Trust me." مطابقت داشته باشد، حتی اگر هر دو حالت متفاوتی نسبت به عبارت جستجو داشته باشند. این تست شکست‌خورده ما است و به دلیل اینکه هنوز تابع search_case_insensitive تعریف نشده است، کامپایل نخواهد شد. می‌توانید یک پیاده‌سازی موقتی که همیشه یک وکتور خالی برمی‌گرداند اضافه کنید، مشابه کاری که برای تابع search در لیستینگ 12-16 انجام دادیم تا تست کامپایل شده و شکست بخورد.

پیاده‌سازی تابع search_case_insensitive

تابع search_case_insensitive که در لیستینگ 12-21 نشان داده شده است، تقریباً مشابه تابع search خواهد بود. تنها تفاوت این است که ما عبارت جستجو و هر خط را کوچک‌حرف می‌کنیم تا صرف‌نظر از مورد ورودی‌ها، هنگام بررسی اینکه آیا خط شامل عبارت جستجو است، هر دو به یک مورد تبدیل شوند.

Filename: src/lib.rs
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

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

    results
}

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
}

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

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

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

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

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-21: تعریف تابع search_case_insensitive برای کوچک‌حرف کردن عبارت جستجو و خط قبل از مقایسه آنها

ابتدا عبارت جستجوی query را کوچک‌حرف می‌کنیم و آن را در یک متغیر جدید با همان نام ذخیره می‌کنیم، جایگزین متغیر اصلی می‌شود. فراخوانی to_lowercase بر روی عبارت جستجو ضروری است تا صرف‌نظر از اینکه عبارت جستجو "rust"، "RUST"، "Rust" یا "rUsT" باشد، به گونه‌ای عمل کنیم که انگار عبارت جستجو "rust" است و به حروف کوچک و بزرگ حساس نباشد. در حالی که to_lowercase یونیکد پایه‌ای را مدیریت می‌کند، اما 100٪ دقیق نخواهد بود. اگر ما یک برنامه واقعی می‌نوشتیم، می‌خواستیم در اینجا کمی بیشتر کار کنیم، اما این بخش درباره متغیرهای محیطی است، نه یونیکد، بنابراین در اینجا به همین میزان بسنده می‌کنیم.

توجه کنید که اکنون query یک رشته (String) به جای برش رشته (string slice) است، زیرا فراخوانی to_lowercase داده‌های جدید ایجاد می‌کند به جای اینکه به داده‌های موجود اشاره کند. به عنوان مثال، بگویید عبارت جستجو "rUsT" است: آن رشته شامل یک u یا t کوچک نیست که بتوانیم استفاده کنیم، بنابراین باید یک String جدید شامل "rust" تخصیص دهیم. وقتی اکنون query را به عنوان یک آرگومان به متد contains منتقل می‌کنیم، نیاز داریم که یک علامت & اضافه کنیم چون امضای contains به گونه‌ای تعریف شده است که یک برش رشته دریافت می‌کند.

بعداً یک فراخوانی به to_lowercase بر روی هر line اضافه می‌کنیم تا همه کاراکترها کوچک‌حرف شوند. اکنون که line و query را به کوچک‌حرف تبدیل کرده‌ایم، مطمئن می‌شویم که مطابقت‌ها صرف‌نظر از مورد عبارت جستجو پیدا شوند.

بیایید ببینیم آیا این پیاده‌سازی تست‌ها را پاس می‌کند یا خیر:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

عالی! تست‌ها پاس شدند. حالا بیایید تابع جدید search_case_insensitive را از تابع run فراخوانی کنیم. ابتدا یک گزینه پیکربندی به ساختار Config اضافه می‌کنیم تا بین جستجوی حساس به حروف کوچک و بزرگ و غیرحساس به حروف کوچک و بزرگ سوئیچ کنیم. افزودن این فیلد باعث ایجاد خطاهای کامپایل می‌شود زیرا هنوز این فیلد را در هیچ جا مقداردهی نکرده‌ایم:

Filename: src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

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

    results
}

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
}

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

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

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

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

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

We added the ignore_case field that holds a Boolean. Next, we need the run function to check the ignore_case field’s value and use that to decide whether to call the search function or the search_case_insensitive function, as shown in Listing 12-22. This still won’t compile yet.

Filename: src/lib.rs
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

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

    results
}

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
}

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

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

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

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

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-22: Calling either search or search_case_insensitive based on the value in config.ignore_case

توابع مربوط به کار با متغیرهای محیطی در ماژول env در کتابخانه استاندارد قرار دارند. بنابراین در بالای فایل src/lib.rs این ماژول را وارد محدوده (scope) می‌کنیم. سپس از تابع var از ماژول env استفاده خواهیم کرد تا بررسی کنیم آیا مقدار خاصی برای یک متغیر محیطی به نام IGNORE_CASE تنظیم شده است یا خیر، همان‌طور که در لیستینگ 12-23 نشان داده شده است.

Filename: src/lib.rs
use std::env;
// --snip--

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

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

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

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

    results
}

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
}

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

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

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

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

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}
Listing 12-23: بررسی مقدار متغیر محیطی با نام IGNORE_CASE

اینجا یک متغیر جدید به نام ignore_case ایجاد می‌کنیم. برای مقداردهی آن، تابع env::var را فراخوانی کرده و نام متغیر محیطی IGNORE_CASE را به آن می‌دهیم. تابع env::var یک Result برمی‌گرداند که در صورت تنظیم بودن متغیر محیطی به هر مقداری، مقدار Ok با مقدار متغیر محیطی را دارد. اگر متغیر محیطی تنظیم نشده باشد، مقدار Err برگردانده می‌شود.

ما از متد is_ok روی Result استفاده می‌کنیم تا بررسی کنیم که آیا متغیر محیطی تنظیم شده است، که نشان می‌دهد برنامه باید جستجو را به صورت غیرحساس به حروف کوچک و بزرگ انجام دهد. اگر متغیر محیطی IGNORE_CASE به هیچ مقداری تنظیم نشده باشد، is_ok مقدار false برمی‌گرداند و برنامه جستجو را به صورت حساس به حروف کوچک و بزرگ انجام می‌دهد. ما به مقدار متغیر محیطی نیازی نداریم، فقط می‌خواهیم بررسی کنیم که آیا تنظیم شده است یا نه. بنابراین از is_ok به جای متدهایی مانند unwrap، expect یا دیگر متدهای مرتبط با Result استفاده می‌کنیم.

ما مقدار متغیر ignore_case را به نمونه Config منتقل می‌کنیم تا تابع run بتواند این مقدار را بخواند و تصمیم بگیرد که آیا باید تابع search_case_insensitive یا search را فراخوانی کند.

امتحان کردن برنامه

حالا بیایید برنامه را امتحان کنیم! ابتدا برنامه را بدون تنظیم متغیر محیطی و با عبارت جستجوی to اجرا می‌کنیم. این عبارت باید با هر خطی که شامل کلمه to به صورت تمام حروف کوچک باشد، مطابقت داشته باشد:

$ cargo run -- to poem.txt

برنامه همچنان باید به درستی کار کند و تنها خطوطی که کاملاً با عبارت مطابقت دارند را برگرداند. حالا برنامه را با متغیر محیطی IGNORE_CASE که به مقدار 1 تنظیم شده است اجرا می‌کنیم و همان عبارت جستجو to را امتحان می‌کنیم:

$ IGNORE_CASE=1 cargo run -- to poem.txt

در صورت استفاده از PowerShell، نیاز است که متغیر محیطی را تنظیم کنید و سپس برنامه را به صورت دستورات جداگانه اجرا کنید:

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt

این دستور باعث می‌شود که IGNORE_CASE برای مدت زمان نشست ترمینال شما تنظیم باقی بماند. می‌توانید آن را با دستور Remove-Item حذف کنید:

PS> Remove-Item Env:IGNORE_CASE

برنامه باید خطوطی که شامل to هستند و ممکن است حروف بزرگ داشته باشند را برگرداند:

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

عالی! حالا برنامه minigrep ما می‌تواند جستجوهای غیرحساس به حروف کوچک و بزرگ را انجام دهد که با یک متغیر محیطی کنترل می‌شود. حالا شما می‌دانید چگونه گزینه‌هایی را که از طریق آرگومان‌های خط فرمان یا متغیرهای محیطی تنظیم می‌شوند مدیریت کنید.

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

ماژول std::env ویژگی‌های مفید بسیاری برای کار با متغیرهای محیطی دارد: مستندات آن را بررسی کنید تا ببینید چه امکاناتی در دسترس است.