clap

Rustで最も人気のあるコマンドライン引数パーサー。高速で機能豊富、カスタマイズ可能な設計が特徴です。

rustclicommand-linederivebuilder

GitHub概要

clap-rs/clap

A full featured, fast Command Line Argument Parser for Rust

ホームページ:docs.rs/clap
スター15,326
ウォッチ65
フォーク1,113
作成日:2015年2月25日
言語:Rust
ライセンス:Apache License 2.0

トピックス

argument-parserargument-parsingcommand-linecommand-line-parserparsed-argumentspositional-argumentsrustsubcommands

スター履歴

clap-rs/clap Star History
データ取得日時: 2025/7/25 02:04

フレームワーク

clap

概要

clapは、Rustで最も人気のあるコマンドラインパーサーライブラリです。「Command Line Argument Parser」の略で、強力で柔軟なCLIアプリケーションを簡単に構築できるように設計されています。v4では、Derive APIが主要な推奨アプローチとなり、より直感的で型安全なCLI開発が可能になりました。現在のバージョンは4.5.41(2025年)で、多くの有名なRustプロジェクトで採用されています。

詳細

clap v4は、API の洗練、ユーザーエクスペリエンスの向上、パフォーマンスの改善に焦点を当てた大幅な進化を遂げています。最も注目すべき変更は、structoptから統合されたDerive APIが、CLIを構築するための主要で推奨される方法となったことです。clapは元々structoptをベースに開発され、v3でstructoptを直接統合し、v4では統一されたAPIを提供しています。

Rustエコシステムでの地位

clapがRustのCLIライブラリで最も人気な理由:

  • 型安全性: Rustの強力な型システムを活用した引数検証
  • 高性能: バイナリサイズが小さく、解析速度が高速
  • 豊富な機能: シンプルなCLIから複雑なサブコマンド構造まで対応
  • 優れたDX: コンパイル時エラーチェックと優れたエラーメッセージ
  • 活発なメンテナンス: 定期的なアップデートとバグ修正

主な特徴

  • Derive API: 構造体とアトリビュートを使用した宣言的なCLI定義
  • Builder API: 動的CLIのための柔軟な手続き的アプローチ
  • 洗練されたヘルプ出力: モダンで読みやすいヘルプメッセージ
  • 改善されたエラーメッセージ: rustcスタイルの診断的エラー報告
  • 型安全性: 強力な型システムによる引数の検証と変換
  • シェル補完: Bash、Zsh、Fish、PowerShell、Elvishの自動補完サポート
  • 高パフォーマンス: バイナリサイズの削減と解析速度の向上
  • カスタマイズ可能: 機能フラグによる細かい制御

v4の新機能

  • 新しいアトリビュート: #[command(...)]#[arg(...)]#[value(...)]による明確な記述
  • Arg::num_args(range): 引数の数を柔軟に指定する統一されたAPI
  • スタイリング: Command::stylesによるヘルプ出力のカスタマイズ
  • 動的文字列処理: &'static strCow<'static, str>Box<str>の柔軟な処理
  • 依存関係の軽量化: v4ではViperのような重い依存関係を除去
  • より良いエラーメッセージ: rustcスタイルの診断情報とカラー出力

最新の動向(2024-2025)

  • 継続的な改善: 定期的なパッチリリースでバグ修正と最適化
  • コミュニティ支援: crossterm、inquire、comfy-tableなどの関連クレートとの連携
  • シェル補完の強化: より高度な動的補完機能
  • Builder APIとDerive APIの相互運用: 両方のAPIを同時に使用可能

メリット・デメリット

メリット

  • 型安全で直感的なDerive API
  • 豊富な機能と高い柔軟性
  • 優れたドキュメントとコミュニティサポート
  • 高いパフォーマンスと小さなバイナリサイズ
  • 複雑なCLIアプリケーションにも対応可能
  • 標準的なコマンドライン規約に準拠

デメリット

  • 単純なスクリプトには過剰な場合がある
  • 学習曲線がやや急(特にBuilder API)
  • コンパイル時間への影響
  • 多くの機能フラグによる複雑さ

主要リンク

書き方の例

基本的なDerive APIの例

use clap::Parser;

/// シンプルなプログラムで人に挨拶する
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// 挨拶する人の名前
    #[arg(short, long)]
    name: String,

    /// 挨拶する回数
    #[arg(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
    let args = Args::parse();
    
    for _ in 0..args.count {
        println!("こんにちは、{}さん!", args.name);
    }
}

サブコマンドの例

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// ファイルをインデックスに追加
    Add {
        /// 追加するファイル
        #[arg(short, long)]
        files: Vec<String>,
    },
    /// 変更をコミット
    Commit {
        /// コミットメッセージ
        #[arg(short, long)]
        message: String,
        
        /// 前回のコミットを修正
        #[arg(long)]
        amend: bool,
    },
    /// リポジトリのステータスを表示
    Status {
        /// 短い形式で表示
        #[arg(short, long)]
        short: bool,
    },
}

fn main() {
    let cli = Cli::parse();
    
    match &cli.command {
        Commands::Add { files } => {
            println!("ファイルを追加: {:?}", files);
        }
        Commands::Commit { message, amend } => {
            if *amend {
                println!("前回のコミットを修正: {}", message);
            } else {
                println!("コミット: {}", message);
            }
        }
        Commands::Status { short } => {
            if *short {
                println!("M  file1.rs\nA  file2.rs");
            } else {
                println!("変更されたファイル:\n  file1.rs\n新規ファイル:\n  file2.rs");
            }
        }
    }
}

Builder APIの例

use clap::{Command, Arg, ArgAction};

fn main() {
    let matches = Command::new("myapp")
        .version("1.0")
        .author("あなたの名前 <[email protected]>")
        .about("プログラムの簡単な説明")
        .arg(
            Arg::new("config")
                .short('c')
                .long("config")
                .value_name("FILE")
                .help("設定ファイルを指定")
                .action(ArgAction::Set)
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .help("詳細な出力を有効化")
                .action(ArgAction::Count)
        )
        .get_matches();
    
    if let Some(config) = matches.get_one::<String>("config") {
        println!("設定ファイル: {}", config);
    }
    
    match matches.get_count("verbose") {
        0 => println!("通常モード"),
        1 => println!("詳細モード"),
        2 => println!("デバッグモード"),
        _ => println!("超詳細モード"),
    }
}

高度な機能の例

use clap::{Parser, ValueEnum};
use std::path::PathBuf;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// 入力ファイル
    #[arg(value_name = "FILE")]
    input: Option<PathBuf>,
    
    /// 出力形式
    #[arg(short, long, value_enum, default_value_t = OutputFormat::Json)]
    format: OutputFormat,
    
    /// ポート番号(1024-65535)
    #[arg(short, long, value_parser = clap::value_parser!(u16).range(1024..))]
    port: u16,
    
    /// 詳細レベル(複数回指定可能)
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
    
    /// 設定ファイル(環境変数からも読み込み可能)
    #[arg(short, long, env = "MYAPP_CONFIG")]
    config: Option<PathBuf>,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum OutputFormat {
    Json,
    Yaml,
    Toml,
}

fn main() {
    let cli = Cli::parse();
    
    if let Some(input) = cli.input {
        println!("入力ファイル: {}", input.display());
    }
    
    println!("出力形式: {:?}", cli.format);
    println!("ポート: {}", cli.port);
    println!("詳細レベル: {}", cli.verbose);
    
    if let Some(config) = cli.config {
        println!("設定ファイル: {}", config.display());
    }
}

カスタムバリデーションの例

use clap::Parser;
use std::net::IpAddr;

#[derive(Parser)]
struct Cli {
    /// サーバーアドレス
    #[arg(long, value_parser = parse_server_address)]
    server: (IpAddr, u16),
    
    /// メールアドレス
    #[arg(long, value_parser = validate_email)]
    email: String,
}

fn parse_server_address(s: &str) -> Result<(IpAddr, u16), String> {
    let parts: Vec<&str> = s.split(':').collect();
    if parts.len() != 2 {
        return Err("アドレスは 'IP:ポート' の形式で指定してください".to_string());
    }
    
    let ip: IpAddr = parts[0].parse()
        .map_err(|_| "無効なIPアドレスです".to_string())?;
    let port: u16 = parts[1].parse()
        .map_err(|_| "無効なポート番号です".to_string())?;
    
    Ok((ip, port))
}

fn validate_email(s: &str) -> Result<String, String> {
    if s.contains('@') && s.contains('.') {
        Ok(s.to_string())
    } else {
        Err("有効なメールアドレスを入力してください".to_string())
    }
}

fn main() {
    let cli = Cli::parse();
    println!("サーバー: {:?}", cli.server);
    println!("メール: {}", cli.email);
}

実用的なCLIツールの例

use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use std::fs;

/// ファイル管理CLIツール
#[derive(Parser)]
#[command(name = "filemgr")]
#[command(about = "効率的なファイル管理ツール")]
#[command(version = "1.0.0")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
    
    /// 詳細な出力を有効化
    #[arg(short, long, global = true)]
    verbose: bool,
    
    /// ドライランモード(実際には実行しない)
    #[arg(short = 'n', long, global = true)]
    dry_run: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// ファイルをコピー
    Copy {
        /// ソースファイル/ディレクトリ
        source: PathBuf,
        
        /// 宛先ディレクトリ
        dest: PathBuf,
        
        /// 既存ファイルを上書き
        #[arg(short, long)]
        force: bool,
        
        /// ディレクトリを再帰的にコピー
        #[arg(short, long)]
        recursive: bool,
    },
    
    /// ファイルを検索
    Find {
        /// 検索パターン(glob形式)
        pattern: String,
        
        /// 検索開始ディレクトリ
        #[arg(short = 'd', long, default_value = ".")]
        directory: PathBuf,
        
        /// ファイルタイプでフィルタ
        #[arg(short = 't', long, value_enum)]
        file_type: Option<FileType>,
        
        /// 最大検索深度
        #[arg(long, value_parser = clap::value_parser!(u32).range(1..=100))]
        max_depth: Option<u32>,
    },
    
    /// ファイル情報を表示
    Info {
        /// 対象ファイル
        files: Vec<PathBuf>,
        
        /// 人間が読みやすい形式で表示
        #[arg(short = 'h', long)]
        human_readable: bool,
        
        /// ハッシュ値を計算
        #[arg(long)]
        checksum: bool,
    },
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum FileType {
    /// ファイルのみ
    File,
    /// ディレクトリのみ
    Dir,
    /// シンボリックリンクのみ
    Symlink,
}

fn main() {
    let cli = Cli::parse();
    
    match &cli.command {
        Commands::Copy { source, dest, force, recursive } => {
            if cli.verbose {
                println!("コピー開始: {} -> {}", source.display(), dest.display());
                println!("設定: force={}, recursive={}, dry_run={}", force, recursive, cli.dry_run);
            }
            
            if !cli.dry_run {
                // 実際のコピー処理をここに実装
                println!("コピーが完了しました");
            } else {
                println!("[ドライラン] コピー処理をスキップしました");
            }
        }
        
        Commands::Find { pattern, directory, file_type, max_depth } => {
            if cli.verbose {
                println!("検索開始: パターン='{}', ディレクトリ='{}'", pattern, directory.display());
                if let Some(ft) = file_type {
                    println!("ファイルタイプフィルタ: {:?}", ft);
                }
                if let Some(depth) = max_depth {
                    println!("最大深度: {}", depth);
                }
            }
            
            // 実際の検索処理をここに実装
            println!("検索結果: 5個のファイルが見つかりました");
        }
        
        Commands::Info { files, human_readable, checksum } => {
            for file in files {
                if cli.verbose {
                    println!("ファイル情報を取得中: {}", file.display());
                }
                
                match fs::metadata(file) {
                    Ok(metadata) => {
                        println!("ファイル: {}", file.display());
                        
                        if *human_readable {
                            println!("  サイズ: {} bytes", metadata.len());
                            println!("  種類: {}", if metadata.is_file() { "ファイル" } else { "ディレクトリ" });
                        } else {
                            println!("  サイズ: {}", metadata.len());
                            println!("  種類: {}", metadata.file_type().is_file());
                        }
                        
                        if *checksum && metadata.is_file() {
                            println!("  ハッシュ: [計算中...]");
                        }
                    }
                    Err(e) => {
                        eprintln!("エラー: {}: {}", file.display(), e);
                    }
                }
            }
        }
    }
}

Derive APIとBuilder APIの相互運用例

use clap::{Args, Command, Parser, Subcommand};

#[derive(Parser)]
#[command(name = "hybrid")]
#[command(about = "Derive APIとBuilder APIを組み合わせた例")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 静的に定義されたコマンド
    Static(StaticArgs),
    
    /// 動的に生成されるコマンド(この部分はBuilder APIで拡張)
    Dynamic,
}

#[derive(Args)]
struct StaticArgs {
    /// 入力ファイル
    #[arg(short, long)]
    input: String,
    
    /// 出力形式
    #[arg(short, long, default_value = "json")]
    format: String,
}

fn main() {
    let mut app = Cli::command();
    
    // Builder APIを使って動的にサブコマンドを追加
    let dynamic_cmd = Command::new("dynamic")
        .about("動的に生成されたコマンド")
        .arg(clap::Arg::new("count")
            .short('c')
            .long("count")
            .value_parser(clap::value_parser!(u32))
            .help("実行回数"))
        .arg(clap::Arg::new("output")
            .short('o')
            .long("output")
            .value_name("FILE")
            .help("出力ファイル"));
    
    // 既存のサブコマンドを置き換え
    if let Some(subcommands) = app.get_subcommands_mut().find(|cmd| cmd.get_name() == "dynamic") {
        *subcommands = dynamic_cmd;
    }
    
    let matches = app.get_matches();
    
    match matches.subcommand() {
        Some(("static", sub_matches)) => {
            let args = StaticArgs::from_arg_matches(sub_matches).unwrap();
            println!("静的コマンド実行: input={}, format={}", args.input, args.format);
        }
        Some(("dynamic", sub_matches)) => {
            let count = sub_matches.get_one::<u32>("count").unwrap_or(&1);
            let output = sub_matches.get_one::<String>("output");
            
            println!("動的コマンド実行: count={}", count);
            if let Some(output) = output {
                println!("出力先: {}", output);
            }
        }
        _ => unreachable!(),
    }
}