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