https://rust-cli.github.io/book/index.html
[[Rust]]でCLIツールを作るための公式ドキュメント。以下は実際にいじってみたメモ。
# メモ
## A command line app in 15 minutes
ドキュメントでは`grrs`というプロジェクトを作成しているが、`tatsuwo`という名前に変えている。
### Project setup
[[Cargo]]を使う。
```bash
$ cargo new tatsuwo
$ cd tatsuwo
$ cargo run
Compiling tatsuwo v0.1.0 (C:\Users\syoum\git\github.com\tadashi-aikawa\tatsuwo)
Finished dev [unoptimized + debuginfo] target(s) in 0.66s
Running `target\debug\tatsuwo.exe`
Hello, world!
```
### Parsing command line arguments
`std::env::args`を使う。
直球な書き方。
```rust
fn main() {
let pattern = std::env::args().nth(1).expect("no pattern given");
let path = std::env::args().nth(2).expect("no path given");
eprintln!("pattern = {:?}", pattern);
eprintln!("path = {:?}", path);
}
```
構造体に入れる書き方。
```rust
#[derive(Debug)]
struct Cli {
pattern: String,
path: std::path::PathBuf,
}
fn main() {
let pattern = std::env::args().nth(1).expect("no pattern given");
let path = std::env::args().nth(2).expect("no path given");
let args = Cli {
pattern,
path: std::path::PathBuf::from(path),
};
eprintln!("args = {:?}", args);
}
```
ライブラリを使うともっとエレガントな書き方ができる。
- [[Rustでコマンドライン引数を扱う方法]]
### First implementation of grrs
grepのようなコマンドを作成する。[[Rustでファイルの中身を文字列(String)として読み込む]]。
```rust
fn main() {
let args: Cli = Cli::from_args();
let content = std::fs::read_to_string(&args.path).expect("could not read file");
for line in content.lines() {
if line.contains(&args.pattern) {
println!("{}", line);
}
}
eprintln!("args = {:?}", args);
}
```
実行結果。
```shell
$ cargo run -- rust .\Cargo.toml
...
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
args = Cli { pattern: "rust", path: ".\\Cargo.toml" }
```
### Nicer error reporting
[[std.result.Result]]を使う。
```rust
fn main() {
let args: Cli = Cli::from_args();
let result = std::fs::read_to_string(&args.path);
match result {
Ok(content) => {
println!("File content: {}", content);
}
Err(error) => {
println!("Oh noes: {}", error);
}
}
eprintln!("args = {:?}", args);
}
```
実行結果。
```shell
$ cargo run -- rust hogehoge
...
Oh noes: 指定されたファイルが見つかりません。 (os error 2)
args = Cli { pattern: "rust", path: "hoge" }
```
#### 例外処理が必要ない場合
`unwrap`を使う。
```rust
fn main() {
let args: Cli = Cli::from_args();
let content = std::fs::read_to_string(&args.path).unwrap();
println!("{}", content);
}
```
実行するとpanicになる。
```shell
$ cargo run -- rust hogehoge
...
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "指定されたファイルが見つかりません。" }', src\main.rs:13:55
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\tatsuwo.exe rust hoge` (exit code: 101)
```
#### panicを引き起こしたくないとき
[[std.result.Result]]を返却する。`return`のtargetは`content`ではなく`main`関数であることがポイント。
```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Cli = Cli::from_args();
let result = std::fs::read_to_string(&args.path);
let content = match result {
Ok(content) => content,
Err(error) => {
return Err(error.into());
}
};
println!("{}", content);
Ok(())
}
```
[[Question mark operator]]を使うともっと簡潔に書ける。
```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Cli = Cli::from_args();
let content = std::fs::read_to_string(&args.path)?;
println!("{}", content);
Ok(())
}
```
##### Aside
- [[Box]]
- [[®dyn]]
#### Providing Context
独自エラーはこんな風に作れる。
```rust
#[derive(Debug)]
struct CustomError(String);
fn main() -> Result<(), CustomError> {
let args: Cli = Cli::from_args();
let content = std::fs::read_to_string(&args.path).map_err(|err| {
CustomError(format!(
"Error reading `{}`: {}",
&args.path.to_str().unwrap(),
err
))
})?;
println!("{}", content);
Ok(())
}
```
しかし、この方法ではオリジナルのエラー情報を保持できない。対策として[[anyhow]]の`Context`トレイトを使う。
```rust
use anyhow::{Context, Result};
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
struct Cli {
pattern: String,
#[structopt(parse(from_os_str))]
path: std::path::PathBuf,
}
fn main() -> Result<()> {
let args: Cli = Cli::from_args();
let content = std::fs::read_to_string(&args.path)
.with_context(|| format!("could not read file `{}`", &args.path.to_str().unwrap()))?;
println!("{}", content);
Ok(())
}
```
結果にはオリジナルのエラー情報も含まれる。
### Output for humans and machines
- 標準出力は[[printlnマクロ]]を参照
- 標準エラー出力は[[eprintlnマクロ]]を参照
#### パフォーマンスを損ねないために
ターミナルへのprintはパフォーマンスが悪くボトルネックになりやすい。対策が必要。
- [[®BufWriter]]を使ってflush回数を減らす
- デフォルトだと8kBごとにflushする。
- lockを取得する
#### 処理が長い場合はProgress Barを検討する
[[®indicatif]]を使う。
#### Logを出力する
[[log]]と[[env_logger]]を使う。
```rust
use anyhow::{Context, Result};
use log::{info, trace, warn};
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
struct Cli {
pattern: String,
#[structopt(parse(from_os_str))]
path: std::path::PathBuf,
}
fn main() -> Result<()> {
env_logger::init();
info!("これから処理を開始します");
let args: Cli = Cli::from_args();
info!("関数をparseしました。");
warn!("これからファイルを読み込みます. 失敗するかもしれません..");
let content = std::fs::read_to_string(&args.path)
.with_context(|| format!("could not read file `{}`", &args.path.to_str().unwrap()))?;
info!("成功!");
trace!("{}", content);
Ok(())
}
```
[[PowerShell]]で実行した結果。実際には色もついている。
```powershell
> $env:RUST_LOG="info"
> cargo run -- n .\src\main.rs
...
[2021-02-27T13:00:44Z INFO tatsuwo] これから処理を開始します
[2021-02-27T13:00:44Z INFO tatsuwo] 関数をparseしました。
[2021-02-27T13:00:44Z WARN tatsuwo] これからファイルを読み込みます. 失敗するかもしれません..
[2021-02-27T13:00:44Z INFO tatsuwo] 成功!
```
##### verbose flagでログレベルを制御する
[[®clap-verbosity-flag]]を使うとあるがactiveではないのが気になる。
### 備考
[[clap]]のv3-betaは[[structopt]]を取り込んでパワーアップしてそうなので、こっちを使った方がいいのかもしれない。