커맨드 라인 인수 받기

언제나처럼 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으로 넘겨진 커맨드 라인 인수의 반복자 (iterator) 를 반환합니다. 반복자에 대한 모든 것은 13장에서 다룰 예정입니다. 지금은 반복자에 대한 두 가지 세부 사항만 알면 됩니다: 반복자는 일련의 값들을 생성하고, 반복자의 collect 메서드를 호출하여 반복자가 생성하는 모든 요소를 담고 있는 벡터 같은 컬렉션으로 바꿀 수 있다는 것입니다.

예제 12-1의 코드는 minigrep 프로그램이 넘겨진 어떤 커맨드 라인 인수들을 읽은 후, 그 값들을 벡터로 모아주도록 해 줍니다.

파일명: src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}

예제 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의 타입을 명시적으로 표기하였습니다. 러스트에서는 타입을 명시할 필요가 거의 없지만, 러스트가 여러분이 원하는 종류의 컬렉션을 추론할 수는 없으므로 collect는 타입 표기가 자주 필요한 함수 중 하나입니다.

마지막으로 디버그 매크로를 사용하여 벡터를 출력합니다. 먼저 인수 없이 코드를 실행해 보고, 그다음 인수 두 개를 넣어 실행해 봅시다:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
[src/main.rs:5] args = [
    "target/debug/minigrep",
]
$ cargo run -- needle haystack
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 1.57s
     Running `target/debug/minigrep needle haystack`
[src/main.rs:5] args = [
    "target/debug/minigrep",
    "needle",
    "haystack",
]

벡터의 첫 번째 값이 "target/debug/minigrep", 즉 이 바이너리 파일의 이름인 점을 주목하세요. 이는 C에서의 인수 리스트의 동작과 일치하며, 프로그램이 실행될 때 호출된 이름을 사용할 수 있게 해 줍니다. 프로그램의 이름에 접근할 수 있는 것은 메시지에 이름을 출력하고 싶을 때라던가 프로그램을 호출할 때 사용된 커맨드 라인 별칭이 무엇이었는지에 기반하여 프로그램의 동작을 바꾸고 싶을 때 종종 편리하게 이용됩니다. 하지만 이 장의 목적을 위해서 지금은 이를 무시하고 현재 필요한 두 인수만 저장하겠습니다.

인수 값들을 변수에 저장하기

이제 프로그램은 커맨드 라인 인수로 지정된 값들에 접근할 수 있습니다. 이제는 두 인수의 값을 변수에 저장할 필요가 있는데, 그렇게 하면 프로그램의 나머지 부분에서 이 값들을 사용할 수 있겠습니다. 예제 12-2에서 이 동작을 수행합니다.

파일명: 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);
}

예제 12-2: 질의 (query) 인수와 파일 경로 인수를 담은 변수 생성하기

벡터를 출력할 때 본 것처럼 프로그램의 이름이 벡터의 첫 번째 값 args[0]을 사용하므로, 인덱스 1에 있는 인수부터 시작하고 있습니다. minigrep이 취하는 첫 번째 인수는 검색하고자 하는 문자열이므로, 첫 번째 인수의 참조자를 query 변수에 집어넣습니다. 두 번째 인수는 파일 경로가 될 것이므로, 두 번째 인수의 참조자를 file_path에 집어넣습니다.

우리 의도대로 코드가 동작하는지 검증하기 위해 이 변수의 값들을 임시로 출력하겠습니다. testsample.txt를 인수로 하여 이 프로그램을 다시 실행해 봅시다:

$ cargo run -- test sample.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

프로그램이 훌륭하게 동작하네요! 필요로 하는 인수 값들이 올바른 변수에 저장되고 있습니다. 나중에는 사용자가 아무런 인수를 제공하지 않았을 때처럼 에러가 발생할 수 있는 특정한 경우를 처리하기 위한 에러 처리 기능을 몇 가지 추가할 것입니다; 지금은 그런 경우를 무시하고 파일 읽기 기능을 추가하는 작업으로 넘어가겠습니다.