メモ > 技術 > プログラミング言語: 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型を使うようにも変更している