커맨드 라인 인수 받기
언제나처럼 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); }
먼저 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);
}
벡터를 출력할 때 본 것처럼 프로그램의 이름이 벡터의 첫 번째 값 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 [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
프로그램이 훌륭하게 동작하네요! 필요로 하는 인수 값들이 올바른 변수에 저장되고 있습니다. 나중에는 사용자가 아무런 인수를 제공하지 않았을 때처럼 에러가 발생할 수 있는 특정한 경우를 처리하기 위한 에러 처리 기능을 몇 가지 추가할 것입니다; 지금은 그런 경우를 무시하고 파일 읽기 기능을 추가하는 작업으로 넘어가겠습니다.