Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

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

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

Filename: src/lib.rs
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
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 را به حروف کوچک تبدیل می‌کنیم و آن را در متغیر جدیدی با همان نام ذخیره می‌کنیم و مقدار اصلی query را شَدو (shadow) می‌کنیم. فراخوانی to_lowercase بر روی query ضروری است تا صرف‌نظر از این‌که کاربر عبارت مورد جستجوی خود را به صورت "rust"، "RUST"، "Rust" یا "rUsT" وارد کند، ما با آن گویی که "rust" وارد شده است برخورد کنیم و نسبت به حروف کوچک و بزرگ حساس نباشیم. هرچند to_lowercase نگاشت پایه‌ی Unicode را انجام می‌دهد، اما صد درصد دقیق نخواهد بود. اگر قصد نوشتن یک برنامه واقعی را داشتیم، نیاز به کار بیشتری در این بخش بود، اما از آن‌جا که این بخش در مورد متغیرهای محیطی است، نه Unicode، در همین حد باقی می‌مانیم.

توجه کنید که اکنون 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/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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 })
    }
}

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(())
}

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/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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 })
    }
}

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(())
}
Listing 12-22: Calling either search or search_case_insensitive based on the value in config.ignore_case

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

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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,
        })
    }
}

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(())
}
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 ویژگی‌های مفید بسیاری برای کار با متغیرهای محیطی دارد: مستندات آن را بررسی کنید تا ببینید چه امکاناتی در دسترس است.