⛓️

Bpaf : Rustにおける軽量かつ柔軟なCommand Line Argument Parser

2023/12/18に公開

Rust Advent Calendar 2023 Series 2 18日目の本記事では、Bpaf という軽量かつ柔軟(Lightweight and flexible)なCommand Line Argument Parserを紹介します。

https://github.com/pacak/bpaf

RustのCommand Line Argument ParserといえばClapが有名です。特にこだわりがない場合はClapを使えばよいと思います。
一方、バイナリサイズやビルド時間を削減したいかつ柔軟なパースを行いたい場合には、Bpafなどの軽量なcrateが選定候補に挙げられます。

Bpaf

Bpafには、以下のような機能・特徴があります。

  • Derive style API と Combinatoric style API を提供
    • Derive style API:derive feature (bpaf_derive crateを利用して、re-exportする)
    • Clap v3で実装された Derive APIに近い
  • UTF-8以外の文字列を含むコマンドライン引数のフルサポート
  • bash, zsh, fish, elvishに対する補完を提供
    • autocomplete feature
  • helpオプション生成・カスタマイズ
  • 柔軟性と再利用性
    • 任意の時点で、各サブパーサの現在の解析状態に基づいて追加の検証やフォールバック値の適用が可能
    • BpafのParserはmonolithicではないため、複数のバイナリ・workspaceメンバー・別プロジェクトにおいても共有可能

また、可能な限り、 parse, don’t validate のアプローチに従っている点やパーサーの抽象化に関数型プログラミングの概念(Applicative Functor)を用いていることなども特徴です。

Exampleコード (Derive API)

Bpafでは、Derive APIとCombinatoric APIの2つが提供されていますが、これは意図的です。どちらのAPIも同時に使うことができます。
Derive APIを使う場合は、エディタからの補完は少ないですがタイプ数も少ないです。Combinatori APIを使う場合は、タイプ数が多いですが、procマクロ(bpaf_derive crate)に依存せず、エディタからの補完が増えます。

Derive APIのほうが可読性が高いと思うので、ここではBpaf公式のExamplesのDerive APIの例を載せます。
両方のAPIを使った例は、BpafのREADME.mdに書かれています。

// https://github.com/pacak/bpaf/blob/master/examples/derive.rs
//! pretty basic derive example with external function

use bpaf::{short, Bpaf, Parser};
use std::path::PathBuf;

#[derive(Debug, Clone, Bpaf)]
#[bpaf(options, version)]
#[allow(dead_code)]
struct Opts {
    /// Activate debug mode
    #[bpaf(short, long)]
    debug: bool,
    /// this comment is ignored
    #[bpaf(external(verbose))]
    verbose: usize,
    /// Set speed
    #[bpaf(argument("SPEED"), fallback(42.0))]
    speed: f64,
    /// Output file
    output: PathBuf,

    #[bpaf(guard(positive, "must be positive"), fallback(1))]
    number_of_cars: u32,
    files_to_process: Vec<PathBuf>,
}

fn verbose() -> impl Parser<usize> {
    // number of occurrences of the v/verbose flag capped at 3
    short('v')
        .long("verbose")
        .help("Increase the verbosity\nYou can specify it up to 3 times\neither as -v -v -v or as -vvv")
        .req_flag(())
        .many()
        .map(|xs| xs.len())
        .guard(|&x| x <= 3, "It doesn't get any more verbose than this")
}

/// Guard for nb_cars
fn positive(input: &u32) -> bool {
    *input > 1
}

fn main() {
    println!("{:#?}", opts().run());
}

コマンド実行結果:

▶ cargo run -- --debug -vvv --speed 60.5 --output /path/to/output.txt --number-of-cars 3 --files-to-process file1.txt
Opts {
    debug: true,
    verbose: 3,
    speed: 60.5,
    output: "/path/to/output.txt",
    number_of_cars: 3,
    files_to_process: [
        "file1.txt",
    ],
}

パース後、型安全にパース結果を利用可能です。

versionの出力結果:

▶ cargo run -- --version
Version: 0.1.0

デフォルトでは、Cargo.tomlから抽出されます。

helpの出力結果:

▶ cargo run -- --help
Usage: bpaf-example [-d] [-v]... [--speed=SPEED] --output=ARG [--number-of-cars=ARG] [--files-to-process
=ARG]...

Available options:
    -d, --debug               Activate debug mode
    -v, --verbose             Increase the verbosity You can specify it up to 3 times either as -v -v
                              -v or as -vvv
        --speed=SPEED         Set speed
        --output=ARG          Output file
        --number-of-cars=ARG
        --files-to-process=ARG
    -h, --help                Prints help information
    -V, --version             Prints version information

Bpafを利用しているプロジェクト

Bpafは、以下に挙げられるプロジェクトなどのCLIツールで利用されています(bpaf の dependency graphからpick up)。

Biomeは以前はpico_argsを利用していましたが、@ematipicoによるこちらのPR でbpafを利用するように変更されました。変更の目的はPRに書かれているように、CLIのhelpやdocumentationの機能をより簡単にメンテナンス可能にできるようにしたかったためです。利用しているcargo featuresは、derivebright-color です(autocomplete も使っても良いかも)。

BiomeのDerive APIを利用したCommand実装は、以下のファイルにあります。実践的な例として参考になると思います。

https://github.com/biomejs/biome/blob/main/crates/biome_cli/src/commands/mod.rs#L29-L260

Alternatives

本記事では筆者の経験の都合上、他のCommand Line Argument Parserとの比較は行っていません。ですので、その代わりに、知る限りのbpafの代替となり得るcrateを列挙しておきます。

以下のRepositoryでは、いくつかのCommand Line Argument Parserを対象としたベンチーマークが記録されており、選定の際に役立つと思います。

https://github.com/rosetta-rs/argparse-rosetta-rs

上記のベンチマークは、Clapのメンテナ(@epage)によって、メンテナンスされているようです。ちなみにLexoptは、ripgrepで採用されています。

https://github.com/BurntSushi/ripgrep/pull/2626

終わりに

本記事では、Rustにおける軽量かつ柔軟なCommand Line Argument ParserのBpafを紹介しました。Clapほど機能が豊富でなくてもよい・バイナリサイズ・ビルド時間などを削減したいという場合の選択肢となればよいと思います。

https://github.com/pacak/bpaf

今回はBpafの内部のコードについて触れていません。以下のページなどでは、Bpafで利用されている抽象的な概念について書かれているので、内部に興味がある方は読んでみると面白いと思います。

ちなみにBpafの “af” の部分は、作者いわくApplicative Functorの略とのことです(”Bp”は何を表していたか忘れたらしい)。

https://www.reddit.com/r/rust/comments/xlzx3v/comment/ipnf3og/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

記事を書く上で、Bpafの内部実装に興味が湧きました。利用しているツールでもあるので、どこかの機会でちゃんとコードリーディング(及び コントリビューション)します。

参考資料


GitHubで編集を提案

Discussion