mcpField
C
cli-commands

cli-commands

github.com/gitbutlerapp開発21.9k39 回インストール今日更新
リポジトリ

cli-commands について

Rust 製コマンドラインツール `but` のコマンド実装を支援する開発者向けスキルです。引数定義、コマンド振り分け、検証、実行、出力の規約を説明します。

主な機能

  1. 01コマンドライン引数の構成
  2. 02コマンド振り分けのパターン
  3. 03resolve と run の分離
  4. 04入力検証とエラー処理
  5. 05出力形式の指針

活用シーン

  1. 01`but` にコマンドを追加
  2. 02Git オブジェクトの型付き引数を定義
  3. 03入力検証と実行を分離
  4. 04コマンド出力を統一

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/gitbutlerapp/gitbutler --skill cli-commands

インストール後、Claude Code で/cli-commandsを入力して読み込まれたか確認します。

詳細

新しい commit3 コマンドを実装するとします。その大まかな構成は次のとおりです。

引数

引数は crates/but/src/args にあります。commit コマンドの場合は crates/but/src/args/commit3.rs になります。

use crate::args::atoms::CliIdArg;

/// Create a commit.
///
/// More details about the command here...
#[derive(Debug, clap::Parser)]
#[cfg_attr(feature = "raw-clap-docs", clap(verbatim_doc_comment))]
#[deny(missing_docs)]
pub struct Platform {
    /// The message to use for the commit.
    #[clap(short, long, group = "commit_message")]
    pub message: Option<Vec<String>>,

    /// Place the commit on the branch `BRANCH`.
    #[clap(short, long, value_name = "BRANCH", group = "targeting")]
    pub branch: Option<Option<CliIdArg>>,

    /// One or more changes to commit.
    pub changes: Vec<CliIdArg>,
}

crates/but/src/args/mod.rs では次のようにします。

#[cfg(feature = "legacy")]
#[cfg_attr(feature = "raw-clap-docs", clap(verbatim_doc_comment))]
#[clap(hide = true, name = "_commit3")]
Commit3(commit3::Platform),

注意点:

Git オブジェクト(コミット、ブランチ、ファイル、ハンクなど)を参照する引数には、String やその他の型が曖昧な値ではなく、crates/but/src/args/atoms/ にある型を使用します。

ブランチ、コミット、ファイルなど、既存の Git オブジェクトを参照する引数には CliIdArg を使用します。これにより、ユーザーは短い ID または完全修飾名を使用できます。

String は、コミットメッセージなど、本当に任意のテキスト入力にのみ使用してください。

Platform とそのすべてのフィールドにはドキュメントが必要です。Subcommands バリアントには意図的にドキュメントコメントを付けません。clap は Platform からコマンドのドキュメントを取得するためです。

#[clap(group = "...")] を使用して、相互に排他的な引数グループを作成します。

文法が複雑なコマンドでは、Platform の隣に pub(crate) const ERROR_EXAMPLES を定義し、args::error_examples に登録できます。このブロックは clap の解析エラーの後に追加されます。最大 4 行とし、各行はそのまま実行できる but <cmd> ... # what it does の呼び出しにします(省略するとエディターが開く場合は -m を含めるなど)。

コマンドの処理

コマンドを処理するため、crates/but/src/lib.rs に match アームを追加します。

match cmd {
    Subcommands::Commit3(commit_args) => {
        use crate::utils::IntermediateChannel;

        let status_after = args.status_after;
        let mut ctx = setup::init_ctx(
            &args,
            InitCtxOptions {
                background_sync: BackgroundSync::Enabled { silent: false },
                ..Default::default()
            },
            out,
        )?;
        out.begin_status_after(status_after);

        let outcome = command::legacy::commit3::commit(
            &mut ctx,
            IntermediateChannel::new(out),
            commit_args,
        )
        .emit_metrics(metrics_ctx)?;
        out.print_cli_output(outcome)?;

        run_status_after_if_requested(status_after, &mut ctx, out);

        Ok(())
    }

    // all the other commands...
}

注意点:

IntermediateChannel を使用してください。コマンドに OutputChannel を渡してはいけません。

コマンドの最終出力を表示するには OutputChannel::print_cli_output を使用してください。これにより、サポートされているすべての形式を処理できます。人間向けの形式のみをサポートする場合は、OutputChannel::print_cli_output_human を使用してください。

コマンドの実装

crates/but/src/command/legacy/commit3.rs

pub fn commit(
    ctx: &mut Context,
    out: IntermediateChannel<'_>,
    args: Platform,
) -> CliResult<CommitOutcome> {
    // get whatever dependencies we need from `Context` such as
    // `RepoExclusiveGuard`, `IdMap`, `RefInfo`, etc.

    // resolve the arguments into a `CommitOperation`
    let commit_operation = resolve(ctx, args)?;

    // Run the operation
    let outcome = run(ctx, commit_operation)?;

    // Return the outcome which will be printed by the caller
    Ok(outcome)
}

fn resolve(ctx: &mut Context, args: Platform) -> CliResult<CommitOperation> {
    let Platform { message, branch, changes } = args;
    // ...
}

fn run(ctx: &mut Context, commit_op: CommitOperation) -> anyhow::Result<CommitOutcome> {
    match commit_op {
        // ...
    }
}

#[must_use]
struct CommitOutcome {
    new_commit: ObjectId,
}

impl CliOutputHuman for CommitOutcome {
    fn on_human(self, out: &mut dyn WriteWithUtils, _theme: &Theme) -> anyhow::Result<()> {
        let Self { new_commit } = self;

        writeln!(
            out,
            "Created commit {}",
            theme::Commit(new_commit, None),
        )?;

        Ok(())
    }
}

impl CliOutput for CommitOutcome {
    fn on_shell(self, out: &mut dyn WriteWithUtils) -> anyhow::Result<()> {
        let Self { new_commit } = self;

        writeln!(out, "{}", new_commit.to_hex_with_len(7))?;

        Ok(())
    }

    fn on_json(self) -> impl serde::Serialize {
        #[derive(Serialize)]
        struct Output {
            commit: HexHash,
        }

        let Self { new_commit } = self;

        Output { commit: new_commit.into() }
    }
}

留意点:

コマンドは resolve、次に run という構成にします。

run は最終的な出力を表示しません。呼び出し側が表示できる、CliOutput/CliOutputHuman を実装した値を返します。

resolve は不正なユーザー入力を拒否する必要があるため、CliResult を返します。

run が返すのは内部エラーのみであるため、anyhow::Result を返します。不正なユーザー入力は resolve で処理します。

resolve は CLI 引数をドメインの対象に変換し、ユーザー入力を検証します。入力を曖昧さの解消や拒否するために、リポジトリの状態を照会してもかまいませんが、そこから導出したデータを操作に保持してはいけません。

run() は現在のリポジトリの状態を読み込み、結果を計算します。コミット数、差分、統計情報、ブランチの詳細、現在の先端、ワークスペースの投影は、通常 run() に属します。それらの識別子とコンテキストから導出できる状態も run() に属します。

操作に crate::args::atoms の型を含めてはいけません。

TUI などの非 CLI 呼び出し元は、resolve() で行うリポジトリ照会を再現することなく、ドメイン識別子から操作を構築できる必要があります。操作を構築するためにコミット数、差分、ブランチのメタデータを計算する必要がある場合、それらのフィールドはおそらく run() に属します。

操作には差分指定(通常は Vec<DiffSpec> の形式)を含めません。代わりに CliId を含め、run で DiffSpecBuilder を使って Vec<DiffSpec> に変換します。これにより、ステータス TUI から run を直接呼び出しやすくなります。

ユーザー入力用の選択 UI やプロンプトは、IntermediateChannel::prepare_for_terminal_input を介してアクセスする InputOutputChannel を使って作成します。

JSON 出力には、コミット ID と変更 ID の両方を含めます。

マージ済みのコミットやブランチを変更しないことを確認するには、AllowMergedArg と MergedUpstream を使います。

コミット、ブランチ、変更 ID などの表示には、theme::Branch や theme::Commit など、theme の新しい型を使います。これにより、色付けを統一できます。

不正なユーザー入力のエラーには bad_input(...) を使い、必要に応じて .arg_name()、.arg_value()、.hint() を追加します。

操作や結果の型に serde::Serialize を実装させないでください。fn on_json の中で必要な型を定義します。これにより、誤って互換性を損なう可能性を減らせます。既存のドメイン型の Serialize を再利用するのは問題ありません。

その他の例

この構成の実例については、次を参照してください。

  • crates/but/src/command/legacy/commit.rs
  • crates/but/src/command/legacy/move.rs
  • crates/but/src/command/legacy/squash.rs
  • crates/but/src/command/legacy/diff.rs

コメント