Environment Variable-দের সাথে কাজ করা

আমরা minigrep-কে আরও উন্নত করব একটি extra feature যোগ করে: case-insensitive searching-এর জন্য একটি option যা user একটি environment variable-এর মাধ্যমে চালু করতে পারবে। আমরা এই feature-টিকে একটি command line option করতে পারতাম এবং users-দের প্রতিবার এটি apply করতে চাইলে সেটি enter করতে বলতে পারতাম, কিন্তু পরিবর্তে এটিকে একটি environment variable করে, আমরা আমাদের user-দের environment variable-টি একবার set করার অনুমতি দিই এবং সেই terminal session-এ তাদের সমস্ত search case-insensitive হয়।

Case-Insensitive search Function-এর জন্য একটি Failing Test লেখা

আমরা প্রথমে একটি নতুন search_case_insensitive function যোগ করি যেটি environment variable-টির একটি value থাকলে call করা হবে। আমরা TDD process টি follow করা চালিয়ে যাব, তাই প্রথম step টি হল আবার একটি failing test লেখা। আমরা নতুন search_case_insensitive function-এর জন্য একটি নতুন test যোগ করব এবং আমাদের পুরানো test-এর নাম one_result থেকে case_sensitive-এ পরিবর্তন করব যাতে দুটি test-এর মধ্যে পার্থক্য স্পষ্ট হয়, যেমনটি Listing 12-20-তে দেখানো হয়েছে।

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

লক্ষ্য করুন যে আমরা পুরানো test-এর contents-ও edit করেছি। আমরা "Duct tape." text-সহ একটি নতুন line যোগ করেছি যেখানে একটি capital D রয়েছে যা "duct" query-র সাথে match করা উচিত নয় যখন আমরা case-sensitive পদ্ধতিতে search করছি। এইভাবে পুরানো test টি পরিবর্তন করা নিশ্চিত করতে সাহায্য করে যে আমরা accidental ভাবে case-sensitive search functionality break করছি না যা আমরা ইতিমধ্যেই implement করেছি। এই test টি এখন pass করা উচিত এবং case-insensitive search-এ কাজ করার সময় এটি pass করতে থাকা উচিত।

Case-insensitive search-এর জন্য নতুন test টি "rUsT" কে তার query হিসেবে ব্যবহার করে। আমরা যে search_case_insensitive function টি যোগ করতে যাচ্ছি, তাতে "rUsT" query-টি "Rust:"-যুক্ত line-টির সাথে match করা উচিত যেখানে একটি capital R রয়েছে এবং "Trust me." line-টির সাথেও match করা উচিত, যদিও query-র থেকে দুটোতেই আলাদা casing রয়েছে। এটি আমাদের failing test, এবং এটি compile হতে fail করবে কারণ আমরা এখনও search_case_insensitive function টি define করিনি। Listing 12-16-এ search function-এর জন্য যেভাবে করেছিলাম, সেভাবে একটি skeleton implementation যোগ করতে পারেন যেটি সব সময় একটি empty vector return করে, যাতে test compile হয়ে fail করে।

search_case_insensitive Function Implement করা

Listing 12-21-এ দেখানো search_case_insensitive function টি প্রায় search function-এর মতোই হবে। পার্থক্য শুধুমাত্র এই যে আমরা query এবং প্রতিটি line-কে lowercase করব যাতে input argument-গুলোর case যাই হোক না কেন, line-টিতে query আছে কিনা তা check করার সময় সেগুলি একই case-এর হবে।

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

প্রথমে আমরা query string-টিকে lowercase করি এবং এটিকে একই নামের একটি নতুন variable-এ store করি, original টিকে shadowing করে। Query-তে to_lowercase call করা প্রয়োজন যাতে user-এর query "rust", "RUST", "Rust", বা "rUsT" যাই হোক না কেন, আমরা query-টিকে "rust" হিসেবে treat করব এবং case-এর প্রতি insensitive হব। যদিও to_lowercase basic Unicode handle করবে, এটি 100% accurate হবে না। যদি আমরা একটি real application লিখতাম, তাহলে আমাদের এখানে আরও কিছু কাজ করতে হত, কিন্তু এই section টি environment variable সম্পর্কে, Unicode সম্পর্কে নয়, তাই আমরা এটিকে এখানেই ছেড়ে দেব।

লক্ষ্য করুন যে query এখন একটি string slice-এর পরিবর্তে একটি String, কারণ to_lowercase call করা existing data-কে reference করার পরিবর্তে new data create করে। উদাহরণস্বরূপ, ধরা যাক query হল "rUsT": সেই string slice-টিতে আমাদের ব্যবহার করার জন্য কোনো lowercase u বা t নেই, তাই আমাদের "rust" ধারণকারী একটি নতুন String allocate করতে হবে। আমরা যখন এখন contains method-এ argument হিসেবে query pass করি, তখন আমাদের একটি ampersand যোগ করতে হবে কারণ contains-এর signature একটি string slice নেওয়ার জন্য define করা হয়েছে।

এরপরে, আমরা প্রতিটি line-এ to_lowercase-এ একটি call যোগ করি সমস্ত character lowercase করার জন্য। এখন যেহেতু আমরা line এবং query কে lowercase-এ convert করেছি, তাই query-র case যাই হোক না কেন আমরা match খুঁজে পাব।

আসুন দেখি এই implementation টি test গুলো pass করে কিনা:

$ 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

দারুণ! সেগুলো pass করেছে। এখন, আসুন run function থেকে নতুন search_case_insensitive function টি call করি। প্রথমে আমরা Config struct-এ একটি configuration option যোগ করব case-sensitive এবং case-insensitive search-এর মধ্যে switch করার জন্য। এই field টি যোগ করলে compiler error হবে কারণ আমরা এখনও এই field টি কোথাও initialize করিনি:

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

আমরা ignore_case field টি যোগ করেছি যেটিতে একটি Boolean রয়েছে। এরপরে, আমাদের run function-এর ignore_case field-এর value check করতে হবে এবং search function বা search_case_insensitive function call করতে হবে কিনা তা decide করতে সেটি ব্যবহার করতে হবে, যেমনটি Listing 12-22-তে দেখানো হয়েছে। এটি এখনও compile হবে না।

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

অবশেষে, আমাদের environment variable-টি check করতে হবে। Environment variable-গুলোর সাথে কাজ করার function গুলো standard library-এর env module-এ রয়েছে, তাই আমরা src/lib.rs-এর উপরের দিকে সেই module-টিকে scope-এ আনি। তারপর আমরা env module থেকে var function টি ব্যবহার করব এটা দেখতে যে IGNORE_CASE নামের একটি environment variable-এর জন্য কোনো value set করা হয়েছে কিনা, যেমনটি Listing 12-23-এ দেখানো হয়েছে।

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

এখানে, আমরা একটি নতুন variable, ignore_case তৈরি করি। এর value set করার জন্য, আমরা env::var function call করি এবং এতে IGNORE_CASE environment variable-এর নাম pass করি। env::var function একটি Result return করে যেটি successful Ok variant হবে যাতে environment variable-টির value থাকবে যদি environment variable-টি কোনো value-তে set করা থাকে। Environment variable-টি set করা না থাকলে এটি Err variant return করবে।

আমরা Result-এর উপর is_ok method টি ব্যবহার করছি এটা check করার জন্য যে environment variable-টি set করা আছে কিনা, যার অর্থ হল প্রোগ্রামটির case-insensitive search করা উচিত। যদি IGNORE_CASE environment variable-টি কোনো কিছুতে set করা না থাকে, তাহলে is_ok, false return করবে এবং প্রোগ্রামটি case-sensitive search করবে। Environment variable-টির value নিয়ে আমাদের মাথা ঘামানোর দরকার নেই, শুধুমাত্র এটি set করা আছে নাকি unset, তাই আমরা unwrap, expect, বা Result-এ দেখা অন্যান্য method-গুলোর পরিবর্তে is_ok check করছি।

আমরা ignore_case variable-এর value-টি Config instance-এ pass করি যাতে run function সেই value টি read করতে পারে এবং Listing 12-22-এ implement করা search_case_insensitive বা search call করতে হবে কিনা তা decide করতে পারে।

আসুন চেষ্টা করে দেখা যাক! প্রথমে আমরা environment variable set না করে এবং to query দিয়ে আমাদের প্রোগ্রামটি run করব, যেটি lowercase-এ to শব্দযুক্ত যেকোনো line-এর সাথে match করা উচিত:

$ 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-এ set করে কিন্তু একই query to দিয়ে প্রোগ্রামটি run করি:

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

আপনি যদি PowerShell ব্যবহার করেন, তাহলে আপনাকে environment variable set করতে হবে এবং প্রোগ্রামটিকে আলাদা command হিসেবে run করতে হবে:

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

এটি আপনার shell session-এর বাকি অংশের জন্য IGNORE_CASE-কে স্থায়ী করবে। এটিকে Remove-Item cmdlet দিয়ে unset করা যেতে পারে:

PS> Remove-Item Env:IGNORE_CASE

আমাদের to যুক্ত line গুলো পাওয়া উচিত যেগুলিতে uppercase letter থাকতে পারে:

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

চমৎকার, আমরা To যুক্ত line-ও পেয়েছি! আমাদের minigrep প্রোগ্রামটি এখন একটি environment variable দ্বারা নিয়ন্ত্রিত case-insensitive searching করতে পারে। এখন আপনি জানেন কীভাবে command line argument বা environment variable ব্যবহার করে set করা option গুলো manage করতে হয়।

কিছু প্রোগ্রাম একই configuration-এর জন্য argument এবং environment variable উভয়কেই অনুমতি দেয়। সেই ক্ষেত্রগুলোতে, প্রোগ্রামগুলো decide করে যে কোনটি প্রাধান্য পাবে। নিজে থেকে আরেকটি exercise-এর জন্য, একটি command line argument বা একটি environment variable-এর মাধ্যমে case sensitivity নিয়ন্ত্রণ করার চেষ্টা করুন। প্রোগ্রামটি case sensitive-এ set করা একটি এবং ignore case-এ set করা একটি দিয়ে run করা হলে command line argument বা environment variable-এর মধ্যে কোনটি প্রাধান্য পাওয়া উচিত তা ঠিক করুন।

std::env module-টিতে environment variable-গুলোর সাথে কাজ করার জন্য আরও অনেক useful feature রয়েছে: কী কী available তা দেখতে এর documentation দেখুন।