Memo

メモ > 技術 > プログラミング言語: Rust > コンソールアプリケーション

コンソールアプリケーション
実践Rustプログラミング入門 - 秀和システム あなたの学びをサポート! https://www.shuwasystem.co.jp/book/9784798061702.html ここでは、逆ポーランド記法で計算できるプログラムを作成する 以下のとおり、プロジェクトを作成する >cd C:\Users\refirio\Rust >cargo new rpncalc --bin >cd rpncalc ■コマンドライン引数の処理(クレートを使用しない場合) src/main.rs の内容を以下のようにする
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("{:?}", args); }
以下のように、引数を処理できるようになる >cargo run ["target\\debug\\rpncalc.exe"] >cargo run 1 a xyz 2.0 ["target\\debug\\rpncalc.exe", "1", "a", "xyz", "2.0"] 実行ファイルを呼び出す場合も、以下のとおり処理できている >rpncalc ["rpncalc"] >rpncalc 1 a xyz 2.0 ["rpncalc", "1", "a", "xyz", "2.0"] ただしこの場合、引数の位置や順序が固定となっている このまま作りこむこともできるが、clapを使用すると容易に実装できる ■コマンドライン引数の処理(clapクレートを使用する場合) 「clap v4.5.9」を使用する https://crates.io/crates/clap 上記ページの「Install」部分に、インストールコマンドとして「clap = "4.5.9"」が表示されている これをもとに、Cargo.toml の最終行を以下のように調整するが、今回は「derive」を使用するために以下のようにする
[dependencies] ↓ [dependencies] clap = { version = "4.5.9", features = ["derive"] }
クレートの使い方は、「Documentation」部分からリンクが張られている 今回は、以下のようにするとコマンドライン引数を取得できる
use clap::Parser; /// Simple program to greet a person #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// Name of the person to greet #[arg(short, long)] name: String, /// Number of times to greet #[arg(short, long, default_value_t = 1)] count: u8, } fn main() { let args = Args::parse(); for _ in 0..args.count { println!("Hello {}!", args.name); } }
そのまま実行すると「必須の引数が無い」のエラーになるが、「--name Taro」を指定すると名前が表示される (cargo run で引数を指定する場合、「--」に続けて指定する) また「--help」を指定するとヘルプが表示されるが、この内容はプログラム内のコメントも使用されている >cargo run error: the following required arguments were not provided: --name <NAME> Usage: rpncalc.exe --name <NAME> For more information, try '--help'. error: process didn't exit successfully: `target\debug\rpncalc.exe` (exit code: 2) >cargo run -- --name Taro Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s Running `target\debug\rpncalc.exe --name Taro` Hello Taro! >cargo run -- --help Compiling rpncalc v0.1.0 (C:\Users\refirio\Rust\rpncalc) Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.56s Running `target\debug\rpncalc.exe --help` Simple program to greet a person! Usage: rpncalc.exe [OPTIONS] --name <NAME> Options: -n, --name <NAME> Name of the person to greet!! -c, --count <COUNT> Number of times to greet!!! [default: 1] -h, --help Print help -V, --version Print version 実行ファイルを直接実行すると、以下のようになる >rpncalc error: the following required arguments were not provided: --name <NAME> Usage: rpncalc --name <NAME> For more information, try '--help'. >rpncalc --name Taro Hello Taro! オプションの「-n」「--name」「-c」「--count」は、構造体で定義した「name」「count」をもとに、自動で決定される (名前や先頭の1文字が重複していると、正しく決定できないのでエラーになる) また、例えば以下のようにすると、オプションを自分で決定できる。この場合、自動判定の内容と同じく「-n」「--name」「-c」「--count」とみなされる (「short」「long」を指定しない場合、位置引数であると解釈される)
struct Args { /// Name of the person to greet #[arg(short = 'n', long = "name")] name: String, /// Number of times to greet #[arg(short = 'c', long = "count", default_value_t = 1)] count: u8, }
詳細は以下のページが解りやすい clapの使い方まとめ - ぽよメモ https://poyo.hatenablog.jp/entry/2022/10/10/170000 今回はプログラムを以下のように修正し、これをベースに機能を追加していくものとする
use clap::Parser; /// RPN Calculator #[derive(Parser, Debug)] #[command( version = "1.0.0", author = "refirio", about = "Super awesome sample RPN calculator." )] struct Args { /// Set the level of verbosity #[arg(short, long)] verbose: bool, /// Formulas written in RPN #[arg(name = "FILE")] formula_file: Option<String>, } fn main() { let args = Args::parse(); match args.formula_file { Some(file) => println!("File specified: {}", file), None => println!("No file specified."), } println!("Is verbosity specified?: {}", args.verbose); }
以下のとおり実行できる >cargo run Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s Running `target\debug\rpncalc.exe` No file specified. Is verbosity specified?: false >cargo run -- --version Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s Running `target\debug\rpncalc.exe --version` rpncalc 1.0.0 >cargo run -- --help Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s Running `target\debug\rpncalc.exe --help` Super awesome sample RPN calculator. Usage: rpncalc.exe [OPTIONS] [FILE] Arguments: [FILE] Formulas written in RPN Options: -v, --verbose Set the level of verbosity -h, --help Print help -V, --version Print version ■ファイルの読み込み
use clap::Parser; use std::fs::File; use std::io::{BufRead, BufReader}; /// RPN Calculator #[derive(Parser, Debug)] #[command( version = "1.0.0", author = "refirio", about = "Super awesome sample RPN calculator." )] struct Args { /// Set the level of verbosity #[arg(short, long)] verbose: bool, /// Formulas written in RPN #[arg(name = "FILE")] formula_file: Option<String>, } fn main() { let args = Args::parse(); if let Some(path) = args.formula_file { let f = File::open(path).unwrap(); let reader = BufReader::new(f); for line in reader.lines() { let line = line.unwrap(); println!("{}", line); } } else { println!("No file is specified."); } }
プログラムを実行する場所に「input.txt」を作成し、その中に「1 1 +」と書いた場合、 以下のように実行してファイルの内容を読み込むことができる >cargo run -- input.txt 1 1 + >cargo run No file is specified. ■標準入力の読み取り
use clap::Parser; use std::fs::File; use std::io::{stdin, BufRead, BufReader}; /// RPN Calculator #[derive(Parser, Debug)] #[command( version = "1.0.0", author = "refirio", about = "Super awesome sample RPN calculator." )] struct Args { /// Set the level of verbosity #[arg(short, long)] verbose: bool, /// Formulas written in RPN #[arg(name = "FILE")] formula_file: Option<String>, } fn main() { let args = Args::parse(); if let Some(path) = args.formula_file { let f = File::open(path).unwrap(); let reader = BufReader::new(f); run(reader, args.verbose); } else { let stdin = stdin(); let reader = stdin.lock(); run(reader, args.verbose); } } fn run<R: BufRead>(mut reader: R, verbose: bool) { let mut line = String::new(); if reader.read_line(&mut line).unwrap() > 0 { println!("{}", line.trim_end()); } }
引数なしで実行すると、以下のように「入力した文字がそのまま表示される」となる >cargo run test test また今回は、run関数の内容を以下のようにする
fn run<R: BufRead>(reader: R, verbose: bool) { for line in reader.lines() { let line = line.unwrap(); println!("{}", line); } }
引数なしで実行すると、以下のように「入力した文字がそのまま表示される」となる また入力した後にプログラムは終了せず、再度入力できるようになる(「Ctrl+C」で終了できる) >cargo run test test ■計算の準備
use clap::Parser; use std::fs::File; use std::io::{stdin, BufRead, BufReader}; /// RPN Calculator #[derive(Parser, Debug)] #[command( version = "1.0.0", author = "refirio", about = "Super awesome sample RPN calculator." )] struct Args { /// Set the level of verbosity #[arg(short, long)] verbose: bool, /// Formulas written in RPN #[arg(name = "FILE")] formula_file: Option<String>, } struct RpnCalculator(bool); impl RpnCalculator { pub fn new(verbose: bool) -> Self { Self(verbose) } pub fn eval(&self, formula: &str) -> i32 { 0 } } fn main() { let args = Args::parse(); if let Some(path) = args.formula_file { let f = File::open(path).unwrap(); let reader = BufReader::new(f); run(reader, args.verbose); } else { let stdin = stdin(); let reader = stdin.lock(); run(reader, args.verbose); } } fn run<R: BufRead>(reader: R, verbose: bool) { let calc = RpnCalculator::new(verbose); for line in reader.lines() { let line = line.unwrap(); let answer = calc.eval(&line); println!("{}", answer); } }
この時点では、入力された内容に関わらず「0」を返す >cargo run -- input.txt 0 0 0 >cargo run 1 1 + 0 ■計算の実装
use clap::Parser; use std::fs::File; use std::io::{stdin, BufRead, BufReader}; /// RPN Calculator #[derive(Parser, Debug)] #[command( version = "1.0.0", author = "refirio", about = "Super awesome sample RPN calculator." )] struct Args { /// Set the level of verbosity #[arg(short, long)] verbose: bool, /// Formulas written in RPN #[arg(name = "FILE")] formula_file: Option<String>, } struct RpnCalculator(bool); impl RpnCalculator { pub fn new(verbose: bool) -> Self { Self(verbose) } pub fn eval(&self, formula: &str) -> i32 { let mut tokens = formula.split_whitespace().rev().collect::<Vec<_>>(); self.eval_inner(&mut tokens) } pub fn eval_inner(&self, tokens: &mut Vec<&str>) -> i32 { let mut stack = Vec::new(); while let Some(token) = tokens.pop() { if let Ok(x) = token.parse::<i32>() { stack.push(x); } else { let y = stack.pop().expect("invalid syntax"); let x = stack.pop().expect("invalid syntax"); let res = match token { "+" => x + y, "-" => x - y, "*" => x * y, "/" => x / y, "%" => x % y, _ => panic!("invalid token"), }; stack.push(res); } // もし「-v」オプションが指定されていれば(無名フィールドの先頭要素に値があれば)、この時点でのトークンとスタックの状態を出力する if self.0 { println!("{:?} {:?}", tokens, stack); } } if stack.len() == 1 { stack[0] } else { panic!("invalid syntax") } } } fn main() { let args = Args::parse(); if let Some(path) = args.formula_file { let f = File::open(path).unwrap(); let reader = BufReader::new(f); run(reader, args.verbose); } else { let stdin = stdin(); let reader = stdin.lock(); run(reader, args.verbose); } } fn run<R: BufRead>(reader: R, verbose: bool) { let calc = RpnCalculator::new(verbose); for line in reader.lines() { let line = line.unwrap(); let answer = calc.eval(&line); println!("{}", answer); } }
以下のとおり、逆ポーランド記法で計算できる またオプションとして「-v」を指定すれば、計算の過程も表示できる >cargo run -- input.txt 2 21 1000000 >cargo run 1 1 + 2 ■テストの追加 main.rs の最後に以下を追加する
#[cfg(test)] mod tests { use super::*; #[test] fn test_ok() { assert_eq!(2 * 2, 4); } }
以下のとおり、テストを実行できる >cargo test running 1 test test tests::test_ok ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 以下のとおり、テストを修正する(テスト内容の追加)
#[cfg(test)] mod tests { use super::*; #[test] fn test_ok() { let calc = RpnCalculator::new(false); assert_eq!(calc.eval("5"), 5); assert_eq!(calc.eval("50"), 50); assert_eq!(calc.eval("-50"), -50); assert_eq!(calc.eval("2 3 +"), 5); assert_eq!(calc.eval("2 3 -"), -1); assert_eq!(calc.eval("2 3 *"), 6); assert_eq!(calc.eval("2 3 /"), 0); assert_eq!(calc.eval("2 3 %"), 2); } #[test] #[should_panic] fn test_ng() { let calc = RpnCalculator::new(false); assert_eq!(calc.eval("2 3 ^"), 5); } }
以下のとおり、テストを実行できる >cargo test running 2 tests test tests::test_ok ... ok test tests::test_ng - should panic ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ■エラーハンドリングの追加 まずはエラーハンドリングしないプログラムを用意する エラーが発生したらexpectにより、「panic!」へエラーメッセージを送るようにはしている
use std::fs; fn get_int_from_file() -> i32 { let path = "number.txt"; let num_str = fs::read_to_string(path).expect("failed to open the file."); let ret = num_str .trim() .parse::<i32>() .expect("failed to parse string to a number."); ret * 2 } fn main() { println!("{}", get_int_from_file()); }
プログラムを実行する場所に「number.txt」を作成し、その中に「2」と書いた場合、 以下のように実行して2倍にした値を表示する >cargo run 4 「number.txt」が見つからない場合、以下のようにpanicが発生する >cargo run thread 'main' panicked at src/main.rs:6:44: failed to open the file.: Os { code: 2, kind: NotFound, message: "指定されたファイルが見つかりません。" } note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace error: process didn't exit successfully: `target\debug\rpncalc.exe` (exit code: 101) このプログラムに、エラーハンドリングを追加する
use std::fs; fn get_int_from_file() -> Result<i32, String> { let path = "number.txt"; let num_str = fs::read_to_string(path).map_err(|e| e.to_string())?; let ret = num_str .trim() .parse::<i32>() .map(|t| t * 2) .map_err(|e| e.to_string()); ret } fn main() { match get_int_from_file() { Ok(x) => println!("{}", x), Err(e) => println!("{}", e), } }
Result型を使って、返す値によって処理を分岐させる 今回は「正常終了時はi32を返し、エラー時はStringを返す」とする get_int_from_file関数内に「?」があるが、これはResult型を返す関数で使える演算子 今回の場合は「fs::read_to_string(path) がOkなら値を返し、Errならエラー内容を文字列にして関数からエラーを返す」となっている mapとmap_errは、Okの場合の処理とErrの場合の処理を記述できるもの 「.parse::<i32>() がOkなら値を2倍にして返し、Errならエラー内容を文字列にして関数からエラーを返す」となっている 以下のように実行でき、エラー時はエラーメッセージが出力される >cargo run 4 >cargo run 指定されたファイルが見つかりません。 (os error 2) ■エラーハンドリングの追加(anyhowクレートを使用する場合) 「anyhow v1.0.86」を使用する anyhow - crates.io: Rust Package Registry https://crates.io/crates/anyhow Cargo.toml の「[dependencies]」部分に以下を追加する
anyhow = "1.0"
プログラムを以下のように変更する get_int_from_fileの返す方は「i32」のみでいい エラー部分は「context」もしくは「with_context」とすることで処理できる
use std::fs; use anyhow::{Context, Result}; fn get_int_from_file() -> Result<i32> { let path = "number.txt"; let num_str = fs::read_to_string(path).with_context(|| format!("failed to read string from {}", path))?; let ret = num_str .trim() .parse::<i32>() .map(|t| t * 2) .context("failed to parse string"); ret } fn main() { match get_int_from_file() { Ok(x) => println!("{}", x), Err(e) => println!("{}", e), } }
以下のとおり実行できる >cargo run 4 >cargo run failed to read string from number.txt … ファイルが存在しない場合 >cargo run failed to parse string … ファイルの内容が数字で無い場合 ■エラーハンドリングの追加(thiserrorクレートを使用する場合) ※未検証 anyhowクレートは自分のアプリケーション作成に、 thiserrorは他のアプリケーションから呼び出されるライブラリ作成に、 それぞれ向いているらしい ■逆ポーランド記法のプログラムにエラー処理を追加
use clap::Parser; use std::path::PathBuf; use std::fs::File; use std::io::{stdin, BufRead, BufReader}; use anyhow::{bail, ensure, Context, Result}; /// RPN Calculator #[derive(Parser, Debug)] #[command( version = "1.0.0", author = "refirio", about = "Super awesome sample RPN calculator." )] struct Args { /// Set the level of verbosity #[arg(short, long)] verbose: bool, /// Formulas written in RPN #[arg(name = "FILE")] formula_file: Option<PathBuf>, } struct RpnCalculator(bool); impl RpnCalculator { pub fn new(verbose: bool) -> Self { Self(verbose) } pub fn eval(&self, formula: &str) -> Result<i32> { let mut tokens = formula.split_whitespace().rev().collect::<Vec<_>>(); self.eval_inner(&mut tokens) } pub fn eval_inner(&self, tokens: &mut Vec<&str>) -> Result<i32> { let mut stack = Vec::new(); let mut pos = 0; while let Some(token) = tokens.pop() { pos += 1; if let Ok(x) = token.parse::<i32>() { stack.push(x); } else { let y = stack.pop().context(format!("invalid syntax at {}", pos))?; let x = stack.pop().context(format!("invalid syntax at {}", pos))?; let res = match token { "+" => x + y, "-" => x - y, "*" => x * y, "/" => x / y, "%" => x % y, _ => bail!("invalid token at {}", pos), }; stack.push(res); } // もし「-v」オプションが指定されていれば(無名フィールドの先頭要素に値があれば)、この時点でのトークンとスタックの状態を出力する if self.0 { println!("{:?} {:?}", tokens, stack); } } ensure!(stack.len() == 1, "invalid syntax"); Ok(stack[0]) } } fn main() -> Result<()> { let args = Args::parse(); if let Some(path) = args.formula_file { let f = File::open(path)?; let reader = BufReader::new(f); run(reader, args.verbose) } else { let stdin = stdin(); let reader = stdin.lock(); run(reader, args.verbose) } } fn run<R: BufRead>(reader: R, verbose: bool) -> Result<()> { let calc = RpnCalculator::new(verbose); for line in reader.lines() { let line = line?; match calc.eval(&line) { Ok(answer) => println!("{}", answer), Err(e) => eprintln!("{:#?}", e), } } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_ok() { let calc = RpnCalculator::new(false); assert_eq!(calc.eval("5").unwrap(), 5); assert_eq!(calc.eval("50").unwrap(), 50); assert_eq!(calc.eval("-50").unwrap(), -50); assert_eq!(calc.eval("2 3 +").unwrap(), 5); assert_eq!(calc.eval("2 3 -").unwrap(), -1); assert_eq!(calc.eval("2 3 *").unwrap(), 6); assert_eq!(calc.eval("2 3 /").unwrap(), 0); assert_eq!(calc.eval("2 3 %").unwrap(), 2); } #[test] #[should_panic] fn test_ng() { let calc = RpnCalculator::new(false); assert_eq!(calc.eval("2 3 ^").unwrap(), 5); } }
anyhowのbailとensureについては、以下が参考になる Rust, anyhowのマクロ(anyhow!, bail!, ensure!)の使い方 | rs.nkmk.me https://rs.nkmk.me/rust-anyhow-macro/ またエラー処理に加えて、パスをString型ではなくstd::path::PathBuf型を使うようにも変更している

Advertisement