명령줄 인수 받기
늘 하던 대로 cargo new 로 새 프로젝트를 만들어 봅시다. 이미 시스템에 grep
도구가 있을 수 있으므로, 우리의 프로젝트 이름은 구분을 위해 minigrep 이라고
하겠습니다.
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
첫 번째 작업은 minigrep 이 두 개의 명령줄 인수, 즉 파일 경로와 검색할 문자열을
받게 만드는 것입니다. 즉, cargo run 뒤에 “이후 인수는 cargo 가 아니라 우리
프로그램에 전달된다”는 뜻의 하이픈 두 개, 검색할 문자열, 그리고 검색 대상 파일 경로를
이런 식으로 넘기고 싶습니다.
$ cargo run -- searchstring example-filename.txt
지금 cargo new 가 만들어 준 기본 프로그램은 우리가 넘긴 인수를 처리하지 못합니다.
crates.io 에는 명령줄 인수를 다루는 프로그램 작성을 도와주는
라이브러리도 있지만, 지금은 이 개념을 배우는 중이므로 직접 구현해 보겠습니다.
인수 값 읽기
minigrep 이 명령줄 인수 값을 읽게 하려면, 러스트 표준 라이브러리의
std::env::args 함수가 필요합니다. 이 함수는 minigrep 에 전달된 명령줄
인수들에 대한 반복자를 반환합니다. 반복자는 13장에서
자세히 다루지만, 지금은 두 가지만 알면 됩니다. 반복자는 값들의 연속을 만들어 내고,
우리는 반복자에 collect 메서드를 호출해 그 값들을 벡터 같은 컬렉션으로 모을 수
있습니다.
목록 12-1의 코드는 minigrep 프로그램이 전달된 명령줄 인수를 읽고, 그 값을 벡터에
모으게 해 줍니다.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
먼저 std::env 모듈을 use 문으로 스코프로 가져와 args 함수를 쓸 수 있게
합니다. std::env::args 함수가 모듈 두 단계를 거쳐 중첩되어 있다는 점에
주목하세요. 7장에서 이야기했듯이, 원하는
함수가 여러 단계의 모듈 안에 있을 때는 함수 자체보다는 부모 모듈을 스코프로 가져오는
편을 선택했습니다. 이렇게 하면 std::env 의 다른 함수들도 쉽게 사용할 수 있고,
use std::env::args 를 한 뒤 함수 이름을 그냥 args 라고만 쓰는 것보다 모호성도
적습니다. args 가 현재 모듈 안에 정의된 함수처럼 보일 수도 있기 때문입니다.
args 함수와 잘못된 유니코드
std::env::args 는 어떤 인수라도 잘못된 유니코드를 포함하고 있으면 패닉을
일으킨다는 점에 주의하세요. 프로그램이 잘못된 유니코드를 담은 인수도 받아들여야
한다면, 대신 std::env::args_os 를 사용해야 합니다. 이 함수는 String
대신 OsString 값을 만들어 내는 반복자를 반환합니다. 여기서는 단순화를 위해
std::env::args 를 사용합니다. OsString 은 플랫폼마다 다르고 String
보다 다루기 더 복잡하기 때문입니다.
main 첫 줄에서 env::args 를 호출하고, 그 반복자가 만들어 내는 모든 값을 담은
벡터로 바꾸기 위해 즉시 collect 를 사용합니다. collect 는 여러 종류의 컬렉션을
만들 수 있기 때문에, 여기서는 문자열 벡터를 원한다는 뜻으로 args 의 타입을
명시적으로 주석 처리합니다. 러스트에서는 타입을 직접 적어야 하는 경우가 드물지만,
collect 는 원하는 컬렉션 종류를 러스트가 추론하기 어려워서 자주 타입 주석이
필요한 함수 중 하나입니다.
마지막으로 디버그 형식으로 벡터를 출력합니다. 먼저 인수 없이, 그 다음 두 개의 인수를 넣고 이 코드를 실행해 봅시다.
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
벡터의 첫 번째 값이 "target/debug/minigrep" 인데, 이것은 우리 바이너리 이름입니다.
이는 C의 인수 목록 동작과 같은 방식입니다. 즉 프로그램은 “자신이 어떤 이름으로
호출되었는지” 도 함께 인수로 받습니다. 프로그램 이름을 메시지에 출력하거나, 어떤
명령줄 별칭으로 호출되었는지에 따라 프로그램 동작을 바꾸고 싶을 때는 이런 정보가
유용합니다. 하지만 이 장에서는 그것을 무시하고, 필요한 두 인수만 저장하겠습니다.
인수 값을 변수에 저장하기
현재 프로그램은 명령줄 인수로 넘긴 값들에 접근할 수 있습니다. 이제 이 두 인수 값을 변수에 저장해, 나머지 프로그램 전체에서 사용할 수 있게 해야 합니다. 목록 12-2가 그 작업을 보여 줍니다.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
}
벡터를 출력했을 때 봤듯이, 프로그램 이름은 벡터의 첫 번째 값 args[0] 을 차지합니다.
따라서 실제 인수는 인덱스 1부터 시작합니다. minigrep 이 받는 첫 번째 인수는
검색할 문자열이므로, 첫 번째 인수에 대한 참조를 query 변수에 넣습니다. 두 번째
인수는 파일 경로이므로, 두 번째 인수에 대한 참조를 file_path 변수에 넣습니다.
코드가 의도대로 동작하는지 확인하기 위해, 지금은 이 변수들의 값을 잠시 출력해 둡니다.
test 와 sample.txt 인수를 주고 프로그램을 다시 실행해 봅시다.
$ cargo run -- test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
좋습니다. 프로그램이 동작합니다! 필요한 인수 값들이 올바른 변수에 저장되고 있습니다. 나중에는 사용자가 인수를 하나도 주지 않는 경우 같은 잠재적 오류 상황을 다루기 위한 에러 처리도 추가할 것입니다. 하지만 지금은 그 상황을 잠시 무시하고, 파일을 읽는 기능을 먼저 추가해 보겠습니다.