환경 변수 활용
환경 변수로 대소문자 구분 검색을 선택하도록 설정하고 실패 테스트에서 시작해 별도 검색 함수를 구현합니다.
minigrep에 추가 기능을 넣어서 개선시켜 보겠습니다.
바로 환경 변수를 통해 사용자가 켤 수 있는 대소문자를 구분하지 않는 검색 옵션입니다.
이 기능을 커맨드 라인 옵션으로 만들어서 필요한 경우 사용자가 매번 입력하도록 요구할 수도 있겠으나, 환경 변수로 만듦으로써 사용자는 이 환경 변수를 한 번만 설정하고 난 다음 그 터미널 세션 동안에는 모든 검색을 대소문자 구분 없이 할 수 있게 됩니다.
대소문자 무시 검색의 비교값과 반환값 흐름
rUsT→rust 한 번 준비, 각 원본 줄 임시 lowercase 비교 후 Rust./Trust me. 참조 누적과 Duct tape. 제외가 구체적이다. 임시 String을 반환하지 않는다.
RUST · MINIGREP
소문자로 비교하고 원본 줄을 반환한다
질의어 rUsT는 한 번 소문자로 바꾸고 각 줄은 비교할 때 소문자로 바꿉니다. 결과에는 원래 대소문자와 구두점을 보존합니다.
1 · 질의어 준비
"rUsT".to_lowercase()
→ "rust"새 String을 한 번 만듭니다. 원래 query의 슬라이스는 바꾸지 않습니다.
2 · 각 줄 비교
"Rust." → "rust." → contains: true
"Trust me." → "trust me." → true
"Duct tape." → "duct tape." → false각 줄에 to_lowercase().contains(&query)를 적용합니다.
3 · 원본 줄 저장
["Rust.", "Trust me."]소문자 임시 String이 아니라 contents에서 빌린 원본 &str을 반환합니다. 변환된 임시값의 참조를 저장하지 않습니다.
4 · 설정은 호출자에서
Config가 환경 입력을 읽고 run이 검색 함수를 선택합니다. search_case_insensitive 자체는 환경 변수를 읽지 않아 직접 테스트할 수 있습니다.
Unicode 소문자 매핑은 로케일별 비교나 완전한 case folding과 같지 않습니다. 변환은 할당을 포함하고 문자 수나 바이트 길이가 늘어날 수 있습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 여섯 노드와 여섯 관계다. query를 한 번 소문자로 준비하고 원본 각 줄을 소문자 임시 String으로 바꿔 contains를 검사한다. 일치하면 원본 Rust. / Trust me. 참조를 결과에 넣고 Duct tape.는 제외한다. 점선 원문 대여는 임시 소문자 String이 아닌 contents의 줄이 반환 수명의 원천임을 뜻한다. query와 원문 입력은 독립이며 순차 실행 사슬이 아니다. 환경 선택은 아래 설명처럼 호출자 책임이다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 패턴 준비 rUsT를 한 번 lowercase로 만든다. |
|
|
| 임시 비교 원본 줄의 임시 lowercase로 contains를 판단한다. |
|
|
| 원본 반환 일치한 원래 표기의 줄을 누적한다. |
|
|
대소문자 무시 검색의 비교값과 반환값 흐름
rUsT→rust 한 번 준비, 각 원본 줄 임시 lowercase 비교 후 Rust./Trust me. 참조 누적과 Duct tape. 제외가 구체적이다. 임시 String을 반환하지 않는다.
RUST · MINIGREP
소문자로 비교하고 원본 줄을 반환한다
질의어 rUsT는 한 번 소문자로 바꾸고 각 줄은 비교할 때 소문자로 바꿉니다. 결과에는 원래 대소문자와 구두점을 보존합니다.
1 · 질의어 준비
"rUsT".to_lowercase()
→ "rust"새 String을 한 번 만듭니다. 원래 query의 슬라이스는 바꾸지 않습니다.
2 · 각 줄 비교
"Rust." → "rust." → contains: true
"Trust me." → "trust me." → true
"Duct tape." → "duct tape." → false각 줄에 to_lowercase().contains(&query)를 적용합니다.
3 · 원본 줄 저장
["Rust.", "Trust me."]소문자 임시 String이 아니라 contents에서 빌린 원본 &str을 반환합니다. 변환된 임시값의 참조를 저장하지 않습니다.
4 · 설정은 호출자에서
Config가 환경 입력을 읽고 run이 검색 함수를 선택합니다. search_case_insensitive 자체는 환경 변수를 읽지 않아 직접 테스트할 수 있습니다.
Unicode 소문자 매핑은 로케일별 비교나 완전한 case folding과 같지 않습니다. 변환은 할당을 포함하고 문자 수나 바이트 길이가 늘어날 수 있습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 여섯 노드와 여섯 관계다. query를 한 번 소문자로 준비하고 원본 각 줄을 소문자 임시 String으로 바꿔 contains를 검사한다. 일치하면 원본 Rust. / Trust me. 참조를 결과에 넣고 Duct tape.는 제외한다. 점선 원문 대여는 임시 소문자 String이 아닌 contents의 줄이 반환 수명의 원천임을 뜻한다. query와 원문 입력은 독립이며 순차 실행 사슬이 아니다. 환경 선택은 아래 설명처럼 호출자 책임이다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 패턴 준비 rUsT를 한 번 lowercase로 만든다. |
|
|
| 임시 비교 원본 줄의 임시 lowercase로 contains를 판단한다. |
|
|
| 원본 반환 일치한 원래 표기의 줄을 누적한다. |
|
|
대소문자를 구분하지 않는 search 함수에 대한 실패하는 테스트 작성하기
IGNORE_CASE 설정 경계와 명시적 우선순위
env var Result/is_ok→Config bool 저장→run의 한 검색 함수 선택의 실행 경계이다. 0/빈 Unicode도 true이며 CLI 확장 우선순위는 미구현이다.
RUST · MINIGREP
IGNORE_CASE의 현재 규칙과 확장 과제를 구분한다
본문은 env::var("IGNORE_CASE").is_ok() 하나로 검색 모드를 정합니다. 선택 CLI 플래그와 우선순위는 마지막의 확장 과제입니다.
1 · 현재 입력 규칙
env::var("IGNORE_CASE").is_ok()값의 내용이 아니라 유효한 Unicode 환경 값을 읽었는지를 검사합니다. 값이 1 또는 0이어도 true입니다. 빈 값이 실제 존재하면 true입니다.
2 · 없거나 읽을 수 없음
설정이 없으면 false입니다. 비유니코드 값도 VarError::NotUnicode이므로 false입니다. 바이트 수준 존재 여부가 필요하면 var_os로 별도 계약을 정합니다.
3 · Config에서 고정 → run에서 선택
ignore_case == false → search
ignore_case == true
→ search_case_insensitive실행 시작 때 읽은 bool을 Config에 저장합니다. 검색 함수는 환경을 직접 읽지 않습니다.
4 · CLI 플래그는 확장 과제
CLI에 명시적인 true/false를 추가한다면 미지정 상태도 구분해야 합니다. CLI와 환경이 충돌할 때의 우선순위를 선택·문서화하고 기본값·환경·CLI·충돌 사례를 테스트합니다.
환경 설정 테스트는 자식 프로세스의 Command::env / env_remove로 격리할 수 있습니다. Rust 2024의 프로세스 전역 환경 변경에는 안전성 조건이 있어 병렬 테스트의 부모 환경을 바꾸지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 현재 본문의 다섯 노드와 다섯 관계다. IGNORE_CASE를 Unicode 문자열로 읽었는지 is_ok로 판단한다. 성공은 0이나 빈 문자열도 true 설정이며 없음/비Unicode는 false 설정이다. 두 설정은 대안이고 run이 저장된 bool로 search 또는 search_case_insensitive 하나를 선택한다. CLI의 명시 옵션과 우선순위는 확장 설계이며 현재 이 그래프에 구현된 것이 아니다. 분기들이 위아래에 있어도 서로를 통과하는 흐름은 없다. |
| 노드·연결선 라벨 | run이 저장된 bool로 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 환경 읽기 IGNORE_CASE의 env::var Result를 확인한다. |
|
|
| Config 저장 is_ok 결과를 ignore_case bool에 저장한다. |
|
|
| 한 함수 선택 run이 bool에 맞는 검색 하나를 호출한다. |
|
|
IGNORE_CASE 설정 경계와 명시적 우선순위
env var Result/is_ok→Config bool 저장→run의 한 검색 함수 선택의 실행 경계이다. 0/빈 Unicode도 true이며 CLI 확장 우선순위는 미구현이다.
RUST · MINIGREP
IGNORE_CASE의 현재 규칙과 확장 과제를 구분한다
본문은 env::var("IGNORE_CASE").is_ok() 하나로 검색 모드를 정합니다. 선택 CLI 플래그와 우선순위는 마지막의 확장 과제입니다.
1 · 현재 입력 규칙
env::var("IGNORE_CASE").is_ok()값의 내용이 아니라 유효한 Unicode 환경 값을 읽었는지를 검사합니다. 값이 1 또는 0이어도 true입니다. 빈 값이 실제 존재하면 true입니다.
2 · 없거나 읽을 수 없음
설정이 없으면 false입니다. 비유니코드 값도 VarError::NotUnicode이므로 false입니다. 바이트 수준 존재 여부가 필요하면 var_os로 별도 계약을 정합니다.
3 · Config에서 고정 → run에서 선택
ignore_case == false → search
ignore_case == true
→ search_case_insensitive실행 시작 때 읽은 bool을 Config에 저장합니다. 검색 함수는 환경을 직접 읽지 않습니다.
4 · CLI 플래그는 확장 과제
CLI에 명시적인 true/false를 추가한다면 미지정 상태도 구분해야 합니다. CLI와 환경이 충돌할 때의 우선순위를 선택·문서화하고 기본값·환경·CLI·충돌 사례를 테스트합니다.
환경 설정 테스트는 자식 프로세스의 Command::env / env_remove로 격리할 수 있습니다. Rust 2024의 프로세스 전역 환경 변경에는 안전성 조건이 있어 병렬 테스트의 부모 환경을 바꾸지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 현재 본문의 다섯 노드와 다섯 관계다. IGNORE_CASE를 Unicode 문자열로 읽었는지 is_ok로 판단한다. 성공은 0이나 빈 문자열도 true 설정이며 없음/비Unicode는 false 설정이다. 두 설정은 대안이고 run이 저장된 bool로 search 또는 search_case_insensitive 하나를 선택한다. CLI의 명시 옵션과 우선순위는 확장 설계이며 현재 이 그래프에 구현된 것이 아니다. 분기들이 위아래에 있어도 서로를 통과하는 흐름은 없다. |
| 노드·연결선 라벨 | run이 저장된 bool로 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 환경 읽기 IGNORE_CASE의 env::var Result를 확인한다. |
|
|
| Config 저장 is_ok 결과를 ignore_case bool에 저장한다. |
|
|
| 한 함수 선택 run이 bool에 맞는 검색 하나를 호출한다. |
|
|
먼저 환경 변숫값이 있을 때 호출될 새로운 함수 search_case_insensitive를 추가하겠습니다.
계속하여 TDD 프로세스를 따를 것이므로, 첫 번째 단계는 다시 한번 실패하는 테스트를 작성하는 것입니다.
새로운 함수 search_case_insensitive를 추가하고 이전 테스트 이름은 예제 11-20처럼 두 테스트 간의 차이를 명확하게 하기 위해 one_result에서 case_sensitive로 바꾸겠습니다.
#[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도 수정되었음을 유의하세요.
대문자 D를 사용한 "Duct tape." 라인을 추가했고 이는 대소문자를 구분하는 방식으로 검색할 때는 질의어 "duct"에 매칭되지 않아야 합니다.
이렇게 예전 테스트를 변경하는 것은 이미 구현된 대소문자를 구분하는 검색을 우발적으로 깨트리지 않도록 확인하는 데 도움을 줍니다.
이 테스트는 지금 통과되어야 하며 대소문자를 구분하지 않는 검색에 대해 작업을 하는 중에도 계속해서 통과되어야 합니다.
대소문자를 구분하지 않는 검색을 위한 새로운 테스트에서는 질의어로 "rUsT"를 사용합니다.
추가하려는 search_case_insensitive 함수에서 질의어 "rUsT"는 질의어와 대소문자가 다르더라도 대문자 R로 시작하는 "Rust."를 포함하는 라인 및 "Trust me." 라인과 매칭되어야 합니다.
이것이 실패하는 테스트고, 아직 search_case_insensitive 함수를 정의하지 않았으므로 컴파일에 실패할 것입니다.
예제 11-16에서 테스트가 컴파일되고 실패하는 것을 지켜보기 위해 했었던 것과 마찬가지로, 간편하게 언제나 빈 벡터를 반환하는 뼈대 구현을 추가해봅시다.
search_case_insensitive 함수 구현하기
예제 11-21에서 보시는 search_case_insensitive 함수는 search 함수와 거의 똑같이 생겼을 것입니다.
유일한 차이점은 query와 각 line을 소문자로 만들어서 입력된 인수의 대소문자가 어떻든 간에 질의어가 라인에 포함되어 있는지 확인할 때는 언제나 같은 소문자일 것이란 점입니다.
search_case_insensitive 함수 정의하기
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
}
먼저 query 문자열을 소문자로 만들어서 같은 이름의 변수를 가리는 방식으로 저장합니다.
질의어에 대해 to_lowercase가 호출되므로 사용자의 질의어가 "rust", "RUST", "Rust", 혹은 "rUsT"이든 상관없이 이 질의어를 "rust"로 취급하여 대소문자를 구분하지 않게 될 것입니다.
to_lowercase는 유니코드 소문자 매핑을 수행합니다. 다만 소문자로 변환한 뒤 contains로 찾는 이 검색은 로케일별 비교나 완전한 case folding과 같지는 않습니다.
실제 애플리케이션에서는 필요한 문자열 비교 규칙을 따로 정해야 하지만, 이 절은 환경 변수로 검색 방식을 고르는 흐름에 집중하므로 현재 구현을 유지하겠습니다.
to_lowercase의 호출이 존재하는 데이터를 참조하지 않고 새로운 데이터를 만들기 때문에, query가 이제 문자열 슬라이스가 아니라 String이 되었음을 주의하세요.
예를 들어 질의어가 "rUsT"라고 해봅시다.
입력 슬라이스를 직접 변경하지 않고 소문자 변환 결과인 "rust"를 담은 새로운 String을 할당합니다.
이 예에서는 새 String인 query를 &query로 빌려 문자열 패턴으로 전달합니다. 문자열의 소유권을 contains에 넘기는 것이 아닙니다. contains의 일반 시그니처는 Pattern을 받아 &str 외에도 문자 같은 패턴을 지원하므로, 모든 호출이 문자열 슬라이스만 받는다고 해석하지 마세요. str::contains를 참고하세요.
다음으로 line의 모든 글자를 소문자로 만들기 위해 to_lowercase 호출을 추가합니다.
이제 line과 query를 소문자로 변환했으니 질의어의 대소문자에 상관없이 매칭된 라인들을 찾아낼 것입니다.
이 구현이 테스트를 통과하는지 살펴봅시다.
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [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 구조체에 추가하겠습니다.
아직 이 필드를 어디서도 초기화하고 있지 않기 때문에 필드를 추가하는 것만으로는 컴파일 에러가 날 것입니다.
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
부울린 값을 갖는 ignore_case 필드를 추가했습니다.
다음으로, 예제 11-22에서 보시는 것처럼 run 함수가 ignore_case 필드의 값을 검사하여 search 함수 혹은 search_case_insensitive 함수 중 어느 쪽을 호출할지 결정하는 것이 필요합니다.
아직은 컴파일되지 않을 것입니다.
예제 11-22:config.ignore_case의 값에
기초하여 search나 search_case_insensitive를 호출하기
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(())
}
마지막으로 환경 변수의 검사가 필요합니다.
환경 변수 사용을 위한 함수는 표준 라이브러리의 env 모듈에 있으므로, src/lib.rs 상단에서 이 모듈을 스코프로 가져옵니다.
그런 다음 예제 11-23처럼 env 모듈의 var 함수를 사용하여 IGNORE_CASE라는 이름의 환경 변수에 어떤 값이 설정되었는지 확인해보겠습니다.
IGNORE_CASE라는 이름의 환경 변수의
값을 검사하기
use std::env;
// --생략--
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,
})
}
}
여기서는 ignore_case라는 새 변수를 만들었습니다.
이 값을 설정하기 위해서 env::var 함수를 호출하고 환경 변수의 이름 IGNORE_CASE를 넘겼습니다.
env::var 함수는 Result를 반환하며, 해당 환경 변수에 어떤 값이 설정되어 있으면 그 값을 담은 Ok 배리언트가 반환됩니다.
만일 환경 변수가 설정되어 있지 않다면 Err 배리언트가 반환될 것입니다.
환경 변수가 설정되었는지 확인하기 위해서 Result의 is_ok 메서드를 사용 중인데, 이는 프로그램이 대소문자를 구분하지 않는 검색을 해야 함을 뜻합니다.
IGNORE_CASE가 없거나 값이 유효한 Unicode가 아니면 is_ok는 거짓값을 반환하고 대소문자를 구분합니다. 유효한 값이 존재하면 "0"이나 빈 문자열을 포함해 참이 됩니다.
이 환경 변수의 값에 대해서는 고려하지 않고 그저 값이 설정되어 있는지 아닌지만 고려하므로, 여기서는 unwrap이나 expect 혹은 Result에서 사용했던 다른 메서드들 대신 is_ok를 사용하고 있습니다.
이 ignore_case 변수의 값을 Config 인스턴스에 전달했으므로, run 함수는
예제 11-22에 구현된 것처럼 이 값을 읽어서 search_case_insensitive 혹은
search의 호출 여부를 결정할 수 있습니다.
한번 시도해봅시다!
먼저 환경 변수 설정 없이 질의어 to를 넣어 프로그램을 실행시킬 것인데, 이는 모두 소문자인 단어 ‘to’가 포함된 어떤 라인과 매칭되어야 합니다.
$ cargo run -- to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [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위의 IGNORE_CASE=1 command 형식은 POSIX 계열 셸에서 이 명령과 자식 프로세스에만 값을 전달합니다. 그 명령이 끝난 뒤 셸 세션의 설정을 바꾸는 export와는 범위가 다릅니다.
PowerShell에서는 같은 접두 문법 대신 $Env:IGNORE_CASE에 먼저 할당하고 프로그램을 실행합니다. 아래의 세미콜론은 두 문장을 이어 쓰는 표시입니다.
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt이 할당은 현재 PowerShell 프로세스에 남고 이후 실행하는 자식 프로세스도 값을 물려받습니다. 새 독립 터미널을 위한 영구 설정을 저장한 것은 아닙니다. 같은 창에서 대소문자를 구분하는 첫 예제를 다시 실행하려면 아래처럼 먼저 해제해야 합니다.
Remove-Item cmdlet으로 설정을 해제할 수 있습니다.
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 프로그램은 지금부터 환경 변수에 의해 제어되는 대소문자 구별 없는 검색 기능을 사용할 수 있게 되었습니다.
이제는 커맨드 라인 인수 혹은 환경 변수를 통한 옵션 설정을 관리하는 방법을 알게 되었습니다.
어떤 프로그램들은 같은 환경값에 대해 인수와 환경 변수 모두를 사용할 수 있게 합니다.
그러한 경우에는 보통 한쪽이 다른 쪽에 대해 우선순위를 갖도록 결정합니다.
연습용으로 대소문자 구분 옵션을 커맨드 라인 혹은 환경 변수로 제어하는 시도를 직접 해보세요.
한쪽은 대소문자를 구분하도록 하고 다른 쪽은 대소문자 구분을 무시하도록 설정되어 실행되었을 경우에는 커맨드 라인 인수 쪽 혹은 환경 변수 쪽이 우선권을 갖도록 하는 결정이 필요합니다.
이때 설계의 핵심은 기본값, 환경 변수, 명령줄 인수, 실행 검증을 서로 분리해서 생각하는 것입니다.
환경 변수는 편리하지만 눈에 잘 보이지 않으므로, 어떤 입력이 최종 설정을 결정했는지 코드와 테스트에서 확인할 수 있어야 합니다.
환경 변수로 검색 정책을 바꿀 때는 Config::build에서 설정을 읽고,
run에서 단 한 번 분기하며, 두 검색 함수의 테스트가 서로의 동작을 침범하지
않는지 확인하는 흐름으로 정리할 수 있습니다.
std::env 모듈에는 환경 변수를 다루기 위한 유용한 기능들이 많이 있습니다.
어떤 것들이 가능한지는 문서를 확인해보세요.
예시
내용이 "Rust\nrust\nRUST\n"이고 질의가 "rust"라면 원 search는 둘째 줄 하나를 고릅니다. search_case_insensitive는 비교할 질의와 각 줄을 소문자 문자열로 만들지만, 결과에는 소문자로 바꾼 임시 문자열이 아니라 원래 line을 넣습니다. 따라서 이 예의 반환값은 Rust, rust, RUST의 세 원문 줄입니다. 출력의 대소문자를 통일하는 함수가 아닙니다. Unicode 소문자 변환과 모든 언어의 검색·정규화 정책도 같은 개념으로 일반화하지 않습니다.
현재 Config::build에는 명령줄 대소문자 옵션이 없고 env::var("IGNORE_CASE").is_ok()만 있습니다. 값이 유효한 문자열로 존재하면 "0"과 ""도 켜짐입니다. 문자열 "false"를 불리언 거짓으로 파싱하지 않습니다. 없는 변수와 Unicode 문자열로 읽을 수 없는 값은 이 검사에서 거짓입니다. env::var의 오류 조건을 입력 계약과 함께 확인합니다.
명령줄 우선권은 원 코드에 이미 있는 기능이 아니라 직접 추가할 설계입니다. 파싱을 마친 별도 옵션이 Option<bool>로 주어진다고 정하면, None은 환경 정책을 쓰고 Some(false)·Some(true)는 각각 명시한 정책을 우선하도록 다음 순수 함수로 결정할 수 있습니다.
fn choose_ignore_case(cli: Option<bool>, env_present: bool) -> bool {
cli.unwrap_or(env_present)
}이 함수는 실제 인수 파서를 추가하지 않고 이미 파싱한 값의 우선권만 정합니다. 특히 ‘옵션 없음’과 ‘거짓으로 지정’을 둘 다 false 하나로 저장하면 두 경우를 구별할 수 없습니다.
이 우선권은 부모 프로세스의 환경을 바꾸지 않고 아래 여섯 조합으로 검사할 수 있습니다. 앞의 tests 모듈에 choose_ignore_case 함수도 스코프에 들어오도록 두고 추가하세요. env_present는 실제 환경 값이 아니라 env::var(...).is_ok()의 결과를 전달받았다는 뜻입니다.
#[test]
fn explicit_cli_policy_wins() {
let cases = [
(None, false, false),
(None, true, true),
(Some(false), false, false),
(Some(false), true, false),
(Some(true), false, true),
(Some(true), true, true),
];
for (cli, env_present, expected) in cases {
assert_eq!(choose_ignore_case(cli, env_present), expected);
}
}Some(false)와 환경의 켜짐이 충돌하는 조합이 핵심입니다. 두 값을 단순히 cli || env_present로 합치면 명시한 거짓을 무시하게 되어 이 사례에서 틀립니다. 이 예는 정책 함수의 검사 방법이며 CLI 파싱이나 환경 읽기까지 실행한 결과가 아닙니다.
독립 연습: 앞의 세 줄을 두고 환경 변수가 유효한 "0"인 원 코드가 선택하는 줄을 적어 보세요. 확장 설계에서 같은 환경 상태에 Some(false)를 주었을 때와, 환경 변수가 없는 상태에 Some(true)를 주었을 때도 각각 적습니다. 대소문자 무시 결과에 원문의 Rust가 남는 이유도 설명하세요.
존재 검사·명시 옵션·반환 바이트 구별하기
원 코드의 "0"은 존재 검사에 성공하므로 세 줄 모두 선택합니다. 확장 함수에 Some(false)를 주면 환경의 켜짐을 덮어 대소문자를 구분하므로 rust 한 줄만 선택합니다. 환경이 없어도 Some(true)면 세 줄을 선택합니다. 비교용 소문자 문자열은 일치 여부에만 쓰고 results.push(line)은 원문의 참조를 보관하므로 Rust의 원래 대소문자가 남습니다. 확장 옵션의 실제 파싱·프로세스 환경 변경·Cargo 실행은 여기서 검증한 결과가 아닙니다.
환경 변수 사용하기 학습 판단 기준표
환경 변수 사용하기를 구현할 때 컴파일러가 막는 지점과 테스트로 확인할 지점을 분리해 봅니다.
| 확인할 계약 | 구체적인 확인 방법 | 구별해야 하는 잘못된 구현 |
|---|---|---|
| 원문과 비교 문자열의 구별 | rUsT 검색 결과가 Rust.와 Trust me. 그대로인지 검사 | 소문자 임시값을 결과로 반환하거나 대여 |
| 환경 문자열 읽기 성공 여부 | 없음은 거짓, 유효한 "0"·""는 참이라는 규칙 확인 | 환경 값을 불리언 문자열로 파싱한다고 가정 |
| 명시 옵션의 우선권 | Some(false)와 환경 참이 충돌해도 거짓인지 검사 | 옵션 없음과 명시 거짓을 동일시하거나 OR로 합침 |
여기서 Config::build는 실행 시작 때 설정을 읽고 고정합니다. 부모 환경을 바꾸며 병렬 단위 테스트를 돌리는 대신, 순수한 정책 함수와 두 검색 함수를 먼저 직접 검사하고 실제 환경 연결은 별도 자식 프로세스에서 확인할 수 있습니다.