Rust에서 명령행 인자 파싱

내 이 세상 도처에서 쉴 곳을 찾아보았으나, 마침내 찾아낸, 컴퓨터가 있는 구석방보다 나은 곳은 없더라.

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>,
}

코드에서 portworkers 값을 얻을 때 다음과 같이 하면 된다.

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을 사용하면 복잡한 코드를 작성하지 않고도 선언적으로 깔끔하고 우아하게 명령행 인자를 처리할 수 있다.

참고