ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

fzfで末尾に?がついてしまう現象

以下のような現象。

f:id:rasukarusan:20180825134545p:plain

多段scpを試みたときに出会った現象。ProxyCommandによる多段scpができなかったので、しょうがなく愚直に2回scpをしようとした。 expectで踏み台サーバーsshして、ls。lsの結果をfzfで絞り込んでscpする以下のようなスクリプトを組んだ。

serverFiles=`expect -c "
    spawn ssh -t serverA \"ssh $targetServer\"
    expect \"$USER\"
    send \"ls -1\n\"
    expect \"]\"
    expect \"$USER\"
    send \"exit\n\"
    "`
# 先頭行から6行目まではexpectの実行コマンドなので削除し、lsの結果だけを表示する
targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`
# 対象サーバーからserverAにコピー
ssh serverA "scp -r $USER@$targetServer:$targetFile $LOCAL_PATH"
# ローカルにコピー
scp -r serverA:$targetFile $LOCAL_PATH
# serverAにコピーしたファイルを削除
ssh serverA "rm -r $targetFile"

実行結果

▶sh scpServer.sh
: No such file...

scpで「そんなファイルないよ」と怒られる。

末尾の「?」は改行コードのCRだった

結論から言うと

targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`

targetFile=`echo "${serverFiles}"| tr -d '\r' | sed -e '1,6d' | grep -v $USER | fzf`

のようにtr -d '\r'をすればうまくいった。 どうやらlsした結果にCRが紛れ込んでいたようだった。

CRにたどり着くまでの道のり

推測1:fzfで絞り込んだ結果の$targetFileが空になっている?

targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`
echo $targetFile

# 実行結果
~
▶sh scpServer.sh
hoge.txt

変数名にはちゃんと入っているようだ。

推測2:「?」を消してみる

tr -d "?"で削除を試みたが結果は変わらず。。。

推測3:文字コードがおかしい?

どうやら目には見えないが内部的になにか変化しているのだろうと推測し、まずは文字コードからあたってみることにした。 nkf --guessをしてみる。

targetFile=`echo "${serverFiles}"| sed -e '1,6d' | grep -v $USER | fzf`
echo  $targetFile | nkf --guess

# 実行結果
▶sh scpServer.sh
ASCII (CRLF)

ほうほうなるほど。試しに普通にechoで文字列をawk --guessしたときどうなるか見てみる

echo "hoge" | nkf --guess
ASCII (LF)

ここで理解した。CRが紛れ込んでいたんだなと。 ecpect -c内の\nがsendでやると\rに変換されるのかなあ。 なぜCRが紛れ込むのかはまだ調査中だが、とりあえず一件落着だった。

fzfで捗る自作コマンド一覧(zsh)

みんな大好きfzf

fzfは結果をインタラクティブに絞り込むだけのコマンドだが、組み合わせ次第でかなり使えるコマンドだ。日本ではpecoの方が有名だが海外ではfzfの方が人気らしい。

github.com

筆者が思うpecoと比較したときのfzfのメリットを述べる。

  • 画面クリアがなく目に優しい
  • fuzzy search(曖昧検索)が可能
  • 絞り込んだものをパイプつなぎでコマンドを適用でき、結果をpreviewとして画面に表示できる
  • カスタマイズが豊富

特にfuzzy searchとpreviewできる点が素晴らしい。またfzfの絞り込み結果の画面サイズを指定できたりとカスタマイズ性が高いのもGOOD。

以下はgit logの結果をfzfで絞り込み、previewにgit diffを表示、ENTERで詳細をless表示させているもの。(ちなみに別で全く同じ動作をするtiggコマンドというものがあるらしい)

f:id:rasukarusan:20180812205449g:plain

私のfzfのデフォルトの設定は以下。複数選択と色合いを変化、あとデフォルトだと絞り込み画面が下からでるのでreverseで直下に出している。

# fzfの設定
export FZF_DEFAULT_OPTS='--color=fg+:11 --height 70% --reverse --select-1 --exit-0 --multi'

実際に業務でも使っているコマンド一覧をつらつらと紹介していく。

爆速でディレクトリの移動をするfzf-cdr

これがない時のTerminalの移動はもう考えられない。cdrで過去に行ったことのあるディレクトリを表示し、fzfで絞り込んでディレクトリに移動する。 ちなみに私は「change directory direct」という意味で「cdd」というaliasをつけている。

f:id:rasukarusan:20180812211238g:plain

# fzf-cdr 
alias cdd='fzf-cdr'
function fzf-cdr() {
    target_dir=`cdr -l | sed 's/^[^ ][^ ]*  *//' | fzf`
    target_dir=`echo ${target_dir/\~/$HOME}`
    if [ -n "$target_dir" ]; then
        cd $target_dir
    fi
}

cdrの設定は以下のようにしている。ココらへんちょっと適当かもしれない(いつ書いたか忘れてた)。

# cdrの設定
autoload -Uz is-at-least
if is-at-least 4.3.11
then
  autoload -Uz chpwd_recent_dirs cdr add-zsh-hook
  add-zsh-hook chpwd chpwd_recent_dirs
  zstyle ':chpwd:*'      recent-dirs-max 500
  zstyle ':chpwd:*'      recent-dirs-default yes
  zstyle ':completion:*' recent-dirs-insert both
fi

ctrl+zでストレスなくバックグラウンドに入れる&出す

vimでファイルを開いていて、ふとterminalに戻って操作をしたくなる時があります。そんなときは大体ctrl+zで一旦バックに入れてfgで戻ってくる、という動作をすると思います。 ただ戻るのに一々fgを打つのも面倒くさいのでctrl+zで行ったり来たりするように設定しています。

f:id:rasukarusan:20180812211833g:plain

# fgを使わずctrl+zで行ったり来たりする
fancy-ctrl-z () {
  if [[ $#BUFFER -eq 0 ]]; then
    BUFFER="fg"
    zle accept-line
  else
    zle push-input
    zle clear-screen
  fi
}
zle -N fancy-ctrl-z
bindkey '^Z' fancy-ctrl-z

ただ、これだと直前のバックグランドに入れたものとしか行き来できないので、2個前のプロセスに戻りたい時などはfzfで選択しています。

# fgをfzfで
alias fgg='_fgg'
function _fgg() {
    wc=$(jobs | wc -l | tr -d ' ')
    if [ $wc -ne 0 ]; then
        job=$(jobs | awk -F "suspended" "{print $1 $2}"|sed -e "s/\-//g" -e "s/\+//g" -e "s/\[//g" -e "s/\]//g" | grep -v pwd | fzf | awk "{print $1}")
        wc_grep=$(echo $job | grep -v grep | grep 'suspended')
        if [ "$wc_grep" != "" ]; then
            fg %$job
        fi
    fi
}

agでヒットした行をvimで開く

これは最近vimプラグインで完結するようになったから使わなくなってしまったが、一時期はNOTE:とかFIXME:を探して修正しに行くのに結構使ってた。 f:id:rasukarusan:20180812213417g:plain

# agの結果をfzfで絞り込み選択するとvimで開く
alias agg="_agAndVim"
function _agAndVim() {
    if [ -z "$1" ]; then
        echo 'Usage: agg PATTERN'
        return 0
    fi
    result=`ag $1 | fzf`
    line=`echo "$result" | awk -F ':' '{print $2}'`
    file=`echo "$result" | awk -F ':' '{print $1}'`
    if [ -n "$file" ]; then
        vim $file +$line
    fi
}

各ブランチのgit logを見ながらcheckoutする

これももはや定番かもしれない、gitの操作をfzfに食わせるものだ。 gitの操作は手打ちでやると面倒くさいものも多いので、絞り込みで曖昧に覚えているものを打ち込んで選択するのがすごい楽。

f:id:rasukarusan:20180812213947g:plain

# git checkout branchをfzfで選択
alias co='git checkout $(git branch -a | tr -d " " |fzf --height 100% --prompt "CHECKOUT BRANCH>" --preview "git log --color=always {}" | head -n 1 | sed -e "s/^\*\s*//g" | perl -pe "s/remotes\/origin\///g")'

ちなみに「co」は「CheckOut」の略だ。 git checkoutの他にも

  • git add -pをfzfでdiffを見ながら選択
  • git checkout fileをfzfで選択
  • git resetをfzfでdiffを見ながら選択

といった操作をすべてfzfでやっている。

終わり

fzfのREADMEにあるExample集にもかなりの数があるので参考にしたい。 fzf最高。