rustで作るcli tool

私は、cli(command line interface) toolを好んで使うのですが、自分で作ることもあります。

特に、ワンバイナリなtoolは、CIやdockerを回すときに便利でgolangをよく使います。goはshellとの親和性が高いので色々な場所で扱いやすく、非常に素晴らしい言語です。

ただ、最近は、rustが人気みたい。理由としては、高速で、サイズが小さく、安全であることが挙げられます。デメリットはコンパイルが遅いこと。

しかし、rustも非常に魅力なので、`base64 {encode,decode}`などをcli tool化しながら、rustを学んでいこうという記事です。

まず、テンプレを作成します。`src/main.rs`が本体になります。

```sh
$ cargo new udrs
$ cd udrs
$ vim src/main.rs
```

ここで、cli optionなどを書きやすくするframeworkの[seahorse](https://github.com/ksk001100/seahorse)を導入します。

```rust:src/main.rs
use std::env;
use seahorse::{App, Command, Context};

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("t")
				.usage("udrs t {}")
				.action(t),
				);
	app.run(args);
}

fn t(_c: &Context) {
	let t = "hello world.";
	println!("{}",t);
}
```

```toml:Cargo.toml
[dependencies]
seahorse = "0.7.1"
```

では、実行してみましょう。ここでは、オプションを`t`で指定しています。

```sh
$ cargo run t
hello world.
```

次に、引数をbase64でencode, decodeするオプションを実装してみようと思います。

```toml:Cargo.toml
[dependencies]
base64 = "0.9.2"
```

```rust:src/main.rs
use std::env;
use seahorse::{App, Command, Context};

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("t")
            .usage("udrs t {}")
            .action(t),
            )
        .command(
            Command::new()
            .name("e")
            .usage("udrs e {}")
            .action(e),
            )
        .command(
            Command::new()
            .name("d")
            .usage("udrs d {}")
            .action(d),
            );
    app.run(args);
}

fn t(_c: &Context) {
    let t = "hello world.";
    println!("{}",t);
}

fn e(c: &Context) {
    println!("{}", base64::encode(&c.args[0]));
}

//fn d(c: &Context) {
//    println!("{:?}", &base64::decode(&c.args[0]).unwrap());
//}
fn d(c: &Context) {
    let by = base64::decode(&c.args[0]).unwrap();
    let res = by.iter().map(|&s| s as char).collect::<String>();
    println!("{:?}",res);
}

```

これで、`e,d`オプションが追加されました。base64でencodeしてみましょう。

```sh
$ cargo run e "hello world."
aGVsbG8gd29ybGQu
```

次は、decodeしてみます。

```sh
$ cargo run d "aGVsbG8gd29ybGQu"
hello world.
```

うまくいきました。

testも追加してみましょう。

```rust:src/main.rs
...

#[cfg(test)]
mod tests {
    #[test]
    fn base64_encode() {
        let expected = "aGVsbG8gd29ybGQu";
        let actual = base64::encode("hello world.");
        assert_eq!(expected, actual);
    }
}
```

```sh
$ cargo test
test tests::base64_encode ... ok
```

面倒なコマンドは、makefile化することもできます。

```makefile
LOG_LEVEL := debug
APP_ARGS  := "hello world."
export RUST_LOG=url=$(LOG_LEVEL)
PREFIX := $(HOME)/.cargo

run:
	cargo run $(APP_ARGS)

test:
	cargo test

check:
	cargo check $(OPTION)

install:
	cargo install --force --root $(PREFIX) --path .
```

```sh
$ make run
help
```

runだけでなく、buildしてコマンドの挙動を確認してみましょう。

```sh
$ cargo build
$ ./target/debug/udrs
```

バイナリの配布は、travis-ciでbuildして、github releasesにdeployします。

```yml:.travis.yml
dist: trusty
language: rust
services: docker
rust:
  - stable
sudo: required
env:
  global:
    - NAME=udrs

matrix:
  include:
    - env: TARGET=x86_64-unknown-linux-musl
    - env: TARGET=x86_64-apple-darwin
      os: osx
    - env: TARGET=x86_64-pc-windows-gnu

before_install:
  - rustup self update

install:
  - source ~/.cargo/env
  - cargo install --force cross

script:
  - cross test --target $TARGET --release

before_deploy:
  - cross build --target $TARGET --release
  - bin=$NAME
  - if [[ $TARGET = "x86_64-pc-windows-gnu" ]]; then bin=$NAME.exe; fi
  - tar czf $NAME-$TRAVIS_TAG-$TARGET.tar.gz -C target/$TARGET/release $bin

deploy:
  api_key:
    secure: "xxx"
  file_glob: true
  file: $NAME-$TRAVIS_TAG-$TARGET.*
  on:
    tags: true
  provider: releases
  skip_cleanup: true

cache: cargo
before_cache:
  - chmod -R a+r $HOME/.cargo

branches:
  only:
    - /^v?\d+\.\d+\.\d+.*$/
    - master
```

`secure: "xxx"`は、`travis`コマンドで発行します。`github-access-token`をgetして以下のコマンドを実行。

```sh
$ sudo gem i travis
$ travis login
$ travis encrypt $GITHUB_ACCESS_TOKEN

$ cat .travis.yml
deploy:
  api_key:
    secure: "xxx"
```

tagをpushすると、travisが走ります。

```sh
$ git add .
$ git commit -m "first"
$ git push origin master

$ git tag -a "0.1.0" -m "v0.1.0"
$ git push origin --tags
```

https://github.com/syui/udrs