کار با متغیرهای محیطی
ما باینری minigrep
را با افزودن یک ویژگی اضافی بهبود میدهیم: گزینهای برای جستجوی حساسنبودن به حروف کوچک و بزرگ که کاربر میتواند از طریق یک متغیر محیطی آن را فعال کند. میتوانستیم این ویژگی را بهصورت گزینهای در خط فرمان پیادهسازی کنیم و از کاربران بخواهیم که هر بار آن را وارد کنند، اما با استفاده از یک متغیر محیطی، به کاربران اجازه میدهیم که فقط یکبار این متغیر را تنظیم کنند و در کل آن نشست ترمینال، تمام جستجوهای آنها بهصورت حساسنبودن به حروف انجام شود.
نوشتن یک تست شکستخورده برای تابع search_case_insensitive
ابتدا تابع جدیدی به نام search_case_insensitive
به کتابخانه minigrep
اضافه میکنیم که زمانی فراخوانی میشود که متغیر محیطی مقدار داشته باشد. ما همچنان از فرایند توسعه آزمونمحور (TDD) پیروی میکنیم، بنابراین گام اول، نوشتن یک تست شکستخورده است. یک تست جدید برای تابع search_case_insensitive
اضافه میکنیم و نام تست قبلیمان را از one_result
به case_sensitive
تغییر میدهیم تا تفاوت بین این دو تست واضحتر شود، همانطور که در لیست 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)
);
}
}
توجه کنید که ما متن تست قدیمی را نیز ویرایش کردهایم. ما یک خط جدید با متن "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
خواهد بود. تنها تفاوت این است که ما عبارت جستجو و هر خط را کوچکحرف میکنیم تا
صرفنظر از مورد ورودیها، هنگام بررسی اینکه آیا خط شامل عبارت جستجو است، هر دو به یک مورد
تبدیل شوند.
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)
);
}
}
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.
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(())
}
search
or search_case_insensitive
based on the value in config.ignore_case
در نهایت، باید بررسی کنیم که آیا متغیر محیطی تنظیم شده است یا خیر. توابع مربوط به کار با متغیرهای محیطی در ماژول env
از کتابخانه استاندارد قرار دارند که در بالای فایل src/main.rs از قبل در scope قرار دارد. برای بررسی اینکه آیا متغیر محیطیای به نام IGNORE_CASE
مقداری دارد یا نه، از تابع var
در ماژول env
استفاده میکنیم، همانطور که در لیست 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(())
}
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
ویژگیهای مفید بسیاری برای کار با متغیرهای محیطی دارد: مستندات آن را بررسی کنید
تا ببینید چه امکاناتی در دسترس است.