11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustでコマンドラインツールを作ってみる

Last updated at Posted at 2023-01-24

概要

Rustを色々触ってコマンドラインツールを作ってみる。
Rust公式ドキュメント

私は helpman というコマンドを作ってみました。現在、v1.0.4
helpman コマンドは情報量が多いので、独自のマニュアルを新規作成、編集、閲覧できるものを作りました。コマンドの詳細は後日載せます。

Git Release
Crate.io

インストール

私の環境はMac(Intel)なので以下のコマンドを叩く。(Linuxも同じ)

terminal
% curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Windowsは rustup-init.exe をインストールする。以降はMacでの説明のみ。

~/.cargo/bin配下にツールがインストールされる。
rustup, cargo, rustcなどがある。

terminal
% 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 を以下のように作る。

main.rs
fn main() {
    println!("Hello World!");
}

そして、実行

terminal
% rustc main.rs
% ./main       
Hello World!

Cargo

CargoはRustのビルドシステム兼パッケージマネージャ。

terminal
% cargo new hello_cargo
     Created binary (application) `hello_cargo` package
% tree .
.
└── hello_cargo
    ├── Cargo.toml
    └── src
        └── main.rs

主なコマンド

cargo build

ビルドして実行してみる。

terminal
% 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を使うとこれらを一発でコンパイルできる。

terminal
% 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に自動で追記してくれる

Cargo.toml
[dependencies]
+ anyhow = "1.0.68"

これは以下のように叩くだけでいい。

terminal
% cargo add anyhow@0.1.41

フォーマッター

rustfmtcargo-fmtを使う。(元から入ってる)
以下のように叩くと自動でフォーマットされる。

terminal
cargo fmt

rustfix

自動でwarningを修正してくれる。

% cargo fix

Linter

リンターではClippyというものがある。

terminal
% cargo clippy

Rustの文字列の型には &strString がある。
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()に渡したものが出力される。

src/main.js
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

構造体を使う

src/main.rs
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
    )
}

出力すると

terminal
% cargo run Taro 18
name is Taro, age is 18

[derive(Debug)]

#[derive(Debug)]を使うと構造体をfmt::Debugで出力する

src/main.rs
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);
}

出力すると

terminal
 % cargo run Taro 18
「:?」を使った場合  User { name: "Taro", age: 18 }
「:#?」を使った場合 is User {
    name: "Taro",
    age: 18,
}
「1:?」 : 18, 「0:?」 : "Taro"

##clap

Cargo.tomlにclapを追加する。

Cargo.toml
[package]
name = "grrs"
version = "0.1.0"
edition = "2021"

+ [dependencies]
+ clap = { version = "4.0", features = ["derive"] }

以下のようなfoo.textを用意。

foo.txt
abcd
1234
pqrs
9876

main.rsを以下のように編集する。

src/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の時は省略不可。

terminal
% 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つ目の引数を含む行を出力している。

src/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");

    for line in content.lines() {
        // textを含む行を出力
        if line.contains(&args.text) {
            println!("{}", line);
        }
    }
}

実行結果

terminal
% 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が見つからなかった時は unwrappanic を起こすか
match で明示的にエラー処理を行う。
expect よりパフォーマンスが優れている。

matchを使った場合

src/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");
+   let result = std::fs::read_to_string(&args.file);
+   let content = match result {
+       Ok(content) => content,
+       Err(error) => {
+           panic!("エラー内容 : {}", error);
+       }
+   };
    println!("{}", content);
}

実行結果

terminal
% 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だと以下のように簡潔にかける。

src/main.rs
//***省略***
fn main() {
    let args = Cli::parse();
+   let content = std::fs::read_to_string(&args.file).unwrap();
+   println!("{}", content);
}

実行結果

terminal
% 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!を起こさずにエラーを出力することも可能。

src/main.rs
//***省略***

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(())
}

実行結果

terminal
% 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を明示していない最終行で;はつけてはいけない

「?」 を使ったらもっと簡潔にかける

src/main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.file)?;
    println!("{}", content);
    Ok(())
}

実行結果

terminal
% 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" }

「?」を使ったときのエラーのカスタマイズ

src/main.rs
// ***省略***
#[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(())
}

実行結果

terminal
% 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に追加する。

Cargo.toml
[dependencies]
clap = { version = "4.0", features = ["derive"] }
+ anyhow = "1.0"

以下のように main.rs を修正。

src/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(())
}

実行結果

terminal
% 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

terminal
% 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!

src/main.rs
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 となり少し速くなった。

terminal
% 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! を使う。

src/main.rs
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 と数倍速くなった。

terminal
% 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

バッファリング

デフォルトではバッファリングされないので、バッファリングしてあげる。

src/main.rs
#![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

terminal
% 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

またバッファリングは以下のようにも書ける。

src/main.rs
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 となる。

terminal
% 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 を追記。

Cargo.toml
[dependencies]
+ indicatif = {version = "*", features = ["rayon"]}

main.rsを以下のように編集。

src/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");
}

これを実行するとプログレスバーが出てくる。

terminal
% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.12s
     Running `target/debug/grrs`
█████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 11674248/100000000

Logging

Cargo.tomlenv_loggerlog を追記する。

Cargo.toml
[dependencies]
+ env_logger = "0.9.0"
+ log = "0.4"

src/bin/output-log.rsを作成

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がある。

terminal
% 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

公式ドキュメント

まずはインストールする。

terminal
cargo install cross --git https://github.com/cross-rs/cros

以下の2つのコマンドのどちらかを叩くと対象の環境のリストが表示される。

terminal
rustc --print target-list
rustup target list

例えば

terminal
// 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に追加したらいけるらしい。公式ドキュメントに記載。

Cross.toml
[target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main-centos"

今回はcargo zigbuildを使う。
事前に zig をインストールしておかなければならない。

terminal
% 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

参考文献

コマンドツールを作る際にお世話になった記事。

  1. Rust公式ドキュメント
  2. The Rust Programming Language 日本語版
  3. Git-clippy
  4. エラーが出たときの RUST_BACKTRACE=1 を設定する
  5. &strとStringを理解しようと思ったらsliceやmutを理解できてないことに気づいた話
  6. Rustのデータ型
  7. 命名
  8. rust String &str の変換と、文字列 数値 の変換
  9. Rustの所有権について
  10. expect()よりunwrap_or_else()を使うべき場合
  11. Rust のエラーハンドリングはシンタックスシュガーが豊富で完全に初見殺しなので自信を持って使えるように整理してみたら完全に理解した
  12. Rustの行末セミコロンはいるのかいらないのか
  13. Rustのコマンドライン解析ライブラリ『Clap』 〜導入編〜
  14. Rustで手軽にCLIツールを作れるclapを軽く紹介する
  15. Rust で derive(Debug) を安心して使うために秘匿情報をマスクする
  16. Rustで高速な標準出力
  17. Rust:logでログ出力を行う
  18. Rust のエラーハンドリングはシンタックスシュガーが豊富で完全に初見殺しなので自信を持って使えるように整理してみたら完全に理解した
  19. RustのCLI作成チュートリアルやっている中のメモ書き
  20. clap v3 のdriveベースのパースのサンプル
  21. Rust における From<T> とか Into<T> とかの考え方
  22. std::any::type_name はバッチリ型名を返してくれるわけではなさそう
  23. Rust入門
  24. Rust を始める時に少しだけ読み書きしやすくなる Result と Option の話
  25. Rust でバイナリを配布する
  26. RustでWindows⇔Linuxをクロスコンパイルする
  27. Rust でバイナリを配布する
11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?