Rust製CLI作成支援パッケージSeahorseの紹介
めでたくメジャーバージョンがリリースされたので、 僕が最近ちょっとだけコミットしたRustのパッケージの紹介をします。
Seahorse
Seahorseは、RustでCLIツールを制作するときに助けになるパッケージです。
CLIツールを作る人がSeahorseを使うことで、以下のような処理を書くのが容易になります。
- ツールのメタ情報の設定
- サブコマンドを用いた処理の分岐
- コマンドライン引数の設定と、与えられた引数の適切な型へのパース
下にそれぞれの使い方を簡単に説明します。
① ツールのメタ情報の設定
とは
CLIツールとしてのメタ情報(名前や使い方や説明など、ヘルプテキストに表示されるもの;ヘルプテキストとは典型的には $ <コマンド> --help
で出力されるもの )を設定することができます。
たとえば、$ cargo
を実行すると
$ cargo Rust's package manager USAGE: cargo [OPTIONS] [SUBCOMMAND] OPTIONS: -V, --version Print version info and exit --list List installed commands --explain <CODE> Run `rustc --explain CODE` -v, --verbose Use verbose output (-vv very verbose/build.rs output) -q, --quiet No output printed to stdout --color <WHEN> Coloring: auto, always, never --frozen Require Cargo.lock and cache are up to date --locked Require Cargo.lock is up to date --offline Run without accessing the network -Z <FLAG>... Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details -h, --help Prints help information
こんな印字がされます。
自分でCLIツールを作る際に、これはどのように実装するでしょうか。
println!("Rust's package manager\n"); println!("USAGE:"); println!(" cargo [OPTIONS] [SUBCOMMAND]\n"); . . .
死にたくなりますね。
実装例
Seahorseを使うとこんなふうに書けます。
fn main() { let args: Vec<String> = env::args().collect(); let app = App::new("cargo") .description("Rust's package manager") .usage("cargo [OPTIONS] [SUBCOMMAND]") .flag( Flag::new("version", FlagType::Bool) .alias("v") .usage("-v, --version: Print version info and exit"), ) .flag( Flag::new("list", FlagType::Bool) .usage("--list: List installed commands"), ); app.run(args); }
これを実行してヘルプテキストを表示させると、
Name cargo Description: Rust's package manager Usage: cargo [OPTIONS] [SUBCOMMAND] -v, --version: Print version info and exit --list: List installed commands
こんなのが出力されます。
ヘルプテキストを印字するにしては過度な抽象化だと思うかもしれませんが、
③に書くようにSeahorseでフラグを設定すると引数をパースするのが簡単になるという明確なメリットがあります。
ヘルプテキストが生成できる、というのはどちらかというと副次的な機能です。
② サブコマンドを用いた処理の分岐
とは
メインコマンドの後に続くコマンドがサブコマンドです。
gitを例にとって説明しますと、
$ git
をメインコマンドとすると、$ git status
や $ git log
等を実行するときの status
や log
がサブコマンドにあたります。
実装例
さてSeahorseで、$ git status
と $ git log
を受け付けて適当な印字をするアプリケーションを作ってみましょう。
fn status_action(c: &Context) { println!("status..."); } fn log_action(c: &Context) { println!("log..."); } fn main() { let args: Vec<String> = std::env::args().collect(); let app = App::new("gitではないもの") .command(Command::new("log").action(log_action)) .command(Command::new("status").alias("st").action(status_action)); app.run(args); }
サブコマンドとはこういう使い方をするもの、という説明をするためだけに書いたので無能なgitが出来上がりました。
これでビルドすれば $ <メインコマンド> log
も $ <メインコマンド> status
も $ <メインコマンド> st
も受け付けるCLIツールができます。
③ コマンドライン引数の設定と、与えられた引数の適切な型へのパース
とは
$ git log --author rnitta -n 10
というコマンドを打ったときに、
これらのスペースで区切られた文字列群はRustの世界からは std::env::args()
で取得することができます。非常に便利な関数ですね。
std::env::args().collect::<Vec<String>>()
とすれば、["git", "log", "--author", "rnitta", "-n", "10"]
のようなVecが得られます。
これをパースして取り出しやすくしてくれる仕組みがあります。
実装例
fn main() { let args: Vec<String> = std::env::args().collect(); let app = App::new("git").command( Command::new("log") .usage("git log <options>") .action(log_action) .flag(Flag::new("author", FlagType::String).usage("git log --author <author_name>")) .flag( Flag::new("number", FlagType::Int) .usage("git log --number(-n) <n>") .alias("n"), ), ); app.run(args); } fn log_action(c: &Context) { if let Ok(author) = c.string_flag("author") { println!("Author is {}", author); } if let Ok(number) = c.int_flag("number") { // => ちゃんとisizeにパースしてくれます! println!("Number is {}", number); } }
$ cargo run -- log --author rnitta -n 2 Author is rnitta Number is 2
コマンドライン引数を適当に抽象化してくれて便利ですね。
Seahorseの特徴
Seahorseは機能がミニマルなところが特徴的だと思います。
ミニマル はともすれば「機能が少ないだけでしょ」「実装サボってるだけでしょ」と思うかもしれませんが、 ミニマルはミニマルでいいところもあります。 それは、できることが多くない代わりに学習コストが低いことです。
同じCLI作成支援パッケージであるclapの使い方を調べてみましょう。
GitHub - clap-rs/clap: A full featured, fast Command Line Argument Parser for Rust
マクロで書くこともできるし、メソッドスタイルでも書くことができるし、yamlでも設定が書ける...多機能ですが
ちょっとプロジェクトが大きくてエラーに当たったときに中身を読みに行くのがいやになってしまいますね。
一方でseahorseはREADMEを1分眺めれば大半の機能は把握できます。
- メソッドチェーン的なスタイルでメタ情報が設定できて、
- サブコマンドを設定することで複数の機能を1ツールにできて、
- コマンドライン引数の値が特定の型にパースしやすくなる。
v1.0.0時点で提供されている機能はこんなところなはずです。 コード量もかなり控えめです。
あなたが仮にgitとかffmpegぐらいサブコマンドやオプションの多く複雑なツールを作ろうとしているならSeahorseでは力不足かもしれませんが、 多くのCLIツールではSeahorseで十分なはずです。
Any Contributions are Welcome
ちょっとしたCLIツールをRustで作る時に使ってみたり 使用方法をより詳細に説明するポストを書いてみたり バグをissueに投げてみたり 新機能を提案して実装してみたり リポジトリにStarしてみたり おまちしております
Seahorseを使ってつくってみたもの
Unity 適当にアニメーションする
FlutterでいうところのTweenみたいのを簡単につくる
UnityのGUIでAnimation作って、AnimationControllerで云々とかやりたくない人(僕)向け
private IEnumerator fadeout() { for (int i = 1; i < 101; i++) { yield return null; DetailPanel.alpha = (100 - i) / 100f; } }
StartCoroutine(fadeout())
🤔🤔🤔🤔🤔
60fpsなら100フレ=1.5秒ぐらいでフェードアウトできる。^^
記述量が少なくて済むのでやる気ないときにオススメ
タプル構造体を構造体のように扱うことはできても逆はできない
#[derive(Debug)] struct Data(i32); fn main() { println!("{:?}", Data{ 0: 0 }); // => Data(0) }
これは合法なのに
#[derive(Debug)] struct Data2 { 0: i32 } fn main() { println!("{:?}", Data2(4)); }
これはコンパイルエラーになってしまう
というだけのアレ
std::any::type_name はバッチリ型名を返してくれるわけではなさそう
こちらを見て本当にクロージャかなと色々試していて気づいたこと
(記事の内容はあんまり関係ありません)
std::any::type_name::<T>()
は型引数の名前を返してくれます。
のでこんな形で値を渡すことで、その値の型が(文字列で)得られます
fn print_typeof<T>(_: &T) { println!("{}", std::any::type_name::<T>()); } fn main() { print_typeof(&0u8); // => u8 }
が、そんなに気の利いた関数ではないらしく、
型に別名つけてても無視されます
type NewType = u8; println!("{}", std::any::type_name::<NewType>()); // => u8
また、関数を渡すと関数名が返ってきます。
print_typeof(&main); // => playground::main
この挙動が紛らわしく、
#[derive(Debug)] struct Data(i32); fn print_type_of<T>(x: T) { println!("{}", std::any::type_name::<T>()); } fn main() { print_type_of(Data); // => Playground::Data }
この挙動の理解に時間がかかってしまったというお話でした。 タプル構造体Dataのコンストラクタの関数の型はDataなのか????と思ってしまったというお話
docker-composeで一つのコンテナ(サービス)だけをno-cacheでビルドする
一つのコンテナをビルドするだけなら
$ docker-compose build worker
キャッシュなしで全てビルドするなら
$ docker-compose build --no-cache
じゃあ一つのコンテナをキャッシュなしでビルドするには
$ docekr-compose build worker --no-cache
や! と思って実行してみたら
docker-compose build worker --no-cache ERROR: No such service: --no-cache
面白い冗談ですね。
コマンドラインオプションのパースに失敗している感があります。
正しくは
$ docker-compose build --no-cache worker
でした。おしり。
Unity Android実機デバッグをする
実務経験としてはiOS -> Flutter -> Unity とたどってきたため、 FlutterやUnityでAndroidアプリは作ったことがあるものの、 Androidなーんもわからん状態なのでAndroidの実機デバッグをどうやればいいか全くわからんマンであった。
今日Android実機のみで発現する難解なバグに遭遇し、Android実機デバッグを強いられ、ようやっと実機の実行ログを見る方法を会得したのでメモしておきます (monitor でログ見れます みたいな古い記事を信じて時間を無駄にしてしまったため)
UnityのBuild Settingsから Export Project にチェックを付けて Build
Gradle プロジェクトが吐かれるので、 Android Studio等からそのプロジェクトを開いて
AndroidつないでRunしたら
オラオラヨイショ。アザシタ。
UIWidget 非同期処理をDebounceする
状況
- UniRx
- UIWidgets
こういった文字列検索フォームを作っていて、
検索ロジックは「サーバーに検索文字列をPOSTしてそのレスポンスを反映する」というものでローカルで完結しない。
サーバーにアクセスする都合上、(大きな問題にはならないと思うが)TextField
のonChangeごとにPOSTを投げていたのでは、
リクエスト数が膨大になってしまうし、しかも入力途中に発生するPOSTは完全に無駄になってしまうのでイベントを間引きしたい。
debounce とは、用語として正しいのかしらないけどjqueryのコレ からきている。
一定時間実行をpendingにして最後のイベントのみを実行する ということを指しています(ぼくは。)
解決方法
汎用的な正攻法は多分 CancellationToken
みたいなもので正しく非同期処理をキャンセルすること だと思うが、
今回はこれで事足りたよ、という解決法
private TextEditingController _searchTextController = new TextEditingController(); public override Widget build(BuildContext context) { return new TextField( controller: _searchTextController, onChanged: (string val) => { SearchingTextSubject.OnNext(val); }, ); }
private async UniTask fetchSearchResult() { string searchTextMemo = this.SearchingText; // 後で画面反映をキャンセルするかどうかを判定するための素敵なフラグ await Task.Delay(200); // 200msのdebounce if (searchTextMemo != this.SearchingText) return; // 検索中に入力が変わってしまったので、画面反映をキャンセルする WWWForm form = new WWWForm(); form.AddField("q", this.SearchingText); using (UnityWebRequest www = UnityWebRequest.Post(Constant.API_DOMAIN + "/api/videos/search", form)) { var res = await www.SendWebRequest(); if (searchTextMemo != this.SearchingText) return; // 検索中に入力が変わってしまったので、画面反映をキャンセルする if (res.isNetworkError || res.isHttpError || res.responseCode != 200) { using (WindowProvider.of(context).getScope()) { // エラーレスポンスを画面に反映する処理 } } else { // 正常レスポンスをデシリアライズして画面に反映する処理 } } }
POSTする前と、画面反映する前に「本当に処理継続していいっすか?」という判定ロジックをいれて解決解決。