🐙

Rustでコマンドラインツール開発 オプションをいい感じに設定!

2024/11/23に公開

目標

お手軽にコマンドライン引数を取得しつつ、ヘルプ表示にも対応させる

例:異常終了(存在しないコマンドを指定)
terminal
$ command -l # 存在しないコマンドを指定
Unrecognized option: 'l'

 書式: command [options]

Options:
    -V, --value 1 or 2 or 3 etc..
                        入力した値を表示
    -h, --help          ヘルプを表示
    -v, --version       バージョンを表示
例:正常終了(コマンドライン引数を使用)
terminal
$ command -V 100
入力された値: 100

$ command -V "← 画面に表示されるよ"
入力された値: ← 画面に表示されるよ

$ command -hvV 複数オプション

 書式: command [options]

Options:
    -V, --value 1 or 2 or 3 etc..
                        入力した値を表示
    -h, --help          ヘルプを表示
    -v, --version       バージョンを表示

 Version 0.1.0 

入力された値: 複数オプション

プロジェクトの作成

以下のクレートを利用します。

  • getopts
terminal
$ cargo new command # プロジェクトを作成
$ cd command # プロジェクトに移動
$ cargo add getopts # クレートをプロジェクトに追加

実装

import

getoptsからオプション設定に使用するOptionsををimportしましょう。

他にも以下標準ライブラリも使用するため、一緒にimportします。

  • env : コマンドライン引数を取得する
  • exit : コマンドの異常終了、正常終了を可能にする
/src/main.rs
use getopts::Options;
use std::{env, process::exit};

コマンドライン引数の読み込み

fn main() {
    // コマンドライン引数を取得
    let args: Vec<String> = env::args().collect();
    // プログラム名を取得("cat","ls"などのコマンド名)
    let progname = args[0].clone();
}

オプションの作成

今回は下記のオプションに対応したいと思います。

  • V: 入力した値を表示
  • h: ヘルプを表示
  • v: バージョンを表示

インスタンスを作成

fn main()
    let mut opts = Options::new();

オプションを追加

オプションの種類も色々ありそうですが、本記事では2種類紹介します。

汎用的なオプション

fn main()
opts.optopt(
    "V", // 短いオプション名を設定
    "value", // 長いオプション名を設定
    "入力した値を表示", // コマンドの説明を設定
    "1 or 2 or 3 etc.." // 設定可能な値の例などのヒントを設定
    );

フラグオプション
(コマンドライン引数にオプションが含まれるか、否かの情報だけを必要とするオプション)

fn main()
opts.optflag(
    "h", // 短いオプション名を設定
    "help", // 長いオプション名を設定
    "ヘルプを表示" // コマンドの説明を設定
    );
opts.optflag("v", "version", "バージョンを表示");

ヘルプの表示

usage関数を用意し、ヘルプ文言を出力する。

/src/main.rs
/**
 * ヘルプの表示
 * 
 * progname プログラム名
 * opts オプション情報
 */
fn usage(progname: &str, opts: getopts::Options) {
    // 書式の説明
    let brief = format!("\n 書式: {progname} [options]");

    // optopt, optflagなどで設定したオプションの情報を文字列に追加する
    let usage: String = opts.usage(&brief);
    eprint!("{usage}");
}
上記の関数を使用した出力例
表示例
 書式: {progname} [options]

Options:
    -V, --value 1 or 2 or 3 etc..
                        入力した値を表示
    -h, --help          ヘルプを表示
    -v, --version       バージョンを表示

コマンドラインの解析

fn main()
    // コマンドライン引数から該当するオプションを取得
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            eprintln!("{f}");
            usage(&progname, opts); //上のusage関数を使用
            exit(1); // 異常終了
        }
    };

Err(f) では、パースに失敗した原因を返してくれる

terminal
$ cargo build # ビルド
$ cd target/debug # 成果物の出力されたフォルダに移動
$ ./command -l # 存在しないコマンドを指定
Unrecognized option: 'l' # <= Err(f) から受け取った文言

 書式: ./command [options]

Options:
    -V, --value 1 or 2 or 3 etc..
                        入力した値を表示
    -h, --help          ヘルプを表示
    -v, --version       バージョンを表示

matches.freeでオプション以外のコマンドライン引数を取得できる

使用例
    // 書式: command [options] ファイル名 AA BB
    // ファイル名を取得する。
    let file_name = match matches.free.get(0){
        Some(s) => s,
        None => exit(1) // 取得できない場合、異常終了
    };
    let AA = match matches.free.get(1){
        Some(s) => s, None => exit(1)
    };
    let BB = match matches.free.get(1){
        Some(s) => s, None => exit(1)
    };
    let file_name = matches.free[2]; // BB

コマンドライン引数による分岐

フラグオプションの参照方法

解析結果matchesopt_presentを利用する

fn main()
    // -h ヘルプの表示
    if matches.opt_present("h") {
        usage(&progname, opts);
    }

    // -v バージョン表示
    if matches.opt_present("v") {
        let version = option_env!("GIT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
        eprintln!("\n Version {version} \n");
    }

汎用オプションの参照方法

解析結果matchesopt_strを利用する

fn main()
    // -V 入力値を利用
    match matches.opt_str("V") {
        Some(s) => println!("入力された値: {}", s), // 入力値を参照できる
        None => {},
    };

完成したソースコード

/src/main.rs
use std::{env, process::exit};
use getopts::Options;

/**
 * ヘルプの表示
 * 
 * progname プログラム名
 * opts オプション情報
 */
fn usage(progname: &str, opts: getopts::Options) {
    // 書式の説明
    let brief = format!("\n 書式: {progname} [options]");
    // optopt, optflagなどで設定したオプションの情報を文字列に追加する
    let usage: String = opts.usage(&brief);
    eprint!("{usage}");
}

fn main() {
    // コマンドライン引数を取得
    let args: Vec<String> = env::args().collect();
    // プログラム名を取得("cat","ls"などのコマンド名)
    let progname = args[0].clone();

    // オプション設定
    let mut opts = Options::new();
    opts.optopt("V","value","入力した値を表示","1 or 2 or 3 etc..");
    opts.optflag("h", "help", "ヘルプを表示");
    opts.optflag("v", "version", "バージョンを表示");

    // コマンドライン引数から該当するオプションを取得
    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            eprintln!("{f}");
            usage(&progname, opts);
            exit(1);
        }
    };

    // -h ヘルプの表示
    if matches.opt_present("h") {
        usage(&progname, opts);
    }

    // -v バージョン表示
    if matches.opt_present("v") {
        let version: &str = option_env!("GIT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
        eprintln!("\n Version {version} \n");
    }
    
    // -V 入力値を利用
    match matches.opt_str("V") {
        Some(s) => println!("入力された値: {}", s),
        None => {},
    };
}

課題

  • ローカライズにも対応させると、近年の主要コマンドラインツールみたいに振る舞えそう

Discussion