본문 바로가기

환경 변수 활용

환경 변수로 대소문자 구분 검색을 선택하도록 설정하고 실패 테스트에서 시작해 별도 검색 함수를 구현합니다.

minigrep에 기능을 하나 더해 개선합니다. 사용자가 환경 변수로 켤 수 있는, 대소문자를 구분하지 않는 검색 옵션입니다. 이 기능을 명령줄 옵션으로 만들어 쓸 때마다 사용자가 입력하게 할 수도 있지만, 환경 변수로 만들면 사용자가 한 번 설정해 두는 것만으로 그 터미널 세션에서 하는 모든 검색이 대소문자를 구분하지 않게 됩니다. 아래 그림은 이 글에서 만들 대소문자 무시 검색이 무엇을 비교하고 무엇을 돌려주는지를 보여 줍니다.

대소문자 무시 검색의 비교값과 반환값 흐름

rUsT→rust 한 번 준비, 각 원본 줄 임시 lowercase 비교 후 Rust./Trust me. 참조 누적과 Duct tape. 제외가 구체적이다. 임시 String을 반환하지 않는다.

RUST · MINIGREP

소문자로 비교하고 원본 줄을 반환한다

질의어 rUsT는 한 번 소문자로 바꾸고 각 줄은 비교할 때 소문자로 바꿉니다. 결과에는 원래 대소문자와 구두점을 보존합니다.

비교 문자열과 반환 줄의 원본을 나눈다여섯노드여섯관계. query를한번소문자로준비하고원본각줄을소문자임시String으로바꿔contains를검사한다. 일치면원본Rust./Trustme.참조를결과에넣고Ducttape.는제외한다. 점선원문대여는임시소문자String이아닌contents의줄이반환수명의원천임을뜻한다. query와원문입력은독립이며순차실행사슬이아니다. 환경선택은아래설명처럼호출자책임이다.질의: rUsT → rust한 번 만든 비교 Stringcontents의 원본 줄Rust. / Trust me. / Duct tape.각 줄의 임시 Stringline.to_lowercase()rust 포함?Vec<&'a str>Rust. / Trust me. 원문제외Duct tape.비교 패턴예아니요원문 대여
비교 문자열과 반환 줄의 원본을 나눈다여섯노드여섯관계. query를한번소문자로준비하고원본각줄을소문자임시String으로바꿔contains를검사한다. 일치면원본Rust./Trustme.참조를결과에넣고Ducttape.는제외한다. 점선원문대여는임시소문자String이아닌contents의줄이반환수명의원천임을뜻한다. query와원문입력은독립이며순차실행사슬이아니다. 환경선택은아래설명처럼호출자책임이다.질의: rUsT → rust한 번 만든 Stringcontents 원본 줄Rust. / Trust me.Duct tape.각 줄의 임시 Stringline.to_lowercase()rust 포함?Vec<&'a str>Rust. / Trust me. 원문제외Duct tape.비교 패턴예아니요원문 대여

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로 만든다.

  • 입력: rUsT
  • 비교 패턴: rust
  • 패턴 정규화는 매 줄 처리 전에 한 번 한다.
임시 비교

원본 줄의 임시 lowercase로 contains를 판단한다.

  • 일치: Rust. / Trust me.
  • 비일치: Duct tape.
  • 비교용 String과 반환할 원본 참조를 구분한다.
원본 반환

일치한 원래 표기의 줄을 누적한다.

  • 반환: Rust. / Trust me.의 &str
  • 임시 String: 반환하지 않음
  • 함수 종료 뒤 사라질 임시 문자열의 참조를 보관하지 않는다.

대소문자 무시 검색의 비교값과 반환값 흐름

rUsT→rust 한 번 준비, 각 원본 줄 임시 lowercase 비교 후 Rust./Trust me. 참조 누적과 Duct tape. 제외가 구체적이다. 임시 String을 반환하지 않는다.

RUST · MINIGREP

소문자로 비교하고 원본 줄을 반환한다

질의어 rUsT는 한 번 소문자로 바꾸고 각 줄은 비교할 때 소문자로 바꿉니다. 결과에는 원래 대소문자와 구두점을 보존합니다.

비교 문자열과 반환 줄의 원본을 나눈다여섯노드여섯관계. query를한번소문자로준비하고원본각줄을소문자임시String으로바꿔contains를검사한다. 일치면원본Rust./Trustme.참조를결과에넣고Ducttape.는제외한다. 점선원문대여는임시소문자String이아닌contents의줄이반환수명의원천임을뜻한다. query와원문입력은독립이며순차실행사슬이아니다. 환경선택은아래설명처럼호출자책임이다.질의: rUsT → rust한 번 만든 비교 Stringcontents의 원본 줄Rust. / Trust me. / Duct tape.각 줄의 임시 Stringline.to_lowercase()rust 포함?Vec<&'a str>Rust. / Trust me. 원문제외Duct tape.비교 패턴예아니요원문 대여
비교 문자열과 반환 줄의 원본을 나눈다여섯노드여섯관계. query를한번소문자로준비하고원본각줄을소문자임시String으로바꿔contains를검사한다. 일치면원본Rust./Trustme.참조를결과에넣고Ducttape.는제외한다. 점선원문대여는임시소문자String이아닌contents의줄이반환수명의원천임을뜻한다. query와원문입력은독립이며순차실행사슬이아니다. 환경선택은아래설명처럼호출자책임이다.질의: rUsT → rust한 번 만든 Stringcontents 원본 줄Rust. / Trust me.Duct tape.각 줄의 임시 Stringline.to_lowercase()rust 포함?Vec<&'a str>Rust. / Trust me. 원문제외Duct tape.비교 패턴예아니요원문 대여

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로 만든다.

  • 입력: rUsT
  • 비교 패턴: rust
  • 패턴 정규화는 매 줄 처리 전에 한 번 한다.
임시 비교

원본 줄의 임시 lowercase로 contains를 판단한다.

  • 일치: Rust. / Trust me.
  • 비일치: Duct tape.
  • 비교용 String과 반환할 원본 참조를 구분한다.
원본 반환

일치한 원래 표기의 줄을 누적한다.

  • 반환: Rust. / Trust me.의 &str
  • 임시 String: 반환하지 않음
  • 함수 종료 뒤 사라질 임시 문자열의 참조를 보관하지 않는다.

그림의 예에서 검색어 "rUsT"는 to_lowercase로 한 번만 바꿔 새 String "rust"를 만들며, 원래 검색어 슬라이스는 바꾸지 않습니다. contents의 각 줄 Rust., Trust me., Duct tape.는 비교할 때마다 소문자로 바꾼 임시 String을 만들어 "rust"가 들어 있는지 확인합니다. 앞의 두 줄은 들어 있고 Duct tape.는 빠집니다. 결과에는 소문자로 바꾼 임시값이 아니라 contents에서 빌린 원래 줄을 담으므로, 원래의 대소문자와 구두점이 그대로 남습니다. 환경 변수를 읽는 일은 Config가, 어느 검색 함수를 쓸지 고르는 일은 run이 맡으므로, 검색 함수 자체는 환경 변수를 읽지 않아 직접 테스트할 수 있습니다.

대소문자를 구분하지 않는 search 함수에 대한 실패하는 테스트 작성하기

먼저 환경 변수에 값이 있을 때 부를 새 함수 search_case_insensitive를 minigrep 라이브러리에 더합니다. 계속 TDD 과정을 따르므로, 첫 단계는 다시 실패하는 테스트를 쓰는 것입니다. 새 함수 search_case_insensitive의 테스트를 더하고, 두 테스트의 차이가 분명하도록 예전 테스트의 이름을 one_result에서 case_sensitive로 바꿉니다.

src/lib.rs
#[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 함수를 정의하지 않았으므로 컴파일되지 않습니다. 테스트 주도 라이브러리 개발에서 search 함수에 했던 것처럼, 언제나 빈 벡터를 돌려주는 뼈대를 먼저 더해 테스트가 컴파일되고 실패하는 것을 확인해도 좋습니다.

search_case_insensitive 함수 구현하기

search_case_insensitive 함수는 search 함수와 거의 같습니다. 다른 점은 query와 각 line을 소문자로 바꾼다는 것뿐입니다. 그러면 입력 인수의 대소문자가 어떻든, 줄에 검색어가 들어 있는지 확인할 때 둘의 대소문자가 같아집니다.

src/lib.rs
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 문자열을 소문자로 바꾸고, 같은 이름의 새 변수에 넣어 원래의 query를 가립니다. 검색어에 to_lowercase를 부르는 것은 꼭 필요합니다. 그래야 사용자의 검색어가 "rust"든 "RUST"든 "Rust"든 "rUsT"든, 검색어를 "rust"로 다루고 대소문자를 신경 쓰지 않게 됩니다. to_lowercase는 기본적인 유니코드를 다루지만 100퍼센트 정확하지는 않습니다. 실제 애플리케이션을 만든다면 여기서 할 일이 더 많겠지만, 이 절의 주제는 유니코드가 아니라 환경 변수이므로 이 정도로 둡니다.

이제 query는 문자열 슬라이스가 아니라 String입니다. to_lowercase를 부르면 기존 데이터를 가리키는 대신 새 데이터를 만들기 때문입니다. 예를 들어 검색어가 "rUsT"라면, 그 문자열 슬라이스에는 쓸 수 있는 소문자 u나 t가 없으므로 "rust"를 담은 새 String을 할당해야 합니다. 이제 contains 메서드에 query를 인수로 넘길 때는 앰퍼샌드를 붙여야 합니다. contains는 Pattern 트레이트를 구현한 타입을 검색 패턴으로 받는데, &str과 &String은 이 트레이트를 구현하지만 소유한 String은 구현하지 않기 때문입니다.

다음으로 각 line에 to_lowercase를 불러 모든 문자를 소문자로 바꿉니다. line과 query를 소문자로 바꿨으니, 검색어의 대소문자와 상관없이 일치하는 줄을 찾게 됩니다. 이 구현이 테스트를 통과하는지 봅니다.

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

통과했습니다. 다만 소문자로 바꾸는 일은 매번 새 String을 할당하고, 유니코드의 일부 문자는 소문자가 되면서 글자 수나 바이트 길이가 늘어나기도 합니다. 또 이 방법은 언어마다 다른 비교 규칙이나 완전한 대소문자 접기(case folding)와 같지 않습니다. 이제 run 함수에서 새 search_case_insensitive 함수를 부릅니다. 먼저 Config 구조체에 대소문자를 구분할지를 정하는 설정 항목을 더합니다. 이 필드를 더하면 아직 이 필드를 초기화하는 곳이 없으므로 컴파일 오류가 납니다.

src/lib.rs
pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

불리언 값을 담는 ignore_case 필드를 더했습니다. 다음으로, run 함수가 ignore_case 필드의 값을 확인해 search 함수와 search_case_insensitive 함수 가운데 무엇을 부를지 정하게 합니다. 이 코드도 아직 컴파일되지 않습니다.

src/lib.rs (컴파일되지 않음)
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/main.rs의 맨 위에서 스코프로 가져왔습니다. src/lib.rs에서도 env 모듈의 var 함수로 IGNORE_CASE라는 환경 변수에 값이 설정되어 있는지 확인합니다.

src/lib.rs
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가 어떻게 검색 함수 하나를 고르는 데까지 이어지는지를 보여 줍니다.

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 플래그와 우선순위는 마지막의 확장 과제입니다.

환경의 읽기 결과를 저장하고 검색 함수만 선택한다현재본문의다섯노드다섯관계. IGNORE_CASE를Unicode문자열로읽었는지is_ok로판단한다. 성공은0이나빈문자열도true설정이며없음/비Unicode는false설정이다. 두설정은대안이고run이저장한bool로search또는search_case_insensitive하나를선택한다. CLI의명시옵션과우선순위는확장설계로현재이그래프에구현된것이아니다. 분기들이위아래여도서로를통과하는흐름은없다.IGNORE_CASEenv::var의 Resultis_ok()?Config.ignore_case = true0 / 빈 값도 유효 문자열Config.ignore_case = false없음 / 비Unicoderun이 저장한 bool로search 또는search_case_insensitive 선택예아니요
환경의 읽기 결과를 저장하고 검색 함수만 선택한다현재본문의다섯노드다섯관계. IGNORE_CASE를Unicode문자열로읽었는지is_ok로판단한다. 성공은0이나빈문자열도true설정이며없음/비Unicode는false설정이다. 두설정은대안이고run이저장한bool로search또는search_case_insensitive하나를선택한다. CLI의명시옵션과우선순위는확장설계로현재이그래프에구현된것이아니다. 분기들이위아래여도서로를통과하는흐름은없다.IGNORE_CASEenv::var의 Resultis_ok()?ignore_case = true0 / 빈 문자열도 trueignore_case = false없음 / 비Unicoderun의 bool 분기search 또는search_case_insensitive 선택예아니요대안 합류

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를 확인한다.

  • 검사: env::var(...).is_ok()
  • 0/빈 Unicode: Ok이면 true
  • 이 구현은 문자열 0을 false로 해석하는 파서가 아니다.
Config 저장

is_ok 결과를 ignore_case bool에 저장한다.

  • 필드: Config.ignore_case
  • 값: true 또는 false
  • 환경 읽기는 설정 생성 경계에서 한 번 수행한다.
한 함수 선택

run이 bool에 맞는 검색 하나를 호출한다.

  • true: case-insensitive search
  • false: case-sensitive search
  • 미구현 CLI 우선순위나 두 검색의 연속 실행을 추가하지 않는다.

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 플래그와 우선순위는 마지막의 확장 과제입니다.

환경의 읽기 결과를 저장하고 검색 함수만 선택한다현재본문의다섯노드다섯관계. IGNORE_CASE를Unicode문자열로읽었는지is_ok로판단한다. 성공은0이나빈문자열도true설정이며없음/비Unicode는false설정이다. 두설정은대안이고run이저장한bool로search또는search_case_insensitive하나를선택한다. CLI의명시옵션과우선순위는확장설계로현재이그래프에구현된것이아니다. 분기들이위아래여도서로를통과하는흐름은없다.IGNORE_CASEenv::var의 Resultis_ok()?Config.ignore_case = true0 / 빈 값도 유효 문자열Config.ignore_case = false없음 / 비Unicoderun이 저장한 bool로search 또는search_case_insensitive 선택예아니요
환경의 읽기 결과를 저장하고 검색 함수만 선택한다현재본문의다섯노드다섯관계. IGNORE_CASE를Unicode문자열로읽었는지is_ok로판단한다. 성공은0이나빈문자열도true설정이며없음/비Unicode는false설정이다. 두설정은대안이고run이저장한bool로search또는search_case_insensitive하나를선택한다. CLI의명시옵션과우선순위는확장설계로현재이그래프에구현된것이아니다. 분기들이위아래여도서로를통과하는흐름은없다.IGNORE_CASEenv::var의 Resultis_ok()?ignore_case = true0 / 빈 문자열도 trueignore_case = false없음 / 비Unicoderun의 bool 분기search 또는search_case_insensitive 선택예아니요대안 합류

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를 확인한다.

  • 검사: env::var(...).is_ok()
  • 0/빈 Unicode: Ok이면 true
  • 이 구현은 문자열 0을 false로 해석하는 파서가 아니다.
Config 저장

is_ok 결과를 ignore_case bool에 저장한다.

  • 필드: Config.ignore_case
  • 값: true 또는 false
  • 환경 읽기는 설정 생성 경계에서 한 번 수행한다.
한 함수 선택

run이 bool에 맞는 검색 하나를 호출한다.

  • true: case-insensitive search
  • false: case-sensitive search
  • 미구현 CLI 우선순위나 두 검색의 연속 실행을 추가하지 않는다.

Result에 is_ok 메서드를 불러 환경 변수가 설정되어 있는지 확인합니다. 설정되어 있다면 프로그램은 대소문자를 구분하지 않는 검색을 해야 합니다. is_ok는 값의 내용이 아니라 유니코드로 올바른 값을 읽었는지만 봅니다. 그래서 값이 1이든 0이든, 빈 문자열이 실제로 설정되어 있든 모두 true가 됩니다. 환경 변수가 없으면 false이고, 값이 유니코드로 올바르지 않아 VarError::NotUnicode가 와도 false입니다. 바이트 수준에서 있는지만 알고 싶다면 env::var_os를 써서 규칙을 따로 정합니다.

IGNORE_CASE 환경 변수가 설정되어 있지 않으면 is_ok는 false를 돌려주고, 프로그램은 대소문자를 구분하는 검색을 합니다. 환경 변수의 값은 상관없고 설정되어 있는지 아닌지만 중요하므로, unwrap이나 expect 또는 Result에 대해 지금까지 본 다른 메서드 대신 is_ok를 씁니다. 실행을 시작할 때 읽은 이 bool을 Config에 저장해 두고, run이 그 값으로 search나 search_case_insensitive 가운데 하나를 부릅니다. 검색 함수는 환경 변수를 직접 읽지 않습니다. 명령줄 옵션으로 이 동작을 바꾸는 기능은 그림에서처럼 이 글의 마지막 연습 문제로 남겨 둡니다.

ignore_case 변수의 값을 Config 인스턴스에 넘기므로, run 함수가 그 값을 읽어 search_case_insensitive와 search 가운데 무엇을 부를지 정할 수 있습니다. 시험해 봅니다. 먼저 환경 변수를 설정하지 않고 검색어 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을 쓴다면 환경 변수를 설정하는 명령과 프로그램을 실행하는 명령을 따로 실행해야 합니다. 아래처럼 세미콜론으로 이어 한 줄에 적어도 두 명령이 차례로 실행되는 것일 뿐이며, bash의 앞의 명령처럼 그 실행 하나에만 적용되는 설정이 아닙니다.

$Env:IGNORE_CASE=1; cargo run -- to poem.txt

이렇게 하면 셸 세션이 이어지는 동안 IGNORE_CASE가 계속 설정되어 있습니다. Remove-Item cmdlet으로 설정을 지울 수 있습니다.

Remove-Item Env:IGNORE_CASE

이제 대문자가 든 to도 일치하는 줄이 나와야 합니다. 앞의 실행 결과와 비교해, To로 시작하는 두 줄이 더해졌는지 보세요.

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

훌륭합니다. To가 든 줄도 나왔습니다. 이제 minigrep 프로그램은 환경 변수로 대소문자를 구분하지 않는 검색을 할 수 있습니다. 명령줄 인수나 환경 변수로 설정하는 옵션을 다루는 방법을 알게 되었습니다.

환경 변수에 의존하는 동작을 테스트할 때는 테스트 프로세스의 환경을 직접 바꾸기보다, 실행 파일을 자식 프로세스로 실행하면서 Command::env나 env_remove로 그 프로세스의 환경만 정하는 편이 안전합니다. Rust 2024 에디션에서는 프로세스 전체의 환경 변수를 바꾸는 env::set_var가 unsafe 함수이며, 테스트는 기본으로 병렬 실행되므로 한 테스트가 바꾼 환경을 다른 테스트가 읽을 수 있기 때문입니다.

어떤 프로그램은 같은 설정을 인수와 환경 변수 모두로 받습니다. 그때는 둘 가운데 무엇이 우선하는지를 프로그램이 정합니다. 직접 연습해 보려면, 명령줄 인수나 환경 변수로 대소문자 구분을 정할 수 있게 해 보세요. 하나는 대소문자를 구분하라고, 다른 하나는 구분하지 말라고 설정한 채로 프로그램을 실행했을 때 명령줄 인수와 환경 변수 가운데 무엇이 우선할지 정하면 됩니다.

std::env 모듈에는 환경 변수를 다루는 쓸모 있는 기능이 더 많이 있습니다. 무엇을 쓸 수 있는지 보려면 std::env 문서를 살펴보세요.

정리

  • search_case_insensitive는 검색어를 한 번 소문자 String으로 바꾸고, 각 줄을 소문자로 바꾼 임시값과 비교해, 일치하면 원래의 줄을 돌려줍니다.
  • to_lowercase는 새 String을 만들므로 contains에는 &query처럼 참조를 넘깁니다. 이 변환은 언어별 비교나 완전한 대소문자 접기와 같지 않습니다.
  • env::var("IGNORE_CASE").is_ok()는 값의 내용과 상관없이 유니코드로 올바른 값이 설정되어 있으면 true입니다.
  • 환경 변수는 Config::build에서 한 번 읽어 ignore_case에 저장하고, run이 그 값으로 검색 함수 하나를 고르므로, 검색 함수는 환경 없이 테스트할 수 있습니다.
  • 셸에서는 IGNORE_CASE=1 cargo run -- ...로, PowerShell에서는 $Env:IGNORE_CASE=1로 설정하고 Remove-Item Env:IGNORE_CASE로 지웁니다.

연습 문제

1. 무엇이 켜질까

IGNORE_CASE를 (가) 0, (나) 빈 문자열, (다) 설정하지 않음으로 두고 cargo run -- to poem.txt를 실행하면 각각 몇 줄이 나오나요?

해설 보기

(가)와 (나)는 env::var가 Ok를 돌려주므로 is_ok()가 true가 되어 대소문자를 구분하지 않는 검색을 하고 네 줄이 나옵니다. 값이 0이라고 꺼지지는 않습니다. (다)는 Err이므로 대소문자를 구분하는 검색을 하고 소문자 to가 든 두 줄만 나옵니다.

2. 명령줄 옵션 더하기

--ignore-case나 --case-sensitive 같은 명령줄 옵션도 받도록 고친다면, 환경 변수와 함께 무엇을 정해야 하나요?

해설 보기

옵션을 주지 않은 상태와 일부러 끈 상태를 나눠야 하므로, 설정을 Option<bool>처럼 세 상태로 다루는 것이 좋습니다. 그다음 명령줄 옵션과 환경 변수가 서로 다르게 설정되었을 때 무엇이 우선할지 정해 도움말에 적고, 기본값만 있을 때, 환경 변수만 있을 때, 옵션만 있을 때, 둘이 충돌할 때를 각각 테스트합니다. 흔히 명령줄 옵션을 더 우선합니다.

다음 글 표준 에러 출력에서는 오류 메시지를 표준 출력 대신 표준 에러로 보내, 검색 결과와 나눕니다.