はじめに
Ctrl+R で履歴を検索するたびに思っていたことがある。
「このコマンド、また使いたいんだけど、なんて打ったっけ。」
atuin も fzf も hstr も試した。どれも優秀だった。でも根本的な問題は解決しなかった。履歴は「全部」記録する。自分が必要なのは「また使いたい一部」だけ。
10,000件の履歴から1件を見つける作業を繰り返すうちに、発想が逆転した。
「検索する」じゃなくて「育てる」ツールを作ればいい。
それが rcmd です。
rcmd とは
CLI のための個人コマンド知識ベース。
覚えておきたいコマンドだけを手元に残し、{{プレースホルダー}} でテンプレート化して、タグと全文検索で呼び出す。ストレージは TOML ファイル1枚なので dotfiles で複数マシンに同期できる。
# 保存する
rcmd save "docker run --rm -it -v {{path}}:/work {{image}} bash" \
-d "カレントディレクトリをマウントしてインタラクティブシェル" \
-t docker -t dev
# 呼び出す
rcmd get k3f9 --set path=$PWD --set image=alpine
# そのまま実行する
rcmd run k3f9 --set path=. --set image=alpine
atuin / fzf との違い
| atuin / fzf / hstr | rcmd | |
|---|---|---|
| 対象 | 全履歴 | 手動で選んだコマンド |
| アプローチ | 検索 | キュレーション |
| テンプレート | なし |
{{placeholder}} 置換あり |
| 説明・タグ | なし | あり |
| ストレージ | DB / バイナリ | TOML(手編集可) |
両方使えばいい、というのが自分のスタンス。atuin は「あのコマンドどこ行った」を救う。rcmd は「このコマンドは残しておく」を管理する。
なぜ作ったか
きっかけ:docker コマンドが壊れていた
docker の volume マウント付き interactive シェルを毎回ゼロから書いていた。--rm -it -v $(pwd):/work を何度入力したか数えたくない。
メモアプリに貼っておいたこともあった。でも貼ったまま忘れる。ノート検索よりシェルの方が速い。
既存ツールへの不満
-
alias→ 引数が固定になる。汎用化しにくい -
function→.zshrcが育ちすぎる。管理が辛い - ノートアプリ → コンテキストスイッチが発生する
-
.shファイル群 → どこに置いたか忘れる
「コマンドライン内で完結して、テンプレート化できて、検索できる」ものがなかった。
主な機能
{{プレースホルダー}} 置換
コマンドにプレースホルダーを埋め込んで、呼び出し時に値を注入する。
# 保存時
rcmd save "ffmpeg -i {{input}} -vf scale={{width}}:-1 {{output}}" \
-d "動画リサイズ" -t ffmpeg
# 呼び出し時
rcmd run a1b2 --set input=video.mp4 --set width=720 --set output=out.mp4
# --dry-run でプレビューしてから実行
rcmd run a1b2 --set input=video.mp4 --set width=720 --set output=out.mp4 --dry-run
--dry-run は地味に便利で、破壊的操作の前に展開結果を確認できる。
タグ & 全文検索
# タグで絞り込む
rcmd list -t docker
# キーワードで検索(1行出力なので fzf と組み合わせやすい)
rcmd search volume
# 詳細表示
rcmd show k3f9
出力を1行フォーマットにしたのは意図的な設計。fzf にパイプしてインタラクティブに選べる。
# fzf と組み合わせた例
rcmd search docker | fzf | awk '{print $1}' | xargs rcmd show
シェル履歴からの一括インポート
rcmd history --unique --reverse | fzf | rcmd save -d "説明を書く" -t imported
まっさらから始める必要はない。今ある履歴から「また使いたいもの」を選んで登録できる。
TOML ストレージ
~/.config/rcmd/store.toml # macOS / Linux
%APPDATA%\rcmd\store.toml # Windows
中身はこんな感じ:
[commands.k3f9]
command = "docker run --rm -it -v {{path}}:/work {{image}} bash"
description = "カレントディレクトリをマウントしてインタラクティブシェル"
tags = ["docker", "dev"]
created_at = "2026-06-01T12:00:00"
プレーンテキストなので手で編集できる。$RCMD_STORE 環境変数でパスを上書きすれば、dropbox や dotfiles リポジトリの任意の場所に置ける。
技術選定
Python + typer
依存は typer のみ。
typer を選んだ理由は3つ:
-
型アノテーションだけでCLIが生える —
argparseのボイラープレートを書かなくていい -
--helpが自動で綺麗 — ドキュメントを別で書かなくていい -
uv と相性がいい —
uv sync && uv run rcmd --helpで即起動
import typer
app = typer.Typer()
@app.command()
def save(
command: str,
description: str = typer.Option("", "-d", "--description"),
tags: list[str] = typer.Option([], "-t", "--tag"),
):
"""コマンドを保存する"""
...
型ヒントを書くだけでオプション・ヘルプ・バリデーションが揃う。CLIツールを Python で作るなら今は typer が最短距離だと思う。
ストレージに TOML を選んだ理由
SQLite も検討した。でも TOML にした。
SQLite の問題:
- バイナリなので git diff が読めない
- 手編集できない
- dotfiles に入れても差分が追いにくい
TOML の問題:
- 大量データには向かない(個人用途なので問題なし)
- インデックスがない(全文検索は Python 側で処理)
個人コマンド集なら数百件が上限。SQLite のメリットが薄く、TOML の可読性・可搬性の方が価値が高いと判断した。
ID の生成
各コマンドには k3f9 のような短いランダムIDを付与している。
import secrets
import string
def generate_id(length: int = 4) -> str:
chars = string.ascii_lowercase + string.digits
return ''.join(secrets.choice(chars) for _ in range(length))
UUID は長すぎて入力しにくい。4文字のランダム英数字で衝突確率は約 0.06%(1,679,616通り)。コマンド数が数百件の個人用途では十分。
インストール
git clone https://github.com/ymatsuza/rcmd.git && cd rcmd
uv sync
uv run rcmd --help
uv がない場合は astral.sh/uv から。
グローバルで使いたい場合:
# PATH が通った場所にシンボリックリンクを張る
ln -s $(pwd)/bin/rcmd ~/.local/bin/rcmd
実際の使い方(自分の場合)
週1で棚卸しする
rcmd history --unique --reverse | fzf | rcmd save を週1で走らせて、先週のコマンドから「残しておきたいもの」を登録する。
タグは用途より文脈で付ける
docker、ffmpeg のようなツール名タグよりも、work、hobby、setup のような文脈タグの方が後から見つけやすかった。両方付けるのが今のところ最善。
dotfiles で同期する
$RCMD_STORE=~/dotfiles/rcmd/store.toml を .zshrc に書いて dotfiles リポジトリに入れている。マシンを変えても git pull で全部復元する。
今後やりたいこと
-
rcmd exportでバックアップ・インポート - シェル補完(zsh / bash / fish)
-
rcmd runの実行ログ(いつ何を実行したかの追跡) - Web UI(ローカルで立ち上げてブラウザから管理)
まとめ
シェル履歴の「検索」ではなく「キュレーション」を目的にしたツールを作った。
作ってから3ヶ月、自分の一番使うCLIツールのひとつになった。docker run を毎回ゼロから打つストレスがなくなった。
フィードバック・PR・star、歓迎しています。