Rustでサブコマンド付きCLIツールを作成する方法を徹底解説

Rustでコマンドラインインターフェース(CLI)ツールを設計する際、サブコマンドを導入することで、より柔軟で直感的な操作が可能になります。例えば、GitやCargoのようなCLIツールでは、git commitcargo buildのように、異なる操作をサブコマンドで区別しています。Rustのエコシステムには、CLIツールの作成をサポートする強力なクレートとしてclapがあります。本記事では、clapを利用してサブコマンドを持つCLIツールを作成する方法をステップバイステップで解説します。CLIツールの設計の基本から、サブコマンドの追加、引数やオプションの設定、エラーハンドリングまで、実践的な内容をカバーします。

目次

CLIツールにおけるサブコマンドの概要


コマンドラインインターフェース(CLI)ツールにおけるサブコマンドとは、特定の機能や操作を分けて実行するための区切りのようなものです。例えば、Gitのgit addgit commitにおける「add」や「commit」がサブコマンドにあたります。

サブコマンドを使用する利点


CLIツールにサブコマンドを導入することで、以下のような利点があります。

  • 機能の整理:複数の機能を一つのコマンドで実現するのではなく、サブコマンドで分けることで、直感的な操作が可能になります。
  • 可読性の向上:ツールの使い方が明確になり、ドキュメントやヘルプが理解しやすくなります。
  • 拡張性:新機能を追加する際にサブコマンドを導入することで、構造を維持しつつ拡張しやすくなります。

Rustでサブコマンドを設計する際のポイント


RustでCLIツールを作成する際には、clapクレートを活用することでサブコマンドの実装が容易になります。clapでは、clap::App::subcommandメソッドを使って、簡単にサブコマンドを追加できます。例えば、以下のような設計が可能です。

mytool
├── add
├── remove
└── list

このように、メインのコマンドから各サブコマンドを通じて異なる処理を実行できるよう設計することで、CLIツールの使い勝手が向上します。

`clap`クレートの基本的な使い方

RustでCLIツールを作成する際に非常に便利なのが、clapクレートです。clapはCLIの引数解析やヘルプメッセージ生成を効率的に行うためのライブラリで、直感的に使えるAPIを提供しています。

`clap`のインストール方法


まず、Cargo.tomlに以下を追加してclapをインストールします。

[dependencies]
clap = { version = "4", features = ["derive"] }

バージョン4では、derive機能を使うことで、構造体にアノテーションを付けて簡単に引数を定義できます。

基本的なCLIツールの例


簡単なCLIツールを作成するコード例を示します。

use clap::{Parser};

/// 簡単なCLIツールの説明
#[derive(Parser)]
#[command(name = "mytool")]
#[command(about = "A simple example CLI tool", long_about = None)]
struct Cli {
    /// ファイル名を指定するオプション
    #[arg(short, long)]
    file: String,
}

fn main() {
    let args = Cli::parse();
    println!("指定されたファイル名: {}", args.file);
}

コードの解説

  • #[derive(Parser)]: clapParserトレイトを導入し、CLI引数の解析を行います。
  • #[command(name = "...")]: コマンド名や概要を設定します。
  • #[arg(short, long)]: -fまたは--fileのように、引数のショートオプションとロングオプションを定義します。
  • Cli::parse(): コマンドライン引数を解析してCli構造体にデータを格納します。

このように、clapを使えば、シンプルなCLIツールを素早く構築できます。次に、サブコマンドを追加する方法を見ていきましょう。

サブコマンドの追加方法

Rustのclapクレートを使えば、サブコマンドを簡単に追加することができます。サブコマンドは、CLIツールに複数の異なる操作や処理を実装する際に役立ちます。

サブコマンドを定義する手順

サブコマンドを追加するために、以下の手順を踏みます。

  1. clapクレートをインストール
    Cargo.tomlに以下の依存関係を追加します。
   [dependencies]
   clap = { version = "4", features = ["derive"] }
  1. サブコマンド用の列挙型を作成
    サブコマンドを列挙型(enum)として定義し、それぞれのサブコマンドに必要な引数を設定します。
  2. メイン構造体でサブコマンドを指定

具体的なコード例

use clap::{Parser, Subcommand};

/// メインのCLIツールの定義
#[derive(Parser)]
#[command(name = "mytool", about = "サブコマンド付きのCLIツール")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

/// サブコマンドの定義
#[derive(Subcommand)]
enum Commands {
    /// ファイルを追加するサブコマンド
    Add {
        /// 追加するファイル名
        #[arg(short, long)]
        file: String,
    },
    /// ファイルを削除するサブコマンド
    Remove {
        /// 削除するファイル名
        #[arg(short, long)]
        file: String,
    },
}

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Add { file } => {
            println!("ファイルを追加します: {}", file);
        }
        Commands::Remove { file } => {
            println!("ファイルを削除します: {}", file);
        }
    }
}

コードの解説

  • #[derive(Parser)]: メインのCLIツールに必要な引数やサブコマンドを定義します。
  • #[command(subcommand)]: サブコマンドを指定するためのフィールドです。
  • enum Commands: サブコマンドを列挙型として定義し、それぞれのサブコマンドに引数を設定します。
  • マッチ文: どのサブコマンドが実行されたかを判定し、それぞれの処理を実行します。

実行例

$ cargo run -- add --file example.txt
ファイルを追加します: example.txt

$ cargo run -- remove --file example.txt
ファイルを削除します: example.txt

このように、clapクレートを使用することで、サブコマンドを簡単に導入し、CLIツールに多機能な操作を追加できます。

サブコマンドの引数とオプションの設定

サブコマンドに特定の引数やオプションを設定することで、より柔軟で実用的なCLIツールを作成できます。Rustのclapクレートでは、サブコマンドごとに独自の引数やオプションを簡単に定義できます。

サブコマンドに引数を設定する

サブコマンドごとに引数を追加するには、列挙型のバリアントにフィールドを追加します。

以下の例では、addサブコマンドにファイル名の引数を設定しています。

use clap::{Parser, Subcommand};

/// メインのCLIツール定義
#[derive(Parser)]
#[command(name = "mytool", about = "サブコマンド付きCLIツール")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

/// サブコマンドの定義
#[derive(Subcommand)]
enum Commands {
    /// ファイルを追加するサブコマンド
    Add {
        /// 追加するファイル名
        file: String,
    },
}

サブコマンドにオプションを設定する

サブコマンドの引数にオプションを加えることで、柔軟な操作が可能になります。以下の例では、addサブコマンドにファイル名とオプションの--verboseを追加しています。

#[derive(Subcommand)]
enum Commands {
    /// ファイルを追加するサブコマンド
    Add {
        /// 追加するファイル名
        file: String,

        /// 詳細な情報を表示するオプション
        #[arg(short, long)]
        verbose: bool,
    },
}

サブコマンドで引数とオプションを利用する

次に、main関数でサブコマンドの引数やオプションを処理します。

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Add { file, verbose } => {
            if *verbose {
                println!("詳細モードでファイルを追加: {}", file);
            } else {
                println!("ファイルを追加: {}", file);
            }
        }
    }
}

実行例

$ cargo run -- add example.txt
ファイルを追加: example.txt

$ cargo run -- add example.txt --verbose
詳細モードでファイルを追加: example.txt

オプションにデフォルト値を設定する

オプションにはデフォルト値を設定することも可能です。

#[derive(Subcommand)]
enum Commands {
    /// ファイルを追加するサブコマンド
    Add {
        /// ファイルのタグを指定(デフォルトは "default")
        #[arg(short, long, default_value = "default")]
        tag: String,
    },
}

まとめ

  • 引数: 必須の値としてサブコマンドに設定します。
  • オプション: 任意の値やフラグとして設定でき、デフォルト値を設定することも可能です。
  • clapクレート: 柔軟な引数とオプションの設定が簡単にでき、CLIツールの操作性を向上させます。

これでサブコマンドに引数やオプションを追加する基本が理解できました。

複数のサブコマンドを持つCLIツールの設計

CLIツールが複数のサブコマンドを持つ場合、構造をシンプルで分かりやすく保つことが重要です。Rustのclapクレートを活用することで、効率よく複数のサブコマンドを設計・管理できます。

複数のサブコマンドを定義する

まず、複数のサブコマンドを定義するために、列挙型(enum)を活用します。以下の例では、addremovelistという3つのサブコマンドを定義しています。

use clap::{Parser, Subcommand};

/// メインのCLIツール定義
#[derive(Parser)]
#[command(name = "mytool", about = "複数のサブコマンドを持つCLIツール")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

/// サブコマンドの定義
#[derive(Subcommand)]
enum Commands {
    /// ファイルを追加するサブコマンド
    Add {
        /// 追加するファイル名
        file: String,
    },
    /// ファイルを削除するサブコマンド
    Remove {
        /// 削除するファイル名
        file: String,
    },
    /// ファイル一覧を表示するサブコマンド
    List,
}

サブコマンドごとの処理を実装

各サブコマンドに対して異なる処理を実装します。match文を使って、呼び出されたサブコマンドごとに適切な処理を実行します。

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Add { file } => {
            println!("ファイルを追加します: {}", file);
        }
        Commands::Remove { file } => {
            println!("ファイルを削除します: {}", file);
        }
        Commands::List => {
            println!("ファイルの一覧を表示します");
        }
    }
}

サブコマンドの構造を整理するベストプラクティス

  1. 明確な機能分割
  • サブコマンドごとに1つの役割を持たせることで、CLIツールが分かりやすくなります。
    例: addはファイル追加、removeはファイル削除、listは一覧表示。
  1. 一貫性のある命名
  • サブコマンド名はシンプルで一貫性を保つと、直感的に理解しやすくなります。
    例: addremoveupdatelistなど。
  1. ヘルプメッセージの充実
  • 各サブコマンドに適切な説明を付け、ヘルプメッセージを充実させます。
   #[derive(Subcommand)]
   enum Commands {
       /// 新しいファイルを追加します
       Add {
           file: String,
       },
       /// 既存のファイルを削除します
       Remove {
           file: String,
       },
       /// すべてのファイルを一覧表示します
       List,
   }

実行例

以下のように各サブコマンドを実行できます。

$ cargo run -- add example.txt
ファイルを追加します: example.txt

$ cargo run -- remove example.txt
ファイルを削除します: example.txt

$ cargo run -- list
ファイルの一覧を表示します

まとめ

  • サブコマンドを分割することで機能を整理し、直感的なCLIツールを構築できます。
  • enumでサブコマンドを定義し、match文で処理を分岐させることでシンプルに管理できます。
  • ヘルプメッセージや命名規則を整えることで、ユーザビリティが向上します。

これで複数のサブコマンドを持つCLIツールを設計・管理する基本が理解できました。

エラーハンドリングとヘルプメッセージ

CLIツールにおいて、ユーザーがエラーに直面した際の適切なフィードバックや、操作方法を示すヘルプメッセージは非常に重要です。Rustのclapクレートを活用することで、エラーハンドリングとヘルプメッセージを簡単に実装できます。

デフォルトのヘルプメッセージの生成

clapは自動的にヘルプメッセージを生成します。サブコマンドや引数に説明を追加することで、ユーザーに分かりやすいヘルプを提供できます。

例: サブコマンドに説明を追加したコード

use clap::{Parser, Subcommand};

/// メインのCLIツール定義
#[derive(Parser)]
#[command(name = "mytool", about = "サブコマンド付きCLIツール", long_about = "これはサブコマンドをサポートするサンプルCLIツールです。")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

/// サブコマンドの定義
#[derive(Subcommand)]
enum Commands {
    /// 新しいファイルを追加します
    Add {
        /// 追加するファイル名
        #[arg(short, long)]
        file: String,
    },
    /// 既存のファイルを削除します
    Remove {
        /// 削除するファイル名
        #[arg(short, long)]
        file: String,
    },
}

ヘルプメッセージの表示

このコードを実行して--helpを指定すると、以下のようなヘルプメッセージが表示されます。

$ cargo run -- --help
mytool 
サブコマンド付きCLIツール

USAGE:
    mytool <COMMAND>

COMMANDS:
    add      新しいファイルを追加します
    remove   既存のファイルを削除します
    help     ヘルプを表示する

エラーハンドリングの実装

Rustのclapクレートは、入力が不正な場合に自動的にエラーメッセージを生成します。

例: 不正なサブコマンドを入力した場合

$ cargo run -- invalid
error: unrecognized subcommand 'invalid'

カスタムエラーメッセージの追加

clapのエラーメッセージをカスタマイズしたい場合は、expectmatchを使用して処理できます。

例: ファイル引数が空の場合のカスタムエラー

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Add { file } => {
            if file.is_empty() {
                eprintln!("エラー: ファイル名が指定されていません。");
                std::process::exit(1);
            }
            println!("ファイルを追加します: {}", file);
        }
        Commands::Remove { file } => {
            if file.is_empty() {
                eprintln!("エラー: 削除するファイル名が指定されていません。");
                std::process::exit(1);
            }
            println!("ファイルを削除します: {}", file);
        }
    }
}

ヘルプメッセージを表示するサブコマンド

サブコマンドごとのヘルプを表示するには、--helpオプションを使います。

$ cargo run -- add --help
add 
新しいファイルを追加します

USAGE:
    mytool add --file <FILE>

OPTIONS:
    -f, --file <FILE>    追加するファイル名

まとめ

  • 自動生成されるヘルプメッセージを活用して、CLIツールの使い方を分かりやすく伝えましょう。
  • エラーメッセージを適切に設定し、不正な入力に対するフィードバックを提供しましょう。
  • カスタムエラーハンドリングで、さらに詳細なエラー処理を実装できます。

これにより、ユーザーに優しいCLIツールが設計でき、操作ミスを防ぐことができます。

サブコマンドを使った具体的なCLIツールの例

ここでは、Rustのclapクレートを使い、サブコマンドを含む具体的なCLIツールを作成します。この例では、簡易的なタスク管理ツールを作成し、以下の機能をサブコマンドとして提供します。

  1. add:タスクを追加する
  2. remove:タスクを削除する
  3. list:タスクの一覧を表示する

タスク管理CLIツールのコード例

use clap::{Parser, Subcommand};
use std::collections::HashMap;
use std::sync::Mutex;
use lazy_static::lazy_static;

/// タスク管理CLIツールの定義
#[derive(Parser)]
#[command(name = "taskmgr", about = "簡易タスク管理CLIツール")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

/// サブコマンドの定義
#[derive(Subcommand)]
enum Commands {
    /// 新しいタスクを追加する
    Add {
        /// タスクのタイトル
        #[arg(short, long)]
        title: String,

        /// タスクの説明
        #[arg(short, long)]
        description: Option<String>,
    },
    /// 既存のタスクを削除する
    Remove {
        /// 削除するタスクのタイトル
        #[arg(short, long)]
        title: String,
    },
    /// タスクの一覧を表示する
    List,
}

// タスクを保存するためのグローバルなデータ構造
lazy_static! {
    static ref TASKS: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
}

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Commands::Add { title, description } => {
            let mut tasks = TASKS.lock().unwrap();
            tasks.insert(title.clone(), description.clone().unwrap_or_default());
            println!("タスク '{}' を追加しました。", title);
        }
        Commands::Remove { title } => {
            let mut tasks = TASKS.lock().unwrap();
            if tasks.remove(title).is_some() {
                println!("タスク '{}' を削除しました。", title);
            } else {
                println!("タスク '{}' は存在しません。", title);
            }
        }
        Commands::List => {
            let tasks = TASKS.lock().unwrap();
            if tasks.is_empty() {
                println!("現在、タスクはありません。");
            } else {
                println!("登録されているタスク一覧:");
                for (title, description) in tasks.iter() {
                    println!("- {}: {}", title, description);
                }
            }
        }
    }
}

コードの解説

  1. メイン構造体 Cli
  • CLIツールのエントリーポイントで、サブコマンドを受け付けます。
  1. Commands列挙型
  • Add:タスクを追加するためのサブコマンド。タイトルと任意の説明を引数として受け取ります。
  • Remove:指定されたタイトルのタスクを削除するサブコマンド。
  • List:現在登録されているタスク一覧を表示するサブコマンド。
  1. グローバルなタスク管理用データ構造
  • lazy_static!Mutex<HashMap>を使って、タスクを保存するグローバルなデータ構造を定義しています。
  1. match文による処理の分岐
  • 各サブコマンドに応じた処理が実行されます。

ツールの実行例

  1. タスクの追加
   $ cargo run -- add --title "Buy milk" --description "Purchase milk from the store"
   タスク 'Buy milk' を追加しました。
  1. タスクの一覧表示
   $ cargo run -- list
   登録されているタスク一覧:
   - Buy milk: Purchase milk from the store
  1. タスクの削除
   $ cargo run -- remove --title "Buy milk"
   タスク 'Buy milk' を削除しました。
  1. タスクが存在しない場合の削除
   $ cargo run -- remove --title "Do homework"
   タスク 'Do homework' は存在しません。

まとめ

この具体的なタスク管理CLIツールの例を通じて、以下のポイントが理解できます。

  • clapを使ったサブコマンドの実装方法
  • 引数とオプションの活用
  • シンプルなエラーハンドリングとフィードバック

このようにRustとclapを使えば、柔軟で機能的なCLIツールを簡単に作成できます。

サブコマンド付きCLIツールのテスト方法

サブコマンドを含むCLIツールを作成したら、適切に動作するかテストすることが重要です。Rustでは、標準のcargo testフレームワークとassert_cmdクレートを使ってCLIツールのテストを効率的に行えます。

テストに必要なクレートの追加

まず、Cargo.tomlにテストで使用するクレートを追加します。

[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.0"
  • assert_cmd: CLIコマンドの実行と結果のアサーションをサポート。
  • predicates: 出力の検証に使える便利な条件を提供。

基本的なテストの書き方

以下の例では、サブコマンド付きのタスク管理CLIツールをテストします。

tests/cli_tests.rs ファイルを作成して、テストコードを書きます。

use assert_cmd::Command;
use predicates::str::contains;

#[test]
fn test_add_task() {
    let mut cmd = Command::cargo_bin("taskmgr").unwrap();
    cmd.args(&["add", "--title", "Test Task", "--description", "This is a test task"])
        .assert()
        .success()
        .stdout(contains("タスク 'Test Task' を追加しました。"));
}

#[test]
fn test_remove_task() {
    let mut cmd = Command::cargo_bin("taskmgr").unwrap();
    cmd.args(&["remove", "--title", "Test Task"])
        .assert()
        .success()
        .stdout(contains("タスク 'Test Task' を削除しました。"));
}

#[test]
fn test_list_tasks() {
    let mut cmd = Command::cargo_bin("taskmgr").unwrap();
    cmd.args(&["list"])
        .assert()
        .success()
        .stdout(contains("現在、タスクはありません。"));
}

テストコードの解説

  1. Command::cargo_bin("taskmgr")
  • ビルドされたバイナリを呼び出します。"taskmgr"Cargo.toml[package]セクションで指定されたバイナリ名です。
  1. .args(&["..."])
  • コマンドライン引数としてサブコマンドとオプションを指定します。
  1. .assert().success()
  • コマンドが正常に終了することを確認します。
  1. .stdout(contains("..."))
  • コマンドの標準出力が期待する内容を含んでいるか確認します。

エラーハンドリングのテスト

不正な引数や存在しないサブコマンドに対するエラーハンドリングもテストしましょう。

#[test]
fn test_invalid_command() {
    let mut cmd = Command::cargo_bin("taskmgr").unwrap();
    cmd.args(&["invalid"])
        .assert()
        .failure()
        .stderr(contains("error: unrecognized subcommand 'invalid'"));
}

#[test]
fn test_remove_nonexistent_task() {
    let mut cmd = Command::cargo_bin("taskmgr").unwrap();
    cmd.args(&["remove", "--title", "Nonexistent Task"])
        .assert()
        .success()
        .stdout(contains("タスク 'Nonexistent Task' は存在しません。"));
}

テストの実行

以下のコマンドでテストを実行します。

$ cargo test

テスト結果の例

running 5 tests
test test_add_task ... ok
test test_remove_task ... ok
test test_list_tasks ... ok
test test_invalid_command ... ok
test test_remove_nonexistent_task ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

まとめ

  • assert_cmdでCLIツールを簡単にテストできる。
  • 正常系と異常系の両方をテストして、信頼性を高める。
  • predicatesを使うことで、出力の検証が柔軟に行える。

これで、サブコマンド付きCLIツールのテスト方法について理解できました。

まとめ

本記事では、Rustでサブコマンド付きCLIツールを作成する方法について詳しく解説しました。clapクレートを活用することで、CLIツールの構築が効率的かつ柔軟に行えることを示しました。以下のポイントを押さえておきましょう。

  1. サブコマンドの概要:CLIツールにサブコマンドを導入することで、機能の整理と操作性が向上します。
  2. clapクレートの基本:シンプルな構造体や列挙型でサブコマンドや引数、オプションを定義できます。
  3. 引数とオプションの設定:各サブコマンドに特定の引数やオプションを設定し、柔軟な操作が可能です。
  4. エラーハンドリングとヘルプメッセージ:ユーザーに優しいエラーメッセージとヘルプを提供することで、使いやすさを向上できます。
  5. 具体的なCLIツール例:実際にタスク管理ツールを作成することで、サブコマンドの実装方法を理解しました。
  6. テスト方法assert_cmdpredicatesを活用し、CLIツールの動作確認を効率的に行えます。

これらの知識を活用すれば、実用的で拡張性のあるCLIツールをRustで開発できるようになります。ぜひ、今回学んだ内容を基に、自分のプロジェクトに応用してみてください。

コメント

コメントする

目次