ZshでSubverionのコマンド補完が効かない場合の応急処置

はじめに

手元のMacで、いつの間にかzshのコマンド補完でSubversionの補完が効かなくなったので、その原因と対処法について。

現象が再現する現在のバージョンは以下の通り。

  • macOS Mojave 10.14
  • zsh 5.6.2 (x86_64-apple-darwin18.0.0)
  • svn version 1.10.2 (r1835932)

追記(2018年11月3日)

fpathの値がおかしかったのが原因だった模様。brewでzshを入れる前には/binにZsh 5.3が入っていた。

$ which zsh
/bin/zsh

$ zsh --version
zsh 5.3 (x86_64-apple-darwin18.0)

また、fpathの値はこんな感じ。

$ print -l $fpath
/usr/local/share/zsh/site-functions
/usr/share/zsh/site-functions
/usr/share/zsh/5.3/functions

この状態ではsvnの補完は効かない。

$ svn ad [tab]
_svn:33: bad set of key/value pairs for associative array

さて、ここでbrewでZshを入れる。

$ brew install zsh

$ which zsh 
/usr/local/bin/zsh

$ zsh --version
zsh 5.6.2 (x86_64-apple-darwin18.0.0)

ところが、fpathの値が古いままになっている。

$ print -l $fpath 
/usr/local/share/zsh/site-functions
/usr/share/zsh/site-functions
/usr/share/zsh/5.3/functions

これを、ちゃんと新しく入ったZshに合わせる。

$ fpath=(/usr/local/share/zsh/site-functions /usr/local/share/zsh/functions)

$ svn add --[tab]
Completing option
--auto-props                  --non-recursive             
--config-dir                  --parents                   
--config-option               --password                  
--depth                       --password-from-stdin       
--force                       --quiet                     
--force-interactive           --targets                   
--no-auth-cache               --trust-server-cert         
--no-auto-props               --trust-server-cert-failures
--no-ignore                   --username                  
--non-interactive    

できた!

というわけで、少なくとも僕の環境では、fpathの値がおかしかったのが原因でした。

以下はobsoleteな情報ですが、参考のため残しておきます。

現象と原因

zshのコマンド補完を有効にした状態で、svnのサブコマンドを補完しようとすると以下のようなエラーが出る。

$ svn a[tab]
_svn:33: bad set of key/value pairs for associative array

svn addまで打ってから補完しようとすると、addなんてコマンド知らんと怒られる。

$ svn add [tab]
Completing unknown svn command: add

原因はSubversion関連の補完の関数_svnで、svn helpのパースに失敗しているから。まず、その関数の置き場所を調べる。zshが調べるパスはfpathに入っている。

$ echo $fpath
/usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/5.3/functions

今回の場合は、/usr/share/zsh/5.3/functions/_subversionの以下の場所が原因。

  if [[ -n $state ]] && (( ! $+_svn_cmds )); then
    typeset -gHA _svn_cmds
    if _cache_invalid svn-cmds || ! _retrieve_cache svn-cmds; then
      _svn_cmds=(
  ${=${(f)${${"$(_comp_locale; _call_program commands svn help)"#l#*Available subcommands:}%%Subversion is a tool*}}/(#s)[[:space:]]#(#b)([a-z]##)[[:space:]]#(\([a-z, ?]##\))#/$match[1] :$match[1]${match[2]:+:${${match[2]//[(),]}// /:}}:}
      )
      _store_cache svn-cmds _svn_cmds
    fi
  fi

ここで、まだ_svn_cmds_が定義されておらず、キャッシュにも入っていなければ、svn helpを実行し、その結果をパースすることで連想配列を作成、その結果を_svn_cmdsへ代入している。以下、この連想配列を使ってsvnのコマンド補完を行っている。しかし、なんらかの原因でsvn helpの結果のパースに失敗し、連想配列_svn_cmdsの作成に失敗している。

応急処置

ネットでググると、同様な問題を抱えている人が何人かおり、svn helpを実行してパースするところを修正することで対応しているらしいのだが、それらのパッチを適用してもうちの環境ではうまくいかなかった。そこで、力技で対応する。

まず、正しく補完が動いている環境で、_svn_cmdsがどういう値を持っているかを見てみる。_svn_cmdstypeset -gHA _svn_cmdsとして定義されている。このうち-gは「グローバル変数にしなさい」という指定なので、外から見ることができる。ついでに-Hは「値を隠しなさい」、-Aは「連想配列として宣言しなさい」 という指定である。

さて、別の環境でsvn a[tab]と打って、無事に補完された後(つまり_svn_cmdsが定義された後)に、_svn_cmdsの値を表示する。

$ echo $_svn_cmds 
:patch: :status:stat:st: :help:?:h: :propdel:pdel:pd: :switch:sw: :diff:di: :propget:pget:pg: :changelist:cl: :cat: :move:mv:rename:ren: :resolved: :unlock: :propedit:pedit:pe: :propset:pset:ps: :update:up: :add: :list:ls: :relocate: :cleanup: :import: :mkdir: :blame:praise:annotate:ann: :lock: :proplist:plist:pl: :checkout:co: :delete:del:remove:rm: :info: :merge: :export: :copy:cp: :upgrade: :mergeinfo: :resolve: :revert: :commit:ci: :log:

普通に表示すると連想配列の「値(value)」しか表示されない。両方表示するには(kv)をつける。

$ echo ${(kv)_svn_cmds} 
patch :patch: status :status:stat:st: help :help:?:h: propdel :propdel:pdel:pd: switch :switch:sw: diff :diff:di: propget :propget:pget:pg: changelist :changelist:cl: cat :cat: move :move:mv:rename:ren: resolved :resolved: unlock :unlock: propedit :propedit:pedit:pe: propset :propset:pset:ps: update :update:up: add :add: list :list:ls: relocate :relocate: cleanup :cleanup: import :import: mkdir :mkdir: blame :blame:praise:annotate:ann: lock :lock: proplist :proplist:plist:pl: checkout :checkout:co: delete :delete:del:remove:rm: info :info: merge :merge: export :export: copy :copy:cp: upgrade :upgrade: mergeinfo :mergeinfo: resolve :resolve: revert :revert: commit :commit:ci: log :log:

コマンドがキー、そのコマンドの別名をコロンをデリミタとしてつなげたものが値であることがわかる。で、これが得られるように_subverionの内部の関数を書き換えればよいのだが、それはzsh力の低い僕にはちと厳しい。なのでこの値をそのまま突っ込んでしまう。

まず、元ファイル_subversionを適当な場所、例えばホームディレクトリにコピーする。

$ cp /usr/share/zsh/5.3/functions/_subversion .subversion.mine

手元においた.subversion.mineの該当箇所を以下のように修正。

  if [[ -n $state ]] && (( ! $+_svn_cmds )); then
    typeset -gHA _svn_cmds
    if _cache_invalid svn-cmds || ! _retrieve_cache svn-cmds; then
    _svn_cmds=(
    patch :patch: status :status:stat:st: help :help:\?:h: propdel :propdel:pdel:pd: switch :switch:sw: diff :diff:di: propget :propget:pget:pg: changelist :changelist:cl: cat :cat: move :move:mv:rename:ren: resolved :resolved: unlock :unlock: propedit :propedit:pedit:pe: propset :propset:pset:ps: update :update:up: add :add: list :list:ls: relocate :relocate: cleanup :cleanup: import :import: mkdir :mkdir: blame :blame:praise:annotate:ann: lock :lock: proplist :proplist:plist:pl: checkout :checkout:co: delete :delete:del:remove:rm: info :info: merge :merge: export :export: copy :copy:cp: upgrade :upgrade: mergeinfo :mergeinfo: resolve :resolve: revert :revert: commit :commit:ci: log :log:)
     _store_cache svn-cmds _svn_cmds
    fi
  fi

ここで、help :help:\?:h:? のところはエスケープしないといけないことに注意。

これを、たとえば.zshrcの最後に

[ -f ~/.subversion.mine ] source .subversion.mine

とか書いておけば、.subversion.mineがある環境では勝手に読み込んでくれる。

まとめ

Mac上でのzshでsubversionの補完が効かなくなった場合の応急処置を書いた。これが応急処置であることは自覚しているので、恒久的に治す方法があれば教えてくださるとうれしいです。