Rust製CLI作成支援パッケージSeahorseの紹介

めでたくメジャーバージョンがリリースされたので、 僕が最近ちょっとだけコミットしたRustのパッケージの紹介をします。

seahorse_logo

Seahorse

github.com

Seahorseは、RustでCLIツールを制作するときに助けになるパッケージです。
CLIツールを作る人がSeahorseを使うことで、以下のような処理を書くのが容易になります。

  1. ツールのメタ情報の設定
  2. サブコマンドを用いた処理の分岐
  3. コマンドライン引数の設定と、与えられた引数の適切な型へのパース

下にそれぞれの使い方を簡単に説明します。

① ツールのメタ情報の設定

とは

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 等を実行するときの statuslog がサブコマンドにあたります。

f:id:rnitta:20200609165605p:plain

実装例

さて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

f:id:rnitta:20200604131827p:plain

マクロで書くこともできるし、メソッドスタイルでも書くことができるし、yamlでも設定が書ける...多機能ですが
ちょっとプロジェクトが大きくてエラーに当たったときに中身を読みに行くのがいやになってしまいますね。

一方でseahorseはREADMEを1分眺めれば大半の機能は把握できます。

  • メソッドチェーン的なスタイルでメタ情報が設定できて、
  • サブコマンドを設定することで複数の機能を1ツールにできて、
  • コマンドライン引数の値が特定の型にパースしやすくなる。

v1.0.0時点で提供されている機能はこんなところなはずです。 コード量もかなり控えめです。

あなたが仮にgitとかffmpegぐらいサブコマンドやオプションの多く複雑なツールを作ろうとしているならSeahorseでは力不足かもしれませんが、 多くのCLIツールではSeahorseで十分なはずです。

Any Contributions are Welcome

ちょっとしたCLIツールをRustで作る時に使ってみたり 使用方法をより詳細に説明するポストを書いてみたり バグをissueに投げてみたり 新機能を提案して実装してみたり リポジトリにStarしてみたり おまちしております

github.com

Seahorseを使ってつくってみたもの

お前らのコミットは汚い - Qiita

github.com

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秒ぐらいでフェードアウトできる。^^

記述量が少なくて済むのでやる気ないときにオススメ

std::any::type_name はバッチリ型名を返してくれるわけではなさそう

qiita.com

こちらを見て本当にクロージャかなと色々試していて気づいたこと
(記事の内容はあんまり関係ありません)

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

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a16e3e2ec6b2e41aad376ec378b580b3

また、関数を渡すと関数名が返ってきます。

print_typeof(&main); // => playground::main

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2321f4d670ddb136afd2d021ac578b96

この挙動が紛らわしく、

#[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
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=576a8f71ef69b2bdb49d08c06ae083d9

この挙動の理解に時間がかかってしまったというお話でした。 タプル構造体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から f:id:rnitta:20200314200605p:plain Export Project にチェックを付けて Build

Gradle プロジェクトが吐かれるので、 Android Studio等からそのプロジェクトを開いて f:id:rnitta:20200314200343p:plain

AndroidつないでRunしたら

f:id:rnitta:20200314200452p:plain

オラオラヨイショ。アザシタ。

UIWidget 非同期処理をDebounceする

状況

  • UniRx
  • UIWidgets

f:id:rnitta:20200217133335p:plain

こういった文字列検索フォームを作っていて、
検索ロジックは「サーバーに検索文字列をPOSTしてそのレスポンスを反映する」というものでローカルで完結しない。

サーバーにアクセスする都合上、(大きな問題にはならないと思うが)TextField のonChangeごとにPOSTを投げていたのでは、 リクエスト数が膨大になってしまうし、しかも入力途中に発生するPOSTは完全に無駄になってしまうのでイベントを間引きしたい。

debounce とは、用語として正しいのかしらないけどjqueryのコレ からきている。
一定時間実行をpendingにして最後のイベントのみを実行する ということを指しています(ぼくは。)

解決方法

汎用的な正攻法は多分 CancellationToken みたいなもので正しく非同期処理をキャンセルすること だと思うが、

今回はこれで事足りたよ、という解決法

Widget

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する前と、画面反映する前に「本当に処理継続していいっすか?」という判定ロジックをいれて解決解決。