ちょっとしたプログラムを書く場合に、再帰処理を使うとプログラムをシンプルに作成できる場合があります。そのため、再帰処理が記述できる人とそうでない人では、プログラミング力に差ができます。今回は、再帰を分かりやすく理解する例として、ファイル検索ツールを作ってみましょう。
ファイル検索ツールを作ろう
今回は、再帰検索の例として、ワイルドカードを利用して、ファイルを検索するコマンドラインツールを作ってみましょう。最初に、カレントディレクトリにあるファイルの検索ツールを作り、その後で、再帰を利用してサブディレクトリの検索に対応するように改造してみましょう。
簡単なlsコマンドのようなものを作ってみよう
最初に、カレントディレクトリにあるファイルを検索するコマンドラインツールを作ってみましょう。せっかくなので、lsコマンドのようにワイルドカードが使えるようにしてみましょう。
ワイルドカードを実装する場合も、再帰処理を使うと比較的簡単に実装できるのですが、今回は、Rustのライブラリを利用することにします。Rustでは便利なライブラリがたくさん公開されており、crates.ioにアクセスして検索してみると、多くの便利なライブラリを見つけることができるでしょう。
「wildcard」や「wildcard_ex」という名前ずばりそのままのクレートがあります。今回は、「wildcard_ex」を使ってみましょう。実は、このクレート、筆者が先日、本コラムを書こうと思って作って公開したものです。このクレートを使うと、手軽にワイルドカードを使ったファイル検索が実現できます。
Rustでクレートをインストールするには、「cargo」というコマンドを使って手軽にインストールできます。これは、Rustのビルドシステムであり、パッケージマネージャーです。
ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して、以下のコマンドを実行します。
# プロジェクトのディレクトリを作成する
mkdir enum_files
cd enum_files
# プロジェクトを初期化して、wildcard_exを追加
cargo init
cargo add wildcard_ex
上記コマンドを実行すると、次のようなプロジェクトのひな形が生成されます。
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs
ファイル検索のプログラム
それでは、プログラムを作ってみましょう。src/main.rsを下記のように書き換えましょう。(なお、こちらからもにソースコードを取得できます。)
use wildcard_ex::is_match;
fn main() {
// コマンドライン引数を取得 --- (*1)
let args: Vec<String> = std::env::args().collect();
// 引数が1つ以上あるかチェック --- (*2)
if args.len() < 2 {
// 引数が1つもない場合はエラーメッセージを表示して終了
eprintln!("Usage: {} <検索パターン>", args[0]);
std::process::exit(1);
}
// 検索パターンを取得 --- (*3)
let pattern = &args[1];
// カレントディレクトリのファイル一覧を取得 --- (*4)
let entries = std::fs::read_dir(".").unwrap();
// ファイル一覧を1つずつ取り出して検索パターンにマッチするかチェック --- (*5)
for entry in entries {
let entry = entry.unwrap();
let fname = entry.file_name();
let fname = fname.to_str().unwrap();
if is_match(pattern, fname) { // ワイルドカードで検索 --- (*6)
println!("[発見] {}", fname);
}
}
}
プログラムを実行してみましょう。最初に、Rustのソースディレクトリにある「Cargo.*」にマッチするファイルを検索してみます。すると、「Cargo.toml」と「Cargo.lock」という2つのファイルを見つけて表示します。
# Rustのコンパイルと実行
cargo run "Cargo.*"
動作が確認できたら、プログラムも確認してみましょう。(*1)ではコマンドライン引数を取得します。そして、(*2)では引数が2つ未満かどうかをチェックします。ここで、args[0]には実行ファイルのファイルパスが代入されています。それで、検索パターンが指定されていない時には、使い方を表示してプログラムを終了します。
(*3)では検索パターンを取得して、(*4)でカレントディレクトリにあるファイル・ディレクトリの一覧を取得します。それから、(*5)では1つ1つのエントリを確認して、(*6)でワイルドカードのパターンにファイル名がマッチするか確認して、マッチした場合に画面にファイル名を出力します。
ちなみに、ファイルがワイルドカードのパターンにマッチするかを確認するis_match関数は次のように利用します。マッチすればtrueを、マッチしなければfalseを返します。
[書式] is_match関数
let b = is_match(パターン, ファイル名);
例えば、下記のように記述します。assert_eq!はRustのプログラムをテストするための標準マクロです。「assert_eq!(値1, 値2)」のように利用して、値1と値2が同じであることをテストできます。下記の例では、関数is_matchの2つの利用例がいずれもtrueを返すことをテストしています。
use wildcard_ex::is_match;
fn main() {
assert_eq!(is_match("*.txt", "abc.txt"), true);
assert_eq!(is_match("test*.txt", "test1234.txt"), true);
}
再帰的に検索できるように改良しよう
ここまでの部分で、単にカレントディレクトリにあるファイルを検索するツールを作りました。次に、コマンドライン引数に「-r」オプションがあれば、サブディレクトリの中も含めて検索するように改良してみましょう。
なお、OSのファイルシステムは、木構造になっています。ルートドライブの下にファイルやフォルダ(ディレクトリ)があり、フォルダを開くと、その下にまた、ファイルやフォルダがあります。ファイルやフォルダの様子を思い浮かべてみると、フォルダが太い枝であり、そこから複数の細い枝が伸びている様子に似ていることでしょう。こうした木構造のデータを全て検査したい場合には、再帰を使う必要があります。
この点を踏まえて、先ほどの、src/main.rsを次のように書き換えましょう。(こちらからもソースコードを参照できます)
use wildcard_ex::is_match;
fn main() {
// コマンドライン引数を取得 --- (*1)
let args: Vec<String> = std::env::args().collect();
// 引数が1つ以上あるかチェック
if args.len() < 2 {
// 引数が1つもない場合はエラーメッセージを表示して終了
eprintln!("Usage: {} [-r] <検索パターン>", args[0]);
std::process::exit(1);
}
// 引数に "-r"があれば、再帰的に検索する --- (*2)
let mut recursive = false;
let pattern = if &args[1] == "-r" {
recursive = true;
&args[2] // パターンを得る --- (*3)
} else { &args[1] };
// パターンに合うファイル一覧を取得 --- (*4)
let files = enum_files(".", pattern, recursive);
for file in files {
println!("[発見] {}", file);
}
}
// 再帰的にファイルを列挙する関数 --- (*5)
fn enum_files(dir: &str, pattern: &str, recursive: bool) -> Vec<String> {
let mut files = vec![];
let entries = std::fs::read_dir(dir).unwrap(); // 一覧を取得
for entry in entries {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() { // ディレクトリの場合 --- (*6)
if recursive { // 再帰的に検索
let mut subfiles = enum_files(
&path.to_string_lossy(), pattern, recursive);
files.append(&mut subfiles);
}
continue;
}
// ファイル名を取得してパターンにマッチするかチェック --- (*7)
let fname = entry.file_name().to_string_lossy().to_string();
if pattern == "" || is_match(pattern, &fname) {
files.push(entry.path().to_string_lossy().to_string());
}
}
files
}
プログラムをビルドにするには、ターミナルで下記のコマンドを実行します。
cargo build
すると、target/debugディレクトリに、「enum_files」(Windowsでは「enum_files.exe」)という実行ファイルが作成されます。
それで、ターミナルで、以下のように記述すると、カレントディレクトリ以下にある、拡張子が「.rs」のファイルを列挙します。
# Windowsの場合
.\target\debug\enum_files.exe -r "*.rs"
# macOSの場合
./target/debug/enum_files -r "*.rs"