การทำงานกับตัวแปรสภาพแวดล้อม (Working with Environment Variables)
เราจะปรับปรุงไฟล์ไบนารี minigrep โดยการเพิ่มฟีเจอร์พิเศษ: ตัวเลือกสำหรับการค้นหาแบบไม่สนอักษรตัวพิมพ์ใหญ่-เล็ก (case-insensitive searching) ที่ผู้ใช้สามารถเปิดใช้งานผ่านตัวแปรสภาพแวดล้อม (environment variable) ได้ เราอาจจะทำให้ฟีเจอร์นี้เป็นตัวเลือกบรรทัดคำสั่งและต้องป้อนทุกครั้งที่ผู้ใช้ต้องการให้มีผล แต่นำมาทำให้เป็นตัวแปรสภาพแวดล้อมแทน ทำให้ผู้ใช้ของเราสามารถตั้งค่าตัวแปรสภาพแวดล้อมเพียงครั้งเดียว และให้การค้นหาทั้งหมดของพวกเขากลายเป็นการค้นหาที่ไม่สนตัวพิมพ์ใหญ่-เล็กตลอดทั้งเซสชันเทอร์มินัลนั้นได้
การเขียนการทดสอบที่ล้มเหลวสำหรับการค้นหาแบบไม่สนตัวพิมพ์ใหญ่-เล็ก (Writing a Failing Test for Case-Insensitive Search)
ขั้นแรกเราเพิ่มฟังก์ชัน search_case_insensitive ใหม่ไปยังไลบรารี minigrep ซึ่งจะถูกเรียกใช้เมื่อตัวแปรสภาพแวดล้อมมีค่า เราจะยังคงปฏิบัติตามกระบวนการ TDD ต่อไป ดังนั้นขั้นตอนแรกคือการเขียนการทดสอบที่ล้มเหลวอีกครั้ง เราจะเพิ่มการทดสอบใหม่สำหรับฟังก์ชัน 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 ของการทดสอบเก่าด้วยเช่นกัน เราได้เพิ่มบรรทัดใหม่ด้วยข้อความ "Duct tape." ที่ใช้ตัว D พิมพ์ใหญ่ ซึ่งไม่ควรตรงกับคำค้นหา "duct" เมื่อเราค้นหาแบบสนตัวพิมพ์ใหญ่-เล็ก การเปลี่ยนการทดสอบเก่าด้วยวิธีนี้ช่วยรับประกันว่าเราจะไม่เผลอทำให้ฟังก์ชันการค้นหาแบบสนตัวพิมพ์ใหญ่-เล็กที่เราได้ปรับใช้ไปแล้วพัง การทดสอบนี้ควรจะผ่านในตอนนี้และควรจะผ่านต่อไปในขณะที่เราทำงานกับการค้นหาแบบไม่สนตัวพิมพ์ใหญ่-เล็ก
การทดสอบใหม่สำหรับการค้นหาแบบ ไม่สน ตัวพิมพ์ใหญ่-เล็ก ใช้คำค้นหาเป็น "rUsT" ในฟังก์ชัน search_case_insensitive ที่เรากำลังจะเพิ่ม คำค้นหา "rUsT" ควรจะตรงกับบรรทัดที่มีคำว่า "Rust:" ที่ใช้ตัว R พิมพ์ใหญ่ และตรงกับบรรทัด "Trust me." แม้ว่าทั้งคู่จะมีตัวพิมพ์ที่แตกต่างจากคำค้นหา นี่คือการทดสอบที่ล้มเหลวของเรา และมันจะล้มเหลวในการคอมไพล์เนื่องจากเรายังไม่ได้นิยามฟังก์ชัน search_case_insensitive คุณสามารถเพิ่มโครงสร้างเบื้องต้นที่คืนค่าเวกเตอร์ว่างเสมอได้ คล้ายกับวิธีที่เราทำกับฟังก์ชัน search ใน Listing 12-16 เพื่อดูการทดสอบคอมไพล์และล้มเหลว
การปรับใช้ฟังก์ชัน search_case_insensitive (Implementing the search_case_insensitive Function)
ฟังก์ชัน search_case_insensitive ดังแสดงใน Listing 12-21 จะเกือบเหมือนกับฟังก์ชัน search ทุกประการ ข้อแตกต่างเพียงอย่างเดียวคือเราจะแปลง query และแต่ละ line ให้เป็นตัวพิมพ์เล็กทั้งหมด เพื่อไม่ว่าค่าของอาร์กิวเมนต์อินพุตจะเป็นตัวพิมพ์แบบใด มันจะกลายเป็นตัวพิมพ์เดียวกันเมื่อเราตรวจสอบว่าบรรทัดนั้นมีคำค้นหาหรือไม่
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 ให้เป็นตัวพิมพ์เล็กและเก็บไว้ในตัวแปรใหม่ที่มีชื่อเดียวกัน ซึ่งจะบัง (shadow) query เดิม การเรียกใช้ to_lowercase บนคำค้นหาถือเป็นสิ่งจำเป็น เพื่อไม่ว่าคำค้นหาของผู้ใช้จะเป็น "rust", "RUST", "Rust", หรือ "rUsT" เราจะปฏิบัติต่อคำค้นหาประหนึ่งว่าเป็น "rust" และไม่สนใจตัวพิมพ์ แม้ว่า to_lowercase จะจัดการกับ Unicode พื้นฐานได้ แต่ก็ไม่ได้แม่นยำ 100 เปอร์เซ็นต์ หากเรากำลังเขียนแอปพลิเคชันจริง เราอาจต้องการทำเพิ่มเติมตรงนี้อีกนิด แต่บทนี้เป็นเรื่องเกี่ยวกับตัวแปรสภาพแวดล้อม ไม่ใช่ Unicode ดังนั้นเราจะคงไว้เท่านี้
สังเกตว่าตอนนี้ query เป็น String แทนที่จะเป็น string slice เนื่องจากผลการเรียก to_lowercase จะสร้างข้อมูลใหม่ขึ้นมาแทนที่จะเป็นการอ้างอิงข้อมูลที่มีอยู่ สมมติว่าคำค้นหาคือ "rUsT" เป็นตัวอย่าง: string slice นั้นไม่ได้มีตัว u หรือ t พิมพ์เล็กไว้ให้เราใช้ ดังนั้นเราจึงต้องจัดสรรหน่วยความจำสำหรับ String ใหม่ที่มี "rust" เมื่อเราส่ง query เป็นอาร์กิวเมนต์ให้กับเมธอด contains ในตอนนี้ เราจำเป็นต้องเพิ่ม ampersand (&) เนื่องจากซิกเนเจอร์ของ contains ถูกนิยามให้รับ string slice
ถัดไป เราเพิ่มการเรียกใช้ 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(())
}
เราเพิ่มฟิลด์ ignore_case ที่เก็บค่าบูลีน (Boolean) ถัดไป เราจำเป็นต้องให้ฟังก์ชัน run ตรวจสอบค่าของฟิลด์ 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(())
}
สุดท้าย เราจำเป็นต้องตรวจสอบตัวแปรสภาพแวดล้อม ฟังก์ชันสำหรับการทำงานกับตัวแปรสภาพแวดล้อมอยู่ในโมดูล env ในไลบรารีมาตรฐาน ซึ่งถูกนำเข้าสู่ขอบเขตไว้แล้วที่ด้านบนของ src/main.rs เราจะใช้ฟังก์ชัน var จากโมดูล env เพื่อตรวจสอบดูว่ามีการตั้งค่าใดๆ สำหรับตัวแปรสภาพแวดล้อมที่ชื่อว่า 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 ในการตั้งค่าของมัน เราเรียกใช้ฟังก์ชัน 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 ตามที่เราได้ปรับใช้ใน Listing 12-22
มาลองใช้กันดู! ขั้นแรก เราจะรันโปรแกรมของเราโดยไม่ได้ตั้งค่าตัวแปรสภาพแวดล้อม และใช้คำค้นหา 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 ยังคงอยู่ตลอดช่วงเวลาที่เหลือของเชลล์เซสชันของคุณ มันสามารถยกเลิกการตั้งค่าได้ด้วย 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 ของเราสามารถทำการค้นหาแบบไม่สนตัวพิมพ์ใหญ่-เล็กโดยควบคุมผ่านตัวแปรสภาพแวดล้อมได้แล้ว ตอนนี้คุณรู้วิธีจัดการตัวเลือกที่ตั้งค่าโดยใช้พารามิเตอร์บรรทัดคำสั่งหรือตัวแปรสภาพแวดล้อมแล้ว
โปรแกรมบางตัวอนุญาตให้ใช้อาร์กิวเมนต์ และ ตัวแปรสภาพแวดล้อมสำหรับการกำหนดค่าเดียวกัน ในกรณีเหล่านั้น โปรแกรมจะตัดสินว่าอย่างใดอย่างหนึ่งมีลำดับความสำคัญสูงกว่า สำหรับแบบฝึกหัดเพิ่มเติมของคุณเอง ลองควบคุมความไวต่อตัวพิมพ์ใหญ่-เล็กผ่านทางอาร์กิวเมนต์บรรทัดคำสั่งหรือตัวแปรสภาพแวดล้อมดู ตัดสินใจว่าอาร์กิวเมนต์บรรทัดคำสั่งหรือตัวแปรสภาพแวดล้อมควรจะมีลำดับความสำคัญสูงกว่า หากโปรแกรมถูกรันโดยตั้งค่าอย่างหนึ่งให้สนตัวพิมพ์ใหญ่-เล็ก และอีกอย่างหนึ่งให้ไม่สนตัวพิมพ์ใหญ่-เล็ก
โมดูล std::env บรรจุฟีเจอร์ที่มีประโยชน์มากกว่านี้อีกมากมายสำหรับการทำงานกับตัวแปรสภาพแวดล้อม: ลองดูเอกสารประกอบเพื่อดูว่ามีอะไรให้ใช้งานบ้าง