커맨드라인 인자 받아들이기

항상 그렇듯이 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이 전달받은 커맨드라인 인자 값을 읽으려면 Rust 표준 라이브러리에서 제공하는 std::env::args 함수를 사용해야 한다. 이 함수는 minigrep에 전달된 커맨드라인 인자들의 이터레이터를 반환한다. 이터레이터에 대해서는 13장에서 자세히 다룬다. 지금은 이터레이터가 일련의 값을 생성하고, 이터레이터에 collect 메서드를 호출해 벡터와 같은 컬렉션으로 변환할 수 있다는 점만 알면 된다.

리스트 12-1의 코드는 minigrep 프로그램이 전달받은 커맨드라인 인자를 읽고, 그 값을 벡터로 수집한다.

Filename: src/main.rs
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}
Listing 12-1: 커맨드라인 인자를 벡터로 수집하고 출력하기

먼저 use 문을 사용해 std::env 모듈을 스코프로 가져와 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의 타입을 명시적으로 어노테이션해 문자열 벡터를 원한다는 것을 나타낸다. Rust에서는 타입을 어노테이션할 필요가 거의 없지만, collect는 Rust가 원하는 컬렉션 종류를 추론할 수 없기 때문에 종종 어노테이션이 필요하다.

마지막으로, 디버그 매크로를 사용해 벡터를 출력한다. 이 코드를 인자 없이 실행한 후 두 개의 인자를 전달해 실행해 보자:

$ 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와 같이 코드를 작성한다.

Filename: src/main.rs
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}");
}
Listing 12-2: 쿼리 인자와 파일 경로 인자를 저장할 변수 생성

벡터를 출력했을 때 확인했듯이, 프로그램 이름은 벡터의 첫 번째 값인 args[0]에 저장된다. 따라서 인자는 인덱스 1부터 시작한다. minigrep이 받는 첫 번째 인자는 검색할 문자열이므로, 첫 번째 인자의 참조를 query 변수에 저장한다. 두 번째 인자는 파일 경로이므로, 두 번째 인자의 참조를 file_path 변수에 저장한다.

코드가 의도한 대로 동작하는지 확인하기 위해 이 변수들의 값을 임시로 출력한다. 이제 testsample.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

잘 동작한다! 필요한 인자 값이 올바른 변수에 저장되고 있다. 나중에 사용자가 인자를 제공하지 않는 경우와 같은 잠재적인 오류 상황을 처리하기 위해 에러 핸들링을 추가할 것이다. 지금은 그 상황을 무시하고 파일 읽기 기능을 추가하는 데 집중한다.