[Rust 가이드] 12.7. 환경 변수 사용
Source: Dev.to
12.7.0 시작하기 전에
12장에서는 샘플 프로젝트를 구축합니다: 명령줄 프로그램. 이 프로그램은 grep(Global Regular Expression Print)이며, 전역 정규식 검색 및 출력을 위한 도구입니다. 지정된 파일에서 지정된 텍스트를 검색하는 것이 기능입니다.
이 프로젝트는 다음 단계로 나뉩니다:
- 명령줄 인수 받기
- 파일 읽기
- 리팩터링: 모듈 및 오류 처리 개선
- TDD(테스트 주도 개발)를 사용해 라이브러리 기능 구현
- 환경 변수 사용(이번 글)
- 표준 출력이 아닌 표준 오류에 오류 메시지 쓰기
이 글이 도움이 되었다면 좋아요, 북마크, 팔로우를 눌러 주세요. 시리즈를 따라가며 계속 학습하시길 바랍니다.
아래는 이전 글까지 작성된 전체 코드입니다.
lib.rs:
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result {
if args.len() Result> {
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
pub fn search(query: &str, contents: &'a str) -> Vec {
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 one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
main.rs:
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let args: Vec = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
이전 섹션에서는 비즈니스 로직을 lib.rs로 옮기는 작업을 마쳤습니다. 이는 테스트 작성에 매우 유용합니다. lib.rs에 있는 로직은 명령줄에서 프로그램을 실행하지 않고도 다양한 인수로 직접 호출하고 반환값을 확인할 수 있기 때문입니다. 즉, 비즈니스 로직을 직접 테스트할 수 있습니다.
TDD는 Test‑Driven Development의 약자로 보통 다음 순서를 따릅니다:
- 실패하는 테스트를 작성하고 실행해, 기대한 이유로 실패하는지 확인한다.
- 새로운 테스트를 통과시키기에 충분한 최소 코드를 작성하거나 수정한다.
- 방금 추가·수정한 코드를 리팩터링하고, 테스트가 여전히 통과하는지 확인한다.
- 1단계로 돌아가 계속 진행한다.
TDD는 여러 소프트웨어 개발 방법 중 하나에 불과하지만, 코드 설계를 안내하고 지원하는 데 도움이 됩니다. 테스트를 먼저 작성하고 이를 통과하는 코드를 작성하면 개발 과정에서 높은 테스트 커버리지를 유지할 수 있습니다.
이번 글에서는 TDD를 활용해 프로그램의 검색 로직을 구현합니다: 파일 내용에서 지정된 문자열을 찾아 일치하는 줄을 리스트에 넣는 함수 search_case_insensitive를 만들 것입니다.
우선 대소문자를 구분하지 않는 함수 이름을 search_case_insensitive로 정합니다.
먼저 테스트 모듈을 수정해 대소문자를 구분하는 테스트와 구분하지 않는 테스트를 모두 포함하도록 합니다:
#[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의 몸체를 작성합니다:
pub fn search_case_insensitive(query: &str, contents: &'a str) -> Vec {
vec![]
}
외부에서 호출할 수 있게 하려면 pub으로 선언해야 합니다.
함수에 라이프타임 어노테이션이 필요한 이유는 매개변수가 여러 개이고, 반환값의 라이프타임이 어느 매개변수와 맞아야 할지 러스트가 판단할 수 없기 때문입니다. 반환되는 Vec의 요소는 contents에서 가져온 문자열 슬라이스이므로 반환값은 contents와 같은 라이프타임을 가져야 합니다. 따라서 두 매개변수와 반환값에 동일한 'a 라이프타임을 붙이고, query는 라이프타임이 필요 없습니다.
함수 몸체는 아직 컴파일만 되면 되며, TDD의 첫 단계는 실패하는 테스트를 만드는 것이므로 현재는 실패가 의도된 결과입니다.
이 시점에서 테스트를 실행하면 확실히 실패하지만, 괜찮습니다. 바로 TDD 첫 단계가 기대하는 상황이기 때문입니다.
search_case_insensitive 코드는 search와 매우 유사하므로 몇 가지 작은 변경만 하면 됩니다. 로직은 간단합니다: query를 소문자로 바꾸고, 텍스트의 각 줄을 소문자로 변환해 비교합니다:
pub fn search_case_insensitive(query: &str, contents: &'a str) -> Vec {
let mut results = Vec::new();
let query = query.to_lowercase();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
to_lowercase 메서드는 문자열을 모두 소문자로 변환합니다.
to_lowercase의 결과는 String이므로 새 query는 &str이 아니라 소유된 String이 됩니다. 루프 안에서는 contains가 String을 직접 받지 못하므로 &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
두 테스트 모두 통과했습니다.
함수가 정상 동작하므로 run 함수 안에서 호출하도록 수정합니다. 먼저 Config 구조체에 일반 검색을 사용할지 대소문자를 구분하지 않는 검색을 사용할지 결정할 수 있는 필드를 추가합니다:
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
run을 수정해 설정을 확인하도록 합니다:
pub fn run(config: Config) -> Result> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
새로운 Config 생성자도 환경 변수에 따라 case_sensitive 값을 설정하도록 바꿔야 합니다:
impl Config {
pub fn new(args: &[String]) -> Result {
if args.len() Result {
if args.len() Result> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
(코드가 중복되어 보이지만 원문 그대로 유지했습니다.)
아래는 전체 코드 스니펫을 정리한 것입니다.
search:
pub fn search(query: &str, contents: &'a str) -> Vec {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
search_case_insensitive:
pub fn search_case_insensitive(query: &str, contents: &'a str) -> Vec {
let mut results = Vec::new();
let query = query.to_lowercase();
for line in contents.lines() {
// (본문에 이어지는 구현은 원문을 따릅니다)
}
}