dotfilesいじりが趣味の岩下(@ToruIwashita)です。
今回はzshの補完でこんな感じの動きをさせる話です。
はじめに
みなさんはタイポに悩まされる事はありませんか?僕はタイピングする度にタイポを繰り返し、やりたい事をやるための入力に手間取ることに悩み、日々ストレスを感じています。 タイポしないとか、やりたい事をサクッとやるためにはどうしたら良いのか。その答えはタイピングをしない事だと思います。 もし自分の頭とPCをつなげて指を動かさずに入力できたなら、タイポのストレスから開放されるはずなので、早くそういう未来が来ることを切に願います。 が、夢見ているだけでは目の前のストレスは消えないわけで。今現在はそういう技術がまだ手元にないし、じゃあ極力タイピングをしないようにしようと、日々改善活動を行っています。
今回はその活動(dotfilesいじり)の中で、gitに関連する操作のタイピング数を減らすためにしている事を紹介します。
Zshの補完の話
zshでタイピングを減らすためにできる事といったら、widgetを作ってキーに割り当てる事や補完関数を作って入力を補助する事などがありますが、今回は補完について紹介します。
普段の作業の中で入力を面倒に感じるのは、例えば、ブランチで変更のあったファイルに何らかの操作をしたい時にそのファイルパスを入力する、といった場面です。 これを解消するためはに、単純に補完関数を作ってコマンドに割り当てれば良いのですが、同じような補完関数を使いたくなる場面は多々あるので、まずは以下の様な汎用的な関数を作り、それを補完関数で使用するようにしています。
__git-inside-work-tree() { [[ $(git rev-parse --is-inside-work-tree 2>/dev/null) == true ]] } __git-status() { __git-inside-work-tree || return print "$(git status --short --porcelain)" } # 分岐元のブランチと現在のブランチのHEADを比較して変更の合ったファイルリストを返す __git-changed-list() { __git-inside-work-tree || return print $(git diff --name-only origin/HEAD...HEAD) } # 以降はgit statusの結果で各状態のファイルリストを返す関数 __git-modified-list() { __git-inside-work-tree || return local -a git_status_res git_status_res=(${(@f)"$(__git-status)"}) print ${(R)${(M)git_status_res:#?M*}#?M[[:space:]]} } __git-untracked-list() { __git-inside-work-tree || return local -a git_status_res git_status_res=(${(@f)"$(__git-status)"}) print ${(R)${(M)git_status_res:#\?\?*}#\?\?[[:space:]]} } __git-staged-list() { __git-inside-work-tree || return local -a git_status_res git_status_res=(${(@f)"$(__git-status)"}) print ${(R)${(M)git_status_res:#M?*}#M?[[:space:]]} } __git-both-modified-list() { __git-inside-work-tree || return local -a git_status_res git_status_res=(${(@f)"$(__git-status)"}) print ${(R)${(M)git_status_res:#UU*}#UU[[:space:]]} }
zshには変数展開フラグという強力な機能があるので、${(R)${(M)git_status_res:#?M*}#?M[[:space:]]}
という簡素な記述でgit status
の結果(ある程度加工済み)から欲しい状態のファイルのリストを簡単に取得することができます。
後は適当にコマンドに補完を割り当てたり、ラッパー関数を作って割り当てていくだけでブランチで変更のあったファイルなどを簡単に補完できるようになります。
git-diff-files() { git diff $* } __git-modified-files() { compadd $(__git-modified-list) } _git-diff-files() { _arguments '*: :__git-modified-files' } compdef _git-diff-files git-diff-files
そして補完関数はコマンドのオプションに割り当てる事もできるので、例えば自分はrspecのラッパー関数を作り各状態のファイルを補完させる感じにして、ファイルパスのタイピングを省いています。以下がブログの先頭で動かしていたものの中身になります。
brspec() { local -a args file_paths local self_cmd help usage self_cmd=$0 help="Try \`$self_cmd --help' for more information." usage=`cat <<EOF usage: $self_cmd [spec file] [-c --changed-file <spec file>] [-m --modified-file <spec file>] [-u --untracked-file <spec file>] [-h --help] EOF` while (( $# > 0 )); do case "$1" in -c | --changed-file) if (( ! $#2 )) || [[ "$2" =~ ^-+ ]]; then print "$self_cmd: option requires an argument '$1'\n$help" 1>&2 return 1 fi file_paths+=("$2") shift 2 ;; -m | --modified-file) if (( ! $#2 )) || [[ "$2" =~ ^-+ ]]; then print "$self_cmd: option requires an argument '$1'\n$help" 1>&2 return 1 fi file_paths+=("$2") shift 2 ;; -u | --untracked-file) if (( ! $#2 )) || [[ "$2" =~ ^-+ ]]; then print "$self_cmd: option requires an argument '$1'\n$help" 1>&2 return 1 fi file_paths+=("$2") shift 2 ;; -h | --help) print $usage return 0 ;; -- | -) # Stop option processing shift; file_paths+=("$@") break ;; -*) print "$self_cmd: unknown option '$1'\n$help" 1>&2 return 1 ;; *) file_paths+=("$1") shift 1 ;; esac done if (( ! $#file_paths )); then print $usage return 1 fi if [[ -f './bin/rspec' ]]; then cmd='./bin/rspec' else cmd='bundle exec rspec' fi print $cmd eval "$cmd $file_paths" } __git-changed-spec-files() { # サフィックスが_spec.rbのファイルのみを補完する compadd ${(M)$(__git-changed-list)#*_spec.rb} } __git-modified-spec-files() { compadd ${(M)$(__git-modified-list)#*_spec.rb} } __git-untracked-spec-files() { compadd ${(M)$(__git-untracked-list)#*_spec.rb} } _brspec() { _arguments \ '(-c --changed-file)'{-c,--changed-file}'[With changed file completion]: :__git-changed-spec-files' \ '(-m --modified-file)'{-m,--modified-file}'[With modified file completion]: :__git-modified-spec-files' \ '(-u --untracked-file)'{-u,--untracked-file}'[With untraced file completion]: :__git-untracked-spec-files' \ '(-h --help)'{-h,--help}'[Show this help text]' \ '*: :_files' } compdef _brspec brspec
このような感じで入力が面倒だなと感じた時にサクッと補完関数を作ってしまえば、タイピング数が減りストレスから開放されるので、快適に作業をする事ができます。補完関数ひとつ作るにもシンプルかつ汎用的に使える部分を作り出し、使い回す事はとても有益です。
最後に
この記事を書くときも「補完関数」の変換が「補間関数」になってしまい、またタイポしてるよと悲しい気分になりました。 クラウドソーシングのクラウドワークスでは「がんばらないためにがんばるエンジニア」を募集しています。