Rust에서 명령행 인자 파싱
Rust에서는 명령행 인자를 어떻게 처리할까? 예전에 Rust로 작성한 HTTP 에코 서버 reflexive는 포트 번호가 하드코딩 되어 있고 워커 개수는 디폴트 값을 사용하고 있는데, 포트 번호와 워커 개수를 명령행 인자로 받으려면 어떻게 해야 할까?
std::env::args()
Rust에서는 표준 라이브러리에 있는 std::env::args()
를 통해 명령행 인자를 얻을 수 있다. std::env::args()
의 리턴 타입은 std::env::Args
인데, 명령행 인자에 대한 반복자(iterator)다.
fn main() {
let args = std::env::args();
println!("{:?}", args);
println!("--");
for a in args {
println!("{}", a);
}
}
이 프로그램을 컴파일한 수 명령행 인자를 주어 실행하면 다음과 같은 결과를 얻을 수 있다. cargo run
으로 프로그램을 실행할 때 --
다음에 나오는 인자는 애플리케이션 프로그램에 (cargo
가 아니라) 전달된다.
$ cargo run -- --port 4000 --workers 6
Args { inner: ["./target/debug/arg-test", "--port", "4000", "--workers", "6"] }
--
./target/debug/arg-test
--port
4000
--workers
6
인자 값을 모두 얻었으니 필요에 맞게 쓰면 되지만, 인자를 입맛에 맞게 쓰려면 작업이 좀 필요하다. 예를 들어 port
에는 정수 4000을, workers
에는 정수 6을 할당해 프로그램에서 사용하려면 어떻게 해야 할까?
명령행 인자를 제대로 처리하려면 다음과 같은 경우도 어떻게 처리할 지 고려해야 한다.
- 필수 인자를 빠뜨린 경우
--port 3000 --port 4000
과 같이 같은 인자를 여러 번 지정한 경우--port abc
와 같이port
에 숫자가 아닌 엉뚱한 문자열을 지정한 경우- 프로그램에서 지원하지 않는 인자를 지정한 경우
- 도움말 또는 사용법 표시
이 모든 경우를 제대로 처리하려면 명령행 인자 파서를 작성해야 한다. 간단한 작업은 아닐 것이다.
Clap
Rust에서 사용할 수 있는 명령행 인자 파서는 여러 가지가 있지만, 그 중 Clap이 가장 먼저 눈에 들어왔다. 특별한 이유가 있다기 보다는, 처음 읽은 문서가 Clap을 설명하는 것이었다. Clap은 선언적(또는 절차적)으로 명령행 인자 파서를 생성할 수 있게 해주는 라이브러리다.
Clap을 사용하려면 다음과 같이 Cargo.toml
에 종속성을 추가해야 한다.
[dependencies]
clap = { version = "3", features = ["derive"] }
...
그리고 다음과 같이 명령행 인자 값을 저장할 구조체를 만든다.
#[derive(Debug, Parser)]
struct Arguments {
port: u16,
workers: usize,
}
main
함수는 다음과 같이 수정한다.
fn main() {
let args = Arguments::parse();
println!("{:?}", args);
}
cargo run
으로 프로그램을 실행하면 다음과 같은 에러 메시지가 표시된다.
error: The following required arguments were not provided:
<PORT>
<WORKERS>
USAGE:
arg-test <PORT> <WORKERS>
For more information try --help
-h
또는 --help
를 프로그램 인자로 제공하면 도움말이 표시된다.
arg-test
USAGE:
arg-test <PORT> <WORKERS>
ARGS:
<PORT>
<WORKERS>
OPTIONS:
-h, --help Print help information
포트 번호나 워커 개수에 양의 정수가 아닌 다른 값을 지정하면 에러가 발생한다.
$ cargo run -- hello world
error: Invalid value "hello" for '<PORT>': invalid digit found in string
For more information try --help
$ cargo run -- -- -8080 10
error: Invalid value "-8080" for '<PORT>': invalid digit found in string
For more information try --help
다음과 같이 명령행 인자를 제대로 지정하면 Arguments
구조체 필드에 값이 설정된다. 프로그램에서 이 값을 사용하면 된다.
$ cargo run -- 8080 10
Arguments { port: 8080, workers: 10 }
단지 몇 줄의 코드로 이 모든 게 처리된다. 따로 명령행 인자 파서를 작성할 필요도 없고, 에러 처리 코드를 작성할 필요도 없다.
플래그 추가
Clap에서는 플래그 인자를 추가하는 것도 매우 간단하다. Arguments
구조체 멤버에 #[clap(short, long)]
매크로 어노테이션을 추가하면 끝이다. 여기서 short
는 -p
와 같이 짧은 플래그를, long
은 --port
와 같이 긴 플래그를 뜻한다.
#[derive(Debug, Parser)]
struct Arguments {
#[clap(short, long)]
port: u16,
#[clap(short, long)]
workers: usize,
}
cargo run -- --help
으로 프로그램을 실행하면 다음과 같은 에러 메시지가 표시된다.
arg-test
USAGE:
arg-test --port <PORT> --workers <WORKERS>
OPTIONS:
-h, --help Print help information
-p, --port <PORT>
-w, --workers <WORKERS>
이제 프로그램을 실행하려면 플래그를 지정해야 하지만, 인자 순서는 기억하지 않아도 된다. 즉, 인자를 --port 4000 --workers 10
로 주나 --workers 10 --port 4000
로 주나 결과는 같다.
Arguments { port: 4000, workers: 10 }
옵션 인자와 디폴트 값
프로그램을 실행할 때마나 포트와 워커 개수를 지정하는 것은 번거롭다. 명령행 인자를 지정하지 않으면 디폴트 값, 예를 들어 포트는 8080을, 워커 개수는 8을 사용하도록 할 수는 없을까?
인자 타입, 즉, Arguments
구조체에서 멤버의 타입을 Option<T>
로 하면 인자를 옵션으로 만들 수 있다.
#[derive(Debug, Parser)]
struct Arguments {
#[clap(short, long)]
port: Option<u16>,
#[clap(short, long)]
workers: Option<usize>,
}
코드에서 port
나 workers
값을 얻을 때 다음과 같이 하면 된다.
fn main() {
let args = Arguments::parse();
let port = args.port.unwrap_or(8080);
let workers = args.workers.unwrap_or(8);
...
}
이렇게 해도 동작은 하지만, 명령행 인자 정의하는 위치와 디폴트 값을 지정하는 위치가 달라지는 것이 불만이다. 더 깔끔한 방법은 다음과 같이 하는 것이다.
#[derive(Debug, Parser)]
struct Arguments {
#[clap(short, long, default_value_t = 8080)]
port: u16,
#[clap(short, long, default_value_t = 8)]
workers: usize,
}
cargo run -- --help
로 프로그램을 실행하면 다음과 같이 도움말이 표시된다. 도움말에 각 옵션의 디폴트 값도 추가되었다.
arg-test
USAGE:
arg-test [OPTIONS]
OPTIONS:
-h, --help Print help information
-p, --port <PORT> [default: 8080]
-w, --workers <WORKERS> [default: 8]
아무런 인자 없이 프로그램을 실행하면 포트는 8080, 워커 개수는 8로 프로그램이 실행되는 것을 확인할 수 있다.
프로그램 추가 설명
도움말에 프로그램 설명, 버전 등과 같은 간략한 추가 정보를 표시하고 싶을 때는 다음과 같이 어노테이션을 추가한다.
#[derive(Debug, Parser)]
#[clap(author="ntalbs", version, about="A very simple CLI test program.")]
struct Arguments {
...
}
cargo run -- -h
또는 cargo run -- --help
로 프로그램을 실행하면 다음과 같이 도움말이 표시된다.
arg-test 0.1.0
ntalbs
A very simple CLI test program.
USAGE:
arg-test [OPTIONS]
OPTIONS:
-h, --help Print help information
-p, --port <PORT> [default: 8080]
-V, --version Print version information
-w, --workers <WORKERS> [default: 8]
-V
(--version
) 옵션이 추가된 것을 볼 수 있다. -V
또는 --version
옵션을 주어 프로그램을 실행하면 버전 정보를 표시한다. 어노테이션에서 버전 정보를 지정하지 않으면 Cargo.toml
에 있는 프로그램 버전 정보가 사용된다.
결론
Rust 프로그램에서 Clap을 사용해 명령행 인자를 처리하는 방법을 살펴보았다. 이런 마법을 좋아하지 않는 사람도 있지만, 나는 언제나 선언적 코드를 좋아했다. 어쨌는 여기서 내 주요 관심사는 내 프로그램의 CLI를 멋지게 만드는 것이지 명령행 인자 파서를 만드는 게 아니다. Clap을 사용하면 복잡한 코드를 작성하지 않고도 선언적으로 깔끔하고 우아하게 명령행 인자를 처리할 수 있다.
참고
- Parsing command-line arguments
- Command line argument parsing in Rust using Clap
- Clap
- reflexive Clap을 사용해
port
와workser
를 명령행 인자로 지정할 수 있도록 Rust 에코 서버를 수정한 코드를 볼 수 있다.