RustのCLIツールにバージョン情報を出力するオプションを追加する方法

Rust Advent Calendar 2019の24日目の記事です。

バイナリファイルを配布して運用してもらうとなると、version情報の出力は必須オプションですね。 この記事ではCargo.tomlのversionを出力する方法を、RustでCLIツールを開発する際の標準的なcrateであるClapとgetoptsそれぞれについて解説していきます。 その後gitのコミットハッシュなどの追加情報を出力する方法を検討します。

Cargo.tomlのversionを表示したい場合

Cargo.tomlにはversion属性があるので、semantic versioningを信じるのであれば、この値を確認しておけば大丈夫です。

Clapを使っている場合

clap-rs/clap: A full featured, fast Command Line Argument Parser for Rust

やりたいことがそのまま載っているサンプルがあります。 clap/09_auto_version.rs at master · clap-rs/clap

#[macro_use]
extern crate clap;
use clap::App;
fn main() {
    App::new("myapp")
        .about("does awesome things")
        .version(crate_version!())
        .get_matches();
}

-Vまたは--versoinオプションをつけることでバージョン情報が出力されるようになります。 crate_version!というClap内で定義したマクロを使っています。

getoptsを使っている場合

rust-lang/getopts getoptsはコマンドライン引数のパースに特化したシンプルなcrateです。

extern crate getopts;

fn main(){
    let mut opts = getopts::Options::new();
    opts.optflag("v", "version", "output version information and exit");

    if matches.opt_present("version") {
        let version = env!("CARGO_PKG_VERSION");
        println!("myapp {:}", version);
        return;
    }
}

これで-vまたは--versoinオプションをつけることでバージョン情報が出力されるようになります。 RustにはCargoが定義する環境変数というものがあり、env!("CARGO_PKG_VERSION")で環境変数からCargo.toml内のversionに定義された値を取得しています。

gitのコミットハッシュを表示したい場合

私はsemantic versioningに馴染みが浅く、version情報を更新し忘れることが多いです。そこで、gitのコミットハッシュも合わせて表示したいと考えました。

コンパイル時にgitのコミットハッシュを得る方法

buildスクリプト内でgit rev-parse HEADというコマンドを実行し、返り値としてコミットハッシュを得る方法を考えました。 git_commit_hash.txtとしてファイルに書き出すところまでbuild.rsで行います。

use std::fs::File;
use std::io::Write;
use std::process::Command;

fn main(){
    let commit_hash = Command::new("git")
        .args(&["rev-parse", "HEAD"])
        .output()
        .expect("failed to execute command \"git rev-parse HEAD\"");
    let dest_path = format!("{}/src/git_commit_hash.txt", env!("CARGO_MANIFEST_DIR"));
    let mut f = File::create(&dest_path).unwrap();
    f.write_all(
        format!(
            "\"{}\"",
            String::from_utf8(commit_hash.stdout.to_vec())
                .unwrap()
                .replace("\n", "")
        )
        .as_bytes(),
    )
    .unwrap();
}

読み込みはincludeマクロで行う

includeマクロを使うことで、特定ファイルの文字列をそこに埋め込む事ができます。今回はこの方法でコンパイル時に定数として埋め込んでしまいます。

extern crate getopts;

fn main(){
    let mut opts = getopts::Options::new();
    opts.optflag("v", "version", "output version information and exit");

    if matches.opt_present("version") {
        let version = env!("CARGO_PKG_VERSION");
        let git_commit_hash = include!("git_commit_hash.txt");
        println!("myapp {:} {:}", version, git_commit_hash);
        return;
    }
}

これで--versionオプションで、Cargo.tomlのversion情報に加えて、gitのコミットハッシュなどの追加情報を出力するオプションを開発することができました。

同僚募集

私はフォルシアという会社で働いていて、仕事でもRustを書いているのですが、RustとWebアプリに興味があってお客さんと話しながら開発していきたい志向の方がいたら、ぜひお話させてください。自分から積極的に動ける人にはすごく楽しい会社です。 Shinjuku.rsというイベントも開催しています。次回は1/21(火)です。

DMお待ちしています。