RustのCLIフレームワークを作ってみた

# 作った経緯
RustのCLIフレームワークは[clap](https://github.com/clap-rs/clap)が有名ですが機能が多く、そこまでの機能は必要じゃないけどCLIを簡単に構築できるライブラリをRustの勉強がてら作ってみました。

# 作ったCLIフレームワーク
[![](https://repository-images.githubusercontent.com/226840735/d3e77500-51a0-11ea-845e-3cc87714278b)](https://github.com/ksk001100/seahorse)

**A minimal CLI framework written in Rust**

アイコンは開発を手伝って頂いている@rnitta さんが作ってくれました。
やはり[コミットハッシュがキレイ](https://qiita.com/rnitta/items/c998172872ef49be93f1)な人はセンスがありますね。


## 特徴
- ミニマル
- 難しい設定などはなく割と簡単に使える
- 型(Bool, String, Int, Float)指定できるオプションフラグ
- 依存クレートがない
- **アイコンがスゴく可愛い**

# Quick Start
最小限の使い方は以下の通りです。


```bash
$ cargo new cli_tool
$ cd cli_tool
$ echo 'seahorse = "0.6.2"' >> Cargo.toml
```

```rust
use std::env;
use seahorse::{App};

fn main() {
    let args: Vec<String> = env::args().collect();
    let app = App::new()
        .name(env!("CARGO_PKG_NAME"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .version(env!("CARGO_PKG_VERSION"))
        .usage("cli_tool [args]")
        .action(|c| println!("Hello, {:?}", c.args));

    app.run(args);
}
```

```
$ cargo build --release
$ ./target/release/cli_tool --help
Name:
	cli_tool

Author:
	hogehoge <hogehoge@gmail.com>

Usage:
	cli_tool [args]

Version:
	0.1.0

$ ./target/release/cli_tool John
Hello, ["John"]
```

# ActionとContext
`Action`がコマンドの処理部分になります。
`Action`の定義は`Context`を引数とした関数型です。

```rust
type Action = fn(Context);
```

`Context`は`Action`の引数でのみ使う型になります。
`Context`はコマンドラインから受け取った引数からフラグとフラグの値を取り除いて残った配列をフィールドとして持ちます。
また、`Context`は各フラグの値を取り出すためのメソッドを持ちます。

```rust
fn action(c: &Context) {
    println!("{:?}", c.args);
    println!("{}", c.bool_flag("bool");
    
    match c.string_flag("string") {
        Some(s) => println!("{}", s),
        None => println!("No string..."),
    }
   
    match c.int_flag("int") {
        Some(i) => println!("{}", i),
        None => println!("No integer..."),
    }

    match c.float_flag("float") {
        Some(f) => println!("{}", f),
        None => println!("No float..."),
    }
}
```



# Flag
`Flag`は`--flag`や`-f`のようなコマンドラインでフラグを渡す場合に定義します。
フラグとして受け取る値の型は`FlagType`から選択できます。

```rust
enum FlagType {
    Bool,
    String,
    Int,
    Float,
}
```

```rust
use std::env;
use seahorse::{App, Context, Flag, FlagType};

fn main() {
    let args: Vec<String> = env::args().collect();
    let app = App::new()
        .name(env!("CARGO_PKG_NAME"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .version(env!("CARGO_PKG_VERSION"))
        .usage("cli_tool [args]")
        .action(action)
        .flag(Flag::new("bye", "cli [arg] --bye", FlagType::Bool))
        .flag(Flag::new("age", "cli [arg] --age [age]", FlagType::Int));

    app.run(args);
}

fn action(c: &Context) {
    if c.bool_flag("bye") {
        println!("Bye, {:?}", c.args);
    } else {
        println!("Hello, {:?}", c.args);
    }

    if let Some(age) = c.int_flag("age") {
        println!("{:?} is {} years old.", c.args, age);
    }
}
```

```bash
$ cli_tool John --bye
Bye, ["John"]

$ cli_tool John --age 20
["John"] is 20 years old.

$ cli_tool John --bye --age=30
Bye, ["John"]
["John"] is 30 years old.
```

## エイリアス
フラグにエイリアスを設定する場合は`alias`メソッドをメソッドチェーンします。

```rust
let int_flag = Flag::new("integer", "cli [arg] --integer(-int, -i) [integer]", FlagType::Int)
    .alias("int")
    .alias("i");
```

# Command
アクションを複数登録する場合は`Command`を使います。

```rust
use seahorse::{App, Command, Context, Flag, FlagType};
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let app = App::new()
        .name(env!("CARGO_PKG_NAME"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .version(env!("CARGO_PKG_VERSION"))
        .usage("cli_tool [command] [x] [y]")
        .command(
            Command::new()
                .name("add")
                .usage("cli add [x] [y]")
                .action(add),
        )
        .command(
            Command::new()
                .name("sub")
                .usage("cli sub [x] [y]")
                .action(sub),
        );

    app.run(args);
}

fn add(c: &Context) {
    let x: i32 = c.args[0].parse().unwrap();
    let y: i32 = c.args[1].parse().unwrap();
    println!("{} + {} = {}", x, y, x + y);
}

fn sub(c: &Context) {
    let x: i32 = c.args[0].parse().unwrap();
    let y: i32 = c.args[1].parse().unwrap();
    println!("{} - {} = {}", x, y, x - y);
}
```

```bash
$ cli_tool add 10 3
10 + 3 = 13

$ cli_tool sub 34 10
34 - 10 = 24
```

また、`Command`は`App`と同じように`flag`メソッドでオプションフラグを設定することができます。

```rust
let command = Command::new()
    .name("hello")
    .usage("cli hello [arg]")
    .action(action)
    .flag(Flag::new("bye", "cli hello [arg] --bye", FlagType::Bool));
```

# さいごに
ざっくりとした機能は以上になります。

***issue, PR, Starお待ちしています！！***
https://github.com/ksk001100/seahorse