Làm việc với Biến Môi trường
Chúng ta sẽ cải thiện minigrep
bằng cách thêm một tính năng bổ sung: một tùy
chọn cho tìm kiếm không phân biệt chữ hoa chữ thường mà người dùng có thể bật
thông qua một biến môi trường. Chúng ta có thể biến tính năng này thành một tùy
chọn dòng lệnh và yêu cầu người dùng nhập nó mỗi khi họ muốn áp dụng, nhưng thay
vào đó, bằng cách sử dụng biến môi trường, chúng ta cho phép người dùng đặt biến
môi trường một lần và thực hiện tất cả các tìm kiếm của họ không phân biệt chữ
hoa chữ thường trong phiên terminal đó.
Viết một Test Thất bại cho Hàm search
Không Phân biệt Chữ hoa Chữ thường
Đầu tiên chúng ta thêm một hàm search_case_insensitive
mới sẽ được gọi khi
biến môi trường có giá trị. Chúng ta sẽ tiếp tục tuân theo quy trình TDD, vì vậy
bước đầu tiên vẫn là viết một test thất bại. Chúng ta sẽ thêm một test mới cho
hàm search_case_insensitive
mới và đổi tên test cũ từ one_result
thành
case_sensitive
để làm rõ sự khác biệt giữa hai test, như được hiển thị trong
Listing 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)
);
}
}
Lưu ý rằng chúng ta cũng đã chỉnh sửa contents
của test cũ. Chúng ta đã thêm
một dòng mới với nội dung "Duct tape."
sử dụng chữ D viết hoa, nó không nên
khớp với truy vấn "duct"
khi chúng ta tìm kiếm phân biệt chữ hoa chữ thường.
Việc thay đổi test cũ theo cách này giúp đảm bảo chúng ta không vô tình phá vỡ
chức năng tìm kiếm phân biệt chữ hoa chữ thường mà chúng ta đã triển khai. Test
này bây giờ nên pass và nên tiếp tục pass khi chúng ta làm việc trên tìm kiếm
không phân biệt chữ hoa chữ thường.
Test mới cho tìm kiếm không phân biệt chữ hoa chữ thường sử dụng "rUsT"
làm
truy vấn. Trong hàm search_case_insensitive
mà chúng ta sắp thêm, truy vấn
"rUsT"
nên khớp với dòng chứa "Rust:"
với chữ R viết hoa và khớp với dòng
"Trust me."
mặc dù cả hai đều có cách viết hoa thường khác với truy vấn. Đây
là test thất bại của chúng ta, và nó sẽ không biên dịch được vì chúng ta chưa
định nghĩa hàm search_case_insensitive
. Hãy thoải mái thêm một cài đặt khung
luôn trả về một vector rỗng, tương tự như cách chúng ta đã làm cho hàm search
trong Listing 12-16 để thấy test biên dịch và thất bại.
Triển khai Hàm search_case_insensitive
Hàm search_case_insensitive
, được hiển thị trong Listing 12-21, sẽ gần giống
với hàm search
. Sự khác biệt duy nhất là chúng ta sẽ chuyển đổi query
và mỗi
line
thành chữ thường để dù đầu vào có chữ hoa hay chữ thường như thế nào,
chúng sẽ giống nhau khi chúng ta kiểm tra xem dòng có chứa truy vấn hay không.
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)
);
}
}
Đầu tiên, chúng ta chuyển đổi chuỗi query
thành chữ thường và lưu trữ nó trong
một biến mới có cùng tên, ghi đè biến query
gốc. Gọi to_lowercase
trên truy
vấn là cần thiết để dù truy vấn của người dùng là "rust"
, "RUST"
, "Rust"
,
hay "rUsT"
, chúng ta sẽ xử lý truy vấn như thể nó là "rust"
và không phân
biệt chữ hoa chữ thường. Mặc dù to_lowercase
sẽ xử lý Unicode cơ bản, nó sẽ
không chính xác 100%. Nếu chúng ta đang viết một ứng dụng thực, chúng ta sẽ muốn
làm thêm một chút công việc ở đây, nhưng phần này là về biến môi trường, không
phải về Unicode, vì vậy chúng ta sẽ để nó như vậy ở đây.
Lưu ý rằng query
bây giờ là một String
thay vì một string slice vì gọi
to_lowercase
tạo ra dữ liệu mới thay vì tham chiếu đến dữ liệu hiện có. Ví dụ,
nếu truy vấn là "rUsT"
: string slice đó không chứa chữ u
hoặc t
viết
thường để chúng ta sử dụng, vì vậy chúng ta phải cấp phát một String
mới chứa
"rust"
. Khi chúng ta truyền query
làm đối số cho phương thức contains
bây
giờ, chúng ta cần thêm dấu và (ampersand) vì chữ ký của contains
được định
nghĩa để nhận một string slice.
Tiếp theo, chúng ta thêm một lệnh gọi to_lowercase
cho mỗi line
để chuyển
tất cả các ký tự thành chữ thường. Bây giờ sau khi chúng ta đã chuyển đổi line
và query
thành chữ thường, chúng ta sẽ tìm thấy các kết quả phù hợp bất kể
truy vấn có chữ hoa hay chữ thường.
Hãy xem liệu cài đặt này có vượt qua các bài kiểm tra không:
$ 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
Tuyệt vời! Chúng đều đã pass. Bây giờ, chúng ta hãy gọi hàm
search_case_insensitive
mới từ hàm run
. Đầu tiên, chúng ta sẽ thêm một tùy
chọn cấu hình vào struct Config
để chuyển đổi giữa tìm kiếm phân biệt chữ hoa
chữ thường và không phân biệt chữ hoa chữ thường. Việc thêm trường này sẽ gây ra
lỗi biên dịch vì chúng ta chưa khởi tạo trường này ở bất kỳ đâu:
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)
);
}
}
Chúng ta đã thêm trường ignore_case
chứa một giá trị Boolean. Tiếp theo, chúng
ta cần hàm run
kiểm tra giá trị của trường ignore_case
và sử dụng nó để
quyết định gọi hàm search
hay hàm search_case_insensitive
, như được hiển thị
trong Listing 12-22. Điều này vẫn chưa thể biên dịch.
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)
);
}
}
Cuối cùng, chúng ta cần kiểm tra biến môi trường. Các hàm để làm việc với biến
môi trường nằm trong module env
trong thư viện tiêu chuẩn, vì vậy chúng ta đưa
module đó vào phạm vi ở đầu src/lib.rs. Sau đó, chúng ta sẽ sử dụng hàm var
từ module env
để kiểm tra xem có giá trị nào được đặt cho biến môi trường có
tên IGNORE_CASE
không, như được hiển thị trong Listing 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)
);
}
}
Ở đây, chúng ta tạo một biến mới, ignore_case
. Để đặt giá trị của nó, chúng ta
gọi hàm env::var
và truyền tên của biến môi trường IGNORE_CASE
. Hàm
env::var
trả về một Result
sẽ là biến thể Ok
thành công chứa giá trị của
biến môi trường nếu biến môi trường được đặt thành bất kỳ giá trị nào. Nó sẽ trả
về biến thể Err
nếu biến môi trường không được đặt.
Chúng ta đang sử dụng phương thức is_ok
trên Result
để kiểm tra xem biến môi
trường có được đặt hay không, nghĩa là chương trình nên thực hiện tìm kiếm không
phân biệt chữ hoa chữ thường. Nếu biến môi trường IGNORE_CASE
không được đặt
thành bất kỳ giá trị nào, is_ok
sẽ trả về false
và chương trình sẽ thực hiện
tìm kiếm phân biệt chữ hoa chữ thường. Chúng ta không quan tâm đến giá trị của
biến môi trường, chỉ quan tâm xem nó được đặt hay không, vì vậy chúng ta đang
kiểm tra is_ok
thay vì sử dụng unwrap
, expect
, hoặc bất kỳ phương thức nào
khác chúng ta đã thấy trên Result
.
Chúng ta truyền giá trị trong biến ignore_case
cho đối tượng Config
để hàm
run
có thể đọc giá trị đó và quyết định gọi search_case_insensitive
hay
search
, như chúng ta đã triển khai trong Listing 12-22.
Hãy thử nó! Đầu tiên, chúng ta sẽ chạy chương trình mà không đặt biến môi trường
và với truy vấn to
, sẽ khớp với bất kỳ dòng nào chứa từ to viết thường:
$ 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!
Có vẻ như điều đó vẫn hoạt động! Bây giờ chúng ta hãy chạy chương trình với
IGNORE_CASE
được đặt thành 1
nhưng với cùng truy vấn to:
$ IGNORE_CASE=1 cargo run -- to poem.txt
Nếu bạn đang sử dụng PowerShell, bạn sẽ cần đặt biến môi trường và chạy chương trình như các lệnh riêng biệt:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
Điều này sẽ làm cho IGNORE_CASE
tồn tại trong phiên shell còn lại của bạn. Có
thể bỏ đặt nó bằng lệnh Remove-Item
:
PS> Remove-Item Env:IGNORE_CASE
Chúng ta nên nhận được các dòng chứa to có thể có chữ viết hoa:
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
Tuyệt vời, chúng ta cũng nhận được các dòng chứa To! Chương trình minigrep
của chúng ta bây giờ có thể thực hiện tìm kiếm không phân biệt chữ hoa chữ
thường được điều khiển bởi một biến môi trường. Bây giờ bạn biết cách quản lý
các tùy chọn được đặt bằng cả đối số dòng lệnh hoặc biến môi trường.
Một số chương trình cho phép đối số và biến môi trường cho cùng một cấu hình. Trong những trường hợp đó, các chương trình quyết định rằng một trong hai có quyền ưu tiên. Đối với một bài tập khác tự thực hiện, hãy thử kiểm soát độ nhạy chữ hoa chữ thường thông qua một đối số dòng lệnh hoặc một biến môi trường. Quyết định xem đối số dòng lệnh hay biến môi trường nên có quyền ưu tiên nếu chương trình được chạy với một cái được đặt thành phân biệt chữ hoa chữ thường và một cái được đặt để bỏ qua chữ hoa chữ thường.
Module std::env
chứa nhiều tính năng hữu ích hơn để xử lý biến môi trường: hãy
xem tài liệu của nó để biết những gì có sẵn.