概要
Rustを色々触ってコマンドラインツールを作ってみる。
Rust公式ドキュメント
私は helpman というコマンドを作ってみました。現在、v1.0.4。
help や man コマンドは情報量が多いので、独自のマニュアルを新規作成、編集、閲覧できるものを作りました。コマンドの詳細は後日載せます。
インストール
私の環境はMac(Intel)なので以下のコマンドを叩く。(Linuxも同じ)
% curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Windowsは rustup-init.exe をインストールする。以降はMacでの説明のみ。
~/.cargo/bin
配下にツールがインストールされる。
rustup, cargo, rustcなどがある。
% ls ~/.cargo/bin/
cargo* cargo-fmt* clippy-driver* rust-gdb* rust-lldb* rustdoc* rustup*
cargo-clippy* cargo-miri* rls* rust-gdbgui* rustc* rustfmt*
バージョン確認
% source "~/.cargo/env"
% rustc --version
rustc 1.66.0 (69f9c33d7 2022-12-12)
% cargo --version
cargo 1.66.0 (d65d197ad 2022-11-15)
更新
rustup update
でできる。
アンインストール
アンインストールはrustup self uninstall
で実行できる。
Hello World
早速、Hello worldしてみる。
まず, main.rs を以下のように作る。
fn main() {
println!("Hello World!");
}
そして、実行
% rustc main.rs
% ./main
Hello World!
Cargo
CargoはRustのビルドシステム兼パッケージマネージャ。
% cargo new hello_cargo
Created binary (application) `hello_cargo` package
% tree .
.
└── hello_cargo
├── Cargo.toml
└── src
└── main.rs
主なコマンド
cargo build
ビルドして実行してみる。
% cargo build
Compiling hello_cargo v0.1.0
(/Users/***/rust/cargo/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 1.31s
% ./target/debug/hello_cargo
Hello, world!
リリース時はcargo build --release
とすると、最適化し、target/releaseに実行ファイルが生成される。
cargo run
cargo run
を使うとこれらを一発でコンパイルできる。
% cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/hello_cargo`
Hello, world!
cargo check
cargo check
をすると実行できるかを瞬時にチェックする。
実行ファイルを生成しないので、cargo build
よりも高速。
% cargo check
Checking hello_cargo v0.1.0 (/Users/Tomoki/workspace/rust/cargo/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
cargo add
Cargo.tomlに自動で追記してくれる
[dependencies]
+ anyhow = "1.0.68"
これは以下のように叩くだけでいい。
% cargo add anyhow@0.1.41
フォーマッター
rustfmt
と cargo-fmt
を使う。(元から入ってる)
以下のように叩くと自動でフォーマットされる。
cargo fmt
rustfix
自動でwarningを修正してくれる。
% cargo fix
Linter
リンターではClippy
というものがある。
% cargo clippy
型
Rustの文字列の型には &str と String がある。
Rustでは &[T]
はsliceを表すので、&strはu8のsliceである。
それに対して、 String はu8のVectorである。
ちなみにStringから&strにするには&をつければいい。
型を出力する関数print_type()
を以下に載せておく。
fn print_type<T>(_: T) {
println!("{}", std::any::type_name::<T>());
}
コマンドラインツール
% cargo new grrs
引数は標準ライブラリのstd::env::args()
を使用する。
引数がないとexpect()
に渡したものが出力される。
fn main() {
let arg1 = std::env::args().nth(1).expect("no arg1 given");
let arg2 = std::env::args().nth(2).expect("no arg2 given");
println!("arg1 : {}, arg2 : {}", arg1, arg2);
}
実行結果
grrs % cargo run a b
arg1 : a, arg2 : b
% cargo run a
thread 'main' panicked at 'no arg2 given', src/main.rs:3:40
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
% cargo run
thread 'main' panicked at 'no arg1 given', src/main.rs:2:43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
環境変数はRUST_BACKTRACEは以下のように設定できる
export RUST_BACKTRACE=1
fullも設定できる
export RUST_BACKTRACE=full
構造体を使う
use std::env;
struct User {
name: String,
age: u8,
}
fn main() {
let arg1 = env::args().nth(1).expect("nameがありません");
let arg2 = env::args().nth(2).expect("ageがありません");
let args = User {
name: arg1,
age: arg2.parse().unwrap()
};
println!(
"name is {}, age is {}",
args.name, args.age
)
}
出力すると
% cargo run Taro 18
name is Taro, age is 18
[derive(Debug)]
#[derive(Debug)]を使うと構造体をfmt::Debug
で出力する
use std::env;
#[derive(Debug)]
struct User {
name: String,
age: u8,
}
fn main() {
let arg1 = env::args().nth(1).expect("nameがありません");
let arg2 = env::args().nth(2).expect("ageがありません");
let args = User {
name: arg1,
age: arg2.parse().unwrap()
};
println!("「:?」を使った場合 {:?}", args);
println!("「:#?」を使った場合 is {:#?}", args);
println!("「1:?」 : {1:?}, 「0:?」 : {0:?}", args.name, args.age);
}
出力すると
% cargo run Taro 18
「:?」を使った場合 User { name: "Taro", age: 18 }
「:#?」を使った場合 is User {
name: "Taro",
age: 18,
}
「1:?」 : 18, 「0:?」 : "Taro"
##clap
Cargo.tomlにclapを追加する。
[package]
name = "grrs"
version = "0.1.0"
edition = "2021"
+ [dependencies]
+ clap = { version = "4.0", features = ["derive"] }
以下のようなfoo.textを用意。
abcd
1234
pqrs
9876
main.rsを以下のように編集する。
#![allow(unused)]
use clap::Parser;
#[derive(Parser)]
struct Cli {
text: String,
file: std::path::PathBuf,
}
fn main() {
let args = Cli::parse();
let content = std::fs::read_to_string(&args.file).expect("could not read file");
println!("input : {}", args.text);
println!("file contents : {}", content);
}
実行するとエラーがいい感じに出力される。
引数は--の後に入力する。--の代わりに main,もしくは省略も可。
ただし、--helpの時は省略不可。
% cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/grrs`
error: The following required arguments were not provided:
<TEXT>
<FILE>
Usage: grrs <TEXT> <FILE>
For more information try '--help'
% cargo run -- --help
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/grrs --help`
Usage: grrs <TEXT> <FILE>
Arguments:
<TEXT>
<FILE>
Options:
-h, --help Print help information
% cargo run -- hoge foo.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/grrs hoge foo.txt`
input : hoge
file contents : abcd
1234
pqrs
9876
ファイルを読み込む
std::fs::read_to_string()でファイルを読み込み、1つ目の引数を含む行を出力している。
#![allow(unused)]
use clap::Parser;
#[derive(Parser)]
struct Cli {
text: String,
file: std::path::PathBuf,
}
fn main() {
let args = Cli::parse();
let content = std::fs::read_to_string(&args.file).expect("could not read file");
for line in content.lines() {
// textを含む行を出力
if line.contains(&args.text) {
println!("{}", line);
}
}
}
実行結果
% cargo run -- 98 foo.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/grrs 98 foo.txt`
9876
match & unwrap & expect
fileが見つからなかった時は unwrap で panic を起こすか
match で明示的にエラー処理を行う。
expect よりパフォーマンスが優れている。
matchを使った場合
#![allow(unused)]
use clap::Parser;
#[derive(Parser)]
struct Cli {
text: String,
file: std::path::PathBuf,
}
fn main() {
let args = Cli::parse();
- let content = std::fs::read_to_string(&args.file).expect("could not read file");
+ let result = std::fs::read_to_string(&args.file);
+ let content = match result {
+ Ok(content) => content,
+ Err(error) => {
+ panic!("エラー内容 : {}", error);
+ }
+ };
println!("{}", content);
}
実行結果
% cargo run -- a a.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.82s
Running `target/debug/grrs a a.txt`
thread 'main' panicked at 'エラー内容 : No such file or directory (os error 2)', src/main.rs:21:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
unwrap
unwrapだと以下のように簡潔にかける。
//***省略***
fn main() {
let args = Cli::parse();
+ let content = std::fs::read_to_string(&args.file).unwrap();
+ println!("{}", content);
}
実行結果
% cargo run a a.txt
Finished dev [unoptimized + debuginfo] target(s) in 1.04s
Running `target/debug/grrs a a.txt`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:12:55
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic!を起こさずにエラーを出力することも可能。
//***省略***
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Cli::parse();
let result = std::fs::read_to_string(&args.file);
let content = match result {
Ok(content) => content,
Err(error) => {
return Err(error.into());
}
};
println!("{}", content);
Ok(())
}
実行結果
% cargo run a a.txt
Finished dev [unoptimized + debuginfo] target(s) in 1.39s
Running `target/debug/grrs a a.txt`
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Ok(())
をOk(());
としてはいけません。
戻り値のある関数はreturnを明示していない最終行で;
はつけてはいけない
「?」 を使ったらもっと簡潔にかける
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Cli::parse();
let content = std::fs::read_to_string(&args.file)?;
println!("{}", content);
Ok(())
}
実行結果
% cargo run a a.txt
Finished dev [unoptimized + debuginfo] target(s) in 1.52s
Running `target/debug/grrs a a.txt`
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
「?」を使ったときのエラーのカスタマイズ
// ***省略***
#[derive(Debug)]
struct CustomError(String);
fn main() -> Result<(), CustomError> {
let args = Cli::parse();
let content = std::fs::read_to_string(&args.file)
.map_err(|err| CustomError(format!("エラー内容: `{}`: {}", args.file.display(), err)))?;
println!("{}", content);
Ok(())
}
実行結果
% cargo run a a.txt
Compiling grrs v0.1.0 (/Users/Tomoki/workspace/rust/cargo/grrs)
Finished dev [unoptimized + debuginfo] target(s) in 1.42s
Running `target/debug/grrs a a.txt`
Error: CustomError("エラー内容: `a.txt`: No such file or directory (os error 2)")
anyhow
anyhowを使うともっとスマートにかける。
まず、Cargo.tomlに追加する。
[dependencies]
clap = { version = "4.0", features = ["derive"] }
+ anyhow = "1.0"
以下のように main.rs を修正。
#![allow(unused)]
+ use anyhow::{Context, Result};
use clap::Parser;
// ***省略***
#[derive(Debug)]
struct CustomError(String);
+ fn main() -> Result<()> {
let args = Cli::parse();
+ let content = std::fs::read_to_string(&args.file)
+ .with_context(|| format!("エラー内容 : {}が見つかりません", args.file.display()))?;
println!("{}", content);
Ok(())
}
実行結果
% cargo run a a.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.82s
Running `target/debug/grrs a a.txt`
Error: エラー内容 : a.txtが見つかりません
Caused by:
No such file or directory (os error 2)
output
eprintln!
println!
は標準出力、eprintln!
は標準エラー出力である。
高速な標準出力
普通のprintln!
fn main() {
for _ in 0..10_000_000 {
println!("");
}
}
速度を計測すると 3.97s
% time cargo run > /dev/null
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/grrs`
cargo run > /dev/null 3.97s user 3.18s system 97% cpu 7.358 total
writeln!
use std::io::{stdout, Write};
fn main() {
let out = stdout();
let mut out = out.lock();
for _ in 0..10_000_000 {
writeln!(out, "").unwrap();
}
}
速度は 3.72s となり少し速くなった。
% time cargo run > /dev/null
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/grrs`
cargo run > /dev/null 3.72s user 3.12s system 97% cpu 7.008 total
write!
writeln!
の代わりに write!
を使う。
use std::io::{stdout, Write};
fn main() {
let out = stdout();
let mut out = out.lock();
for _ in 0..10_000_000 {
write!(out, "").unwrap();
}
}
速度は 0.53s と数倍速くなった。
% time cargo run > /dev/null
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/grrs`
cargo run > /dev/null 0.53s user 0.03s system 77% cpu 0.732 total
バッファリング
デフォルトではバッファリングされないので、バッファリングしてあげる。
#![allow(unused)]
use std::io::{stdout, BufWriter, Write};
fn main() {
let stdout = stdout();
let mut handle = BufWriter::new(stdout); // optional: wrap that handle in a buffer
for _ in 0..10_000_000 {
writeln!(handle, "foo");
}
}
速度は 0.94s
% time cargo run > /dev/null
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/grrs`
cargo run > /dev/null 0.94s user 0.03s system 85% cpu 1.143 total
またバッファリングは以下のようにも書ける。
use std::io::{stdout, Write, BufWriter};
fn main() {
let out = stdout();
let mut out = BufWriter::new(out.lock());
for _ in 0..10_000_000 {
writeln!(out, "").unwrap();
}
}
こちらも速度は 0.94s となる。
% time cargo run > /dev/null
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/grrs`
cargo run > /dev/null 0.94s user 0.04s system 85% cpu 1.138 total
Progress bar
Cargo.tomlに indicatif を追記。
[dependencies]
+ indicatif = {version = "*", features = ["rayon"]}
main.rsを以下のように編集。
#![allow(unused)]
use indicatif::ProgressBar;
fn main() {
let pb = ProgressBar::new(100_000_000);
for i in 0..100_000_000 {
pb.inc(1);
}
pb.finish_with_message("done");
}
これを実行するとプログレスバーが出てくる。
% cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.12s
Running `target/debug/grrs`
█████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 11674248/100000000
Logging
Cargo.toml
に env_logger と log を追記する。
[dependencies]
+ env_logger = "0.9.0"
+ log = "0.4"
src/bin/output-log.rs
を作成
use log::{info, warn};
use env_logger;
fn main() {
env_logger::init();
info!("starting up");
warn!("oops, nothing implemented!");
}
実行結果
log levelは error
, warn
, info
, debug
, trace
がある。
% env RUST_LOG=info cargo run --bin output-log
Finished dev [unoptimized + debuginfo] target(s) in 1.15s
Running `target/debug/log`
[2023-01-02T08:20:40Z INFO log] starting up
[2023-01-02T08:20:40Z WARN log] oops, nothing implemented!
クロスコンパイラ
クロスコンパイラの比較については Rust でバイナリを配布する がよくまとまっていた。
cross
まずはインストールする。
cargo install cross --git https://github.com/cross-rs/cros
以下の2つのコマンドのどちらかを叩くと対象の環境のリストが表示される。
rustc --print target-list
rustup target list
例えば
// Linuxの場合
% cross build --target x86_64-unknown-linux-gnu
// Windowsの場合
% cross build --target x86_64-pc-windows-gnu
// Mac (m1の場合)
$ rustup target add aarch64-apple-darwin
$ cross build --target aarch64-apple-darwin
CentOS7
centOS7はglibcが2.28ではなく、2.27を使用しているので、上記の方法だとエラーが出る。
cross
でも以下のようにCross.tomlに追加したらいけるらしい。公式ドキュメントに記載。
[target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main-centos"
今回はcargo zigbuild
を使う。
事前に zig をインストールしておかなければならない。
% rustup target add x86_64-unknown-linux-gnu
% brew install zig
% cargo zigbuild --target=x86_64-unknown-linux-gnu.2.17
バイナリ配布方法
Git Release
一番手っ取り早い。
しかし、配布方法が基本的に.tar.gz
か.zip
なので解凍する必要がある。
また、Passを通す必要があるので、面倒である。
brew
homebrew (tap)
tapを利用せずにインストールさせたい場合は、 homebrew-core に対してpull requestを送って取り込んでもらうことで可能になる。 いくつかのルール を満たす必要がある。
その他
代表的なパッケージマネージャを挙げておく。
- rpm
- yum
- dpkg
参考文献
コマンドツールを作る際にお世話になった記事。
- Rust公式ドキュメント
- The Rust Programming Language 日本語版
- Git-clippy
- エラーが出たときの RUST_BACKTRACE=1 を設定する
- &strとStringを理解しようと思ったらsliceやmutを理解できてないことに気づいた話
- Rustのデータ型
- 命名
- rust String &str の変換と、文字列 数値 の変換
- Rustの所有権について
- expect()よりunwrap_or_else()を使うべき場合
- Rust のエラーハンドリングはシンタックスシュガーが豊富で完全に初見殺しなので自信を持って使えるように整理してみたら完全に理解した
- Rustの行末セミコロンはいるのかいらないのか
- Rustのコマンドライン解析ライブラリ『Clap』 〜導入編〜
- Rustで手軽にCLIツールを作れるclapを軽く紹介する
- Rust で derive(Debug) を安心して使うために秘匿情報をマスクする
- Rustで高速な標準出力
- Rust:logでログ出力を行う
- Rust のエラーハンドリングはシンタックスシュガーが豊富で完全に初見殺しなので自信を持って使えるように整理してみたら完全に理解した
- RustのCLI作成チュートリアルやっている中のメモ書き
- clap v3 のdriveベースのパースのサンプル
- Rust における
From<T>
とかInto<T>
とかの考え方 - std::any::type_name はバッチリ型名を返してくれるわけではなさそう
- Rust入門
- Rust を始める時に少しだけ読み書きしやすくなる Result と Option の話
- Rust でバイナリを配布する
- RustでWindows⇔Linuxをクロスコンパイルする
- Rust でバイナリを配布する