এনভায়রনমেন্ট ভেরিয়েবল (Environment Variables) নিয়ে কাজ করা
আমরা minigrep
বাইনারিটিকে একটি অতিরিক্ত ফিচার যোগ করে উন্নত করব: case-insensitive (ছোট বা বড় হাতের অক্ষর নির্বিশেষে) সার্চিংয়ের একটি অপশন, যা ব্যবহারকারী একটি এনভায়রনমেন্ট ভেরিয়েবলের মাধ্যমে চালু করতে পারবেন। আমরা এই ফিচারটিকে একটি কমান্ড লাইন অপশন হিসেবে তৈরি করতে পারতাম এবং ব্যবহারকারীদের প্রতিবার এটি ব্যবহার করার জন্য টাইপ করতে বলতে পারতাম। কিন্তু এর পরিবর্তে এটিকে একটি এনভায়রনমেন্ট ভেরিয়েবল হিসেবে তৈরি করার মাধ্যমে আমরা আমাদের ব্যবহারকারীদের একবার এনভায়রনমেন্ট ভেরিয়েবল সেট করার সুযোগ দিচ্ছি, এবং সেই টার্মিনাল সেশনে তাদের সমস্ত সার্চ case-insensitive হবে।
Case-Insensitive search
ফাংশনের জন্য একটি ফেইলিং টেস্ট লেখা
প্রথমে আমরা minigrep
লাইব্রেরিতে একটি নতুন search_case_insensitive
ফাংশন যোগ করব যা এনভায়রনমেন্ট ভেরিয়েবলের ভ্যালু থাকলে কল করা হবে। আমরা TDD প্রক্রিয়া অনুসরণ করা চালিয়ে যাব, তাই প্রথম ধাপটি হলো আবার একটি ফেইলিং টেস্ট লেখা। আমরা নতুন search_case_insensitive
ফাংশনের জন্য একটি নতুন টেস্ট যোগ করব এবং আমাদের পুরনো টেস্টের নাম one_result
থেকে case_sensitive
-এ পরিবর্তন করব যাতে দুটি টেস্টের মধ্যে পার্থক্য স্পষ্ট হয়, যেমনটি লিস্টিং ১২-২০-এ দেখানো হয়েছে।
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 ব্যবহার করা হয়েছে, যা case-sensitive ভাবে সার্চ করার সময় "duct"
কোয়েরির সাথে ম্যাচ করা উচিত নয়। পুরনো টেস্টটি এভাবে পরিবর্তন করা নিশ্চিত করতে সাহায্য করে যে আমরা ইতোমধ্যে প্রয়োগ করা case-sensitive সার্চ ফাংশনালিটি দুর্ঘটনাক্রমে নষ্ট করে ফেলছি না। এই টেস্টটি এখন পাস করা উচিত এবং case-insensitive সার্চ নিয়ে কাজ করার সময়ও পাস করা উচিত।
case-insensitive সার্চের জন্য নতুন টেস্টটি তার কোয়েরি হিসেবে "rUsT"
ব্যবহার করে। আমরা যে search_case_insensitive
ফাংশনটি যোগ করতে চলেছি, সেখানে "rUsT"
কোয়েরিটি বড় হাতের R সহ "Rust:"
লাইনটির সাথে ম্যাচ করা উচিত এবং "Trust me."
লাইনটির সাথেও ম্যাচ করা উচিত, যদিও উভয়ের কেসিং কোয়েরি থেকে ভিন্ন। এটি আমাদের ফেইলিং টেস্ট, এবং এটি কম্পাইল করতে ব্যর্থ হবে কারণ আমরা এখনও search_case_insensitive
ফাংশনটি ডিফাইন করিনি। আপনি নির্দ্বিধায় একটি স্কেলেটন ইমপ্লিমেন্টেশন যোগ করতে পারেন যা সর্বদা একটি খালি ভেক্টর রিটার্ন করে, যেমনটি আমরা লিস্টিং ১২-১৬-তে search
ফাংশনের জন্য করেছিলাম, যাতে টেস্টটি কম্পাইল হয় এবং ফেইল করে।
search_case_insensitive
ফাংশন ইমপ্লিমেন্ট করা
search_case_insensitive
ফাংশনটি, যা লিস্টিং ১২-২১-এ দেখানো হয়েছে, প্রায় search
ফাংশনের মতোই হবে। একমাত্র পার্থক্য হলো আমরা query
এবং প্রতিটি line
-কে লোয়ারকেস (lowercase) করব, যাতে ইনপুট আর্গুমেন্টের কেস যাই হোক না কেন, লাইনটিতে কোয়েরি আছে কিনা তা পরীক্ষা করার সময় তারা একই কেস-এ থাকবে।
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
স্ট্রিংকে লোয়ারকেস করি এবং এটিকে একই নামের একটি নতুন ভেরিয়েবলে সংরক্ষণ করি, যা মূল query
-কে শ্যাডো (shadow) করে। কোয়েরির উপর to_lowercase
কল করা প্রয়োজন যাতে ব্যবহারকারীর কোয়েরি "rust"
, "RUST"
, "Rust"
, বা "
rUsT"
যাই হোক না কেন, আমরা কোয়েরিটিকে "rust"
হিসেবে বিবেচনা করব এবং কেস-এর প্রতি সংবেদনশীল থাকব না। যদিও to_lowercase
বেসিক ইউনিকোড (Unicode) হ্যান্ডেল করবে, এটি ১০০ শতাংশ সঠিক হবে না। যদি আমরা একটি বাস্তব অ্যাপ্লিকেশন লিখতাম, তবে আমাদের এখানে আরও কিছু কাজ করতে হতো, কিন্তু এই বিভাগটি এনভায়রনমেন্ট ভেরিয়েবল সম্পর্কে, ইউনিকোড সম্পর্কে নয়, তাই আমরা এখানেই এটি ছেড়ে দেব।
লক্ষ্য করুন যে query
এখন একটি স্ট্রিং স্লাইসের পরিবর্তে একটি String
, কারণ to_lowercase
কল করা বিদ্যমান ডেটাকে রেফারেন্স করার পরিবর্তে নতুন ডেটা তৈরি করে। উদাহরণস্বরূপ, ধরুন কোয়েরিটি "rUsT"
: সেই স্ট্রিং স্লাইসে আমাদের ব্যবহারের জন্য ছোট হাতের u
বা t
নেই, তাই আমাদের "rust"
ধারণকারী একটি নতুন String
অ্যালোকেট করতে হবে। যখন আমরা এখন contains
মেথডের আর্গুমেন্ট হিসেবে query
পাস করব, আমাদের একটি অ্যামপারস্যান্ড (&) যোগ করতে হবে কারণ contains
-এর সিগনেচার একটি স্ট্রিং স্লাইস নেওয়ার জন্য ডিফাইন করা হয়েছে।
এরপরে, আমরা প্রতিটি line
-এ to_lowercase
-এ একটি কল যোগ করি সমস্ত ক্যারেক্টার লোয়ারকেস করতে। এখন যেহেতু আমরা 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
চমৎকার! তারা পাস করেছে। এখন, আসুন run
ফাংশন থেকে নতুন search_case_insensitive
ফাংশনটি কল করি। প্রথমে আমরা Config
স্ট্রাকটে case-sensitive এবং case-insensitive সার্চের মধ্যে সুইচ করার জন্য একটি কনফিগারেশন অপশন যোগ করব। এই ফিল্ডটি যোগ করলে কম্পাইলার এরর দেখা যাবে কারণ আমরা এখনও কোথাও এই ফিল্ডটি ইনিশিয়ালাইজ করছি না:
ফাইলের নাম: 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(())
}
আমরা ignore_case
ফিল্ডটি যোগ করেছি যা একটি বুলিয়ান (boolean) ধারণ করে। এরপরে, আমাদের run
ফাংশনটিকে ignore_case
ফিল্ডের ভ্যালু পরীক্ষা করতে হবে এবং search
ফাংশন বা search_case_insensitive
ফাংশন কল করার সিদ্ধান্ত নিতে হবে, যেমনটি লিস্টিং ১২-২২-এ দেখানো হয়েছে। এটি এখনও কম্পাইল হবে না।
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(())
}
অবশেষে, আমাদের এনভায়রনমেন্ট ভেরিয়েবল পরীক্ষা করতে হবে। এনভায়রনমেন্ট ভেরিয়েবলের সাথে কাজ করার ফাংশনগুলো standard library-র env
মডিউলে রয়েছে, যা ইতোমধ্যে src/main.rs-এর শীর্ষে স্কোপে রয়েছে। আমরা env
মডিউলের var
ফাংশনটি ব্যবহার করে দেখব IGNORE_CASE
নামের কোনো এনভায়রনমেন্ট ভেরিয়েবলের জন্য কোনো ভ্যালু সেট করা হয়েছে কিনা, যেমনটি লিস্টিং ১২-২৩-এ দেখানো হয়েছে।
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(())
}
এখানে, আমরা একটি নতুন ভেরিয়েবল ignore_case
তৈরি করি। এর ভ্যালু সেট করতে, আমরা env::var
ফাংশন কল করি এবং এতে IGNORE_CASE
এনভায়রনমেন্ট ভেরিয়েবলের নাম পাস করি। env::var
ফাংশনটি একটি Result
রিটার্ন করে যা সফল Ok
ভ্যারিয়েন্ট হবে এবং এনভায়রনমেন্ট ভেরিয়েবলের ভ্যালু ধারণ করবে যদি এনভায়রনমেন্ট ভেরিয়েবলটি কোনো ভ্যালুতে সেট করা থাকে। যদি এনভায়রনমেন্ট ভেরিয়েবলটি সেট না করা থাকে তবে এটি Err
ভ্যারিয়েন্ট রিটার্ন করবে।
এনভায়রনমেন্ট ভেরিয়েবলটি সেট করা আছে কিনা তা পরীক্ষা করার জন্য আমরা Result
-এর is_ok
মেথড ব্যবহার করছি, যার মানে প্রোগ্রামটি একটি case-insensitive সার্চ করবে। যদি IGNORE_CASE
এনভায়রনমেন্ট ভেরিয়েবলটি কিছুতেই সেট না করা থাকে, is_ok
false
রিটার্ন করবে এবং প্রোগ্রামটি একটি case-sensitive সার্চ করবে। আমরা এনভায়রনমেন্ট ভেরিয়েবলের ভ্যালু নিয়ে চিন্তিত নই, শুধু এটি সেট করা আছে নাকি নেই তা নিয়েই ভাবছি, তাই আমরা unwrap
, expect
, বা Result
-এ আমরা দেখেছি এমন অন্য কোনো মেথড ব্যবহার না করে is_ok
পরীক্ষা করছি।
আমরা ignore_case
ভেরিয়েবলের ভ্যালুটি Config
ইনস্ট্যান্সে পাস করি যাতে run
ফাংশনটি সেই ভ্যালুটি পড়তে পারে এবং search_case_insensitive
বা search
কল করার সিদ্ধান্ত নিতে পারে, যেমনটি আমরা লিস্টিং ১২-২২-এ ইমপ্লিমেন্ট করেছি।
চলুন চেষ্টা করে দেখা যাক! প্রথমে আমরা এনভায়রনমেন্ট ভেরিয়েবল সেট না করে এবং to
কোয়েরি দিয়ে আমাদের প্রোগ্রামটি চালাব, যা সমস্ত ছোট হাতের অক্ষরে 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
এ সেট করে এবং একই কোয়েরি to দিয়ে প্রোগ্রামটি চালাই:
$ IGNORE_CASE=1 cargo run -- to poem.txt
আপনি যদি PowerShell ব্যবহার করেন, তবে আপনাকে এনভায়রনমেন্ট ভেরিয়েবল সেট করতে হবে এবং প্রোগ্রামটি আলাদা কমান্ড হিসাবে চালাতে হবে:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
এটি আপনার শেল সেশনের বাকি অংশের জন্য IGNORE_CASE
-কে স্থায়ী করবে। এটি Remove-Item
cmdlet দিয়ে আনসেট করা যেতে পারে:
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!
চমৎকার, আমরা To ধারণকারী লাইনগুলোও পেয়েছি! আমাদের minigrep
প্রোগ্রাম এখন একটি এনভায়রনমেন্ট ভেরিয়েবল দ্বারা নিয়ন্ত্রিত case-insensitive সার্চিং করতে পারে। এখন আপনি জানেন কীভাবে কমান্ড লাইন আর্গুমেন্ট বা এনভায়রনমেন্ট ভেরিয়েবল ব্যবহার করে সেট করা অপশনগুলো পরিচালনা করতে হয়।
কিছু প্রোগ্রাম একই কনফিগারেশনের জন্য আর্গুমেন্ট এবং এনভায়রনমেন্ট ভেরিয়েবল উভয়ই অনুমোদন করে। সেই ক্ষেত্রে, প্রোগ্রামগুলো সিদ্ধান্ত নেয় যে একটির উপর আরেকটির অগ্রাধিকার থাকবে। আপনার নিজের জন্য আরেকটি অনুশীলনী হিসেবে, একটি কমান্ড লাইন আর্গুমেন্ট বা একটি এনভায়রনমেন্ট ভেরিয়েবলের মাধ্যমে case sensitivity নিয়ন্ত্রণ করার চেষ্টা করুন। প্রোগ্রামটি যদি একটি case sensitive এবং অন্যটি ignore case-এ সেট করে চালানো হয় তবে কমান্ড লাইন আর্গুমেন্ট বা এনভায়রনমেন্ট ভেরিয়েবলের মধ্যে কোনটি অগ্রাধিকার পাবে তা নির্ধারণ করুন।
std::env
মডিউলে এনভায়রনমেন্ট ভেরিয়েবলের সাথে কাজ করার জন্য আরও অনেক দরকারী ফিচার রয়েছে: কী কী উপলব্ধ আছে তা দেখতে এর ডকুমেন্টেশন দেখুন।