【HomeBrew】初めてのRustでCLIツール開発
自己紹介
初めまして、フリーランスエンジニアのたおです!
今まで主にTypeScriptやPythonを使った開発をしてきましたが、最近話題のRustに興味を持ち、学習を兼ねて実用的なCLIツールを作ってみました。
この記事では、Rust初心者の私が実際にCLIツール「addpath」を開発する過程で学んだことや、つまずいたポイントをまとめていきます。
なぜaddpathを作ったのか
みなさんも経験があると思いますが、新しいツールをインストールした後に「PATHを通す」作業って地味に面倒ですよね。
# あれ、どこにインストールされたんだっけ...
$ find /usr -name "新しいツール" 2>/dev/null
# .zshrcを開いて...
$ vim ~/.zshrc
# パスを追加して...
export PATH="$PATH:/usr/local/bin/新しいツール"
# 反映させる
$ source ~/.zshrc
この一連の作業を自動化したい!そう思って作ったのがaddpath
です。
addpathの機能
addpath
は以下のような機能を持つCLIツールです:
- 指定したコマンド名でシステム内を検索
- 見つかったパスの候補を一覧表示
- ユーザーが選択したパスを自動的にシェルの設定ファイルに追加
- すでに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の作成手順
-
GitHubにtapリポジトリを作成
- リポジトリ名は
homebrew-tap
にするのが慣例 - 例:
https://github.com/Tao119/homebrew-tap
- リポジトリ名は
-
Formulaファイルを作成
-
Formula/addpath.rb
のようなファイルを作成 - 以下は基本的なFormulaの例:
-
class Addpath < Formula
desc "Automatically adds paths to your shell configuration"
homepage "https://github.com/Tao119/addpath"
url "https://github.com/Tao119/addpath/archive/refs/tags/v0.1.2.tar.gz"
sha256 "実際のSHA256ハッシュ値"
license "MIT"
depends_on "rust" => :build
def install
system "cargo", "install", *std_cargo_args
end
test do
system "#{bin}/addpath", "--help"
end
end
-
GitHubでリリースを作成
- ソースコードのtarballが自動生成される
- このURLをFormulaで使用
-
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を追加せずに直接インストール
$ brew install Tao119/tap/addpath
個人tapのメリット
- 即座に配布可能: 公式リポジトリへのPRを待つ必要がない
- 自由な更新: 自分のペースでアップデートできる
- テスト環境として: 公式公開前の動作確認に使える
注意点
- 個人tapは公式のレビューを受けていないため、利用者は自己責任で使用
- 依存関係の管理に注意が必要
- セキュリティアップデートは自分で管理する必要がある
実際に使ってみる
インストール
HomeBrewを使ってインストールできます:
# 私の個人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で公開しています:
リポジトリには以下が含まれています:
- 完全なソースコード
- READMEファイル
- ライセンス情報
- リリース版のバイナリ
今後の改善点
- 並列処理を使った検索の高速化
- より多くのシェル(fish、PowerShellなど)への対応
- 設定ファイルのバックアップ機能
- より詳細なログ出力
みなさんもRustでCLIツール開発、始めてみませんか?
この記事が参考になりましたら、いいねやストックをお願いします!
質問やコメントもお待ちしています。
Discussion