7rikazhexde’s tech log

技術的な興味関心、備忘録、アウトプットなどを書いています。

【ShellScript】全履歴から選択実行するシェル関数について

はじめに

Bashを使っていると、過去に実行したコマンドを再利用したい場面が頻繁にあります。Ctrl+Rで履歴を検索できますが、より直感的で柔軟な履歴検索を実現するシェル関数を作成しました。

この記事では、fzf(fuzzy finder)を活用した履歴検索関数hg()の実装について、技術的な詳細を解説します。

完全なコードはGistに投稿していますので、そちらもご確認ください。

背景と課題

Bashの履歴検索の制約

コマンドライン作業で過去のコマンドを再利用する際、Bashの標準的なCtrl+R(reverse-i-search)では以下の制約があります。

  • 1行ずつしか表示されず、全体を俯瞰できない
  • 複数の検索ワードでの絞り込みが難しい
  • 視覚的なフィードバックが限定的

fzfのkey bindings(Ctrl+R

fzf(fuzzy finder)は、Goで実装された高速なコマンドラインフィルタリングツールです。fzfインストール時にkey bindingsを有効にするとCtrl+Rの動作がfzfを使用した履歴検索を使用できるようになります。

インストール時の出力
Downloading bin/fzf ...
  - Already exists
  - Checking fzf executable ... 0.66.0
Do you want to enable fuzzy auto-completion? ([y]/n) y
Do you want to enable key bindings? ([y]/n) y #key bindingsの有効化指定

Generate $HOME/.fzf.bash ... OK

Do you want to update your shell configuration files? ([y]/n) y

Update $HOME/.bashrc:
  - [ -f ~/.fzf.bash ] && source ~/.fzf.bash
    - Already exists:
        Line 218:[ -f ~/.fzf.bash ] && source ~/.fzf.bash
    ~ Skipped

Finished. Restart your shell or reload config file.
   source ~/.bashrc  # bash

Use uninstall script to remove fzf.
比較表
機能 標準のCtrl+R fzf版Ctrl+R
表示 1行ずつ 複数行を一覧
検索 部分一致 fuzzy検索
操作 Ctrl+Rで遡る ↑↓で自由に移動
複数ワード 不可 可能

具体例は下記の通りです。

# fzf版Ctrl+R
  docker compose up -d
  docker ps -a
  docker logs container_name
> docker
  3/150
# 全履歴から"docker"を含むものを一覧表示

hg()関数について

fzfのCtrl+Rは便利ですが、さらに以下の機能が欲しいと考えました。

  • 引数で初期フィルタを指定(hg "docker"で即座に絞り込み)
  • 完全一致検索("run"で検索時、"running"を除外)
  • 選択後に即実行(Ctrl+Rはコマンドライン挿入のみ)
  • 重複の自動削除
  • カスタムカラー設定

以降は、これらを実現するhg()関数の実装について解説します。

実装の全体像

前提条件

# fzfのインストール(Ubuntu&Git)
# --allオプションにより、キーbindingsと補完機能が自動的にシェル設定ファイル(`~/.bashrc`)に追加される。
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install --all

# macOS
brew install fzf

基本構造

hg() {
    # 1. オプション解析(-p フラグの判定)
    # 2. 履歴取得とフィルタリング
    # 3. fzfによる対話的選択
    # 4. 選択されたコマンドの実行
}

技術的な実装詳細

1. オプション解析の仕組み

local partial=false
local query=""

while [[ $# -gt 0 ]]; do
    case $1 in
        -p|--partial)
            partial=true
            shift
            ;;
        *)
            query="$1"
            shift
            ;;
    esac
done

技術ポイント

  • $#: 引数の数を取得
  • shift: 引数リストを左にシフト(処理済み引数を削除)
  • case構文: パターンマッチングで-pオプションを判定

2. 履歴処理のパイプライン

各コマンドの役割

history

  • Bashの組み込みコマンド
  • ~/.bash_historyと現在のセッション履歴をマージして出力
  • 出力形式は行番号 コマンドの形式

GREP_COLORS='mt=01;36' grep --color=always -w "$query"

  • GREP_COLORS='mt=01;36'でマッチ部分を明るいシアン色(36)でハイライト
    • mtはmatch(マッチ部分)を示す
    • 01はbold属性
    • 36はシアン色のカラーコード
  • --color=alwaysでパイプ経由でも色情報を保持
  • -wで単語境界でマッチ(完全一致)
    • 例として"run"で検索時、"running"は除外される

tac

  • テキストを行単位で逆順にする(catの逆)
  • 最新のコマンドが上に来るようにする

sed 's/^[ ]*[0-9]*[ ]*//'

  • 正規表現で行番号を削除する処理
    • ^[ ]*は行頭の空白(0個以上)
    • [0-9]*は数字(0個以上)
    • [ ]*は数字後の空白(0個以上)

awk '!seen[$0]++'

  • 重複行の削除を行う
  • 動作の仕組み
  • seen[$0] = 0 (初回) → !0 = true → 出力
  • seen[$0]++ で seen[$0] = 1 に
  • seen[$0] = 1 (2回目以降) → !1 = false → 出力しない
  • 連想配列seenで各行の出現回数を管理

fzf --height 40% --reverse --exact --ansi

  • --height 40%で画面の40%の高さで表示
  • --reverseで検索バーを上部に配置(デフォルトは下部)
  • --exactで完全一致モード(fuzzy検索を無効化)
  • --ansiANSIカラーコードを解釈して色付き表示

sed 's/\x1b\[[0-9;]*m//g'

  • ANSIカラーコード(エスケープシーケンス)を削除
  • \x1bはESCキャラクタ(16進数表記)
  • \[[0-9;]*mはカラーコードのパターン
  • 実行時にカラーコードが含まれないようにする処理

3. 履歴の一覧の取得方法

完全一致モードと部分一致モードの2種類のモードで実行します。

完全一致モード(デフォルト)

hg "run"
# fzf内で "test" と入力
# → "run" AND "test" を両方含む行を表示
  • grep -w: 単語として完全一致
  • fzf --exact: 入力文字列が順番通りに連続して含まれる行のみ

部分一致モード(-pオプション)

hg -p "run"
# fzf内で "test" と入力
# → r, u, n, t, e, s, t を順番に含む行を表示
  • grep-wなし): 部分一致
  • fzf--exactなし): 文字が順番に含まれていればマッチ

4. カラーハイライトの実装

GREP_COLORSの設定

GREP_COLORS='mt=01;36'

フォーマットは次の通りです。

mt=属性;色コード

属性値の種類は以下の通りです。 - 00で通常 - 01でbold(明るい/太字) - 04で下線

色コードの種類は以下の通りです。 - 30は黒 - 31は赤 - 32は緑 - 33は黄 - 34は青 - 35はマゼンタ - 36はシアン - 37は白 - 90-97は明るいバージョン

配置の重要性

# ❌ 効かない
GREP_COLORS='mt=01;36' history | grep --color=always "run"

# ✅ 正しい
history | GREP_COLORS='mt=01;36' grep --color=always "run"

環境変数grepコマンドの直前に配置する必要があります。

使用例

基本的な使い方

引数の文字列に該当する履歴を選択肢として表示する方法

完全一致モードで実行
hg "docker"
部分一致モードで起動
hg -p "docker"

全履歴の検索に対して選択肢を表示

完全一致モードで起動する
hg
部分一致モードで起動する
hg -p

実践的なユースケース

ケース1: 特定のコマンドを探す

hg "npm run"
# → "npm run test", "npm run build" などが候補に
# → fzf内で "test" と入力して絞り込み

ケース2: プロジェクト関連のコマンドを探す

hg -p "project"
# → "cd ~/projects", "vim project.md" など
# → Fuzzy検索で柔軟に絞り込み

ケース3: 長いコマンドを再利用

hg "docker compose"
# → 複雑なdocker-composeコマンドを選択して再実行

ファイル管理のベストプラクティス

~/.bash_functionsへの分離

.bashrcが長くなるのを防ぐため、関数を別ファイルに分離します。

1. 関数ファイルの作成

# ~/.bash_functions を作成
vi ~/.bash_functions

hg()関数の全体をこのファイルに記述します。

2. .bashrcからの読み込み

# ~/.bashrc に追加
if [ -f ~/.bash_functions ]; then
    source ~/.bash_functions
fi

3. 設定の反映

source ~/.bashrc

この方法の利点

この方法には以下の利点があります。

  1. 関心の分離で設定ファイルを目的別に管理できる
  2. 可読性が向上し.bashrcがすっきりする
  3. 保守性が高く関数だけを編集・無効化しやすい
  4. 再利用性があり別のマシンに関数ファイルだけコピー可能

業界標準の構成

多くのLinuxディストリビューションUbuntu等)でも採用されている方法です。

~/.bash_aliases    # エイリアス用
~/.bash_functions  # 関数用
~/.bashrc          # メイン設定ファイル

パフォーマンスの考慮

大量の履歴がある場合

履歴が数万行ある場合、以下の最適化が有効です。

# 履歴サイズの制限
export HISTSIZE=10000
export HISTFILESIZE=10000

# 重複を記録しない
export HISTCONTROL=ignoredups:erasedups

パイプラインの効率

各コマンドは順次実行されるため、早い段階での絞り込みが重要です。

# ✅ 効率的(早めにgrepで絞る)
history | grep "docker" | tac | sed | awk | fzf

# ❌ 非効率的(大量のデータをtacに渡す)
history | tac | grep "docker" | sed | awk | fzf

トラブルシューティング

色が表示されない場合

原因1: GREP_COLORSの配置
# grepの直前に配置する
history | GREP_COLORS='mt=01;36' grep --color=always "run"
原因2: fzfの--ansiオプション不足
# --ansiオプションを必ず含める
fzf --height 40% --reverse --ansi

historyコマンドが空の場合

スクリプトファイルとして実行すると、historyコマンドは空になります。これはシェル関数としてのみ動作する制約です。

必ず.bashrcまたは~/.bash_functionsに関数として定義してください。

まとめ

このhg()関数では、以下の技術を組み合わせています。

  1. Bashの組み込み機能(history, case, 条件判定)
  2. Unix標準ツール(grep, sed, awk, tac
  3. モダンなTUI(fzfによるインタラクティブな選択)
  4. カラーリング(GREP_COLORSによる視覚的フィードバック)

history | grepによるパイプ指定やエイリアスによる登録など効率化は可能ですが、履歴の抽出から実行までをインタラクティブ操作できるようにすることで手間も減り、効率も上がるかと思います。今後処理の追加や改善などするかもしれませんが、気になれば使用してみてください。

参考リンク

以上です。