Vim
Zsh
Peco
Ghq
More than 1 year has passed since last update.

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

リポジトリのdemoに動きがわかるGIFがある。
似たようなものでfzfも良さそう → おい、peco もいいけど fzf 使えよ - Qiita

インストール

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

$ ghinst peco/peco

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

使い方

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

$ ls | peco

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

$ vim `ls | peco`

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

Macでの下準備

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

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

zshを使うなら

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

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

$ brew install coreutils gnu-sed

PATH等を設定しておく

# 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の場合は

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の場合は

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からも抽出。

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る

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

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

$ ghinst motemen/ghq

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

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

.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

余談だけど、

.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してきたリポジトリや、最近更新したリポジトリが上に来て欲しいので、ちょっと工夫する。

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

コメントをつけると

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

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

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

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

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

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

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

としておくとよい。

cdrでpecoる

cdrは移動したディレクトリを記録して、そこに移動するためのzshのコマンド。bashの人とかはenhancdとか使うといいかも。

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

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で使う設定は以下。

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を使う。

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

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で開いているファイルのディレクトリにシェルから行きたい場合もある。
それをやるには以下のような設定をする。

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が必要。
l:ignore_patternのパターンはneomru.vimg:neomru#directory_mru_ignore_patternから拝借した。

pecoり方を変える

$ vim `ls | peco`

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

GHQ - r7km/sで書かれているp()という関数を使うと心理的なハードルが下がる。

$ ghq list -p | p cd

などと入力する。

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

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が使えるようになる。