diff --git a/CHANGELOG.md b/CHANGELOG.md index 275a1a95..f23474b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## [Unreleased] +### Changed +- **シェル補完(bash・zsh)が、`--context NAME` を受け付けるすべてのサブコマンドで `--context` を出すようにしました(#435)。** + これまでは `open` と `project post-start` だけでした。`up`・`down`・`ps`・`login`・`scale`・`build`・`rebuild`、 + `project` / `container` の配下(`logs`・`profile up|down|list` を含む)、`env exec`・`env token` で、`-` を打って補完すると + `--context` が候補に出ます(`devbase up web -` のようにプロジェクト名の後でも出ます)。値(docker context の名前)は補完しません。 + `-` で始まらない語の候補は変わりません。 + ### Fixed - **プロジェクトの `env` に `DEV_SERVICE_NAME=` と空で書いたとき、開発サービス名を `dev` として扱うようにしました(#425)。** これまで `devbase build` は `dev` を建てる一方、`up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・ diff --git a/docs/specifications/cli-argument-resolution.md b/docs/specifications/cli-argument-resolution.md index 21ae4ac8..92bf9bcf 100644 --- a/docs/specifications/cli-argument-resolution.md +++ b/docs/specifications/cli-argument-resolution.md @@ -127,14 +127,16 @@ graph TD | --- | --- | | `bin/devbase` の `_PROJECT_NAME_SUBCOMMANDS` | `project` の直下で `[name]` を取る parser の名前の集合に等しい | | `bin/devbase` の `_NAME_RESOLVABLE_SHORTCUTS` | `cli.SHORTCUTS` のキーと `build` の和に等しい。あわせて、トップレベルで `[name]` を取る parser がすべて `cli.SHORTCUTS` にあること | -| `etc/devbase-completion.bash` | 一時ディレクトリの `projects/` を `DEVBASE_ROOT` にして補完を実行し、`[name]` の集合から補完の例外を引いたすべての道の名前の位置でプロジェクト名が候補に出る。`project` / `container` の直下で `[name]` か `--context` を取るサブコマンド(入れ子は親の `profile` として数える)が、そのグループのサブコマンドの候補に含まれる | -| `etc/_devbase` | zsh を起動せず内容を読む。`case "$words[2]"` から字下げの深さで道の分岐(`up\|down)` のような `\|` 区切りの見出し)を辿り、その本体に `_devbase_project_names` がある。`project_subcommands` / `container_subcommands` の一覧に bash と同じサブコマンドが含まれる | +| `etc/devbase-completion.bash` | 一時ディレクトリの `projects/` を `DEVBASE_ROOT` にして補完を実行し、`[name]` の集合から補完の例外を引いたすべての道の名前の位置でプロジェクト名が候補に出る。`--context` の集合のすべての道(`container` の道は `ct` で始めても)で `devbase <道> -` の候補に `--context` が出る。`[name]` の集合から補完の例外を引いた道では、`devbase <道> web -` の候補にも `--context` が出る。`project` / `container` の直下で `[name]` か `--context` を取るサブコマンド(入れ子は親の `profile` として数える)が、そのグループのサブコマンドの候補に含まれる | +| `etc/_devbase` | zsh を起動せず内容を読む。`case "$words[2]"` から字下げの深さで道の分岐(`up\|down)` のような `\|` 区切りの見出し)を辿り、`[name]` の集合から補完の例外を引いた道の本体に `_devbase_project_names` がある。`--context` の集合のすべての道の本体に、値の補完を持たない `'--context[...]:context:'`(`:context:` の後ろに補完の関数を置かない形)がある。`project_subcommands` / `container_subcommands` の一覧に bash と同じサブコマンドが含まれる | | この節の 2 つの表 | 見出し `` #### `[name]` を受け付けるサブコマンド `` と `` #### `--context` を受け付けるサブコマンド `` の直後の表を読む。1 列目は `devbase <グループ...> ` の形で、間の語が道の頭になる。2 列目にバッククォートで並べた語を `` に入れる。表は `[name]` の集合・`--context` の集合にそれぞれ等しい | 補完の例外は `project profile up` / `down` / `list` の 3 つで、理由は「1 つ目の位置引数は、値が 1 個なら プロファイル名、2 個ならプロジェクト名になり、補完の時点ではどちらか決まらない」。一致テストは例外を 理由つきの一覧として持ち、例外の道が `[name]` の集合に実在すること・理由が空でないことも確かめる。 -補完が `--context` の値を補完するかどうか(今は `open` と `post-start` だけ)は比べない。 +補完は `--context` の値(docker context の名前)を bash でも zsh でも補完しない。zsh は一致テストが上の +`--context` の指定の形で確かめ、bash は `tests/cli/test_completion.py` が `--context` の直後の候補が +0 件であることを代表の道で確かめる。 ### 名前の形 diff --git a/etc/_devbase b/etc/_devbase index c7a850dc..6facdb60 100644 --- a/etc/_devbase +++ b/etc/_devbase @@ -145,23 +145,35 @@ _devbase() { 'rotate:Rotate old snapshots' ) + # --context NAME を取るサブコマンドは、どれも値を補完しない --context を出す (#435)。 case "$words[2]" in login) - _values 'index' 1 2 + _arguments \ + '--context[Docker context]:context:' \ + '1:index:(1 2)' + ;; + build) + _arguments \ + '--context[Docker context]:context:' \ + '1:image:' ;; # トップレベルシノニム: up/down/ps/scale/rebuild は [name] を取るためプロジェクト名を補完。 up|down|rebuild) - _devbase_project_names + _arguments \ + '--context[Docker context]:context:' \ + '1:name:_devbase_project_names' ;; ps) - if [[ "$words[3]" == -* || -n "$words[3]" ]]; then - _arguments '--all[Show all containers]' '-a[Show all containers]' - else - _devbase_project_names - fi + _arguments \ + '--all[Show all containers]' \ + '-a[Show all containers]' \ + '--context[Docker context]:context:' \ + '1:name:_devbase_project_names' ;; scale) - _devbase_project_names + _arguments \ + '--context[Docker context]:context:' \ + '1:name:_devbase_project_names' ;; open) _arguments \ @@ -180,13 +192,19 @@ _devbase() { project) case "$words[3]" in up|down|rebuild) - _devbase_project_names + _arguments \ + '--context[Docker context]:context:' \ + '1:name:_devbase_project_names' ;; login) - _values 'index' 1 2 + _arguments \ + '--context[Docker context]:context:' \ + '1:index:(1 2)' ;; scale) - _devbase_project_names + _arguments \ + '--context[Docker context]:context:' \ + '1:name:_devbase_project_names' ;; ps) # name (positional) と -a フラグを _arguments に統合。 @@ -195,6 +213,7 @@ _devbase() { _arguments \ '--all[Show all containers]' \ '-a[Show all containers]' \ + '--context[Docker context]:context:' \ '1:name:_devbase_project_names' ;; logs) @@ -202,10 +221,13 @@ _devbase() { '--follow[Follow log output]' \ '-f[Follow log output]' \ '--tail[Number of lines]:lines:' \ + '--context[Docker context]:context:' \ '1:name:_devbase_project_names' ;; build) - _arguments '1:image:' + _arguments \ + '--context[Docker context]:context:' \ + '1:image:' ;; list) _arguments \ @@ -216,7 +238,16 @@ _devbase() { '-i[(compat) interactive selection, default]' ;; profile) - _values 'operation' up down list + case "$words[4]" in + up|down|list) + _arguments \ + '--context[Docker context]:context:' \ + '*:profile:' + ;; + *) + _values 'operation' up down list + ;; + esac ;; open) _arguments \ @@ -236,26 +267,48 @@ _devbase() { ;; container|ct) case "$words[3]" in + up|down|rebuild) + _arguments '--context[Docker context]:context:' + ;; login) - _values 'index' 1 2 + _arguments \ + '--context[Docker context]:context:' \ + '1:index:(1 2)' ;; scale) - _values 'new_scale' 1 2 3 4 5 + _arguments \ + '--context[Docker context]:context:' \ + '1:new_scale:(1 2 3 4 5)' ;; ps) - _arguments '--all[Show all containers]' '-a[Show all containers]' + _arguments \ + '--all[Show all containers]' \ + '-a[Show all containers]' \ + '--context[Docker context]:context:' ;; logs) _arguments \ '--follow[Follow log output]' \ '-f[Follow log output]' \ - '--tail[Number of lines]:lines:' + '--tail[Number of lines]:lines:' \ + '--context[Docker context]:context:' ;; build) - _arguments '1:image:' + _arguments \ + '--context[Docker context]:context:' \ + '1:image:' ;; profile) - _values 'operation' up down list + case "$words[4]" in + up|down|list) + _arguments \ + '--context[Docker context]:context:' \ + '*:profile:' + ;; + *) + _values 'operation' up down list + ;; + esac ;; open) _arguments \ @@ -328,7 +381,12 @@ _devbase() { '--keep-last[Keep only the last N backup directories]:n:' ;; exec) - _arguments '*:command:_command_names -e' + _arguments \ + '--context[Docker context]:context:' \ + '*:command:_command_names -e' + ;; + token) + _arguments '--context[Docker context]:context:' ;; encrypt|decrypt) _arguments \ diff --git a/etc/devbase-completion.bash b/etc/devbase-completion.bash index 74f0c484..974091b4 100644 --- a/etc/devbase-completion.bash +++ b/etc/devbase-completion.bash @@ -22,13 +22,16 @@ _devbase_project_names() { } # cur が `-` で始まるときはフラグ候補 ($1) を COMPREPLY に入れる。 -# 第 3 引数に names を渡すと、それ以外のときにプロジェクト名を補完する。 +# それ以外のときは、第 3 引数が names ならプロジェクト名を、ほかの空でない値なら +# その語の並び (例: login の "1 2") を補完する。 _devbase_complete_flags() { local flags="$1" cur="$2" fallback="${3:-}" if [[ "$cur" == -* ]]; then COMPREPLY=($(compgen -W "$flags" -- "$cur")) elif [ "$fallback" = "names" ]; then COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur")) + elif [ -n "$fallback" ]; then + COMPREPLY=($(compgen -W "$fallback" -- "$cur")) fi } @@ -50,11 +53,19 @@ _devbase_completions() { local plugin_subcommands="list install uninstall update info sync repo" local repo_subcommands="add remove list refresh" local snapshot_subcommands="create list restore copy delete rotate" - # ps / logs / open のフラグ候補 (トップレベル・project・container で共通)。 - local ps_flags="--all -a" - local logs_flags="--follow -f --tail" + # --context NAME を取るサブコマンドのフラグ候補 (トップレベル・project・container で共通)。 + # 道の直後と、[name] を取るものはプロジェクト名の直後で出す (#435)。 + local ctx_flags="--context" + local ps_flags="--all -a --context" + local logs_flags="--follow -f --tail --context" local open_flags="--open-index --context" + # --context の値 (docker context の名前) は補完しない。 + if [ "$prev" = "--context" ]; then + COMPREPLY=() + return 0 + fi + case "$cword" in 1) COMPREPLY=($(compgen -W "$commands" -- "$cur")) @@ -62,12 +73,16 @@ _devbase_completions() { 2) case "$prev" in login) - COMPREPLY=($(compgen -W "1 2" -- "$cur")) + _devbase_complete_flags "$ctx_flags" "$cur" "1 2" + ;; + # build は image を取る (候補は出さない)。 + build) + _devbase_complete_flags "$ctx_flags" "$cur" ;; # トップレベルシノニム: up/down/scale/rebuild は [name] を取るため # プロジェクト名を補完する (login=index / build=image は対象外)。 up|down|scale|rebuild) - COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur")) + _devbase_complete_flags "$ctx_flags" "$cur" names ;; # open は [name] と --open-index / --context を取る (PLAN59)。 open) @@ -101,24 +116,35 @@ _devbase_completions() { ;; 3) local group="${words[1]}" - # トップレベルシノニム ps: `devbase ps web -` (group=ps, cword=3) - # でも name 位置が埋まった後にフラグを補完する。project ps と対称化。 - if [ "$group" = "ps" ]; then - _devbase_complete_flags "$ps_flags" "$cur" - fi + # トップレベルシノニム: `devbase ps web -` (group=ps, cword=3) + # のように name 位置が埋まった後でもフラグを補完する。project と対称化。 + case "$group" in + ps) + _devbase_complete_flags "$ps_flags" "$cur" + ;; + open) + _devbase_complete_flags "$open_flags" "$cur" + ;; + up|down|scale|rebuild) + _devbase_complete_flags "$ctx_flags" "$cur" + ;; + esac # project subcommand arguments (推奨グループ) if [ "$group" = "project" ]; then case "$prev" in up|down|rebuild) - COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur")) + _devbase_complete_flags "$ctx_flags" "$cur" names ;; login) - COMPREPLY=($(compgen -W "1 2" -- "$cur")) + _devbase_complete_flags "$ctx_flags" "$cur" "1 2" ;; scale) # `project scale N` / `project scale N` の両形。 # name 補完を提示する (数値はユーザが直接入力)。 - COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur")) + _devbase_complete_flags "$ctx_flags" "$cur" names + ;; + build) + _devbase_complete_flags "$ctx_flags" "$cur" ;; ps) _devbase_complete_flags "$ps_flags" "$cur" names @@ -139,22 +165,21 @@ _devbase_completions() { ;; # post-start は [name] と --context を取る (#371) post-start) - if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--context" -- "$cur")) - else - COMPREPLY=($(compgen -W "$(_devbase_project_names)" -- "$cur")) - fi + _devbase_complete_flags "$ctx_flags" "$cur" names ;; esac fi # container subcommand arguments (非推奨: project へ移行してください) if [ "$group" = "container" ] || [ "$group" = "ct" ]; then case "$prev" in + up|down|build|rebuild) + _devbase_complete_flags "$ctx_flags" "$cur" + ;; login) - COMPREPLY=($(compgen -W "1 2" -- "$cur")) + _devbase_complete_flags "$ctx_flags" "$cur" "1 2" ;; scale) - COMPREPLY=($(compgen -W "1 2 3 4 5" -- "$cur")) + _devbase_complete_flags "$ctx_flags" "$cur" "1 2 3 4 5" ;; profile) COMPREPLY=($(compgen -W "up down list" -- "$cur")) @@ -173,6 +198,10 @@ _devbase_completions() { # env subcommand arguments if [ "$group" = "env" ]; then case "$prev" in + # exec / token は --context を取る (exec は `--` より前だけ) + exec|token) + _devbase_complete_flags "$ctx_flags" "$cur" + ;; init) if [[ "$cur" == -* ]]; then COMPREPLY=($(compgen -W "--reset" -- "$cur")) @@ -249,7 +278,7 @@ _devbase_completions() { ;; 4) local group="${words[1]}" - # project ps/logs: name 位置が埋まった後 (例: `project ps web -`) + # project: name 位置が埋まった後 (例: `project ps web -`) # でもフラグを補完する。subcommand は words[2]。 if [ "$group" = "project" ]; then case "${words[2]}" in @@ -259,8 +288,24 @@ _devbase_completions() { logs) _devbase_complete_flags "$logs_flags" "$cur" ;; + open) + _devbase_complete_flags "$open_flags" "$cur" + ;; + up|down|rebuild|scale|post-start) + _devbase_complete_flags "$ctx_flags" "$cur" + ;; esac fi + # project / container の profile up|down|list の直後 (例: `project profile up -`) + if [ "$group" = "project" ] || [ "$group" = "container" ] || [ "$group" = "ct" ]; then + if [ "${words[2]}" = "profile" ]; then + case "$prev" in + up|down|list) + _devbase_complete_flags "$ctx_flags" "$cur" + ;; + esac + fi + fi # plugin install flags after source argument if [ "$group" = "plugin" ] || [ "$group" = "pl" ]; then if [ "${words[2]}" = "install" ]; then diff --git a/tests/cli/test_completion.py b/tests/cli/test_completion.py index df09bf13..88a250dc 100644 --- a/tests/cli/test_completion.py +++ b/tests/cli/test_completion.py @@ -109,9 +109,9 @@ def test_bash_top_level_ps_name_completion(fake_root): def test_bash_top_level_ps_flag_completion(fake_root): - """`devbase ps -` は -a / --all を補完する (project ps と対称)。""" + """`devbase ps -` は -a / --all / --context を補完する (project ps と対称)。""" out = _bash_complete("devbase ps '-'", 2, fake_root) - assert set(out) == {"--all", "-a"} + assert set(out) == {"--all", "-a", "--context"} def test_bash_project_name_excludes_bad_symlinks(fake_root_with_bad_links): @@ -126,19 +126,19 @@ def test_bash_project_name_excludes_bad_symlinks(fake_root_with_bad_links): def test_bash_project_ps_flag_after_name(fake_root): """`devbase project ps web -` (cword 4) で -a / --all を補完する。""" out = _bash_complete("devbase project ps web '-'", 4, fake_root) - assert set(out) == {"--all", "-a"} + assert set(out) == {"--all", "-a", "--context"} def test_bash_project_logs_flag_after_name(fake_root): """`devbase project logs web -` (cword 4) で -f/--follow/--tail を補完する。""" out = _bash_complete("devbase project logs web '-'", 4, fake_root) - assert set(out) == {"--follow", "-f", "--tail"} + assert set(out) == {"--follow", "-f", "--tail", "--context"} def test_bash_top_level_ps_flag_after_name(fake_root): """`devbase ps web -` (cword 3) で -a / --all を補完する (project ps と対称)。""" out = _bash_complete("devbase ps web '-'", 3, fake_root) - assert set(out) == {"--all", "-a"} + assert set(out) == {"--all", "-a", "--context"} def test_bash_project_list_flags(fake_root): @@ -227,11 +227,11 @@ def test_zsh_completion_mentions_open(): ("devbase open ''", 2, _NAMES), ("devbase open '-'", 2, {"--open-index", "--context"}), ("devbase ps ''", 2, _NAMES), - ("devbase ps '-'", 2, {"--all", "-a"}), + ("devbase ps '-'", 2, {"--all", "-a", "--context"}), ("devbase project ps ''", 3, _NAMES), - ("devbase project ps '-'", 3, {"--all", "-a"}), + ("devbase project ps '-'", 3, {"--all", "-a", "--context"}), ("devbase project logs ''", 3, _NAMES), - ("devbase project logs '-'", 3, {"--follow", "-f", "--tail"}), + ("devbase project logs '-'", 3, {"--follow", "-f", "--tail", "--context"}), ("devbase project open ''", 3, _NAMES), ("devbase project open '-'", 3, {"--open-index", "--context"}), ("devbase project post-start ''", 3, _NAMES), @@ -246,8 +246,8 @@ def test_bash_flag_or_project_name_branch(fake_root, words, cword, expected): @pytest.mark.parametrize("words, cword, expected", [ ("devbase container open '-'", 3, {"--open-index", "--context"}), - ("devbase container ps '-'", 3, {"--all", "-a"}), - ("devbase ct logs '-'", 3, {"--follow", "-f", "--tail"}), + ("devbase container ps '-'", 3, {"--all", "-a", "--context"}), + ("devbase ct logs '-'", 3, {"--follow", "-f", "--tail", "--context"}), ("devbase container open ''", 3, set()), ("devbase container ps ''", 3, set()), ("devbase ct logs ''", 3, set()), @@ -255,3 +255,14 @@ def test_bash_flag_or_project_name_branch(fake_root, words, cword, expected): ]) def test_bash_container_flags_only_without_names(fake_root, words, cword, expected): assert set(_bash_complete(words, cword, fake_root)) == expected + + +# #435: --context の値 (docker context の名前) は補完しない +@pytest.mark.parametrize("words, cword", [ + ("devbase up --context ''", 3), + ("devbase project up web --context ''", 5), + ("devbase container ps --context ''", 4), + ("devbase env exec --context ''", 4), +]) +def test_bash_context_value_is_not_completed(fake_root, words, cword): + assert _bash_complete(words, cword, fake_root) == [] diff --git a/tests/cli/test_name_context_consistency.py b/tests/cli/test_name_context_consistency.py index ee5be0f2..1da729c5 100644 --- a/tests/cli/test_name_context_consistency.py +++ b/tests/cli/test_name_context_consistency.py @@ -5,7 +5,8 @@ 写しを取り残すと、取り残した場所(ファイル)と欠けた・余分なサブコマンドを挙げて落ちる。 - `bin/devbase` の `_PROJECT_NAME_SUBCOMMANDS` / `_NAME_RESOLVABLE_SHORTCUTS` -- 補完 2 ファイル(`etc/devbase-completion.bash` は実行して、`etc/_devbase` は静的に読む) +- 補完 2 ファイル(`etc/devbase-completion.bash` は実行して、`etc/_devbase` は静的に読む)。 + `[name]` の位置のプロジェクト名と、`--context` の旗(値は補完しない)を比べる - 確定仕様 `docs/specifications/cli-argument-resolution.md` の 2 つの表 集合の要素は「サブコマンドへの道筋」のタプルで表す(例: `("project", "profile", "up")`、 @@ -158,9 +159,9 @@ def fake_root(tmp_path): return tmp_path -def _bash_complete(words, devbase_root): - """`devbase ""` の最後の位置で bash 補完を実行し、候補を返す。""" - quoted = " ".join(f'"{w}"' for w in ("devbase",) + tuple(words) + ("",)) +def _bash_complete(words, devbase_root, cur=""): + """`devbase ` の最後の位置で bash 補完を実行し、候補を返す。""" + quoted = " ".join(f'"{w}"' for w in ("devbase",) + tuple(words) + (cur,)) script = f""" set -e source "{BASH_COMPLETION}" @@ -227,6 +228,44 @@ def test_zsh_completion_offers_project_names(): assert not missing, f"etc/_devbase が `[name]` の位置でプロジェクト名(_devbase_project_names)を補完しない: {missing}" +# --------------------------------------------------------------------------- +# 補完: `--context` の旗 +# --------------------------------------------------------------------------- + +def _with_alias(paths): + # `ct` で始めても `container` と同じ候補が出ること + return sorted(paths | {("ct",) + p[1:] for p in paths if p[0] == "container"}) + + +def test_bash_completion_offers_context_after_path(fake_root): + missing = [path for path in _with_alias(CONTEXT_PATHS) + if "--context" not in _bash_complete(path, fake_root, "-")] + assert not missing, f"etc/devbase-completion.bash が `devbase <道> -` で --context を補完しない: {missing}" + + +def test_bash_completion_offers_context_after_name(fake_root): + paths = (NAME_PATHS - set(COMPLETION_NAME_EXCEPTIONS)) & CONTEXT_PATHS + assert paths, "`[name]` と `--context` の両方を取るサブコマンドが無い" + missing = [path for path in sorted(paths) + if "--context" not in _bash_complete(path + ("web",), fake_root, "-")] + assert not missing, (f"etc/devbase-completion.bash が `devbase <道> web -` で --context を補完しない: " + f"{missing}") + + +# 値の補完を持たない `--context` の指定(`:context:` の後ろに補完の関数を置かない) +_ZSH_CONTEXT_SPEC = re.compile(r"'--context\[[^\]']*\]:context:'") + + +def test_zsh_completion_offers_context(): + missing = [] + for path in sorted(CONTEXT_PATHS): + body = _zsh_branch(path) + if body is None or not _ZSH_CONTEXT_SPEC.search(body): + missing.append(path) + assert not missing, (f"etc/_devbase が道の分岐で値を補完しない --context" + f"('--context[...]:context:')を出さない: {missing}") + + # --------------------------------------------------------------------------- # 補完: project / container のサブコマンドの一覧 # ---------------------------------------------------------------------------