ご注文は一度でも行った場所ですか？

前回はココアさんがpecoをチュートリアルしてくれました。
さて、さらにcdコマンドを弄っていきます。
多分、これで最後だと思います。

# 第6羽：cdコマンドはログを取り、pecoを駆りて履歴の山を行く

今回は、一度行ったディレクトリを独自に記録してpecoで選択、移動できるようにしてみます。

```bash:terminal
$ cd -
```
上記を実行すると`$OLD_PWD`という<font color="red">**一つ前に居たパス**</font>まで戻ります。
でも、WindowsさんのGUIは←アイコンを使うともっと前まで戻れますよね？
これが今回のモチベーションです。

# 機能

- 履歴検索機能  
- 履歴整理機能

## 履歴検索機能についての仕様

1. **`cd -`の機能を置き換えること**  
`cd -`が不便ですし、同じ方向の機能なので共存させる意味はないと判断しました。
2. **絶対パスで記録すること**  
絶対パスの方がcdしやすいですしね

##履歴整理機能についての仕様

1. **並びは弄らないこと**  
ログをpecoするときに新しい順番に並べる仕様を壊さないため
2. **重複履歴は新しいモノのみを残して削除すること**  
重複して表示しない機能はありますが、一応
3. **どこかのタイミングで自動実行されること**  
ユーザーが気にしなければならないとかゴミなので自動実行です。


# こんな感じになった

```bash:.bashrc
# 環境変数が定義されているかを確認
if [[ ! -v LOG_DIR ]]; then
  # パスは自由
  export LOG_DIR=${HOME}
fi

# ログディレクトリの存在チェック
if [[ ! -d LOG_DIR ]];then
  # ログディレクトリを一気に作成
  mkdir -p ${LOG_DIR}
fi

# ファイル名は強制固定
CD_HISTORY=${LOG_DIR}/cd_history.log

custom_cdls()
{
  local -r argc=$#
  local destination=$*
  case ${argc} in
    0)if type peco &> /dev/null; then
        local asc_order='sort -f'
        destination=$(find ./ -maxdepth 1 -mindepth 1 -type d | eval $asc_order | peco)
      fi
      ;;
    1)destination=$1
      if type peco &> /dev/null; then
        # pecoを持ってるときだけ「-」機能を置き換える
        if [[ ${destination} == '-' ]];then
          local trim_duplication='awk '\''!dictionaty[$0]++'\'''
          local reverse_order='tac'
          destination=$(\cat ${CD_HISTORY} \
                 | eval ${reverse_order} \
                 | eval ${trim_duplication} \
                 | peco) # Cannot use --query option
        fi
      fi
      ;;
  esac
  # Don't move $HOME
  if [ -z $destination ]; then
    echo 'Missing args';
    return 1;
  fi

  # \cd => builtin cd
  \cd ${destination}
  if [ $? -ne 0 ]; then
    return $?
  fi
  clear && ls

  # カレントパスを保存
  pwd >> ${CD_HISTORY}
}

alias cd='custom_cdls'

_clean_dir_history()
{
  local trim_duplication='awk '\''!dictionaty[$0]++'\'''
  local reverse_order='tac'
  # 履歴ファイルを読み込み
  uniq_ary=($(cat ${CD_HISTORY} \
           | eval $reverse_order \
           | eval $trim_duplication \
           | eval $reverse_order))
  # 一応バックアップしておく
  \cp $CD_HISTORY $CD_HISTORY.bak &> /dev/null
  # ファイル内容を全消し
　:> ${CD_HISTORY}

  for line in "${uniq_ary[@]}"; do
    if [[ -e $line ]]; then
      # パスが存在していたら書き込む
      echo $line >> ${CD_HISTORY}
    fi
  done
}
# 実行
_clean_dir_history
``` 

# スクリプト解説
まず、履歴ファイルのパスを環境変数として持っています。
外からユーザーが環境変数を定義してくれてれば再定義せずにそれを使うようにしています。
自由なところに履歴ファイルを置いてください。
デフォルトだと$HOMEに置くことになります。

また、そこまでのパスが見つからなかった場合は`mkdir -p`で一気に作っています。
sudoを要求されるパスは想定していないので気を付けてください。

## 履歴検索機能
前提として、引数の個数判定で１つだった場合の処理に該当処理を加えています。
引数が「-」だけだったときに処理を差し替えます。

- **cat ${CD_HISTORY}**  
catでファイルの内容を次に流しているだけです。
- **reverse_order**  
最近のモノを一番前に出すために逆転を行います。  
前とかの記事ではコマンドを持ってるか確認していましたが、メンドウになって辞めてしまいました。  
みなさん、コマンドがあるかどうかなんてチェックします？また、どの程度までチェックするんでしょうか？
最初に<font color=red>逆転させておかないと最新のモノを残して重複を削除できない</font>ので注意してください。  
- **trim_duplication**  
履歴に重複する内容があった場合に、同じ候補を出してしまうのは混乱の元なので重複を削除します。  
awkコマンドを用いて辞書式配列を作ることで重複削除を実現しています。  
- **pwd >> ${CD_HISTORY}**  
移動した最後の処理に履歴ファイルに現在のパスを追記します。  
最初は`readlink`コマンドなどを使っていましたが、Macではツライとの話を聞いて考え直したところ  
シンプルに「<font color=red>移動後にカレントパスをファイルに流す</font>」で解決しました。  
もっと上手い解決があればご教授ください。  

## 履歴整理機能
<font color=blue>自動実行タイミングを計るのをスクリプト内で完結させるのは大変メンドクサイ</font>ことに気付いたので、<font color=red>bashが実行されたときに整理する</font>ようにします。
それだとタイミングの問題も起きなさそうですしね！
.bashrcが読み込まれた際に実行されるようにするので、別に関数化する必要はないんですけども、何してるのか明確にするために関数化したので、最後に関数を呼び出して実行しています。

- **cat ${CD_HISTORY}**  
ログファイルを読み込んでパイプに流していきます。  
この状態はまだ、昇順（時系列順）です。
- **reverse_order**  
この降順化処理は「<font color="red">重複する場合は最近のモノを残す</font>」という仕様を満たすために行います。  
この状態では新しい記録が上になっています。
- **trim_duplication**  
新しいモノが上に来ている状態で、上から処理して重複を削除します。  
これで重複しているログは、最近のログだけが残るようになります。
- **reverse_order**  
ログの順列が降順（逆時系列順）になっています。  
このままログファイルに書き出してしまうと時系列が逆転してしまいます。  
最後に<font color=red>時系列を元に戻すためにもう一度順序を逆転させます</font>。
- **for line in "${uniq_ary[@]}";**  
lineに一行ずつ入れて、存在チェックした後にログに追記していくだけで特に書くようなことはないです。  
ただ、for文の@マークがクセのようで、@と\*で<font color=red>ほぼ同機能</font>らしいですが、<font color=red>**@は空白で区切るのに対し、\*はIFSで区切る違いがある**</font>ので今後注意が必要かもしれません。

# 最後に

Windowsの戻るコマンド（？）は一つ以上前に戻れるってのがモチベーションだった訳ですけど、
全然違う機能が出来上がってんじゃん！

**「こまけぇこたぁいいんだよ！！」**AA略