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

العمل مع متغيرات البيئة (Environment Variables)

سنقوم بتحسين البرنامج الثنائي (Binary) لـ minigrep عن طريق إضافة ميزة إضافية: خيار للبحث غير الحساس لحالة الأحرف (Case-insensitive Searching) الذي يمكن للمستخدم تفعيله عبر متغير بيئة (Environment Variable). كان بإمكاننا جعل هذه الميزة خياراً لسطر الأوامر (Command Line Option) ومطالبة المستخدمين بإدخاله في كل مرة يريدون تطبيقه فيها، ولكن بجعله بدلاً من ذلك Environment Variable، فإننا نسمح لمستخدمينا بضبط Environment Variable مرة واحدة وجعل جميع عمليات البحث الخاصة بهم غير حساسة لحالة الأحرف في جلسة الطرفية (Terminal Session) تلك.

كتابة اختبار فاشل للبحث غير الحساس لحالة الأحرف

نضيف أولاً دالة (Function) جديدة باسم search_case_insensitive إلى مكتبة (Library) minigrep والتي سيتم استدعاؤها عندما يكون لـ Environment Variable قيمة. سنستمر في اتباع عملية التطوير القائم على الاختبار (TDD - Test-Driven Development)، لذا فإن الخطوة الأولى هي مرة أخرى كتابة اختبار فاشل (Failing Test). سنضيف اختباراً جديداً لـ Function الجديدة search_case_insensitive ونعيد تسمية اختبارنا القديم من one_result إلى case_sensitive لتوضيح الفروق بين الاختبارين، كما هو موضح في القائمة (Listing) 12-20.

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

لاحظ أننا قمنا بتحرير محتويات (Contents) الاختبار القديم أيضاً. لقد أضفنا سطراً جديداً بالنص "Duct tape." باستخدام حرف D كبير لا ينبغي أن يطابق الاستعلام (Query) "duct" عندما نبحث بطريقة حساسة لحالة الأحرف (Case-sensitive). يساعد تغيير الاختبار القديم بهذه الطريقة في ضمان عدم كسر وظيفة البحث الحساس لحالة الأحرف التي قمنا بتنفيذها بالفعل عن طريق الخطأ. يجب أن ينجح هذا الاختبار الآن ويستمر في النجاح بينما نعمل على البحث غير الحساس لحالة الأحرف.

يستخدم الاختبار الجديد للبحث غير الحساس لحالة الأحرف "rUsT" كـ Query خاص به. في Function التي أوشكنا على إضافتها search_case_insensitive ، يجب أن يطابق Query "rUsT" السطر الذي يحتوي على "Rust:" بحرف R كبير ويطابق السطر "Trust me." على الرغم من أن كليهما لهما حالة أحرف مختلفة عن Query. هذا هو Failing Test الخاص بنا، وسيفشل في التحويل البرمجي (Compile) لأننا لم نقم بعد بتعريف Function search_case_insensitive. لا تتردد في إضافة تنفيذ هيكلي (Skeleton Implementation) يعيد دائماً متجهاً (Vector) فارغاً، على غرار الطريقة التي اتبعناها مع Function search في Listing 12-16 لرؤية الاختبار وهو Compile ويفشل.

تنفيذ دالة search_case_insensitive

ستكون Function search_case_insensitive الموضحة في Listing 12-21، هي نفسها تقريباً Function search. الفرق الوحيد هو أننا سنقوم بتحويل Query وكل سطر (Line) إلى أحرف صغيرة (Lowercase) بحيث أياً كانت حالة أحرف وسائط الإدخال (Input Arguments)، فإنها ستكون بنفس حالة الأحرف عندما نتحقق مما إذا كان Line يحتوي على Query.

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

أولاً، نقوم بتحويل سلسلة (String) الـ Query إلى Lowercase ونخزنها في متغير (Variable) جديد بنفس الاسم، مما يحجب (Shadowing) الـ Query الأصلي. استدعاء to_lowercase على Query ضروري بحيث بغض النظر عما إذا كان Query الخاص بالمستخدم هو "rust" أو "RUST" أو "Rust" أو "rUsT"، فإننا سنعامل Query كما لو كان "rust" ونكون غير حساسين للحالة. بينما سيتعامل to_lowercase مع ترميز يونيكود (Unicode) الأساسي، إلا أنه لن يكون دقيقاً بنسبة 100 بالمائة. إذا كنا نكتب تطبيقاً حقيقياً، فسنرغب في القيام بمزيد من العمل هنا، ولكن هذا القسم يتعلق بـ Environment Variables وليس Unicode، لذا سنكتفي بذلك هنا.

لاحظ أن Query هو الآن String بدلاً من شريحة سلسلة (String Slice) لأن استدعاء to_lowercase ينشئ بيانات جديدة بدلاً من الإشارة إلى بيانات موجودة. لنفترض أن Query هو "rUsT" كمثال: String Slice تلك لا تحتوي على حرف u أو t صغير لنستخدمه، لذا يتعين علينا تخصيص (Allocate) String جديد يحتوي على "rust". عندما نمرر Query كـ Argument إلى طريقة (Method) contains الآن، نحتاج إلى إضافة علامة أند (Ampersand) لأن توقيع (Signature) contains مُعرف ليأخذ String Slice.

بعد ذلك، نضيف استدعاءً لـ to_lowercase على كل Line لتحويل جميع الأحرف إلى أحرف صغيرة. الآن بعد أن قمنا بتحويل Line و Query إلى Lowercase، سنجد التطابقات بغض النظر عن حالة أحرف 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

رائع! لقد اجتازت الاختبارات. الآن دعونا نستدعي Function الجديدة search_case_insensitive من Function run. أولاً، سنضيف خيار تكوين (Configuration Option) إلى هيكل (Struct) Config للتبديل بين البحث الحساس وغير الحساس لحالة الأحرف. ستؤدي إضافة هذا الحقل (Field) إلى حدوث أخطاء في المترجم (Compiler Errors) لأننا لم نقم بتهيئة (Initializing) هذا Field في أي مكان بعد:

اسم الملف: 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(())
}

لقد أضفنا Field ignore_case الذي يحمل قيمة بولينية (Boolean). بعد ذلك، نحتاج إلى Function run للتحقق من قيمة Field ignore_case واستخدام ذلك لتقرير ما إذا كان سيتم استدعاء Function search أو Function search_case_insensitive كما هو موضح في Listing 12-22. هذا لا يزال لن يتم Compile بعد.

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

أخيراً، نحتاج إلى التحقق من Environment Variable. الدوال المخصصة للعمل مع Environment Variables موجودة في وحدة (Module) env في المكتبة القياسية (Standard Library)، والتي هي موجودة بالفعل في النطاق (Scope) في أعلى ملف src/main.rs. سنستخدم Function var من Module env للتحقق مما إذا كان قد تم تعيين أي قيمة لـ Environment Variable يسمى IGNORE_CASE كما هو موضح في Listing 12-23.

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

هنا، نقوم بإنشاء Variable جديد باسم ignore_case. لتعيين قيمته، نستدعي Function env::var ونمرر لها اسم Environment Variable IGNORE_CASE. تعيد Function env::var نتيجة (Result) ستكون متغير (Variant) النجاح Ok الذي يحتوي على قيمة Environment Variable إذا تم تعيين Environment Variable لأي قيمة. وستعيد Variant الخطأ Err إذا لم يتم تعيين Environment Variable.

نحن نستخدم Method is_ok على Result للتحقق مما إذا كان Environment Variable قد تم تعيينه، مما يعني أن البرنامج يجب أن يقوم ببحث غير حساس لحالة الأحرف. إذا لم يتم تعيين Environment Variable IGNORE_CASE لأي شيء، فإن is_ok ستعيد false وسيقوم البرنامج بإجراء بحث حساس لحالة الأحرف. نحن لا نهتم بـ قيمة Environment Variable، فقط ما إذا كان معيناً أو غير معين، لذا نتحقق من is_ok بدلاً من استخدام unwrap أو expect أو أي من الطرق الأخرى التي رأيناها في Result.

نمرر القيمة الموجودة في Variable ignore_case إلى مثيل (Instance) Config بحيث يمكن لـ Function run قراءة تلك القيمة وتقرير ما إذا كان سيتم استدعاء search_case_insensitive أو search كما نفذنا في Listing 12-22.

دعونا نجرب ذلك! أولاً، سنقوم بتشغيل برنامجنا دون تعيين Environment Variable ومع Query to ، والذي يجب أن يطابق أي Line يحتوي على كلمة to بجميع الأحرف الصغيرة:

$ cargo run -- to poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

يبدو أن هذا لا يزال يعمل! الآن دعونا نشغل البرنامج مع تعيين IGNORE_CASE إلى 1 ولكن بنفس Query to:

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

إذا كنت تستخدم PowerShell، فستحتاج إلى تعيين Environment Variable وتشغيل البرنامج كأوامر منفصلة:

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

سيؤدي هذا إلى جعل IGNORE_CASE يستمر لبقية Terminal Session الخاصة بك. يمكن إلغاء تعيينه باستخدام الأمر (Cmdlet) Remove-Item:

PS> Remove-Item Env:IGNORE_CASE

يجب أن نحصل على Lines تحتوي على to والتي قد تحتوي على أحرف كبيرة:

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

ممتاز، لقد حصلنا أيضاً على Lines تحتوي على To! يمكن لبرنامج minigrep الخاص بنا الآن إجراء بحث غير حساس لحالة الأحرف يتم التحكم فيه بواسطة Environment Variable. الآن أنت تعرف كيفية إدارة الخيارات المحددة باستخدام إما وسائط سطر الأوامر (Command Line Arguments) أو Environment Variables.

تسمح بعض البرامج بـ Arguments و Environment Variables لنفس التكوين. في تلك الحالات، تقرر البرامج أن أحدهما له الأولوية. لتمرين آخر بمفردك، حاول التحكم في حساسية حالة الأحرف من خلال إما Command Line Argument أو Environment Variable. قرر ما إذا كان يجب أن تكون الأولوية لـ Command Line Argument أو Environment Variable إذا تم تشغيل البرنامج مع ضبط أحدهما على الحساسية لحالة الأحرف والآخر على تجاهل حالة الأحرف.

يحتوي Module std::env على العديد من الميزات المفيدة الأخرى للتعامل مع Environment Variables: راجع وثائقه (Documentation) لمعرفة ما هو متاح.