自作コマンド
ueyama ruiさんがyoutubeで自作コマンドのライブコーディングをしていて、それを見て
ユーティリティコマンド(e.g. catコマンド、grepコマンド)をRustで自作したくなり、色々調べて見た結果、Rust界隈で有名なκeenさんが2017年に書かれた記事がヒットしました。
これを参考にgrepコマンド,catコマンドを作成していきます。
grepコマンド
grepコマンドは主要な機能として、以下があります。
1.引数にファイルパスとパターン(文字列か正規表現)を取る
2.指定されたファイルからパターンと一致する行を標準出力する
以下は参考記事をもとに写経した内容です。
ライブラリのインポート
// stdクレートのfsモジュールにあるFile型をインポート。以後は`File`として参照できる
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
// std以外のクレートを使う際
extern crate regex;
// regexからRegex型をインポート
use regex::Regex;
ファイルを開く、ファイルをバッファに入れる、正規表現を使う、これらの機能を使うためにライブラリを使えるようにします。
今回は写経なのでライブラリを調べることはありませんが、実際に使いたい機能を持つライブラリを調べる際は、公式ドキュメントや、書籍を頼ることになると思います。
Rustは徐々に日本語の情報が増えているのはありがたいことです。
エラー文
エラー文を作ります。
fn usage() {
println!("rsgrep PATTERN FILENAME");
}
それとは別にunwrap()というものがあり、エラーハンドリングに使われるそうです。
env::args().nth(1).unwrap()
みたいに失敗するかもしれない処理の最後にunwrap()をするようです。
mainの処理
1. 引数からパターンを取り出す
引数を受け取るという処理はもしかしたら引数がないままに、実行されたりして失敗するかもしれません。
そのためOption型を使います。
env::args().nth(1)
で一番目の引数を取り出し、
match式で成功した場合と失敗した場合を記述して変数に束縛します。
fn main() {
// 引数からパターンを取り出す
let pattern = match env::args().nth(1) {
//これで引数一番目を取る
Some(pattern) => pattern,
None => {
usage();
return;
}
};
正規表現
regexクレートを使って先のパターンにマッチするか検索します
パターンを正規表現の形に改めて変換します。
この際、失敗するかもしれないのでResult型にします。
Err(e)という表現はエラーなら結果をエラー文eを返すということでしょう。
“Result型は列挙型として定義されており、Ok(成功時の値)とErr(エラー時の値)の2つのバリアントを持ちます。
戻り値の型はResult<(), String>になります。
”抜粋:: κeen、河野達也、小松礼人 “実践Rust入門 [言語仕様から開発手法まで]”。
(本から引用してみましたが、公式ドキュメントのほうがふさわしい場面でした)
// 取り出したパターンから正規表現のルール?を改めて作る
// 無効な正規表現だった場合などにはエラーが返る
let reg = match Regex::new(&pattern) {
Ok(reg) => reg,
Err(e) => {
println!("Invalid regexp {}: {}", pattern, e);
return;
}
};
// envモジュールのargs関数で、引数を取得
// そのうち2番目を`nth`で取得(0番目はプログラムの名前)
// 引数があるかわからないので、Opthionで返される。<= 使い時
let filename = match env::args().nth(2) {
// あれば取り出す
Some(filename) => filename,
None => {
usage();
return; //<= rustはreturnいらないらしいが、ここはひとまず
}
};
ファイルハンドル
Fileクレートのopen関数でファイルハンドラを使えるようになります。
開けるかどうかわかりません。そのためこれまでと同じように列挙型の一つResult型を使います。
// File構造体のopen関連関数でファイルを開ける
// 失敗する可能性があるため、Resultで返ってくる
// 下の方でもう一度filenameを使うためにここでは&filenameと参照渡しの形
let file = match File::open(&filename) {
Ok(file) => file,
Err(e) => {
println!("An error occurred while opening file {}:{}", filename, e);
return;
}
};
ファイルハンドラからバッファにいれます。それにより値が扱いやすくなります。
// Fileをそのまま使うと遅い、加えてlineメソッドを使うためにBufReaderに含む
// このnewもただの関連関数
let input = BufReader::new(file);
// BufReaderが実装するトレイトのBufReadにあるlinesメソッドを呼び出す
// 返り値はイテレータなのでfor式で繰り返しができる
for文で作業したいところですが、for文を使うにはイテレータが必要です。
lines関数により、一行ずつ取り出すイテレータを作成します。
また、行のパースに失敗したり、ファイルの中の文字コードの問題などにより失敗する可能性があります。そのためResult型を使います。
for line in input.lines() {
// 入力がUTF-8ではないなどの理由で、行のパースに失敗することがあるので、
// lineもResultに含まれている
let line = match line {
Ok(line) => line,
Err(e) => {
println!("An error occurred while reading a line {}", e);
return;
}
};
パターンマッチ
is_match関数を使って、先に作った正規表現のパターンを持つ変数reg
と、バッファから一行取り出した変数line
の内容を比較します。
if reg.is_match(&line) {
// パターンにマッチしたらプリントする
// is_matchはリードオンリーなので参照型を受け取る
println!("{}", line);
}
}
}
実行結果
```zsh:正規表現で[
をgrepする
cargo run --bin main -- '^[[]' Cargo.toml
Finished dev [unoptimized + debuginfo] target(s) in 1.17s
Running `target/debug/main '^[[]' Cargo.toml`
[package]
[dependencies]
[[bin]]
[[bin]]
grepすることが無事できました。
しかし、繰り返しますが写経なので自慢はできません。
# catコマンド
ここからはgrepコマンドを参考に自作した内容です。
引数は一つです。
catコマンドの主要な機能
1.引数にファイルパスを取る
2.指定されたファイルの中身を標準出力する
バッファの中身をどうやって出力するかが、色々分かれるところだと思います。
line()で一行ずつ取り出すイテレータを作成し、for式で標準出力しました。
## コード
```rust
use std::env;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
/**
* 自作catコマンド
* rsgrepを参考に作成する。
* rsgrepはfileを開き、バッファに入れ、それをパターンマッチした結果を返していた。
* そのため、パターンマッチをせず、そのまま標準出力する。
*/
// 使い方
fn usage() {
println!("usage : cat FILENAME");
}
fn main() {
// envモジュールのargs関数で、引数を取得
let filename = match env::args().nth(1) {
Some(filename) => filename, // Opthionであるためokにあらず
None => {
// Errにあらず
usage();
return;
}
};
// File構造体のopen関連関数でファイルを開ける
let file = match File::open(&filename) {
Ok(file) => file,
Err(e) => {
println!(
"ファイルを開く際にエラーが起きました。{}",e);
return;
}
};
// Fileを開いたまま使うと遅く、扱いづらいため、バッファにいれる
let input = BufReader::new(file); // reader
// inputを標準出力する
// lines()で一行ずつ取り出すイテレータを作成
for line in input.lines() {
//unwrapすると出力を簡易に安全に出力できるようだ。でもよくわからない。
//追記 この場合はただ、Ok()をunwrapして中身を取り出しているだけ。
println!("{}", line.unwrap());
// println!("{:?}", line);
}
}
出力結果
test.txtの内容を表示してみます。
hoge
fuga
(私はCargo.tomlでsrc/bin/cat.rsを指定して実行しています。普通はcargo run 引数1
というようになるはずです。)
cargo run --bin cat test.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/cat test.txt`
hoge
fuga
catできている!嬉しい!
cargo run --bin cat
Finished dev [unoptimized + debuginfo] target(s) in 0.12s
Running `target/debug/cat`
usage : cat FILENAME
寡黙は悪です。
エラー文を吐いてくれます。嬉しい!
cargo run --bin cat ''
Finished dev [unoptimized + debuginfo] target(s) in 8.35s
Running `target/debug/cat `
ファイルを開く際にエラーが起きました。No such file or directory (os error 2)
空文字列は境界条件になりやすいのでテストするといいらしいです。
zshは<(コマンド)
で結果を他のコマンドにわたすことができます。
catと自作コマンドの差分を見たいので、diffに渡してみます。
diff <(cargo run --bin cat src/bin/cat.rs) <(cat src/bin/cat.rs)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/cat src/bin/cat.rs`
差分がないので、出力結果が同じだとわかります。
感想
grepに関しては参考記事の写経ですが、catはそれをみて機能を削減する形で書いてみました。
Pythonと違ってふわっとした書き方ができないので、コンパイラにお世話になりまくりです。
特に変数を参照する際、&をつけるつけないは本当によくわかりません。なんとなく2回目使うなら&つけるみたいなイメージしかできていません。コンパイラに指摘されてつけてみたらなんとなく通るということを繰り返しています。
でもこれでC系の言語で起きる未定義動作が起きたりする心配がない(rustも未定義動作があるようですが)のですごいことです。
面白かったです。
Rust製のユーティリティコマンドのサンプルコードがあるサイトなどがあれば知りたいです。
参考文献
他にcatを実装された方
https://tomcky.hatenadiary.jp/entry/20181006/1538832958
http://saba1024.hateblo.jp/entry/2017/12/20/011335
unwrap
https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/error-handling.html#アンラップunwrap-とは