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]]を取り込んでパワーアップしてそうなので、こっちを使った方がいいのかもしれない。