명령줄 인자 받기
명령줄 인자 받기
항상 그렇듯 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에는 명령줄 인자를 받는 프로그램 작성을 도와주는 라이브러리가 몇 가지 있지만, 이 개념을 막 배우는 중이니 직접 구현해 보는 게 좋겠어요.
출처: The Rust Book
인자 값 읽기
minigrep이 넘겨받은 명령줄 인자의 값을 읽을 수 있게 하려면, Rust 표준 라이브러리가 제공하는 std::env::args 함수가 필요해요. 이 함수는 minigrep에 전달된 명령줄 인자들의 반복자(iterator)를 반환합니다. 반복자는 13장에서 완전히 다룰 거예요. 지금은 두 가지만 알면 충분해요: 반복자는 값의 연속을 만들어 내고, 반복자에 collect 메서드를 호출하면 반복자가 만든 원소를 모두 담은 컬렉션(예: 벡터)으로 바꿀 수 있다는 것.
Listing 12-1의 코드는 minigrep 프로그램이 전달받은 명령줄 인자를 읽어 그 값을 벡터로 모아 줍니다.
파일명: 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는 인자에 잘못된 유니코드(invalid Unicode)가 있으면 패닉을 일으킨다는 점을 기억하세요. 프로그램이 잘못된 유니코드를 담은 인자를 받아야 한다면 std::env::args_os를 사용하세요. 이 함수는 String이 아니라 OsString 값을 만들어 내는 반복자를 반환합니다. 여기서는 OsString 값이 플랫폼마다 달라 String보다 다루기 복잡하기 때문에, 단순함을 위해 std::env::args를 사용하기로 했어요.
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 언어의 인자 목록 동작과 일치하는 것으로, 프로그램이 실행 과정에서 자신이 호출된 이름을 사용할 수 있게 해 줍니다. 프로그램 이름을 메시지에 출력하거나, 어떤 명령줄 별칭으로 호출됐는지에 따라 동작을 바꾸고 싶을 때 그 값을 쓰면 편리해요. 하지만 이번 챕터에서는 그 값은 무시하고 필요한 두 인자만 저장하겠습니다.
인자 값을 변수에 저장하기
이제 프로그램이 명령줄 인자로 지정된 값에 접근할 수 있게 됐어요. 이제 두 인자의 값을 변수에 저장해서, 프로그램의 나머지 부분에서 계속 쓸 수 있게 해야 합니다. 이것은 Listing 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}");
}
Listing 12-2: 쿼리 인자와 파일 경로 인자를 담을 변수 만들기
벡터를 출력했을 때 봤듯이 프로그램 이름이 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
잘 동작하네요! 필요한 인자 값들이 올바른 변수에 저장되고 있어요. 나중에 사용자가 인자를 아무것도 안 줬을 때 같은 잠재적 오류 상황을 처리하는 오류 처리를 추가할 거지만, 지금은 그 상황은 일단 무시하고 파일 읽기 기능을 추가하는 쪽으로 나아갈게요.