커맨드 라인 인수 받기
표준 라이브러리의 args 반복자로 검색어와 파일 경로를 읽어 변수에 저장하며 minigrep 명령줄 인터페이스를 시작합니다.
이번 장에서는 여러분이 지금까지 배운 여러 기술들을 요약하고 표준 라이브러리의 기능을 몇 가지 더 탐색해보겠습니다.
파일 및 커맨드 입출력을 통해 상호작용하는 커맨드 라인 도구를 만들면서 이제는 여러분이 이해하고 있을 러스트 개념 몇 가지를 연습해 볼 것입니다.
러스트는 속도, 안정성, 단일 바이너리 출력, 크로스 플랫폼 지원 덕분에 커맨드 라인 도구를 만들기에 적합하므로, 프로젝트를 위해 고전적인 커맨드 라인 검색 도구인 grep(globally search a regular expression and print)을 직접 구현해 보려고 합니다.
가장 단순한 사용례에서 grep은 어떤 특정한 파일에서 특정한 문자열을 검색합니다.
이를 위해 grep은 파일 경로와 문자열을 인수로 받습니다.
그다음 파일을 읽고, 그 파일의 라인 중 문자열 인수를 포함하고 있는 라인을 찾고, 그 라인들을 출력합니다.
그러는 와중에 수많은 다른 커맨드 라인 도구들이 사용하는 터미널의 기능을 우리의 커맨드 라인 도구도 사용할 수 있게 하는 방법을 알아보겠습니다.
먼저 환경 변수의 값을 읽어서 사용자가 커맨드 라인 도구의 동작을 설정하도록 할 것입니다.
또한 표준 출력 콘솔 스트림(stdout) 대신 표준 에러 콘솔 스트림(stderr)에 에러 메시지를 출력하여, 예를 들자면 사용자가 화면을 통해 에러 메시지를 보는 동안에도 성공적인 출력을 파일로 리디렉션할 수 있게끔 할 것입니다.
러스트 커뮤니티 일원인 앤드루 갈란트(Andrew Gallant)가 이미 ripgrep이라는 이름의, 모든 기능을 가진 grep의 매우 빠른 버전을 만들었습니다.
그에 비해서 지금 만들어 볼 버전은 꽤 단순할 예정이지만, 이 장은 여러분에게 ripgrep과 같은 실제 프로젝트를 이해하는 데 필요한 배경 지식을 제공할 것입니다.
이 grep 프로젝트는 지금까지 배운 여러 개념을 조합할 것입니다.
- 코드 조직화하기(6장에서 모듈에 대해 배운 것들을 사용)
- 벡터와 문자열 사용하기(7장의 컬렉션)
- 에러 처리하기(8장)
- 적절한 곳에 트레이트와 라이프타임 사용하기(9장)
- 테스트 작성하기(10장)
아울러 12장과 16장에서 자세히 다루게 될 클로저(closure), 반복자(iterator), 그리고 트레이트 객체(trait object)에 대해서도 간략히 소개하겠습니다.
이제 요구사항을 코드로 옮기는 순서부터 살펴보겠습니다.
언제나처럼 cargo new로 새 프로젝트를 만들어 봅시다.
여러분의 시스템에 이미 설치되어 있을지도 모를 grep 도구와 구분하기 위하여, 우리 프로젝트 이름은 minigrep으로 하겠습니다.
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrepminigrep을 만들기 위한 첫 과제는 두 개의 커맨드 라인 인수를 받는 것입니다.
바로 검색할 파일 경로와 문자열이지요.
아래와 같이 cargo run 뒤에 두 개의 하이픈을 써서, 이후의 인수가 cargo 대신 우리 프로그램에 전달됨을 나타냅니다. 그 뒤에는 검색할 문자열과 파일 경로를 지정합니다.
$ cargo run -- searchstring example-filename.txt현재 cargo new로 생성된 프로그램은 입력된 인수를 처리할 수 없습니다.
crates.io에 있는 몇 가지 라이브러리가 커맨드 라인 인수를 받는 프로그램 작성에 도움 되겠지만, 지금은 이 개념을 막 배우는 중이므로 직접 이 기능을 구현해봅시다.
인수 값 읽기
Cargo 인수와 minigrep 입력 계약의 경계
cargo run -- needle haystack→실행 인수→Vec<String> 소유→query/file_path 두 대여의 구체 값 전달이다. --는 원소가 아니고 길이 검사는 미구현이다.
RUST · MINIGREP
Cargo 경계를 지나 세 인수가 Vec에 모인다
이 절에서는 env::args().collect()로 인수를 모은 뒤 인덱스로 빌립니다. 설정 검증과 Config 생성은 11장 3절에서 도입합니다.
1 · Cargo에 입력
cargo run -- needle haystack--는 Cargo의 구분자입니다. 뒤의 두 값이 실행되는 minigrep에 전달되며 구분자 자체는 argv에 들어가지 않습니다.
2 · 프로그램에서 수집
let args: Vec<String> =
env::args().collect();반복자는 호출 이름과 두 사용자 인수를 소유한 String으로 내놓습니다. 공백을 다시 나누는 작업은 이미 셸이 처리했습니다.
3 · 현재 본문의 위치 계약
args[0] // 호출 이름
args[1] // "needle": query
args[2] // "haystack": file_path&args[1]과 &args[2]를 빌립니다. 아직 길이를 검증하지 않아 누락된 인덱스에 접근하면 패닉이 납니다.
다음 리팩터링과 입력 한계
11장 3절에서는 누락을 Result로 검사합니다. 12장 3절에서는 인수 반복자를 직접 소비해 복사를 줄입니다. args[0]은 관례적 호출 이름이며 신뢰할 경로가 아닙니다.
env::args()는 비유니코드 인수에서 순회 중 패닉이 날 수 있습니다. 해당 입력을 보존해야 한다면 args_os()와 OsString 계약을 사용합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 이 예의 네 노드와 세 흐름이다. Cargo 구분자 뒤 두 값이 실행 프로그램에 전달되고 env::args가 호출 이름과 그 값을 String으로 모아 Vec을 소유한다. 현재 본문은 args[1]과 args[2]를 빌리며 개수를 검증하지 않는다. Cargo의 -- 자체는 원소가 아니고 args[0]은 신뢰할 실행 파일 전체 경로가 아니다. 아래 원 세 단계 카드와 입력 한계를 보조 설명으로 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| Cargo 경계 명령의 -- 뒤가 프로그램 인수다. |
|
|
| 인수 소유 env::args를 Vec<String>으로 모은다. |
|
|
| 대여 전달 query와 file_path가 두 문자열을 빌린다. |
|
|
Cargo 인수와 minigrep 입력 계약의 경계
cargo run -- needle haystack→실행 인수→Vec<String> 소유→query/file_path 두 대여의 구체 값 전달이다. --는 원소가 아니고 길이 검사는 미구현이다.
RUST · MINIGREP
Cargo 경계를 지나 세 인수가 Vec에 모인다
이 절에서는 env::args().collect()로 인수를 모은 뒤 인덱스로 빌립니다. 설정 검증과 Config 생성은 11장 3절에서 도입합니다.
1 · Cargo에 입력
cargo run -- needle haystack--는 Cargo의 구분자입니다. 뒤의 두 값이 실행되는 minigrep에 전달되며 구분자 자체는 argv에 들어가지 않습니다.
2 · 프로그램에서 수집
let args: Vec<String> =
env::args().collect();반복자는 호출 이름과 두 사용자 인수를 소유한 String으로 내놓습니다. 공백을 다시 나누는 작업은 이미 셸이 처리했습니다.
3 · 현재 본문의 위치 계약
args[0] // 호출 이름
args[1] // "needle": query
args[2] // "haystack": file_path&args[1]과 &args[2]를 빌립니다. 아직 길이를 검증하지 않아 누락된 인덱스에 접근하면 패닉이 납니다.
다음 리팩터링과 입력 한계
11장 3절에서는 누락을 Result로 검사합니다. 12장 3절에서는 인수 반복자를 직접 소비해 복사를 줄입니다. args[0]은 관례적 호출 이름이며 신뢰할 경로가 아닙니다.
env::args()는 비유니코드 인수에서 순회 중 패닉이 날 수 있습니다. 해당 입력을 보존해야 한다면 args_os()와 OsString 계약을 사용합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 이 예의 네 노드와 세 흐름이다. Cargo 구분자 뒤 두 값이 실행 프로그램에 전달되고 env::args가 호출 이름과 그 값을 String으로 모아 Vec을 소유한다. 현재 본문은 args[1]과 args[2]를 빌리며 개수를 검증하지 않는다. Cargo의 -- 자체는 원소가 아니고 args[0]은 신뢰할 실행 파일 전체 경로가 아니다. 아래 원 세 단계 카드와 입력 한계를 보조 설명으로 유지한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| Cargo 경계 명령의 -- 뒤가 프로그램 인수다. |
|
|
| 인수 소유 env::args를 Vec<String>으로 모은다. |
|
|
| 대여 전달 query와 file_path가 두 문자열을 빌린다. |
|
|
minigrep이 커맨드 라인 인수로 전달된 값들을 읽을 수 있도록 하기 위해서는 러스트의 표준 라이브러리가 제공하는 std::env::args 함수를 사용할 필요가 있습니다.
이 함수는 minigrep으로 넘겨진 커맨드 라인 인수의 반복자(iterator)를 반환합니다.
반복자에 대한 모든 것은 12장에서 다룰 예정입니다.
지금은 반복자에 대한 두 가지 세부 사항만 알면 됩니다.
반복자는 일련의 값들을 생성하고, 반복자의 collect 메서드를 호출하여 반복자가 생성하는 모든 요소를 담고 있는 벡터 같은 컬렉션으로 바꿀 수 있다는 것입니다.
예제 11-1의 코드는 minigrep 프로그램이 넘겨진 어떤 커맨드 라인 인수들을
읽은 후, 그 값들을 벡터로 모아주도록 해 줍니다.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
먼저 use를 사용하여 std::env 모듈을 스코프로 가져와서 args 함수를 사용할 수 있게 합니다.
std::env::args 함수는 두 단계로 중첩된 모듈에 있는 점을 주목하세요.
6장에서 논의한 것처럼, 하나 이상의 모듈로 중첩된 곳에 원하는 함수가 있는 경우에는, 함수가 아닌 그 부모 모듈을 스코프로 가져오는 선택을 했습니다.
이렇게 하면 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는 타입 표기가 자주 필요한 함수 중 하나입니다.
마지막으로 디버그 매크로를 사용하여 벡터를 출력합니다.
dbg!(args)는 값을 표준 에러(stderr)에 출력하고, Copy가 아닌 Vec<String>의 소유권을 가져간 뒤 그 값을 반환합니다. 이 예제는 반환값을 저장하지 않고 끝나므로 문제없지만, 같은 main에서 뒤이어 args[1]을 읽을 수는 없습니다. 아래 예제 11-2는 앞의 main을 교체한 코드입니다. 디버그 출력을 남기면서 벡터를 계속 쓰고 싶다면 dbg!(&args)로 벡터를 빌립니다.
먼저 인수 없이 코드를 실행해 보고, 그다음 인수 두 개를 넣어 실행해봅시다.
$ 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에서의 인수 리스트의 동작과 일치하며, 프로그램이 실행될 때 호출된 이름을 사용할 수 있게 해 줍니다.
프로그램의 이름에 접근할 수 있는 것은 메시지에 이름을 출력하고 싶을 때라던가 프로그램을 호출할 때 사용된 커맨드 라인 별칭이 무엇이었는지에 기반하여 프로그램의 동작을 바꾸고 싶을 때 종종 편리하게 이용됩니다.
하지만 이 장의 목적을 위해서 지금은 이를 무시하고 현재 필요한 두 인수만 저장하겠습니다.
인수 값들을 변수에 저장하기
이제 프로그램은 커맨드 라인 인수로 지정된 값들에 접근할 수 있습니다.
이제는 두 인수의 값을 변수에 저장할 필요가 있는데, 그렇게 하면 프로그램의 나머지 부분에서 이 값들을 사용할 수 있습니다.
예제 11-2에서 이 동작을 수행합니다.
예제 11-2: 질의 (query) 인수와 파일 경로 인수를 담은 변수 생성하기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에 집어넣습니다.
여기서 두 변수의 타입은 &String이며 문자열의 소유자는 여전히 args입니다. &를 빼서 벡터 인덱스의 String을 그대로 꺼내려 하면, 빌린 벡터 요소에서 소유권을 이동하려 하므로 컴파일되지 않습니다. 지금은 두 값을 읽어서 출력하기만 하므로 복제하지 않고 빌려도 충분합니다.
우리 의도대로 코드가 동작하는지 검증하기 위해 이 변수의 값들을 임시로 출력하겠습니다.
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
프로그램이 훌륭하게 동작하네요!
필요로 하는 인수 값들이 올바른 변수에 저장되고 있습니다.
나중에는 사용자가 아무런 인수를 제공하지 않았을 때처럼 에러가 발생할 수 있는 특정한 경우를 처리하기 위한 에러 처리 기능을 몇 가지 추가할 것입니다; 지금은 그런 경우를 무시하고 파일 읽기 기능을 추가하는 작업으로 넘어가겠습니다.
현재 구현은 입력을 읽는 최소 형태입니다.
다음 단계에서는 이 값을 안전하게 검증하고 설정 구조체로 옮기기 위해 책임을 분리합니다.
예시
보통의 셸에서 cargo run -- "two words" sample.txt라고 입력하면 프로그램이 받는 값은 실행 이름, two words, sample.txt의 세 원소입니다. 따옴표는 공백이 있는 값을 하나로 전달하기 위한 셸 문법이며, query 안에 따옴표가 들어간다는 뜻은 아닙니다. Cargo의 --도 프로그램 인수 벡터의 한 원소가 아닙니다. Cargo의 인수 전달 규칙은 Cargo 옵션과 실행할 프로그램의 인수를 나눕니다.
현재 예제 11-2에서는 args[1]과 args[2]를 차례로 빌린 뒤 두 줄을 출력합니다. 사용자 인수를 하나만 주면 벡터 길이는 보통 2이고, 두 번째 인덱스 접근에서 패닉이 나므로 Searching for 출력에도 도달하지 않습니다. 세 번째 사용자 인수를 더 주면 그 값은 벡터에 남지만 이 코드는 읽지 않습니다. ‘필요한 두 값에 접근한다’와 ‘입력 개수를 검증한다’는 별도의 작업입니다.
실행 이름은 호출자가 전달한 값일 수 있으므로 첫 원소를 신뢰할 수 있는 실행 파일의 전체 경로라고 가정하지 않습니다. 여기서는 인덱스를 설명하기 위한 한 칸이며 검색 대상이 아닙니다. 유효하지 않은 Unicode 인수의 구분은 앞의 args_os 설명을 따릅니다.
독립 연습: 예제 11-2에 cargo run -- rust poem.txt extra를 전달했을 때 두 출력 줄과 읽지 않는 값을 적어 보세요. 이어 cargo run -- rust만 전달하면 두 줄 중 어디까지 출력할 수 있는지, 검증을 추가한다면 어느 인덱스 접근보다 앞에 길이 조건을 둬야 하는지도 설명하세요. 앞 예제의 dbg!(args)를 그대로 남긴 채 이 코드를 이어 붙이면 왜 실행 전에 막히는지, 값을 계속 쓸 수 있는 디버그 호출과 그 출력 스트림도 적어 보세요. 아래는 코드 순서를 읽은 예상이며 새 실행 로그가 아닙니다.
전달된 원소와 사용한 원소를 나누기
첫 경우에는 Searching for rust, In file poem.txt를 출력하고 extra는 사용하지 않습니다. 둘째는 args[2]를 만들 때 패닉이 나므로 출력 두 줄 모두 실행하지 않습니다. 검색어와 경로가 있어야 한다면 인덱스 접근 전에 길이가 최소 3인지 검사해야 합니다. 여분도 거부할지는 별도 계약이며, 다음 절의 최소 개수 검사만으로 여분까지 거부되는 것은 아닙니다.
dbg!(args)를 그대로 남기면 벡터가 이동되어 이후 인덱스 접근에서 이동된 값을 사용하는 문제가 생깁니다. dbg!(&args)는 벡터를 빌려 출력하므로 뒤의 참조 생성이 가능하며, 디버그 출력은 stderr, 두 println! 출력은 stdout으로 나갑니다.
커맨드 라인 인수 받기 학습 판단 기준표
커맨드 라인 인수 받기를 구현할 때 컴파일러가 막는 지점과 테스트로 확인할 지점을 분리해 봅니다.