pecoる

pecoは標準入力で受け取ったテキストデータをインクリメンタルに絞り込んで、選択した行を標準出力に出力するコマンド。

* https://github.com/peco/peco

リポジトリのdemoに動きがわかるGIFがある。
似たようなものでfzfも良さそう → [おい、peco もいいけど fzf 使えよ - Qiita](http://qiita.com/b4b4r07/items/9e1bbffb1be70b6ce033)

## インストール

使うだけなら`go get`する必要はなく、[Releases · peco/peco](https://github.com/peco/peco/releases)にあるバイナリをダウンロードすればよい。
macだとbrewでインストールすることも可。
拙作の[ghinst](https://github.com/tmsanrinsha/ghinst/)を使うと

```bash
$ ghinst peco/peco
```

で`~/bin`にインストールできる


## 使い方

ファイルがいっぱいあるディレクトリで

```bash
$ ls | peco
```

で目的にファイルを見つけたり、見つけたものをvimで開くには

```bash
$ vim `ls | peco`
```

などとやる。よく使うものはシェルの設定ファイルに書いておくと良い。

## Macでの下準備

bash, zshを最新版にしておく。
bashを使うなら

```bash
$ brew install bash
$ echo '/usr/local/bin/bash' | sudo tee -a /etc/shells
$ chsh -s /usr/local/bin/bash
```

zshを使うなら

```bash
$ brew install zsh
$ echo '/usr/local/bin/zsh' | sudo tee -a /etc/shells
$ chsh -s /usr/local/bin/zsh
```

macの標準コマンドはlinuxと挙動が違うので、brewを使ってlinuxに合わせる。

```bash
$ brew install coreutils gnu-sed
```

PATH等を設定しておく

```bash
# coreutils
export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
export MANPATH="/usr/local/opt/coreutils/libexec/gnuman:$MANPATH"

# gnu-sed
export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"
export MANPATH="/usr/local/opt/gnu-sed/libexec/gnuman:$MANPATH"
```

## コマンド履歴でpecoる

`C-r`でコマンド履歴を検索できるけど、pecoを使ったほうが一覧性がよくて、絞り込みも柔軟なので使いやすい。

例えばzshの場合は

```bash
function peco-select-history() {
    # historyを番号なし、逆順、最初から表示。
    # 順番を保持して重複を削除。
    # カーソルの左側の文字列をクエリにしてpecoを起動
    # \nを改行に変換
    BUFFER="$(history -nr 1 | awk '!a[$0]++' | peco --query "$LBUFFER" | sed 's/\\n/\n/')"
    CURSOR=$#BUFFER             # カーソルを文末に移動
    zle -R -c                   # refresh
}
zle -N peco-select-history
bindkey '^R' peco-select-history
```

を.zshrcに書いておくとctrl+rでpecoを使ったコマンド履歴の絞り込みができる。
改行を含むコマンド実行した場合は、historyで改行部分が文字列の\nで出力されるため、sedで変換している。


bashの場合は

```bash
function peco-select-history() {
    local tac
    which gtac &> /dev/null && tac="gtac" || \
        which tac &> /dev/null && tac="tac" || \
        tac="tail -r"
    READLINE_LINE=$(HISTTIMEFORMAT= history | $tac | sed -e 's/^\s*[0-9]\+\s\+//' | awk '!a[$0]++' | peco --query "$READLINE_LINE")
    READLINE_POINT=${#READLINE_LINE}
}
bind -x '"\C-r": peco-select-history'
```

とする。以降bashの例は省く。

## sshでpecoる

ホスト名を絞り込んでsshする設定。最近入ったサーバを上に持ってきたいので履歴でsshから始まるのを抽出後、known_hostsからも抽出。

```bash
function _get_hosts() {
    # historyを番号なし、逆順、ssh*にマッチするものを1番目から表示
    # 最後の項をhost名と仮定してhost部分を取り出す
    local hosts
    ssh_hist="$(history -nrm 'ssh*' 1 | \grep 'ssh ')"
    hosts="$(echo $ssh_hist | perl -pe 's/ssh(\s+-([1246AaCfGgKkMNnqsTtVvXxYy]|[^1246AaCfGgKkMNnqsTtVvXxYy]\s+\S+))*\s+(\S+@)?//' | cut -d' ' -f1)"
    #                                        -----------------------------------------------------------------------   -------
    #                                                        hostnameよりも前にあるオプション                          user@     を削除
    # know_hostsからもホスト名を取り出す
    # portを指定したり、ip指定でsshしていると
    #   [hoge.com]:2222,[\d{3}.\d{3].\d{3}.\d{3}]:2222
    # といったものもあるのでそれにも対応している
    hosts="$hosts\n$(cut -d' ' -f1  ~/.ssh/known_hosts | tr -d '[]' | tr ',' '\n' | cut -d: -f1)"
    hosts=$(echo $hosts | awk '!a[$0]++')
    echo $hosts
}

function peco-ssh() {
    hosts=`_get_hosts`
    local selected_host=$(echo $hosts | peco --prompt="ssh >" --query "$LBUFFER")
    if [ -n "$selected_host" ]; then
        BUFFER="ssh ${selected_host}"
        zle accept-line
    fi
}
zle -N peco-ssh
bindkey '^[s' peco-ssh
```

今回の例だと`zle accept-line`を使って、選択後はすぐにsshをする。

以下のようにオプションを設定したsshの履歴でもホスト名を抽出できる

```
$ cat <<EOT | perl -pe 's/ssh(\s+-([1246AaCfGgKkMNnqsTtVvXxYy]|[^1246AaCfGgKkMNnqsTtVvXxYy]\s+\S+))*\s+(\S+@)?//' | cut -d' ' -f1
ssh example.com
ssh -A example.com
ssh -p 2222 example.com
ssh -A -p 2222 example.com
ssh -A example.com -p 2222
ssh example.com ls
ssh hoge@example.com
ssh -A -p 2222 hoge@example.com -i ~/.ssh/id_rsa ls
EOT
example.com
example.com
example.com
example.com
example.com
example.com
example.com
example.com
```

GNU sedとMacのBSDのsed [^1] で正規表現の違いがあったため、perlを使った。GNU sedがいい人は`hosts=...`の部分を

```
hosts="$(echo $ssh_hist | sed 's/ssh\(\s\+-\([1246AaCfGgKkMNnqsTtVvXxYy]\|[^1246AaCfGgKkMNnqsTtVvXxYy]\s\+\S\+\)\)*\s\+\(\S\+@\)\?//' | cut -d' ' -f1)"
```

に変更して下さい。

## ghqでpecoる

* https://github.com/motemen/ghq

ghqはリポジトリをhost/user/repo形式で管理するためのツール。Rebuild: 42の後、[ghqを使ったローカルリポジトリの統一的・効率的な管理について - Kentaro Kuribayashi's blog](http://blog.kentarok.org/entry/2014/06/03/135300)をきっかけに流行ったと思う。

ghqを使うと、リポジトリのディレクトリがかぶらないし、ディレクトリを自分で切らなくていいのが利点だけど、その分ディレクトリが深くなって、移動が面倒になるけど、pecoをつかって解決する。

```
$ ghinst motemen/ghq
```

などでインストールする。

デフォルトだと`~/.ghq`以下にディレクトリが作られるが、ghq以外で管理するようになる可能性もあるので`~/src`以下で管理するように.gitconfigに以下の設定をしている。

```ini:.gitconfig
[ghq]
	root = ~/src
```

こうしておいて

```
$ ghq get tmsanrinsha/dotfiles
```

などと打つと`~/src/github.com/tmsanrinsha/dotfiles`にgit cloneされる。
`-p`をつけてるか、ssh形式で指定するとsshプロトコルでcloneされる。


```
$ ghq get -p tmsanrinsha/dotfiles
$ ghq get git@github.com:tmsanrinsha/dotfiles.git
```

余談だけど、

```ini:.gitconfig
[url "git@github.com:"]
  pushInsteadOf = https://github.com/
```

を設定しておくと、https形式でcloneしても、pushはssh形式になる。
こうするとfetchはhttps形式なので、パスフレーズが必要なくなり、cronで`git pull`することができる。pushはsshでできる。
各サーバのdotfilesなどをcronで最新に保つときに便利。

で、`ghq look`を使うとリポジトリに移動することができるけど、pecoを使って移動することにする。また今`git clone`してきたリポジトリや、最近更新したリポジトリが上に来て欲しいので、ちょっと工夫する。

```bash
function peco-ghq-cd () {
    # Gitリポジトリを.gitの更新時間でソートする
    local ghq_roots="$(git config --path --get-all ghq.root)"
    local selected_dir=$(ghq list --full-path | \
        xargs -I{} ls -dl --time-style=+%s {}/.git | sed 's/.*\([0-9]\{10\}\)/\1/' | sort -nr | \
        sed "s,.*\(${ghq_roots/$'\n'/\|}\)/,," | \
        sed 's/\/.git//' | \
        peco --prompt="cd-ghq >" --query "$LBUFFER")
    if [ -n "$selected_dir" ]; then
        BUFFER="cd $(ghq list --full-path | grep -E "/$selected_dir$")"
        zle accept-line
    fi
}
zle -N peco-ghq-cd
bindkey '^[g' peco-ghq-cd
```

コメントをつけると

```bash
ghq list --full-path | \ # repositoryのfull pathを取得
        xargs -I{} ls -dl --time-style=+%s {}/.git | \ # .gitの更新時間を表示
        sed 's/.*\([0-9]\{10\}\)/\1/' | \              # タイムスタンプ部分と.gitのフルパス部分を取り出す
        sort -nr | \                                   # タイムスタンプでソート
        sed "s,.*\(${ghq_roots/$'\n'/\|}\)/,," | \     # 複数ある改行切りのghq.rootの改行を\|(sedの正規表現の|)にして、ghq.root部分を削除
        sed 's/\/.git//' | \                           # .gitを削除
        peco --prompt="cd-ghq >" --query "$LBUFFER")
```

これで.gitの更新時間順で

```
cd-ghq >
github.com/tmsanrinsha/dotfiles
github.com/motemen/ghq
```

こんな風に表示される。選択されたら

```bash
BUFFER="cd $(ghq list --full-path | grep -E "/$selected_dir$")"
```

でフルパスを取得して移動する。

vimでも移動できるようにunite-ghqを使う。

* https://github.com/tmsanrinsha/unite-ghq

もともとsorahさんが作ったのを上記のようなソートをするためにforkしたもの。
あと、選択したらvimfilerが開いて欲しい場合は

```vim
call unite#custom_default_action('source/ghq/directory', 'vimfiler')
```

としておくとよい。

## cdrでpecoる

cdrは移動したディレクトリを記録して、そこに移動するためのzshのコマンド。bashの人とかは[enhancd](https://github.com/b4b4r07/enhancd)とか使うといいかも。

以下の様な設定をすると使える。

```bash
if [[ -n $(echo ${^fpath}/chpwd_recent_dirs(N)) && -n $(echo ${^fpath}/cdr(N)) ]]; then
    autoload -Uz chpwd_recent_dirs cdr add-zsh-hook
    add-zsh-hook chpwd chpwd_recent_dirs
    zstyle ':completion:*' recent-dirs-insert both
    zstyle ':chpwd:*' recent-dirs-default true
    zstyle ':chpwd:*' recent-dirs-max 1000
    zstyle ':chpwd:*' recent-dirs-file "$ZDOTDIR/.cache/chpwd-recent-dirs"
fi
```

`$ZDOTDIR`を設定してない場合は`$HOME`にするなど。

pecoで使う設定は以下。

```bash
function peco-cdr () {
    local selected_dir="$(cdr -l | sed 's/^[0-9]\+ \+//' | peco --prompt="cdr >" --query "$LBUFFER")"
    if [ -n "$selected_dir" ]; then
        BUFFER="cd ${selected_dir}"
        zle accept-line
    fi
}
zle -N peco-cdr
bindkey '^[r' peco-cdr
```

これをvimからも使うためにはunite-zsh-cdrを使う。

* https://github.com/rhysd/unite-zsh-cdr.vim

zshの設定でrecent-dirs-fileをを変更している場合は設定してやる

```vim
let g:recent_dirs_file = $ZDOTDIR.'/.cache/chpwd-recent-dirs'
let g:unite_zsh_cdr_chpwd_recent_dirs = g:recent_dirs_file
```

これでvimからcdrで記録されたディレクトリに移動することはできた。しかし、逆にvimで開いているファイルのディレクトリにシェルから行きたい場合もある。
それをやるには以下のような設定をする。

```vim
let g:recent_dirs_file = $ZDOTDIR.'/.cache/chpwd-recent-dirs'
augroup cdr
    autocmd!
    autocmd BufEnter * call s:update_cdr(expand('%:p:h'))
augroup END

function! s:update_cdr(dir)
    " .gitなどのdirectoryは書き込まない
    let l:ignore_pattern = '\%(^\|/\)\.\%(hg\|git\|bzr\|svn\)\%($\|/\)'.
    \   '\|^\%(\\\\\|/mnt/\|/media/\|/temp/\|/tmp/\|\%(/private\)\=/var/folders/\)'

    if !isdirectory(a:dir) || a:dir =~ l:ignore_pattern
        return
    end

    if filereadable(g:recent_dirs_file)
        let l:recent_dirs = readfile(g:recent_dirs_file)
        call insert(l:recent_dirs, "$'".a:dir."'", 0)
        let l:V = vital#of('vital')
        let l:List = l:V.import('Data.List')
        let l:recent_dirs = l:List.uniq(l:recent_dirs)
        call writefile(l:recent_dirs, g:recent_dirs_file)
    endif
endfunction
```

バッファを変更するごとにディレクトリをcdrのファイルに書き込んでいる。この時ユニークなディレクトリを得るためにvital.vimの関数を使っているので、[vital.vim](https://github.com/vim-jp/vital.vim)が必要。
`l:ignore_pattern`のパターンは[neomru.vim](https://github.com/Shougo/neomru.vim)の`g:neomru#directory_mru_ignore_pattern`から拝借した。

## pecoり方を変える

```bash
$ vim `ls | peco`
```

とかなんとなく嫌だ。絞り込んだ後にコマンドを入力したい。

[GHQ - r7km/s](http://r7kamura.github.io/2014/06/21/ghq.html)で書かれている`p()`という関数を使うと心理的なハードルが下がる。

```
$ ghq list -p | p cd
```

などと入力する。

他に、実行結果をpecoに送って、絞り込んだ内容をバッファに出すキーバインドを設定しているといい気がする。

```bash
function peco-buffer() {
    BUFFER=$(eval ${BUFFER} | peco)
    CURSOR=0
}
zle -N peco-buffer
bindkey "^[p" peco-buffer
```

このように設定すると

```
$ ls<M-p>    # <M-p>でコマンドを実行して、結果をpecoに送る
# 選択すると
$ <選択した行>
```

な感じになる。

以上です。m(*_ _)m pecoり。

- - -

[^1]: Macでも`brew install gnu-sed`するとGNU sedを使うことができる。gsedという名前でインストールされるが、`export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"`すればsedでgsedが使えるようになる。