clap
Rustで最も人気のあるコマンドライン引数パーサー。高速で機能豊富、カスタマイズ可能な設計が特徴です。
GitHub概要
スター15,326
ウォッチ65
フォーク1,113
作成日:2015年2月25日
言語:Rust
ライセンス:Apache License 2.0
トピックス
argument-parserargument-parsingcommand-linecommand-line-parserparsed-argumentspositional-argumentsrustsubcommands
スター履歴
データ取得日時: 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 str
、Cow<'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!(),
}
}