3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

初めまして、フリーランスエンジニアのたおです!

今まで主にTypeScriptやPythonを使った開発をしてきましたが、最近話題のRustに興味を持ち、学習を兼ねて実用的なCLIツールを作ってみました。

この記事では、Rust初心者の私が実際にCLIツール「addpath」を開発する過程で学んだことや、つまずいたポイントをまとめていきます。

なぜaddpathを作ったのか

みなさんも経験があると思いますが、新しいツールをインストールした後に「PATHを通す」作業って地味に面倒ですよね。

# どこにインストールされたか確認
$ find /usr -name "新しいツール" 2>/dev/null
# パスをリソースファイルに追加
$ echo export PATH="$PATH:/usr/local/bin/新しいツール" >> ~/.zshrc
# 反映
$ source ~/.zshrc

この一連の作業を自動化したい!そう思って作ったのがaddpathです。

addpathの機能

addpathは以下のような機能を持つCLIツールです:

  1. 指定したコマンド名でシステム内を検索
  2. 見つかったパスの候補を一覧表示
  3. ユーザーが選択したパスを自動的にシェルの設定ファイルに追加
  4. すでにPATHに追加されているものは色分けして表示

使い方

# 例:nodeのパスを探して追加する
$ addpath node

# 追加の検索ディレクトリを指定することも可能
$ addpath python --adddir /home/user/local

Rustプロジェクトの作成

まずは新しいRustプロジェクトを作成します。

$ cargo new addpath
$ cd addpath

必要なクレート(ライブラリ)の選定

Rustでは外部ライブラリのことを「クレート」と呼びます。今回使用したクレートをCargo.tomlに追加します:

[package]
name = "addpath"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = "3.0"        # コマンドライン引数のパース
walkdir = "2.3"     # ディレクトリの再帰的な探索
dirs = "3.0"        # ホームディレクトリなどの取得
rayon = "1.10"      # 並列処理(今回は使用していませんが、将来の拡張用)
crossterm = "0.27"  # ターミナル操作
colored = "2.0"     # 文字列の色付け

実装の詳細

1. コマンドライン引数の処理

Rustでコマンドライン引数を扱うにはclapクレートが便利です。

use clap::{App, Arg};

fn main() {
    let matches = App::new("addpath")
        .version("1.0")
        .author("Tao119")
        .about("Automatically adds paths to your shell configuration")
        .arg(
            Arg::with_name("pkgname")
                .help("The package name to search for")
                .required(true)
                .index(1),
        )
        .arg(
            Arg::with_name("adddir")
                .help("Additional directory to include in the search path")
                .long("adddir")
                .takes_value(true)
                .multiple(true),
        )
        .get_matches();

    let pkgname = matches.value_of("pkgname").unwrap();
}

2. すでにPATHに存在するかチェック

whichコマンドを使って、指定されたコマンドがすでにPATHに存在するかチェックします:

use std::process::Command;

if let Ok(output) = Command::new("which").arg(pkgname).output() {
    if !output.stdout.is_empty() {
        println!("{} is already in the PATH.", pkgname);
        return;
    }
}

3. ディレクトリの再帰的な探索

walkdirクレートを使って、指定されたディレクトリを再帰的に探索します:

use walkdir::{DirEntry, WalkDir};

for entry in WalkDir::new(dir)
    .into_iter()
    .filter_entry(is_not_skippable)
    .filter_map(Result::ok)
{
    if entry.file_type().is_dir() && entry.file_name() == "bin" {
        // binディレクトリを見つけたら、その中を探索
        for sub_entry in WalkDir::new(entry.path())
            .max_depth(1)
            .into_iter()
            .filter_map(Result::ok)
        {
            if sub_entry.file_name().to_string_lossy().contains(pkgname) {
                candidates.push(entry.clone().into_path());
            }
        }
    }
}

特定のディレクトリ(/dev/proc/sys)はスキップするようにしています:

fn is_not_skippable(entry: &DirEntry) -> bool {
    let skip_dirs = ["dev", "proc", "sys"];
    !entry
        .path()
        .components()
        .any(|c| skip_dirs.contains(&c.as_os_str().to_str().unwrap()))
}

4. 見つかったパスの表示

coloredクレートを使って、見つかったパスを色分けして表示します:

use colored::*;

for (index, path) in candidates.iter().enumerate() {
    let path_str = format!("{}", path.display());
    if existing_contents.contains(&path.display().to_string()) {
        println!(
            "{}: {} {}",
            index,
            path_str.bright_black(),
            "(already exists)".to_string().red()
        );
    } else {
        println!("{}: {}", index, path_str.bright_yellow());
    }
}

5. シェル設定ファイルへの追加

選択されたパスを、使用しているシェルに応じて適切な設定ファイルに追加します:

let shell_path = env::var("SHELL").unwrap_or_default();
let config_file = if shell_path.ends_with("/bash") {
    "bashrc"
} else if shell_path.ends_with("/zsh") {
    "zshrc"
} else {
    eprintln!("Unsupported shell");
    return;
};

// ファイルに追記
fn append_to_file(file_path: PathBuf, content: &str) {
    let mut file = OpenOptions::new()
        .append(true)
        .open(file_path)
        .expect("Failed to open file");
    if let Err(e) = writeln!(file, "{}", content) {
        eprintln!("Failed to write to file: {}", e);
    }
}

Rust初心者がつまずいたポイント

1. 所有権とライフタイム

Rustの最大の特徴である所有権システムは、最初は理解が難しかったです。例えば、clone()を使わずに値を移動しようとしてコンパイルエラーになることが多々ありました。

// エラーになる例
let path = entry.into_path();
candidates.push(path);
// pathをもう一度使おうとするとエラー

// 解決策
candidates.push(entry.clone().into_path());

2. エラーハンドリング

RustではResult型を使ったエラーハンドリングが基本です。unwrap()を多用するとパニックの原因になるので、適切にエラー処理をする必要があります。

// 危険な例
let config_contents = read_to_string(&config_path).unwrap();

// より安全な例
let config_contents = read_to_string(&config_path).unwrap_or_default();

3. 文字列の扱い

RustにはString&strという2つの文字列型があり、使い分けに苦労しました。

// &str から String への変換
let s: String = "hello".to_string();
// または
let s: String = String::from("hello");

// String から &str への変換
let s: String = String::from("hello");
let s_ref: &str = &s;

HomeBrewでの個人公開

実は、このツールはすでにHomeBrewで個人的に公開しています!HomeBrewには「tap」という仕組みがあり、個人のリポジトリからFormulaを配布することができます。

HomeBrewのtapとは?

tapは、HomeBrewの公式リポジトリ以外からFormulaを配布するための仕組みです。個人や組織が独自のtapを作成して、自作のツールを配布できます。

個人tapの作成手順

  1. GitHubにtapリポジトリを作成

    • リポジトリ名はhomebrew-tapにするのが慣例
    • 例:https://github.com/Tao119/homebrew-addpath
  2. Formulaファイルを作成

    • Formula/addpath.rbのようなファイルを作成
    • 以下は実際のFormulaの例:
class Addpath < Formula
  desc "cli path utility written in Rust"
  homepage "https://github.com/Tao119/addpath"
  url "https://github.com/Tao119/addpath/releases/download/release0.1.2/addpath-0.1.2-x86_64-apple-darwin.tar.gz"
  sha256 "72d9596da6b049662a28dc68cc867340034e9a23b223d15ef4a6e5db0f2d4b70"
  version "0.1.2"

  def install
    bin.install "addpath"
  end
end
  1. GitHubでリリースを作成

    • ソースコードのtarballが自動生成される
    • このURLをFormulaで使用
  2. SHA256ハッシュ値を取得

# リリースのtarballをダウンロードしてハッシュ値を計算
$ curl -L https://github.com/Tao119/addpath/archive/refs/tags/v0.1.2.tar.gz | shasum -a 256

インストール方法

個人のtapからインストールする場合は、以下のようにユーザー名を指定します:

# tapを追加(初回のみ)
$ brew tap Tao119/tap

# インストール
$ brew install Tao119/tap/addpath

個人tapのメリット

  • 即座に配布可能: 公式リポジトリへのPRを待つ必要がない
  • 自由な更新: 自分のペースでアップデートできる
  • テスト環境として: 公式公開前の動作確認に使える

注意点

  • 個人tapは公式のレビューを受けていないため、利用者は自己責任で使用
  • 依存関係の管理に注意が必要
  • セキュリティアップデートは自分で管理する必要がある

実際に使ってみる

インストール

HomeBrewを使ってインストールできます:

# 私の個人tapからインストール
$ brew tap Tao119/tap
$ brew install Tao119/tap/addpath

または、Rustの環境がある場合はソースからビルド:

$ git clone https://github.com/Tao119/addpath.git
$ cd addpath
$ cargo install --path .

使用例

例えば、nodeのパスを探して追加する場合:

$ addpath node

Searching in directories: ["/usr", "/opt"]
Checking directory: /usr
Checking directory: /opt
0: /usr/local/bin (already exists)
1: /opt/homebrew/bin
Select the path to add by number: 1

Added the following line to your zshrc file:
export PATH="$PATH:/opt/homebrew/bin"

finished setting the path!
Please run the following command to update your shell environment:
source /Users/username/.zshrc

初めてのRust開発でしたが、以下のような学びがありました:

  • 型安全性: コンパイル時にほとんどのエラーが検出されるので、実行時エラーが少ない
  • パフォーマンス: システムプログラミング言語だけあって、実行速度が速い
  • エコシステム: cargo.ioには豊富なクレートがあり、必要な機能を簡単に追加できる
  • 学習曲線: 所有権やライフタイムなど、独特の概念の理解には時間がかかる

Rustは学習コストは高いですが、一度慣れると安全で高速なプログラムが書けるようになります。CLIツールの開発は、Rustの学習に最適な題材だと感じました。

ソースコード

完全なソースコードはGitHubで公開しています:
https://github.com/Tao119/addpath

今後の改善点

  • 並列処理を使った検索の高速化
  • より多くのシェル(fish、PowerShellなど)への対応
  • 設定ファイルのバックアップ機能
  • より詳細なログ出力

みなさんもRustでCLIツール開発、始めてみませんか?


この記事が参考になりましたら、いいねやストックをお願いします!
質問やコメントもお待ちしています。

3
4
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
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?