クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

Gitの作業を効率化するためにZshでやっている事

dotfilesいじりが趣味の岩下(@ToruIwashita)です。

今回はzshの補完でこんな感じの動きをさせる話です。 https://i.giphy.com/xT0Gqx4Raz41nQJkis.gif

はじめに

みなさんはタイポに悩まされる事はありませんか?僕はタイピングする度にタイポを繰り返し、やりたい事をやるための入力に手間取ることに悩み、日々ストレスを感じています。 タイポしないとか、やりたい事をサクッとやるためにはどうしたら良いのか。その答えはタイピングをしない事だと思います。 もし自分の頭と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

このような感じで入力が面倒だなと感じた時にサクッと補完関数を作ってしまえば、タイピング数が減りストレスから開放されるので、快適に作業をする事ができます。補完関数ひとつ作るにもシンプルかつ汎用的に使える部分を作り出し、使い回す事はとても有益です。

最後に

この記事を書くときも「補完関数」の変換が「補間関数」になってしまい、またタイポしてるよと悲しい気分になりました。 クラウドソーシングのクラウドワークスでは「がんばらないためにがんばるエンジニア」を募集しています。

© 2016 CrowdWorks, Inc., All rights reserved.