1650431123
この記事では、Rustアプリケーションに渡されたコマンドライン引数を手動で解析する方法、手動解析が大規模なアプリに適していない理由、およびClapライブラリがこれらの問題の解決にどのように役立つかを説明します。
注意として、変数宣言、if-elseブロック、ループ、構造体などの基本的なRustの読み取りと書き込みに慣れている必要があります。
たとえば、ノードベースのプロジェクトが多数あるprojectsフォルダーがあり、「依存関係パッケージを含むすべてのパッケージのうち、どれを何回使用したか」を知りたいとします。
結局のところ、その合計1GBは、node_modules
すべて固有の依存関係になるわけではありませんね😰…?
プロジェクトでパッケージを使用する回数をカウントする素敵な小さなプログラムを作成したらどうなるでしょうか。
cargo new package-hunter
これを行うには、Rustでプロジェクトを設定しましょう。これで、src/main.rs
ファイルにデフォルトのメイン機能が追加されました。
fn main() {
println!("Hello, world!");
}
次のステップは非常に単純なようです。アプリケーションに渡す引数を取得します。したがって、後で他の引数を抽出するための別の関数を記述します。
fn get_arguments() {
let args: Vec<_> = std::env::args().collect(); // get all arguements passed to app
println!("{:?}", args);
}
fn main() {
get_arguments();
}
それを実行すると、エラーやパニックなしで、素晴らしい出力が得られます。
# anything after '--' is passed to your app, not to cargo
> cargo run -- svelte
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/package-hunter svelte`
["target/debug/package-hunter", "svelte"]
もちろん、最初の引数はアプリケーションを呼び出したコマンドであり、2番目の引数はアプリケーションに渡されたものです。かなり簡単なようです。
これで、名前を取得するカウント関数を作成し、サブディレクトリでその名前のディレクトリをカウントすることができます。
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
/// Not the dracula
fn count(name: &str) -> std::io::Result<usize> {
let mut count = 0;
// queue to store next dirs to explore
let mut queue = VecDeque::new();
// start with current dir
queue.push_back(PathBuf::from("."));
loop {
if queue.is_empty() {
break;
}
let path = queue.pop_back().unwrap();
for dir in fs::read_dir(path)? {
// the for loop var 'dir' is actually a result, so we convert it
// to the actual dir struct using ? here
let dir = dir?;
// consider it only if it is a directory
if dir.file_type()?.is_dir() {
if dir.file_name() == name {
// we have a match, so stop exploring further
count += 1;
} else {
// not a match so check its sub-dirs
queue.push_back(dir.path());
}
}
}
}
return Ok(count);
}
get_arguments
コマンドの後の最初の引数を返すようにを更新し、main
では、その引数を使用して呼び出しcount
ます。
これをプロジェクトフォルダーの1つで実行すると、予期せず完全に機能し、1つのプロジェクトに依存関係が1回だけ含まれるため、カウントが1として返されます。
ここで、ディレクトリを上に移動して実行しようとすると、問題が発生します。通過するディレクトリが増えるため、少し時間がかかります。
理想的には、プロジェクトディレクトリのルートから実行して、その依存関係を持つすべてのプロジェクトを見つけることができますが、これにはさらに時間がかかります。
そのため、妥協して、特定の深さまでディレクトリを探索することにしました。ディレクトリの深さが指定された深さよりも大きい場合、それは無視されます。関数に別のパラメーターを追加し、それを更新して深さを考慮することができます。
/// Not the dracula
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
...
queue.push_back((PathBuf::from("."), 0));
...
let (path, crr_depth) = queue.pop_back().unwrap();
if crr_depth > max_depth {
continue;
}
...
// not a match so check its sub-dirs
queue.push_back((dir.path(), crr_depth + 1));
...
}
これで、アプリケーションは2つのパラメーターを受け取ります。最初にパッケージ名、次に探索する最大深度です。
ただし、深さはオプションの引数にする必要があるため、指定しない場合はすべてのサブディレクトリを探索し、指定しない場合は指定した深さで停止します。
このために、get_arguments
関数を更新して2番目の引数をオプションにすることができます。
fn get_arguments() {
let args: Vec<_> = std::env::args().collect();
let mdepth = if args.len() > 2 {
args[2].parse().unwrap()
} else {
usize::MAX
};
println!("{:?}", count(&args[1], mdepth));
}
これにより、両方の方法で実行でき、次のように機能します。
> cargo run -- svelte
> cargo run -- svelte 5
残念ながら、これはあまり柔軟ではありません。のように引数を逆の順序で指定するとcargo run 5 package-name
、アプリケーションは数値として解析しようとしてクラッシュしますpackage-name
。
さて、引数に独自のフラグを持たせたい場合があります-f
。-d
たとえば、任意の順序で引数を指定できます。(フラグのボーナスUnixポイントも!)
もう一度関数を更新し、get_arguments
今回は引数に適切な構造体を追加するので、解析された引数を返すのが簡単になります。
#[derive(Default)]
struct Arguments {
package_name: String,
max_depth: usize,
}
fn get_arguments() -> Arguments {
let args: Vec<_> = std::env::args().collect();
// atleast 3 args should be there : the command name, the -f flag,
// and the actual file name
if args.len() < 3 {
eprintln!("filename is a required argument");
std::process::exit(1);
}
let mut ret = Arguments::default();
ret.max_depth = usize::MAX;
if args[1] == "-f" {
// it is file
ret.package_name = args[2].clone();
} else {
// it is max depth
ret.max_depth = args[2].parse().unwrap();
}
// now that one argument is parsed, time for seconds
if args.len() > 4 {
if args[3] == "-f" {
ret.package_name = args[4].clone();
} else {
ret.max_depth = args[4].parse().unwrap();
}
}
return ret;
}
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
...
}
fn main() {
let args = get_arguments();
match count(&args.package_name, args.max_depth) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
}
}
これで、またはのような派手な-
フラグを使用して実行できます。cargo run -- -f sveltecargo run -- -d 5 -f svelte
ただし、これにはかなり深刻なバグがいくつかあります。同じ引数を2回指定して、ファイル引数を完全cargo run -- -d 5 -d 7
にスキップするか、無効なフラグを指定すると、エラーメッセージなしで実行されます😭。
file_name
上記の行でが空でないことを確認し、27
誤った値が指定された場合に予想される内容を出力することで、これを修正できます。-d
ただし、を直接呼び出すため、に非数値を渡すと、これもクラッシュunwrap
しparse
ます。
また、このアプリケーションはヘルプ情報を提供しないため、新規ユーザーにとっては扱いにくい場合があります。ユーザーは、どの引数がどの順序で渡されるかわからない場合があり、アプリケーションには、-h
従来のUnixプログラムのように、その情報を表示するためのフラグがありません。
これらはこの特定のアプリにとってはほんの少しの不便ですが、複雑さが増すにつれてオプションの数が増えるにつれて、これらすべてを手動で維持することがますます難しくなります。
これがクラップの出番です。
-h
Clapは、引数の解析ロジックを生成する機能を提供するライブラリであり、引数の説明やヘルプコマンドなど、アプリケーション用のきちんとしたCLIを提供します。
Clapの使用は非常に簡単で、現在の設定にわずかな変更を加えるだけで済みます。
Clapには、多くのRustプロジェクトで使用される2つの一般的なバージョンがあります。V2とV3です。V2は主に、コマンドライン引数パーサーを構築するためのビルダーベースの実装を提供します。
V3は最近のリリース(執筆時点)であり、ビルダーの実装とともにderive
proc-macrosが追加されているため、構造体に注釈を付けることができ、マクロは必要な関数を派生させます。
これらには両方とも独自の利点があり、より詳細な違いと機能のリストについては、ドキュメントとヘルプページを確認してください。これらのドキュメントとヘルプページには、例が示され、どの状況が派生し、ビルダーが適しているかが示されています。
この投稿では、proc-macroでClapV3を使用する方法を説明します。
Clapをプロジェクトに組み込むには、以下をプロジェクトに追加しますCargo.toml
。
[dependencies]
clap = { version = "3.1.6", features = ["derive"] }
これにより、派生機能との依存関係としてClapが追加されます。
get_arguments
それでは、関数とその呼び出しをmain
:から削除しましょう。
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
#[derive(Default)]
struct Arguments {
package_name: String,
max_depth: usize,
}
/// Not the dracula
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
let mut count = 0;
// queue to store next dirs to explore
let mut queue = VecDeque::new();
// start with current dir
queue.push_back((PathBuf::from("."), 0));
loop {
if queue.is_empty() {
break;
}
let (path, crr_depth) = queue.pop_back().unwrap();
if crr_depth > max_depth {
continue;
}
for dir in fs::read_dir(path)? {
let dir = dir?;
// we are concerned only if it is a directory
if dir.file_type()?.is_dir() {
if dir.file_name() == name {
// we have a match, so stop exploring further
count += 1;
} else {
// not a match so check its sub-dirs
queue.push_back((dir.path(), crr_depth + 1));
}
}
}
}
return Ok(count);
}
fn main() {}
次に、derive
構造Arguments
体に追加Parser
してDebug
:
use clap::Parser;
#[derive(Parser,Default,Debug)]
struct Arguments {...}
最後に、でmain
、parseメソッドを呼び出します。
let args = Arguments::parse();
println!("{:?}", args);
cargo run
引数なしでアプリケーションを実行すると、エラーメッセージが表示されます。
error: The following required arguments were not provided:
<PACKAGE_NAME>
<MAX_DEPTH>
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
For more information try --help
これは、手動バージョンよりも優れたエラー報告です。
また、ボーナスとして、-h
引数とその順序を出力できるヘルプのフラグが自動的に提供されます。
package-hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME>
<MAX_DEPTH>
OPTIONS:
-h, --help Print help information
そして今、に数字以外のものを提供するMAX_DEPTH
と、提供された文字列が数字ではないというエラーが発生します。
> cargo run -- 5 test
error: Invalid value "test" for '<MAX_DEPTH>': invalid digit found in string
For more information try --help
それらを正しい順序で提供すると、次の出力が得られますprintln
。
> cargo run -- test 5
Arguments { package_name: "test", max_depth: 5 }
これらすべてに2行の新しい行があり、解析コードやエラー処理を記述する必要はありません。🎉
現在、ヘルプメッセージは引数の名前と順序のみを示しているため、少し当たり障りのないものです。ユーザーが特定の引数の意味を理解できれば、エラーを報告したい場合はアプリケーションのバージョンでさえも役立つでしょう。
クラップは、このためのオプションも提供します。
#[derive(...)]
#[clap(author="Author Name", version, about="A Very simple Package Hunter")]
struct Arguments{...}
これで、-h
出力にすべての詳細が表示-V
され、バージョン番号を出力するためのフラグも提供されます。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME>
<MAX_DEPTH>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
マクロ自体に情報に関する複数の行を書き込むのは少し面倒な場合があるため、代わりに、構造体に使用するドキュメントコメントを追加する///
と、マクロはそれを情報として使用します(両方が存在する場合は、1つマクロ内はドキュメントコメントよりも優先されます):
#[clap(author = "Author Name", version, about)]
/// A Very simple Package Hunter
struct Arguments {...}
これにより、以前と同じヘルプが提供されます。
引数に関する情報を追加するために、引数自体に同様のコメントを追加できます。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME> Name of the package to search
<MAX_DEPTH> maximum depth to which sub-directories should be explored
OPTIONS:
-h, --help Print help information
-V, --version Print version information
これははるかに役立ちます!
-f
ここで、引数フラグ(および-d
)やオプションのdepth引数の設定など、他の機能を復活させましょう。
Clapを使用すると、フラグ引数が途方もなく単純になります。構造体メンバーに。を使用して別のClapマクロアノテーションを追加するだけ#[clap(short, long)]
です。
ここで、short
は、などのフラグの短縮バージョンを-f
指しlong
、はなどの完全なバージョンを指し--file
ます。どちらかまたは両方を選択できます。この追加により、次のようになります。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter --package-name <PACKAGE_NAME> --max-depth <MAX_DEPTH>
OPTIONS:
-h, --help Print help information
-m, --max-depth <MAX_DEPTH> maximum depth to which sub-directories should be explored
-p, --package-name <PACKAGE_NAME> Name of the package to search
-V, --version Print version information
両方の引数にフラグが付いているため、位置引数はありません。cargo run -- test 5
これは、Clapがフラグを探し、引数が提供されていないというエラーを出すため、実行できないことを意味します。
cargo run -- -p test -m 5
代わりに、またはを実行するcargo run -- -m 5 -p test
と、両方が正しく解析され、次の出力が得られます。
Arguments { package_name: "test", max_depth: 5 }
パッケージ名は常に必要なので、位置引数にすることができ、-p
毎回フラグを入力する必要はありません。
これを行うには、を削除し#[clap(short,long)]
ます。これで、フラグのない最初の引数は次のように見なされpackage name
ます。
> cargo run -- test -m 5
Arguments { package_name: "test", max_depth: 5 }
> cargo run -- -m 5 test
Arguments { package_name: "test", max_depth: 5 }
省略引数で注意すべきことの1つは、2つの引数が同じ文字で始まり(つまり、package-name
およびpath
)、両方で短いフラグが有効になっている場合、アプリケーションはデバッグビルドの実行時にクラッシュし、リリースビルドの混乱を招くエラーメッセージを表示することです。 。
したがって、次のいずれかを確認してください。
short
フラグがあります次のステップは、max_depth
オプションにすることです。
引数をオプションとしてマークするには、その引数の型を作成します。Option<T>
ここT
で、は元の型引数です。したがって、この場合、次のようになります。
#[clap(short, long)]
/// maximum depth to which sub-directories should be explored
max_depth: Option<usize>,
これでうまくいくはずです。変更はヘルプにも反映され、必須の引数として最大深度がリストされていません。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter [OPTIONS] <PACKAGE_NAME>
ARGS:
<PACKAGE_NAME> Name of the package to search
OPTIONS:
-h, --help Print help information
-m, --max-depth <MAX_DEPTH> maximum depth to which sub-directories should be explored
-V, --version Print version information
そして、-m
フラグを付けずに実行できます。
> cargo run -- test
Arguments { package_name: "test", max_depth: None }
しかし、これはまだ少し面倒です。ここで、を実行する必要があります。実行match
するmax_depth
場合はNone
、以前と同じように設定しusize::MAX
ます。
しかし、拍手はここでも私たちのために何かを持っています!作成する代わりにOption<T>
、引数が指定されていない場合はデフォルト値を設定できます。
したがって、次のように変更した後:
#[clap(default_value_t=usize::MAX,short, long)]
/// maximum depth to which sub-directories should be explored
max_depth: usize,
の値を指定して、または指定せずにアプリケーションを実行できますmax_depth
(の最大値はusize
システム構成によって異なります)。
> cargo run -- test
Arguments { package_name: "test", max_depth: 18446744073709551615 }
> cargo run -- test -m 5
Arguments { package_name: "test", max_depth: 5 }
main
それでは、前と同じようにカウント関数に接続しましょう。
fn main() {
let args = Arguments::parse();
match count(&args.package_name, args.max_depth) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
}
}
これにより、元の機能が復活しましたが、コードが大幅に減り、いくつかの追加機能が追加されました。
はpackage-hunter
期待どおりに機能していますが、残念ながら、手動の解析段階から存在し、Clapベースのバージョンに持ち込まれた微妙なバグがあります。あなたはそれが何であるかを推測できますか?
小さな小さなアプリにとってはそれほど危険なバグではありませんが、他のアプリケーションにとってはアキレス腱になる可能性があります。私たちの場合、エラーが発生したときに誤った結果が返されます。
次を実行してみてください:
> cargo run -- ""
0 uses found
ここではpackage_name
、空のパッケージ名を許可しない場合に、が空の文字列として渡されます。これは、コマンドを実行するシェルが引数をアプリに渡す方法が原因で発生します。
通常、シェルはスペースを使用してプログラムに渡される引数リストを分割するため、、、、およびの3つのabc def hij
個別の引数として指定されます。abcdefhij
引数にスペースを含めたい場合は、のように引用符で囲む必要があります"abc efg hij"
。このようにして、シェルはこれが単一の引数であることを認識し、そのように渡します。
一方、これにより、空の文字列またはスペースのみの文字列をアプリに渡すこともできます。もう一度、拍手して救助してください!引数の空の値を拒否する方法を提供します。
#[clap(forbid_empty_values = true)]
/// Name of the package to search
package_name: String,
これで、引数として空の文字列を指定しようとすると、エラーが発生します。
> cargo run -- ""
error: The argument '<PACKAGE_NAME>' requires a value but none was supplied
ただし、これでもパッケージ名としてスペースが提供されます。つまり""
、有効な引数です。これを修正するには、名前に先頭または末尾のスペースがあるかどうかを確認し、含まれている場合は拒否するカスタムバリデーターを提供する必要があります。
検証関数を次のように定義します。
fn validate_package_name(name: &str) -> Result<(), String> {
if name.trim().len() != name.len() {
Err(String::from(
"package name cannot have leading and trailing space",
))
} else {
Ok(())
}
}
次に、次のように設定しpackage_name
ます。
#[clap(forbid_empty_values = true, validator = validate_package_name)]
/// Name of the package to search
package_name: String,
ここで、空の文字列またはスペースを含む文字列を渡そうとすると、次のようにエラーが発生します。
> cargo run -- ""
error: The argument '<PACKAGE_NAME>' requires a value but none was supplied
> cargo run -- " "
error: Invalid value " " for '<PACKAGE_NAME>': package name cannot have leading and trailing space
このようにして、解析用のすべてのコードを記述せずに、カスタムロジックを使用して引数を検証できます。
アプリケーションは現在正常に動作していますが、動作しなかった場合に何が起こったかを確認する方法はありません。そのためには、アプリケーションがクラッシュしたときに何が起こったかを確認するために、アプリケーションが実行していることのログを保持する必要があります。
他のコマンドラインアプリケーションと同様に、ユーザーがログのレベルを簡単に設定できる方法が必要です。デフォルトでは、ログが乱雑にならないように、主要な詳細とエラーのみをログに記録する必要がありますが、アプリケーションがクラッシュした場合は、可能な限りすべてをログに記録するモードが必要です。
他のアプリケーションと同様に、-v
フラグを使用してアプリに詳細レベルを取得させましょう。フラグなしは最小ロギング、-v
中間ロギング、および-vv
最大ロギングです。
これを行うために、Clapは、引数の値が発生する回数に設定されるようにする方法を提供します。これはまさにここで必要なことです。別のパラメーターを追加して、次のように設定できます。
#[clap(short、long、parse(from_occurrences))]冗長性:usize、
ここで、フラグを付けずに実行すると-v
、値はゼロになります。それ以外の場合は、-v
フラグが発生した回数をカウントします。
> cargo run -- test
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 0 }
> cargo run -- test -v
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 1 }
> cargo run -- test -vv
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 2 }
> cargo run -- -vv test -v
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 3 }
この値を使用すると、ロガーを簡単に初期化して、適切な量の詳細をログに記録させることができます。
この投稿は引数の解析に焦点を当てているため、ここではダミーのロガーコードを追加していませんが、最後のリポジトリにあります。
アプリケーションが正常に機能しているので、別の機能を追加します。それは、プロジェクトの一覧表示です。そうすれば、プロジェクトの素晴らしいリストが必要なときに、すぐにそれを取得できます。
Clapには、アプリに複数のサブコマンドを提供できる強力なサブコマンド機能があります。これを使用するには、サブコマンドとなる独自の引数を使用して別の構造体を定義します。メイン引数構造体には、すべてのサブコマンドに共通の引数が含まれ、次にサブコマンドが含まれます。
CLIを次のように構成します。
max_depth
パラメーターはメイン構造になりますprojects
コマンドは、オプションの開始パスを使用して検索を開始しますprojects
コマンドは、指定されたディレクトリをスキップするオプションの除外パスリストを取りますしたがって、カウントとプロジェクトの列挙型を次のように追加します。
use clap::{Parser, Subcommand};
...
#[derive(Subcommand, Debug)]
enum SubCommand {
/// Count how many times the package is used
Count {
#[clap(forbid_empty_values = true, validator = validate_package_name)]
/// Name of the package to search
package_name: String,
},
/// list all the projects
Projects {
#[clap(short, long, default_value_t = String::from("."),forbid_empty_values = true, validator = validate_package_name)]
/// directory to start exploring from
start_path: String,
#[clap(short, long, multiple_values = true)]
/// paths to exclude when searching
exclude: Vec<String>,
},
}
ここでは、をバリアントに移動package_name
し、バリアントにオプションCount
を追加します。start_pathexcludeProjects
ここで、ヘルプを確認すると、これらのサブコマンドの両方が一覧表示され、各サブコマンドには独自のヘルプがあります。
次に、それらに対応するためにmain関数を更新できます。
let args = Arguments::parse();
match args.cmd {
SubCommand::Count { package_name } => match count(&package_name, args.max_depth, &logger) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
},
SubCommand::Projects {
start_path,
exclude,
} => {/* TODO */}
}
count
以前のようにコマンドを使用して、使用回数をカウントすることもできます。
> cargo run -- -m 5 count test
max_depth
メイン構造体で定義されているようArguments
に、サブコマンドの前に指定する必要があります。
次に、必要に応じて、プロジェクトのコマンドの除外されたディレクトリに複数の値を指定できます。
> cargo run -- projects -e ./dir1 ./dir2
["./dir1", "./dir2"] # value of exclude vector
値をスペースで区切るのではなく、カスタム文字で区切る場合に備えて、カスタム区切り文字を設定することもできます。
#[clap(short, long, multiple_values = true, value_delimiter = ':')]
/// paths to exclude when searching
exclude: Vec<String>,
:
これで、値を区切るために使用できます。
> cargo run -- projects -e ./dir1:./dir2
["./dir1", "./dir2"]
これで、アプリケーションのCLIが完成しました。プロジェクトリスト関数はここには示されていませんが、自分で作成するか、GitHubリポジトリでコードを確認することができます。
Clapについて理解したので、プロジェクト用にクリーンでエレガントなCLIを作成できます。他にも多くの機能があり、プロジェクトでコマンドラインに特定の機能が必要な場合は、Clapにすでに機能がある可能性があります。
ClapのドキュメントとClapGitHubページをチェックして、Clapライブラリが提供するオプションの詳細を確認できます。
このプロジェクトのコードはここから入手することもできます。読んでくれてありがとう!
ソース:https ://blog.logrocket.com/command-line-argument-parsing-rust-using-clap/
1650431123
この記事では、Rustアプリケーションに渡されたコマンドライン引数を手動で解析する方法、手動解析が大規模なアプリに適していない理由、およびClapライブラリがこれらの問題の解決にどのように役立つかを説明します。
注意として、変数宣言、if-elseブロック、ループ、構造体などの基本的なRustの読み取りと書き込みに慣れている必要があります。
たとえば、ノードベースのプロジェクトが多数あるprojectsフォルダーがあり、「依存関係パッケージを含むすべてのパッケージのうち、どれを何回使用したか」を知りたいとします。
結局のところ、その合計1GBは、node_modules
すべて固有の依存関係になるわけではありませんね😰…?
プロジェクトでパッケージを使用する回数をカウントする素敵な小さなプログラムを作成したらどうなるでしょうか。
cargo new package-hunter
これを行うには、Rustでプロジェクトを設定しましょう。これで、src/main.rs
ファイルにデフォルトのメイン機能が追加されました。
fn main() {
println!("Hello, world!");
}
次のステップは非常に単純なようです。アプリケーションに渡す引数を取得します。したがって、後で他の引数を抽出するための別の関数を記述します。
fn get_arguments() {
let args: Vec<_> = std::env::args().collect(); // get all arguements passed to app
println!("{:?}", args);
}
fn main() {
get_arguments();
}
それを実行すると、エラーやパニックなしで、素晴らしい出力が得られます。
# anything after '--' is passed to your app, not to cargo
> cargo run -- svelte
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/package-hunter svelte`
["target/debug/package-hunter", "svelte"]
もちろん、最初の引数はアプリケーションを呼び出したコマンドであり、2番目の引数はアプリケーションに渡されたものです。かなり簡単なようです。
これで、名前を取得するカウント関数を作成し、サブディレクトリでその名前のディレクトリをカウントすることができます。
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
/// Not the dracula
fn count(name: &str) -> std::io::Result<usize> {
let mut count = 0;
// queue to store next dirs to explore
let mut queue = VecDeque::new();
// start with current dir
queue.push_back(PathBuf::from("."));
loop {
if queue.is_empty() {
break;
}
let path = queue.pop_back().unwrap();
for dir in fs::read_dir(path)? {
// the for loop var 'dir' is actually a result, so we convert it
// to the actual dir struct using ? here
let dir = dir?;
// consider it only if it is a directory
if dir.file_type()?.is_dir() {
if dir.file_name() == name {
// we have a match, so stop exploring further
count += 1;
} else {
// not a match so check its sub-dirs
queue.push_back(dir.path());
}
}
}
}
return Ok(count);
}
get_arguments
コマンドの後の最初の引数を返すようにを更新し、main
では、その引数を使用して呼び出しcount
ます。
これをプロジェクトフォルダーの1つで実行すると、予期せず完全に機能し、1つのプロジェクトに依存関係が1回だけ含まれるため、カウントが1として返されます。
ここで、ディレクトリを上に移動して実行しようとすると、問題が発生します。通過するディレクトリが増えるため、少し時間がかかります。
理想的には、プロジェクトディレクトリのルートから実行して、その依存関係を持つすべてのプロジェクトを見つけることができますが、これにはさらに時間がかかります。
そのため、妥協して、特定の深さまでディレクトリを探索することにしました。ディレクトリの深さが指定された深さよりも大きい場合、それは無視されます。関数に別のパラメーターを追加し、それを更新して深さを考慮することができます。
/// Not the dracula
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
...
queue.push_back((PathBuf::from("."), 0));
...
let (path, crr_depth) = queue.pop_back().unwrap();
if crr_depth > max_depth {
continue;
}
...
// not a match so check its sub-dirs
queue.push_back((dir.path(), crr_depth + 1));
...
}
これで、アプリケーションは2つのパラメーターを受け取ります。最初にパッケージ名、次に探索する最大深度です。
ただし、深さはオプションの引数にする必要があるため、指定しない場合はすべてのサブディレクトリを探索し、指定しない場合は指定した深さで停止します。
このために、get_arguments
関数を更新して2番目の引数をオプションにすることができます。
fn get_arguments() {
let args: Vec<_> = std::env::args().collect();
let mdepth = if args.len() > 2 {
args[2].parse().unwrap()
} else {
usize::MAX
};
println!("{:?}", count(&args[1], mdepth));
}
これにより、両方の方法で実行でき、次のように機能します。
> cargo run -- svelte
> cargo run -- svelte 5
残念ながら、これはあまり柔軟ではありません。のように引数を逆の順序で指定するとcargo run 5 package-name
、アプリケーションは数値として解析しようとしてクラッシュしますpackage-name
。
さて、引数に独自のフラグを持たせたい場合があります-f
。-d
たとえば、任意の順序で引数を指定できます。(フラグのボーナスUnixポイントも!)
もう一度関数を更新し、get_arguments
今回は引数に適切な構造体を追加するので、解析された引数を返すのが簡単になります。
#[derive(Default)]
struct Arguments {
package_name: String,
max_depth: usize,
}
fn get_arguments() -> Arguments {
let args: Vec<_> = std::env::args().collect();
// atleast 3 args should be there : the command name, the -f flag,
// and the actual file name
if args.len() < 3 {
eprintln!("filename is a required argument");
std::process::exit(1);
}
let mut ret = Arguments::default();
ret.max_depth = usize::MAX;
if args[1] == "-f" {
// it is file
ret.package_name = args[2].clone();
} else {
// it is max depth
ret.max_depth = args[2].parse().unwrap();
}
// now that one argument is parsed, time for seconds
if args.len() > 4 {
if args[3] == "-f" {
ret.package_name = args[4].clone();
} else {
ret.max_depth = args[4].parse().unwrap();
}
}
return ret;
}
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
...
}
fn main() {
let args = get_arguments();
match count(&args.package_name, args.max_depth) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
}
}
これで、またはのような派手な-
フラグを使用して実行できます。cargo run -- -f sveltecargo run -- -d 5 -f svelte
ただし、これにはかなり深刻なバグがいくつかあります。同じ引数を2回指定して、ファイル引数を完全cargo run -- -d 5 -d 7
にスキップするか、無効なフラグを指定すると、エラーメッセージなしで実行されます😭。
file_name
上記の行でが空でないことを確認し、27
誤った値が指定された場合に予想される内容を出力することで、これを修正できます。-d
ただし、を直接呼び出すため、に非数値を渡すと、これもクラッシュunwrap
しparse
ます。
また、このアプリケーションはヘルプ情報を提供しないため、新規ユーザーにとっては扱いにくい場合があります。ユーザーは、どの引数がどの順序で渡されるかわからない場合があり、アプリケーションには、-h
従来のUnixプログラムのように、その情報を表示するためのフラグがありません。
これらはこの特定のアプリにとってはほんの少しの不便ですが、複雑さが増すにつれてオプションの数が増えるにつれて、これらすべてを手動で維持することがますます難しくなります。
これがクラップの出番です。
-h
Clapは、引数の解析ロジックを生成する機能を提供するライブラリであり、引数の説明やヘルプコマンドなど、アプリケーション用のきちんとしたCLIを提供します。
Clapの使用は非常に簡単で、現在の設定にわずかな変更を加えるだけで済みます。
Clapには、多くのRustプロジェクトで使用される2つの一般的なバージョンがあります。V2とV3です。V2は主に、コマンドライン引数パーサーを構築するためのビルダーベースの実装を提供します。
V3は最近のリリース(執筆時点)であり、ビルダーの実装とともにderive
proc-macrosが追加されているため、構造体に注釈を付けることができ、マクロは必要な関数を派生させます。
これらには両方とも独自の利点があり、より詳細な違いと機能のリストについては、ドキュメントとヘルプページを確認してください。これらのドキュメントとヘルプページには、例が示され、どの状況が派生し、ビルダーが適しているかが示されています。
この投稿では、proc-macroでClapV3を使用する方法を説明します。
Clapをプロジェクトに組み込むには、以下をプロジェクトに追加しますCargo.toml
。
[dependencies]
clap = { version = "3.1.6", features = ["derive"] }
これにより、派生機能との依存関係としてClapが追加されます。
get_arguments
それでは、関数とその呼び出しをmain
:から削除しましょう。
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
#[derive(Default)]
struct Arguments {
package_name: String,
max_depth: usize,
}
/// Not the dracula
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
let mut count = 0;
// queue to store next dirs to explore
let mut queue = VecDeque::new();
// start with current dir
queue.push_back((PathBuf::from("."), 0));
loop {
if queue.is_empty() {
break;
}
let (path, crr_depth) = queue.pop_back().unwrap();
if crr_depth > max_depth {
continue;
}
for dir in fs::read_dir(path)? {
let dir = dir?;
// we are concerned only if it is a directory
if dir.file_type()?.is_dir() {
if dir.file_name() == name {
// we have a match, so stop exploring further
count += 1;
} else {
// not a match so check its sub-dirs
queue.push_back((dir.path(), crr_depth + 1));
}
}
}
}
return Ok(count);
}
fn main() {}
次に、derive
構造Arguments
体に追加Parser
してDebug
:
use clap::Parser;
#[derive(Parser,Default,Debug)]
struct Arguments {...}
最後に、でmain
、parseメソッドを呼び出します。
let args = Arguments::parse();
println!("{:?}", args);
cargo run
引数なしでアプリケーションを実行すると、エラーメッセージが表示されます。
error: The following required arguments were not provided:
<PACKAGE_NAME>
<MAX_DEPTH>
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
For more information try --help
これは、手動バージョンよりも優れたエラー報告です。
また、ボーナスとして、-h
引数とその順序を出力できるヘルプのフラグが自動的に提供されます。
package-hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME>
<MAX_DEPTH>
OPTIONS:
-h, --help Print help information
そして今、に数字以外のものを提供するMAX_DEPTH
と、提供された文字列が数字ではないというエラーが発生します。
> cargo run -- 5 test
error: Invalid value "test" for '<MAX_DEPTH>': invalid digit found in string
For more information try --help
それらを正しい順序で提供すると、次の出力が得られますprintln
。
> cargo run -- test 5
Arguments { package_name: "test", max_depth: 5 }
これらすべてに2行の新しい行があり、解析コードやエラー処理を記述する必要はありません。🎉
現在、ヘルプメッセージは引数の名前と順序のみを示しているため、少し当たり障りのないものです。ユーザーが特定の引数の意味を理解できれば、エラーを報告したい場合はアプリケーションのバージョンでさえも役立つでしょう。
クラップは、このためのオプションも提供します。
#[derive(...)]
#[clap(author="Author Name", version, about="A Very simple Package Hunter")]
struct Arguments{...}
これで、-h
出力にすべての詳細が表示-V
され、バージョン番号を出力するためのフラグも提供されます。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME>
<MAX_DEPTH>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
マクロ自体に情報に関する複数の行を書き込むのは少し面倒な場合があるため、代わりに、構造体に使用するドキュメントコメントを追加する///
と、マクロはそれを情報として使用します(両方が存在する場合は、1つマクロ内はドキュメントコメントよりも優先されます):
#[clap(author = "Author Name", version, about)]
/// A Very simple Package Hunter
struct Arguments {...}
これにより、以前と同じヘルプが提供されます。
引数に関する情報を追加するために、引数自体に同様のコメントを追加できます。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME> Name of the package to search
<MAX_DEPTH> maximum depth to which sub-directories should be explored
OPTIONS:
-h, --help Print help information
-V, --version Print version information
これははるかに役立ちます!
-f
ここで、引数フラグ(および-d
)やオプションのdepth引数の設定など、他の機能を復活させましょう。
Clapを使用すると、フラグ引数が途方もなく単純になります。構造体メンバーに。を使用して別のClapマクロアノテーションを追加するだけ#[clap(short, long)]
です。
ここで、short
は、などのフラグの短縮バージョンを-f
指しlong
、はなどの完全なバージョンを指し--file
ます。どちらかまたは両方を選択できます。この追加により、次のようになります。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter --package-name <PACKAGE_NAME> --max-depth <MAX_DEPTH>
OPTIONS:
-h, --help Print help information
-m, --max-depth <MAX_DEPTH> maximum depth to which sub-directories should be explored
-p, --package-name <PACKAGE_NAME> Name of the package to search
-V, --version Print version information
両方の引数にフラグが付いているため、位置引数はありません。cargo run -- test 5
これは、Clapがフラグを探し、引数が提供されていないというエラーを出すため、実行できないことを意味します。
cargo run -- -p test -m 5
代わりに、またはを実行するcargo run -- -m 5 -p test
と、両方が正しく解析され、次の出力が得られます。
Arguments { package_name: "test", max_depth: 5 }
パッケージ名は常に必要なので、位置引数にすることができ、-p
毎回フラグを入力する必要はありません。
これを行うには、を削除し#[clap(short,long)]
ます。これで、フラグのない最初の引数は次のように見なされpackage name
ます。
> cargo run -- test -m 5
Arguments { package_name: "test", max_depth: 5 }
> cargo run -- -m 5 test
Arguments { package_name: "test", max_depth: 5 }
省略引数で注意すべきことの1つは、2つの引数が同じ文字で始まり(つまり、package-name
およびpath
)、両方で短いフラグが有効になっている場合、アプリケーションはデバッグビルドの実行時にクラッシュし、リリースビルドの混乱を招くエラーメッセージを表示することです。 。
したがって、次のいずれかを確認してください。
short
フラグがあります次のステップは、max_depth
オプションにすることです。
引数をオプションとしてマークするには、その引数の型を作成します。Option<T>
ここT
で、は元の型引数です。したがって、この場合、次のようになります。
#[clap(short, long)]
/// maximum depth to which sub-directories should be explored
max_depth: Option<usize>,
これでうまくいくはずです。変更はヘルプにも反映され、必須の引数として最大深度がリストされていません。
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter [OPTIONS] <PACKAGE_NAME>
ARGS:
<PACKAGE_NAME> Name of the package to search
OPTIONS:
-h, --help Print help information
-m, --max-depth <MAX_DEPTH> maximum depth to which sub-directories should be explored
-V, --version Print version information
そして、-m
フラグを付けずに実行できます。
> cargo run -- test
Arguments { package_name: "test", max_depth: None }
しかし、これはまだ少し面倒です。ここで、を実行する必要があります。実行match
するmax_depth
場合はNone
、以前と同じように設定しusize::MAX
ます。
しかし、拍手はここでも私たちのために何かを持っています!作成する代わりにOption<T>
、引数が指定されていない場合はデフォルト値を設定できます。
したがって、次のように変更した後:
#[clap(default_value_t=usize::MAX,short, long)]
/// maximum depth to which sub-directories should be explored
max_depth: usize,
の値を指定して、または指定せずにアプリケーションを実行できますmax_depth
(の最大値はusize
システム構成によって異なります)。
> cargo run -- test
Arguments { package_name: "test", max_depth: 18446744073709551615 }
> cargo run -- test -m 5
Arguments { package_name: "test", max_depth: 5 }
main
それでは、前と同じようにカウント関数に接続しましょう。
fn main() {
let args = Arguments::parse();
match count(&args.package_name, args.max_depth) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
}
}
これにより、元の機能が復活しましたが、コードが大幅に減り、いくつかの追加機能が追加されました。
はpackage-hunter
期待どおりに機能していますが、残念ながら、手動の解析段階から存在し、Clapベースのバージョンに持ち込まれた微妙なバグがあります。あなたはそれが何であるかを推測できますか?
小さな小さなアプリにとってはそれほど危険なバグではありませんが、他のアプリケーションにとってはアキレス腱になる可能性があります。私たちの場合、エラーが発生したときに誤った結果が返されます。
次を実行してみてください:
> cargo run -- ""
0 uses found
ここではpackage_name
、空のパッケージ名を許可しない場合に、が空の文字列として渡されます。これは、コマンドを実行するシェルが引数をアプリに渡す方法が原因で発生します。
通常、シェルはスペースを使用してプログラムに渡される引数リストを分割するため、、、、およびの3つのabc def hij
個別の引数として指定されます。abcdefhij
引数にスペースを含めたい場合は、のように引用符で囲む必要があります"abc efg hij"
。このようにして、シェルはこれが単一の引数であることを認識し、そのように渡します。
一方、これにより、空の文字列またはスペースのみの文字列をアプリに渡すこともできます。もう一度、拍手して救助してください!引数の空の値を拒否する方法を提供します。
#[clap(forbid_empty_values = true)]
/// Name of the package to search
package_name: String,
これで、引数として空の文字列を指定しようとすると、エラーが発生します。
> cargo run -- ""
error: The argument '<PACKAGE_NAME>' requires a value but none was supplied
ただし、これでもパッケージ名としてスペースが提供されます。つまり""
、有効な引数です。これを修正するには、名前に先頭または末尾のスペースがあるかどうかを確認し、含まれている場合は拒否するカスタムバリデーターを提供する必要があります。
検証関数を次のように定義します。
fn validate_package_name(name: &str) -> Result<(), String> {
if name.trim().len() != name.len() {
Err(String::from(
"package name cannot have leading and trailing space",
))
} else {
Ok(())
}
}
次に、次のように設定しpackage_name
ます。
#[clap(forbid_empty_values = true, validator = validate_package_name)]
/// Name of the package to search
package_name: String,
ここで、空の文字列またはスペースを含む文字列を渡そうとすると、次のようにエラーが発生します。
> cargo run -- ""
error: The argument '<PACKAGE_NAME>' requires a value but none was supplied
> cargo run -- " "
error: Invalid value " " for '<PACKAGE_NAME>': package name cannot have leading and trailing space
このようにして、解析用のすべてのコードを記述せずに、カスタムロジックを使用して引数を検証できます。
アプリケーションは現在正常に動作していますが、動作しなかった場合に何が起こったかを確認する方法はありません。そのためには、アプリケーションがクラッシュしたときに何が起こったかを確認するために、アプリケーションが実行していることのログを保持する必要があります。
他のコマンドラインアプリケーションと同様に、ユーザーがログのレベルを簡単に設定できる方法が必要です。デフォルトでは、ログが乱雑にならないように、主要な詳細とエラーのみをログに記録する必要がありますが、アプリケーションがクラッシュした場合は、可能な限りすべてをログに記録するモードが必要です。
他のアプリケーションと同様に、-v
フラグを使用してアプリに詳細レベルを取得させましょう。フラグなしは最小ロギング、-v
中間ロギング、および-vv
最大ロギングです。
これを行うために、Clapは、引数の値が発生する回数に設定されるようにする方法を提供します。これはまさにここで必要なことです。別のパラメーターを追加して、次のように設定できます。
#[clap(short、long、parse(from_occurrences))]冗長性:usize、
ここで、フラグを付けずに実行すると-v
、値はゼロになります。それ以外の場合は、-v
フラグが発生した回数をカウントします。
> cargo run -- test
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 0 }
> cargo run -- test -v
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 1 }
> cargo run -- test -vv
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 2 }
> cargo run -- -vv test -v
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 3 }
この値を使用すると、ロガーを簡単に初期化して、適切な量の詳細をログに記録させることができます。
この投稿は引数の解析に焦点を当てているため、ここではダミーのロガーコードを追加していませんが、最後のリポジトリにあります。
アプリケーションが正常に機能しているので、別の機能を追加します。それは、プロジェクトの一覧表示です。そうすれば、プロジェクトの素晴らしいリストが必要なときに、すぐにそれを取得できます。
Clapには、アプリに複数のサブコマンドを提供できる強力なサブコマンド機能があります。これを使用するには、サブコマンドとなる独自の引数を使用して別の構造体を定義します。メイン引数構造体には、すべてのサブコマンドに共通の引数が含まれ、次にサブコマンドが含まれます。
CLIを次のように構成します。
max_depth
パラメーターはメイン構造になりますprojects
コマンドは、オプションの開始パスを使用して検索を開始しますprojects
コマンドは、指定されたディレクトリをスキップするオプションの除外パスリストを取りますしたがって、カウントとプロジェクトの列挙型を次のように追加します。
use clap::{Parser, Subcommand};
...
#[derive(Subcommand, Debug)]
enum SubCommand {
/// Count how many times the package is used
Count {
#[clap(forbid_empty_values = true, validator = validate_package_name)]
/// Name of the package to search
package_name: String,
},
/// list all the projects
Projects {
#[clap(short, long, default_value_t = String::from("."),forbid_empty_values = true, validator = validate_package_name)]
/// directory to start exploring from
start_path: String,
#[clap(short, long, multiple_values = true)]
/// paths to exclude when searching
exclude: Vec<String>,
},
}
ここでは、をバリアントに移動package_name
し、バリアントにオプションCount
を追加します。start_pathexcludeProjects
ここで、ヘルプを確認すると、これらのサブコマンドの両方が一覧表示され、各サブコマンドには独自のヘルプがあります。
次に、それらに対応するためにmain関数を更新できます。
let args = Arguments::parse();
match args.cmd {
SubCommand::Count { package_name } => match count(&package_name, args.max_depth, &logger) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
},
SubCommand::Projects {
start_path,
exclude,
} => {/* TODO */}
}
count
以前のようにコマンドを使用して、使用回数をカウントすることもできます。
> cargo run -- -m 5 count test
max_depth
メイン構造体で定義されているようArguments
に、サブコマンドの前に指定する必要があります。
次に、必要に応じて、プロジェクトのコマンドの除外されたディレクトリに複数の値を指定できます。
> cargo run -- projects -e ./dir1 ./dir2
["./dir1", "./dir2"] # value of exclude vector
値をスペースで区切るのではなく、カスタム文字で区切る場合に備えて、カスタム区切り文字を設定することもできます。
#[clap(short, long, multiple_values = true, value_delimiter = ':')]
/// paths to exclude when searching
exclude: Vec<String>,
:
これで、値を区切るために使用できます。
> cargo run -- projects -e ./dir1:./dir2
["./dir1", "./dir2"]
これで、アプリケーションのCLIが完成しました。プロジェクトリスト関数はここには示されていませんが、自分で作成するか、GitHubリポジトリでコードを確認することができます。
Clapについて理解したので、プロジェクト用にクリーンでエレガントなCLIを作成できます。他にも多くの機能があり、プロジェクトでコマンドラインに特定の機能が必要な場合は、Clapにすでに機能がある可能性があります。
ClapのドキュメントとClapGitHubページをチェックして、Clapライブラリが提供するオプションの詳細を確認できます。
このプロジェクトのコードはここから入手することもできます。読んでくれてありがとう!
ソース:https ://blog.logrocket.com/command-line-argument-parsing-rust-using-clap/