https://github.com/nnao45/zsh-kubectl-completion
まぁこういう物を作ったって話です。
まず初めに
この記事はKubernetesアドベントカレンダー(https://qiita.com/advent-calendar/2018/kubernetes)の5日目の記事です。
Kubernetesをオペレーションしていくと真っ先に思うのが「kubectlをまともに打っていくという行為が辛み」という事です。例えば、「production namespaceのnginxってserviceをdescribeコマンドで詳細を見たい」となると、以下のコマンドを打たないといけません。
$ kubectl --namespace production describe service nginx
なげえ。
いや長いどころか、色々なことを思い出しながらやらなければなりません。
- productionってnamespaceってあるのか?
- nginxってserviceって名前nginx?nginx-svcじゃ無く?
- describeのスペルってこれで合ってる?
などをkubectlに怒られながら打っていくわけです。やってられません。しかもlogsコマンドなんかだとまた記法がちょっと違って、
$ kubectl --namespace staging logs deployments/qicoo-api
これもまたこんなことを思い出しながらやらないといけないはずです。
- stagingってnamespaceってあるのか?
- logsコマンド?logコマンドじゃなかったっけ?
- logsコマンドってpod以外に使う場合はdeployments/qicoo-api?deployments qicoo-api?
覚えてられません。。。毎度 kubectl help
を打つ毎日を過ごす日々がしばらく続き。。。これは辛いと思いGoogle先生に聞くわけです。
そうだ、補完しよう。
そうなるわけです。幸いにもkubectlには元来、 bash
と zsh
に関しては補完コマンドを用意してくれています。
$ kubectl completion <zsh/bash>
これで zsh
と bash
の補完関数がでてくるので、それぞれのシェルに応じて設定してあげればめでたしめでたしですね。
例えば zsh
の場合は@mollifier氏の zload
関数を使えば気軽に試せますので、参考にしてください(zsh で補完関数を作るときに便利な再読み込みするプラグインを作った)。
$ kubectl completion zsh > ./_kubectl
$ zload _kubectl
さて、これで満足、、、カモなのですが、、、ここで貴方が zsh
を使って数ヶ月は経つのであれば、とある疑問に合うはずなのです。そう、それこそが今回の主題なのです。
おことわり:ここからzshの話しかしません。とりあえずもういいやという方でも、使ってみてよさそうであればgithubのページにスターいただければ開発の励みになります。
あれ、このzshの補完って・・・・?
zsherでご聡明な皆さんはすぐにお気づきでしょう、 kubectl completion zsh
がzshの拡張補完でない事を。
これはどういう事なんだろうと思って、以下を見てみると分かります、、、、
$ kubectl completion zsh > _kubectl_zsh
$ kubectl completion bash > _kubectl_bash
$ diff -u _kubectl_bash _kubectl_zsh
--- _kubectl_bash 2018-12-02 12:59:02.000000000 +0900
+++ _kubectl_zsh 2018-12-02 12:58:56.000000000 +0900
@@ -1,3 +1,4 @@
+#compdef kubectl# Copyright 2016 The Kubernetes Authors.
#
@@ -12,6 +13,147 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+
+__kubectl_bash_source() {
+ alias shopt=':'
+ alias _expand=_bash_expand
+ alias _complete=_bash_comp
+ emulate -L sh
+ setopt kshglob noshglob braceexpand
+
+ source "$@"
+}
+
+__kubectl_type() {
+ # -t is not supported by zsh
+ if [ "$1" == "-t" ]; then
+ shift
+
+ # fake Bash 4 to disable "complete -o nospace". Instead
+ # "compopt +-o nospace" is used in the code to toggle trailing
+ # spaces. We don't support that, but leave trailing spaces on
+ # all the time
+ if [ "$1" = "__kubectl_compopt" ]; then
+ echo builtin
+ return 0
+ fi
+ fi
+ type "$@"
+}
+
+__kubectl_compgen() {
+ local completions w
+ completions=( $(compgen "$@") ) || return $?
+
+ # filter by given word as prefix
+ while [[ "$1" = -* && "$1" != -- ]]; do
+ shift
+ shift
+ done
+ if [[ "$1" == -- ]]; then
+ shift
+ fi
+ for w in "${completions[@]}"; do
+ if [[ "${w}" = "$1"* ]]; then
+ echo "${w}"
+ fi
+ done
+}
+
+__kubectl_compopt() {
+ true # don't do anything. Not supported by bashcompinit in zsh
+}
+
+__kubectl_ltrim_colon_completions()
+{
+ if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
+ # Remove colon-word prefix from COMPREPLY items
+ local colon_word=${1%${1##*:}}
+ local i=${#COMPREPLY[*]}
+ while [[ $((--i)) -ge 0 ]]; do
+ COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
+ done
+ fi
+}
+
+__kubectl_get_comp_words_by_ref() {
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[${COMP_CWORD}-1]}"
+ words=("${COMP_WORDS[@]}")
+ cword=("${COMP_CWORD[@]}")
+}
+
+__kubectl_filedir() {
+ local RET OLD_IFS w qw
+
+ __kubectl_debug "_filedir $@ cur=$cur"
+ if [[ "$1" = \~* ]]; then
+ # somehow does not work. Maybe, zsh does not call this at all
+ eval echo "$1"
+ return 0
+ fi
+
+ OLD_IFS="$IFS"
+ IFS=$'\n'
+ if [ "$1" = "-d" ]; then
+ shift
+ RET=( $(compgen -d) )
+ else
+ RET=( $(compgen -f) )
+ fi
+ IFS="$OLD_IFS"
+
+ IFS="," __kubectl_debug "RET=${RET[@]} len=${#RET[@]}"
+
+ for w in ${RET[@]}; do
+ if [[ ! "${w}" = "${cur}"* ]]; then
+ continue
+ fi
+ if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
+ qw="$(__kubectl_quote "${w}")"
+ if [ -d "${w}" ]; then
+ COMPREPLY+=("${qw}/")
+ else
+ COMPREPLY+=("${qw}")
+ fi
+ fi
+ done
+}
+
+__kubectl_quote() {
+ if [[ $1 == \'* || $1 == \"* ]]; then
+ # Leave out first character
+ printf %q "${1:1}"
+ else
+ printf %q "$1"
+ fi
+}
+
+autoload -U +X bashcompinit && bashcompinit
+
+# use word boundary patterns for BSD or GNU sed
+LWORD='[[:<:]]'
+RWORD='[[:>:]]'
+if sed --help 2>&1 | grep -q GNU; then
+ LWORD='\<'
+ RWORD='\>'
+fi
+
+__kubectl_convert_bash_to_zsh() {
+ sed \
+ -e 's/declare -F/whence -w/' \
+ -e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
+ -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
+ -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
+ -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
+ -e "s/${LWORD}_filedir${RWORD}/__kubectl_filedir/g" \
+ -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__kubectl_get_comp_words_by_ref/g" \
+ -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__kubectl_ltrim_colon_completions/g" \
+ -e "s/${LWORD}compgen${RWORD}/__kubectl_compgen/g" \
+ -e "s/${LWORD}compopt${RWORD}/__kubectl_compopt/g" \
+ -e "s/${LWORD}declare${RWORD}/builtin declare/g" \
+ -e "s/\\\$(type${RWORD}/\$(__kubectl_type/g" \
+ <<'BASH_COMPLETION_EOF'
# bash completion for kubectl -*- shell-script -*-__kubectl_debug()
@@ -7720,3 +7862,9 @@
fi# ex: ts=4 sw=4 et filetype=sh
+
+BASH_COMPLETION_EOF
+}
+
+__kubectl_bash_source <(__kubectl_convert_bash_to_zsh)
+_complete kubectl 2>/dev/null
こ、これだけしか差分がない・・・・だと・・・・?
そう、、、、これは bash
をベースにして移植した補完関数なんだということに気づきます。決定的なのが、 zsh
の方の補完が、
__start_kubectl()
{
local cur prev words cword
declare -A flaghash 2>/dev/null || :
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -s || return
else
__kubectl_init_completion -n "=" || return
filocal c=0
local flags=()
local two_word_flags=()
local local_nonpersistent_flags=()
local flags_with_completion=()
local flags_completion=()
local commands=("kubectl")
local must_have_one_flag=()
local must_have_one_noun=()
local last_command
local nouns=()__kubectl_handle_word
}if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_kubectl kubectl
else
complete -o default -o nospace -F __start_kubectl kubectl
fi
なんだと・・・!? complete
コマンドで補完に入れてるのか・・・・!?
completeコマンドはbashのビルトイン関数であり、これが告げる決定的なことは、この補完関数からネイティブにzshの拡張補完にアクセスできないということです。
えぬなお自身、「まぁ実際何個か修正すればzshのフラグ補完もできるようになるだろ」とか思ったが、なにせcomplete
コマンドを関数指定で補完しているのである。これが何がキツイかというと、そもそも zsh
で補完関数を設計する場合は明示的に compadd
などで補完追加したりと、補完するに至る経路が違うのだ。まぁぶっちゃけソースコード読むのだるすぎて修正しきるより1から作った方が早いだろって感じだった。
ということで作った。
- kubernetes v1.12.2対応
- 全コマンドとその配下のオプションの補完
- 本家にある便利機能の以下のフラグを
kubectl
の後につけることで、その後の補完されるリソースをフィルタする機能も完備 --kubeconfig
--cluster
--user
--context
--namespace
--server
- 全オプションの説明をつけた ←new!!
- 全サブコマンドにリソース情報もつけて分かりやすく ←new!!
- zplugやpreztoでプラグイン管理しやすいフォーマットに ←new!!
実装はまだ甘いところが目立つが、まぁ十分動かせる代物にはなったと思いたい。
本体である_kubectl
見てもらうと一気にブラウザが重くなるのでお察しなのですが、zshの補完関数はシェルの縛りで1枚のファイルにまとめる必要があるので、1400行弱のラッパーとパーサーの塊みたいなソースファイルになりましたよと(ちなみにdockerの公式zsh補完は3000行超えなので、まぁ雰囲気それくらいのボリュームになるのは納得されるかなと・・・)。
という感じで、このオプションやコマンドの意味ってなんだっけという時に調べなくてもよくなるわけである。あとリソースを調べるときは kubectl get
を打って、、、じゃなくて補完を打てば概要は付いてくるので便利(だと思って自分は使っているw)
一応バージョンアップと一緒にメンテして行こうと思っている(これを書いてる途中に早速v1.13.0が出たようで、まぁ頑張って実装してないサブコマンドも含めて対応しようと思う、ああ後この記事の英語版もな。。。)。
どうやってインスコするの?
試すだけなら、↑の方にある zload
と同じ要領で追加してOK。
インスコしたい場合は、zplugだと、
zplug "nnao45/zsh-kubectl-completion"
とzshrcに書いて終わりだ。
もしマニュアルで入れたい場合は、 fpath
に ~/.zsh/completion_kubectl
を追加して補完関数を作って入れればいい。
$ curl -fLo ~/.zsh/completion_kubectl \
https://raw.githubusercontent.com/nnao45/zsh-kubectl-completion/master/_kubectl
まぁこの辺とかを見てくれ。
一応技術の話を・・・(需要あるのか?)
迷ったが、一応知りたいごく一部の方に関数の流れを書いておく。zshのデザインパターンとしてはそれほど奇抜なことはしていない王道だと思う。
zshの補完関数は compdef
コマンドで定義を行う。↑のレポジトリの一行目にある、
#compdef kubectl
補完用関数は _
アンダースコアから始める決まりとなっているので、関数ファイルとして作成する場合 _kubectl
として作りfpath
というzshの補完関数をおくべきパスにおいておくとと自動的にzshの補完関数として立ち上げ時に読み込まれる。
zshは様々な補完関数がビルトインで用意されているが、感覚的には例えば一番単純な補完追加関数の compadd
とかを使用した場合は、
$ compadd apply create
などとやれば先ほと compdef
で定義したコマンドに対して補完が追加される。これくらいならシンプルな話だ。
今回は _argument
関数と _alternative
関数、_value
関数を使用して、この補完値に対してさらに説明文や依存する追加補完をかましています。まーこれらを使ってzsh特有の補完表現を実装したわけです。
_argument
関数は --
を含んだフラグを補完させるのが便利で、プラスしてサブコマンド kubectl "get"
を補完にかますのに便利です。
_arguments \
${_global_flags[@]} \
"1: :{_alternative ':basic_cmd:__basic_cmd'}" \
'*:: :->args' && ret=0
ここでは2行目で引数を渡す ${_global_flags[@]}
は呼び出された段階で展開されてあらかじめ _gloal_flag
配列に詰めたフラグを補完に渡してます。
3行めは実行された補完対象から数えて0,1,,,のつまり kubectl get
の get
に当たる位置に補完対象に入れます。この数字にですがとても便利で、入力されたのがフラグの場合はその位置は無視してくれます。 これはつまり kubectl --namespace test get
と入力しても --namespace test
は無視して数えてくれるわけです。
その後4行目は *:: :->args
で、state
変数というビルドイン変数にargs
という値を入れます。それにより、これまたビルドイン変数のwords
配列にサブコマンドである get
、 apply
などが詰まっていき、それにより処理が変えることに実現していくわけです。
またzshは面白くて例えば _argument
に渡すフラグや _alternative
で渡す補完値にはオプションを入れられて、
get:resource:__kube_get_resource--namespace[If present, the namespace scope for this CLI request]:namespaces:__kube_get_namespaces
みたいに定義してこれを補完として追加すると、 get
を補完対象として追加したのちに、補完対象に依存補完値を追加していけたり、--namespace
フラグに説明文をつけ、さらにその後ろに補完させるように関数を叩いたりできます。このように柔軟に・・・闇に・・・組み立てていくわけですね。さ、俺が一から作りなおすしかねーなって思った気持ちもわかっていただけたかt(ry
ま、zshなんで、思い浮かんだことは基本的になんでもできます笑
作ってみて感想
- 補完されるリソースに対してはその場で
kubectl get
を発行しているわけだけど、kubectl get
自体はキャッシュ機能を持っているのでプラグイン側でキャッシュさせる事を考える必要がなかった。 - ソースを見てみればわかるが全オプションは全部ハードコードである。なんでそんな頭おかしい事をしたかというと、以下のコマンドを見て欲しい。
$ kubectl describe --help
Show details of a specific resource or group of resourcesPrint a detailed description of the selected resources, including related resources such as events
or controllers. You may select a single object by name, all objects of that type, provide a name
prefix, or label selector. For example:$ kubectl describe TYPE NAME_PREFIXwill first check for an exact match on TYPE and NAME PREFIX. If no such resource exists, it will
output details for every resource that has a name prefixed with NAME PREFIX.Use "kubectl api-resources" for a complete list of supported resources.Examples:
# Describe a node
kubectl describe nodes kubernetes-node-emt8.c.myproject.internal# Describe a pod
kubectl describe pods/nginx# Describe a pod identified by type and name in "pod.json"
kubectl describe -f pod.json# Describe all pods
kubectl describe pods# Describe pods by label name=myLabel
kubectl describe po -l name=myLabel# Describe all pods managed by the 'frontend' replication controller (rc-created pods
# get the name of the rc as a prefix in the pod the name).
kubectl describe pods frontendOptions:
--all-namespaces=false: If present, list the requested object(s) across all namespaces.
Namespace in current context is ignored even if specified with --namespace.
-f, --filename=[]: Filename, directory, or URL to files containing the resource to describe
--include-uninitialized=false: If true, the kubectl command applies to uninitialized objects.
If explicitly set to false, this flag overrides other flags that make the kubectl commands apply to
uninitialized objects, e.g., "--all". Objects with empty metadata.initializers are regarded as
initialized.
-R, --recursive=false: Process the directory used in -f, --filename recursively. Useful when you
want to manage related manifests organized within the same directory.
-l, --selector='': Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l
key1=value1,key2=value2)
--show-events=true: If true, display events related to the described object.Usage:
kubectl describe (-f FILENAME | TYPE [NAME_PREFIX | -l label] | TYPE/NAME) [options]Use "kubectl options" for a list of global command-line options (applies to all commands).
この謎フォーマットにスペースと記号だらけのhelp文からどうバリデーションして持ってくればいいのか全く思いつかなかったってだけです😭
- もうしばらくしたら一応本家にPR送ってみようと思うが、ま、一から作ったもんで、当然弾かれそう・・・期待していないw
- 補完関数の実装とデザインについては誰得感が凄まじい+異常に長くなるので省略したが、まぁ1日は語れるので(例えばグローバルオプションとローカルオプションの両立や、
--namespace
の後の補完のやり方、kubectl logs
でTYPE/NAME
形式でなんで補完出来るのか(後方補完プレフィックスとかを使ってだなry)?などw) 機会と需要があれば書くかもね。
参考文献について
https://github.com/zsh-users/zsh-completions … 他のzsherの猛者達がどうやって書いているか実装をみるのに必要。みんなハードコーディングしてんなって草が生えます。
http://zsh.sourceforge.net/Doc/Release/Completion-System.html … 公式のzshのドキュメント。非常にまとまっており、困ったらここ。全部英語だがもうzshの技術情報といえばここが原点となる。
http://amzn.asia/d/7pfFpjF … zshの本 (エッセンシャルソフトウェアガイドブック)/著広瀬 雄二 … 超貴重な日本語で書かれたzshの本。2009年出版であるものの、現在のzsh5系の基本構文は網羅されている。とても読みやすく、zsher必携の聖書。
終わりに
よければ今回のプラグインにスターをいただければ開発の励みになりますのでよろしくお願いいたします🙇