😎

Rust で作る!簡単なコマンドライン検索ツール(grep クローン)

に公開

この記事で学べること

この記事では、Rustを使って実用的なコマンドライン検索ツールを段階的に作成していきます。
完成品はgrepコマンドの簡易版で、以下の機能を最終的には実装します。

  • ファイル内の文字列検索
  • 一致した行の行番号表示
  • 検索結果のカラーハイライト

学習できるRustの技術

  • ファイル I/O の基本
  • コマンドライン引数の処理
  • エラーハンドリング

前提条件

必要な知識

  • Rustの基本文法(変数、ループ、match式、構造体)
  • 所有権とボローイングの基本概念

必要な環境

  • Rust開発環境(rustup, rustc, cargoがインストール済み)

私のような初学者の方へ
分からない部分があっても大丈夫です!コードを実際に書きながら理解を深めていきましょう!

Step 1: プロジェクトのセットアップ

まず、新しいRustプロジェクトを作成しましょう。

cargo new rust_minigrep
cd rust_minigrep

解説

  • cargo new:新しいRustプロジェクトを作成するコマンド
  • rust_minigrep:プロジェクト名(お好みで変更可能)

作成されるディレクトリ構造

rust_minigrep/
├── Cargo.toml    # プロジェクトの設定ファイル
├── src/
│   └── main.rs   # メインのソースコード

Step 2: 基本的な検索機能の実装

最初は、シンプルな文字列検索機能から始めましょう。

2-1. 基本版のコード

src/main.rsを以下のように編集します

use std::env;
use std::fs::File;
use std::io::{self, BufRead};

fn main() {
    // コマンドライン引数を取得
    let args: Vec<String> = env::args().collect();

    // 引数の数をチェック(プログラム名 + 検索語 + ファイル名 = 3個)
    if args.len() != 3 {
        eprintln!("使い方: cargo run <検索語> <ファイル名>");
        std::process::exit(1);
    }

    let query = &args[1];      // 検索したい文字列
    let file_path = &args[2];  // 検索対象のファイルパス

    // ファイルを開く(エラーハンドリング付き)
    let file = match File::open(file_path) {
        Ok(file) => file,
        Err(err) => {
            eprintln!("ファイル '{}' を開けませんでした: {}", file_path, err);
            std::process::exit(1);
        }
    };

    // 効率的にファイルを一行ずつ読むためのBufReaderを作成
    let reader = io::BufReader::new(file);

    // ファイルの各行をチェック
    for line in reader.lines() {
        match line {
            Ok(line_content) => {
                // 行に検索語が含まれているかチェック
                if line_content.contains(query) {
                    println!("{}", line_content);
                }
            }
            Err(err) => {
                eprintln!("ファイル読み込みエラー: {}", err);
                break;
            }
        }
    }
}

2-2. コードの詳細解説

🔍 コマンドライン引数の処理

let args: Vec<String> = env::args().collect();
  • env::args():プログラムに渡されたコマンドライン引数を取得
  • .collect():イテレータをVec<String>に変換
  • args[0]にはプログラム名、args[1]以降に実際の引数が入る

📁 ファイルの効率的な読み込み

let reader = io::BufReader::new(file);
  • BufReaderを使うことで、大きなファイルでもメモリ効率よく一行ずつ処理可能
  • 内部でバッファリングされるため、パフォーマンスが向上

エラーハンドリング

match File::open(file_path) {
    Ok(file) => file,
    Err(err) => { /* エラー処理 */ }
}
  • RustのResult型を使った安全なエラーハンドリング
  • ファイルが存在しない場合や読み込み権限がない場合に適切にエラーメッセージを表示

2-3. 動作確認

テスト用のファイルを作成して、動作を確認してみましょう。

test.txt を作成

Hello Rust!
This is a test file.
Rust is a systems programming language.
Python is also great.
Have a nice day with Rust.

実行

cargo run Rust test.txt

期待される出力

Hello Rust!
Rust is a systems programming language.
Have a nice day with Rust.

Step 3: 見た目を改善(行番号とカラー表示)

基本機能ができたので、次は検索結果をより見やすくしましょう。

3-1. 依存関係の追加

文字の色付けにはcoloredクレートを使用します。

cargo add colored

クレートとは? Rustにおけるライブラリのことです。cargo addコマンドで簡単に追加できます。

3-2. 改良版のコード

src/main.rsを以下のように更新します

use std::env;
use std::fs::File;
use std::io::{self, BufRead};
use colored::*; // カラー表示機能をインポート

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() != 3 {
        eprintln!("使い方: cargo run <検索語> <ファイル名>");
        std::process::exit(1);
    }

    let query = &args[1];
    let file_path = &args[2];

    let file = match File::open(file_path) {
        Ok(file) => file,
        Err(err) => {
            eprintln!("ファイル '{}' を開けませんでした: {}", file_path, err);
            std::process::exit(1);
        }
    };

    let reader = io::BufReader::new(file);

    // .enumerate()で行番号(インデックス)も同時に取得
    for (index, line) in reader.lines().enumerate() {
        match line {
            Ok(line_content) => {
                if line_content.contains(query) {
                    // 検索語を赤色・太字でハイライト
                    let highlighted_query = query.red().bold().to_string();
                    let highlighted_line = line_content.replace(query, &highlighted_query);

                    // 行番号(1から開始)を緑色で表示
                    println!("{}: {}", 
                        (index + 1).to_string().green().bold(), 
                        highlighted_line
                    );
                }
            }
            Err(err) => {
                eprintln!("ファイル読み込みエラー: {}", err);
                break;
            }
        }
    }
}

3-3. 新機能の解説

行番号の追加

for (index, line) in reader.lines().enumerate() {
  • .enumerate():イテレータに連番を付ける便利なメソッド
  • (index, line):タプル分割代入で、インデックスと行内容を同時に取得
  • index + 1:0から始まるインデックスを1から始まる行番号に変換

カラーハイライト

let highlighted_query = query.red().bold().to_string();
let highlighted_line = line_content.replace(query, &highlighted_query);
  • .red().bold():文字を赤色・太字に装飾
  • .replace():文字列内の検索語を装飾版に置き換え

色付けのオプション

  • .red(), .green(), .blue(), .yellow() など
  • .bold(), .italic(), .underline() など
  • 組み合わせ可能:.blue().bold().underline()

Step 4: 実際に使ってみよう

4-1. テストファイルの準備

より充実したテストファイルを作成します。
以下は仮に作成したコードなので実際はなんでも良いです。

sample_code.rs

fn main() {
    println!("Hello, Rust!");
    let x = 5;
    let y = 10;
    println!("x + y = {}", x + y);
}

struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn new(name: String, age: u32) -> Person {
        Person { name, age }
    }
    
    fn greet(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

4-2. 様々な検索を試してみる

# "fn" を検索(関数定義を探す)
cargo run fn sample_code.rs

# "println!" を検索(出力文を探す)
cargo run println! sample_code.rs

# "String" を検索(String型の使用箇所を探す)
cargo run String sample_code.rs

期待される出力例("fn"を検索した場合)

1: fn main() {
8: fn new(name: String, age: u32) -> Person {
13: fn greet(&self) {

Step 5: エラーケースの理解

実際では、様々なエラーケースに遭遇します。
このツールがどのように対処するかを確認してみましょう。

5-1. よくあるエラーとその対処

存在しないファイルを指定した場合

cargo run test nonexistent.txt

出力: ファイル 'nonexistent.txt' を開けませんでした: No such file or directory (os error 2)

引数が不足している場合

cargo run test

出力: 使い方: cargo run <検索語> <ファイル名>

検索語が見つからない場合
何も出力されません(これは正常な動作)

追加(できれば良いかも):コードをより良くする改善案

改善案1: 関数分割によるコードの整理

fn search_in_file(query: &str, file_path: &str) -> io::Result<()> {
    let file = File::open(file_path)?;
    let reader = io::BufReader::new(file);

    for (index, line) in reader.lines().enumerate() {
        let line_content = line?;
        if line_content.contains(query) {
            let highlighted_query = query.red().bold().to_string();
            let highlighted_line = line_content.replace(query, &highlighted_query);
            println!("{}: {}", 
                (index + 1).to_string().green().bold(), 
                highlighted_line
            );
        }
    }
    Ok(())
}

改善案2: 設定構造体の導入

struct Config {
    query: String,
    file_path: String,
    case_sensitive: bool,
}

impl Config {
    fn new(args: Vec<String>) -> Result<Config, &'static str> {
        if args.len() != 3 {
            return Err("引数が不足しています");
        }
        
        Ok(Config {
            query: args[1].clone(),
            file_path: args[2].clone(),
            case_sensitive: true,
        })
    }
}

さらなる発展:機能拡張のアイデア

初学者向け拡張

  1. 大文字・小文字を無視する検索

    • line_content.to_lowercase().contains(&query.to_lowercase())を使用
  2. 一致した行数のカウント表示

    • 検索終了後に「○件見つかりました」と表示

挑戦者向け拡張

  1. 正規表現サポート

    • regexクレートを使用して高度なパターンマッチング
  2. 複数ファイル対応

    • ワイルドカード(*.txt)やディレクトリの再帰的検索
  3. 設定オプション

    • -i(大文字小文字無視)、-n(行番号表示/非表示)など

実行例とトラブルシューティング

基本的な使用例

準備

# テストファイルを作成
echo -e "Hello Rust!\nThis is a test file.\nRust is awesome!\nPython is also great.\nHave a nice day." > test.txt

実行

cargo run Rust test.txt

出力

1: Hello Rust!
3: Rust is awesome!

よくある問題と解決方法

問題 原因 解決方法
cargo: command not found Rustがインストールされていない rustup.rsからRustをインストール
No such file or directory ファイルパスが間違っている ファイルの存在と正しいパスを確認
何も出力されない 検索語が見つからない 大文字小文字や綴りを確認
文字化け ファイルの文字エンコーディング UTF-8形式で保存されているか確認

学習のポイント

Rustらしい書き方について

Good(Rustっぽい)

match File::open(file_path) {
    Ok(file) => file,
    Err(err) => {
        eprintln!("エラー: {}", err);
        std::process::exit(1);
    }
}

避けたい書き方

let file = File::open(file_path).unwrap(); // パニックの可能性

パフォーマンスを意識した設計

  • BufReaderを使用することで、大きなファイルでもメモリ効率が良い
  • 一行ずつ処理するため、ファイル全体をメモリに読み込む必要がない
  • contains()メソッドは効率的な文字列検索を提供

まとめ

おつかれさまでした!この記事では以下のことを学びました。

技術的な学習内容

  • Rustでのファイル操作とI/O処理
  • コマンドライン引数の安全な処理方法
  • 外部クレートの導入と活用
  • 実用的なエラーハンドリング

実践的なスキル

  • 段階的な機能開発のアプローチ
  • ユーザビリティを考慮した設計
  • テストとデバッグの方法

次のステップ

  1. コードの理解を深める:各行の動作を詳しく調べてみる
  2. 機能拡張に挑戦:上記の拡張アイデアを実装してみる
  3. 他のツール作成:学んだ知識を活かして別のCLIツールを作成

参考リソース

この記事が、Rust学習の手助けになれば嬉しいです!
何か質問があれば、遠慮なくお聞きください!

Discussion