ใช้งาน Environment Variable
เราจะปรับปรุง binary minigrep โดยเพิ่มฟีเจอร์เพิ่มเติม — option สำหรับ
การค้นหาแบบไม่สนใจตัวพิมพ์ใหญ่/เล็ก ที่ user เปิดได้ผ่าน environment
variable เราทำให้ฟีเจอร์นี้เป็น option บน command line ได้ และต้องการ
ให้ user ใส่ทุกครั้งที่อยากให้ใช้ แต่โดยทำให้มันเป็น environment
variable แทน เราอนุญาตให้ user ของเราตั้ง environment variable ครั้ง
เดียวและให้การค้นหาทั้งหมดของพวกเขาเป็นแบบไม่สนใจตัวพิมพ์ใน session
terminal นั้น
เขียนเทสที่ Fail สำหรับการค้นหาแบบไม่สนใจตัวพิมพ์
ก่อนอื่นเราเพิ่มฟังก์ชันใหม่ search_case_insensitive ให้ library
minigrep ที่จะถูกเรียกเมื่อ environment variable มีค่า เราจะทำตาม
กระบวนการ TDD ต่อ ดังนั้นขั้นตอนแรกอีกครั้งคือเขียนเทสที่ fail เราจะ
เพิ่มเทสใหม่สำหรับฟังก์ชัน 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 ของเทสเก่าด้วย เราเพิ่มบรรทัดใหม่พร้อม text
"Duct tape." โดยใช้ตัว D ใหญ่ที่ไม่ควรตรงกับ query "duct" เมื่อ
เรากำลังค้นหาแบบสนใจตัวพิมพ์ การเปลี่ยนเทสเก่าแบบนี้ช่วยให้แน่ใจว่า
เราไม่ทำลาย functionality การค้นหาแบบสนใจตัวพิมพ์ที่เรา implement ไป
แล้วโดยบังเอิญ เทสนี้ควรผ่านตอนนี้และควรผ่านต่อเนื่องในขณะที่เราทำงาน
กับการค้นหาแบบไม่สนใจตัวพิมพ์
เทสใหม่สำหรับการค้นหาแบบ_ไม่สนใจตัวพิมพ์_ใช้ "rUsT" เป็น query ของ
มัน ในฟังก์ชัน search_case_insensitive ที่เรากำลังจะเพิ่ม query
"rUsT" ควรตรงกับบรรทัดที่มี "Rust:" ที่มีตัว R ใหญ่ และตรงกับ
บรรทัด "Trust me." แม้ทั้งคู่จะมีตัวพิมพ์ต่างจาก query นี่คือเทสที่
fail ของเรา และมันจะ fail ที่จะคอมไพล์เพราะเรายังไม่ได้นิยามฟังก์ชัน
search_case_insensitive รู้สึกอิสระที่จะเพิ่ม implementation
โครงร่างที่ return vector ว่างเสมอ คล้ายกับวิธีที่เราทำกับฟังก์ชัน
search ใน Listing 12-16 เพื่อดูเทสคอมไพล์และ fail
Implement ฟังก์ชัน search_case_insensitive
ฟังก์ชัน search_case_insensitive ที่แสดงใน Listing 12-21 จะเกือบ
เหมือนกับฟังก์ชัน search ความแตกต่างเดียวคือเราจะเปลี่ยน query
และแต่ละ line ให้เป็นตัวพิมพ์เล็ก เพื่อให้ไม่ว่าตัวพิมพ์ของ
อาร์กิวเมนต์ input จะเป็นอะไร พวกมันจะเป็นตัวพิมพ์เดียวกันเมื่อเรา
ตรวจสอบว่าบรรทัดมี 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)
);
}
}
search_case_insensitive ให้เปลี่ยน query และ line ให้เป็นตัวพิมพ์เล็กก่อนเปรียบเทียบก่อนอื่น เราเปลี่ยน string query เป็นตัวพิมพ์เล็กและเก็บมันใน
ตัวแปรใหม่ที่ชื่อเดียวกัน shadowing query เดิม การเรียก
to_lowercase บน query จำเป็นเพื่อให้ไม่ว่า query ของ user คือ
"rust", "RUST", "Rust" หรือ "rUsT" เราจะปฏิบัติกับ query
เหมือนว่ามันเป็น "rust" และไม่สนใจตัวพิมพ์ ในขณะที่ to_lowercase
จะจัดการ Unicode พื้นฐาน มันจะไม่แม่นยำ 100 เปอร์เซ็นต์ ถ้าเราเขียน
application จริง เราจะอยากทำงานเพิ่มที่นี่หน่อย แต่ส่วนนี้เกี่ยวกับ
environment variable ไม่ใช่ Unicode เราจะปล่อยไว้แค่นั้นที่นี่
สังเกตว่า query ตอนนี้เป็น String ไม่ใช่ string slice เพราะการ
เรียก to_lowercase สร้างข้อมูลใหม่ไม่ใช่อ้างถึงข้อมูลที่มีอยู่
สมมติ query เป็น "rUsT" เป็นตัวอย่าง — string slice นั้นไม่มีตัวเล็ก
u หรือ t ให้เราใช้ ดังนั้นเราต้อง allocate String ใหม่ที่บรรจุ
"rust" เมื่อเราส่ง query เป็นอาร์กิวเมนต์ให้เมธอด contains
ตอนนี้ เราต้องเพิ่ม ampersand เพราะ signature ของ contains นิยาม
ให้รับ string slice
ถัดไป เราเพิ่มการเรียก to_lowercase บนแต่ละ line เพื่อเปลี่ยน
character ทั้งหมดเป็นตัวพิมพ์เล็ก ตอนนี้เราเปลี่ยน line และ query
เป็นตัวพิมพ์เล็กแล้ว เราจะหาคู่ที่ตรงไม่ว่า query จะเป็นตัวพิมพ์อะไร
มาดูว่า implementation นี้ผ่านเทสไหม:
$ 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 ก่อนอื่น เราจะเพิ่ม option configuration ให้ struct
Config เพื่อสลับระหว่างการค้นหาแบบสนใจตัวพิมพ์และไม่สนใจ การเพิ่ม
field นี้จะทำให้เกิด error ของ compiler เพราะเรายังไม่ได้ initialize
field นี้ที่ไหน:
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(())
}
เราเพิ่ม field ignore_case ที่เก็บ Boolean ถัดไป เราต้องการให้
ฟังก์ชัน run ตรวจสอบค่าของ field ignore_case และใช้นั้นเพื่อ
ตัดสินว่าจะเรียกฟังก์ชัน search หรือ search_case_insensitive ดัง
ที่แสดงใน Listing 12-22 นี่ยังจะไม่คอมไพล์
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 หรือ search_case_insensitive ตามค่าใน config.ignore_caseสุดท้าย เราต้องตรวจสอบ environment variable ฟังก์ชันสำหรับทำงานกับ
environment variable อยู่ในโมดูล env ใน standard library ซึ่งอยู่ใน
scope แล้วที่ด้านบนของ src/main.rs เราจะใช้ฟังก์ชัน var จาก
โมดูล 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(())
}
IGNORE_CASEที่นี่ เราสร้างตัวแปรใหม่ ignore_case เพื่อตั้งค่าของมัน เราเรียก
ฟังก์ชัน env::var และส่งชื่อของ environment variable IGNORE_CASE
ให้ ฟังก์ชัน env::var return Result ที่จะเป็น variant Ok ที่
สำเร็จที่บรรจุค่าของ environment variable ถ้า environment variable
ถูกตั้งเป็นค่าใด ๆ มันจะ return variant Err ถ้า environment
variable ไม่ถูกตั้ง
เราใช้เมธอด is_ok บน Result เพื่อตรวจสอบว่า environment variable
ถูกตั้งไหม ซึ่งหมายความว่าโปรแกรมควรทำการค้นหาแบบไม่สนใจตัวพิมพ์ ถ้า
environment variable IGNORE_CASE ไม่ถูกตั้งเป็นอะไร is_ok จะ
return false และโปรแกรมจะทำการค้นหาแบบสนใจตัวพิมพ์ เราไม่สนใจ
ค่า ของ environment variable แค่ว่ามันถูกตั้งหรือไม่ ดังนั้นเรา
ตรวจสอบ is_ok แทนการใช้ unwrap, expect หรือเมธอดอื่นที่เราเห็น
บน Result
เราส่งค่าในตัวแปร ignore_case ให้ instance Config เพื่อให้
ฟังก์ชัน run อ่านค่านั้นและตัดสินว่าจะเรียก
search_case_insensitive หรือ search ดังที่เรา implement ใน
Listing 12-22
มาลองกัน! ก่อนอื่น เราจะรันโปรแกรมของเราโดยไม่ตั้ง environment
variable และด้วย query 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
แต่ด้วย 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 ยังคงอยู่ตลอดส่วนที่เหลือของ session shell
ของคุณ มันถูก unset ได้ด้วย cmdlet 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!
ยอดเยี่ยม เรายังได้บรรทัดที่มี To! โปรแกรม minigrep ของเราตอนนี้
ทำการค้นหาแบบไม่สนใจตัวพิมพ์ที่ควบคุมโดย environment variable ได้
ตอนนี้คุณรู้วิธีจัดการ option ที่ถูกตั้งโดยใช้อาร์กิวเมนต์ command
line หรือ environment variable
บางโปรแกรมอนุญาตให้ใช้อาร์กิวเมนต์ และ environment variable สำหรับ configuration เดียวกัน ในกรณีเหล่านั้น โปรแกรมตัดสินว่าอันใดอันหนึ่ง มีความสำคัญกว่า สำหรับการฝึกอื่นด้วยตัวคุณเอง ลองควบคุมความ sensitive ของตัวพิมพ์ผ่านอาร์กิวเมนต์ command line หรือ environment variable ตัดสินว่าอาร์กิวเมนต์ command line หรือ environment variable ควรมีความสำคัญกว่าถ้าโปรแกรมถูกรันด้วยอันหนึ่งตั้งเป็นสนใจ ตัวพิมพ์และอีกอันตั้งเป็นไม่สนใจ
โมดูล std::env มีฟีเจอร์ที่มีประโยชน์อีกมากในการจัดการ environment
variable — ดู documentation ของมันเพื่อดูว่ามีอะไรให้ใช้