Rust 강좌 4 - Rust에서 명령줄 인자 파싱을 쉽게! docopt 사용법 완벽 가이드
Relevancy(유효성): 1.9 stable (매크로는 nightly에서만 사용 가능)
명령줄 프로그램을 작성할 때 가장 귀찮은 작업 중 하나가 인자 파싱(argument parsing)인데요,
예를 들어 myprogram --config=myfile.conf --verbose -o output.txt
같은 명령을 입력했을 때, 이를 어떻게 해석할지 결정하는 작업입니다.
인자 중에는 짧은 버전과 긴 버전이 있고, 일부는 선택적이며, 또 다른 일부는 위치 인자일 수도 있는데요.
인자 파싱을 위한 라이브러리는 굉장히 많습니다. 그중 일부는 각 언어의 표준 라이브러리에도 포함되어 있죠.
러스트(Rust)의 경우에는 getopts
라는 crate가 있습니다.
보통 어느 정도 숙련된 유저라면 가장 먼저 하는 게 문서를 읽는 게 아니라 myprogram -h
(혹은 --help
)를 실행해서 사용 가능한 옵션을 확인하는 것인데요.
getopts
나 다른 라이브러리들은 이런 도움말 요약을 자동으로 생성해주기 때문에 시간을 절약하고 코드 중복을 줄여줍니다.
그렇다면 반대로, 우리가 먼저 사용법 메시지를 작성하면 그에 맞춰 인자 파서를 자동으로 생성해주는 방법은 어떨까요?
이때 등장하는 것이 docopt입니다.
docopt란?
docopt는 2012년, 파이썬 커뮤니티에서 시작된 프로젝트로, 명령줄 인자에 대한 표준 규칙을 정의하려는 시도에서 나왔습니다.
핵심 아이디어는 도움말 메시지가 곧 프로그램의 인터페이스라는 것인데요. 몇 가지 규칙만 따르면 docopt가 그 메시지를 이해하고 인자 파서를 만들어줍니다.
Rust에서의 docopt
먼저 Cargo.toml
파일에 의존성을 추가해야 합니다:
[dependencies]
docopt = "~0.6.78"
docopt_macros = "~0.6.80"
이제 Cargo가 docopt
crate와 매크로를 가져올 텐데요, 매크로는 별도로 배포되므로 따로 추가해야 합니다.
참고: docopt 매크로는 아직 안정화되지 않은 언어 기능(문법 확장, syntax extensions)을 사용하는데요, 현재로서는 docopt!
매크로를 컴파일러의 nightly 버전에서만 사용할 수 있습니다.
이제 예제로 wc
라는 파일의 줄, 단어, 문자 수를 세는 유틸리티를 만들어볼 텐데요, 필요한 라이브러리를 가져오고, 사용법 메시지를 작성할 겁니다:
extern crate rustc_serialize;
extern crate docopt;
use docopt::Docopt;
static USAGE: &'static str = "
Usage: wc [options] [<file>]
Options:
-c, --bytes print the byte counts
-m, --chars print the character counts
-l, --lines print the newline counts
-w, --words print the word counts
-L, --max-line-length print the length of the longest line
-h, --help display this help and exit
-v, --version output version information and exit
";
여기까지는 그냥 긴 문자열을 선언한 것인데요, docopt의 사용법에 맞는 형식에 대해 더 알고 싶다면 docopt 문서를 참고하면 됩니다.
이제 이 옵션들을 구조체로 변환할 수 있도록 해야 합니다.
구조체는 Decodable
트레이트를 구현해야 하는데요.
#[derive(RustcDecodable)]
struct Args {
arg_file: Option<String>,
flag_bytes: bool,
flag_chars: bool,
flag_lines: bool,
flag_words: bool,
flag_max_line_length: bool,
}
인자는 필드 이름과 매핑돼야 하는데요.
스위치처럼 작동하는 플래그(-c
같은 것들)는 flag_
로 시작하는 불리언 필드와 매핑되고, 옵션 인자나 메타변수는 arg_
로 시작하는 필드와 매핑됩니다.
인자가 선택적일 경우 Option
타입으로 표현할 수 있습니다.
이제 명령줄 인자를 실제로 구조체로 변환하는 방법을 보겠습니다.
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.decode())
.unwrap_or_else(|e| e.exit());
이 코드는 좀 복잡해 보이지만, 하나하나 살펴보면 이해할 수 있는데요.
let docopt = match Docopt::new(USAGE) {
Ok(d) => d,
Err(e) => e.exit(),
};
println!("{:?}", docopt);
let args: Args = match docopt.decode() {
Ok(args) => args,
Err(e) => e.exit(),
};
Docopt::new()
는 Result<Docopt, Error>
타입을 반환하는데요,
docopt에서 발생한 에러는 exit()
메서드를 통해 에러 메시지를 출력하고 프로그램을 종료할 수 있습니다.
Docopt
값을 출력하면 많은 디버깅 정보를 얻을 수 있습니다.
decode()
메서드는 인자 객체를 생성해주고, 우리는 Ok
값을 추출해서 사용할 수 있습니다.
이제 args
를 다른 구조체처럼 사용할 수 있습니다.
println!("Counting stuff in {}", args.arg_file.unwrap_or("standard input".to_string()));
if args.flag_bytes {
println!("Counting bytes!");
}
if args.flag_chars {
println!("Counting characters!");
}
docopt 매크로
그런데, 아까 중복을 줄이는 것에 대해 이야기했던 걸 기억하시나요?
위 코드에서 보면 두 가지 정보 소스가 있습니다.
하나는 사용법 문자열이고, 다른 하나는 Args 구조체입니다.
이 두 가지는 사실 같은 개념을 표현하고 있지만, 두 개의 코드를 따로 유지해야 해서 실수할 가능성이 있습니다.
여기서 docopt!
매크로가 등장하는데요, 이 매크로는 구조체를 자동으로 생성해줍니다.
#![feature(plugin)]
#![plugin(docopt_macros)]
extern crate rustc_serialize;
extern crate docopt;
docopt!(Args, "
Usage: wc [options] [<file>]
Options:
-c, --bytes print the byte counts
-m, --chars print the character counts
-l, --lines print the newline counts
-w, --words print the word counts
-L, --max-line-length print the length of the longest line
-h, --help display this help and exit
-v, --version output version information and exit
", arg_file: Option<String>);
이 매크로는 생성할 타입의 이름과 사용법 문자열, 그리고 생성된 필드의 타입을 인자로 받습니다.
또, 사용법 메시지가 docopt 규격에 맞는지 컴파일 시점에 검증해주는데요,
덕분에 런타임 오버헤드도 없고, 가장 중요한 건 더 이상 두 개의 정보 소스를 따로 유지할 필요가 없다는 점입니다.
매크로 덕분에 main()
함수 안의 코드도 더 간결하게 만들 수 있는데요.
구조체가 사용법 메시지에서 생성되기 때문에, 중간에 Result
를 풀어주는 코드도 줄일 수 있습니다.
구조체에는 docopt()
라는 정적 메서드가 있어서 Docopt
값을 반환해 줍니다.
let docopt = Args::docopt();
println!("{:?}", docopt);
let args: Args = docopt.decode().unwrap_or_else(|e| e.exit());
참고로, 최근 docopt는 명령줄 인자 자동 완성 파일을 생성하는 기능도 추가됐습니다.
현재는 bash만 지원되지만, readme 파일을 참고하면 더 자세한 정보를 얻을 수 있습니다.
'Rust' 카테고리의 다른 글
Rust 강좌 6 -JSON과 함께 작업해볼까요? (0) | 2024.10.08 |
---|---|
Rust 강좌 5 - 안전하고 효율적인 Hyper로 HTTP 요청 처리하기 (1) | 2024.09.19 |
Rust 강좌 3. CSV 파일 처리하기: 쉽고 간편한 방법 (1) | 2024.09.07 |
Rust의 메모리 관리와 GC 언어 비교 (0) | 2024.08.08 |
Rust 강좌 2. 프라임 - 소수와 친해지기 (4) | 2024.08.08 |