Skip to content

Commit 7049f94

Browse files
committed
fix: allow global options (--output, -p, etc.) after subcommands
Users expect `zad metrics overview --output json` to work, but Typer only recognizes global options before the subcommand. Added a custom TyperGroup that hoists global options to the front of the arg list before parsing, so they work in any position.
1 parent 63a1807 commit 7049f94

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/zad_cli/cli.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import typer
6+
from typer.core import TyperGroup
67

78
from zad_cli import __version__
89
from zad_cli.commands import (
@@ -21,7 +22,43 @@
2122
from zad_cli.commands.config_cmd import app as config_app
2223
from zad_cli.commands.open_cmd import app as open_app
2324

25+
26+
class _GlobalOptionsGroup(TyperGroup):
27+
"""Hoist global options to before the subcommand so they work in any position."""
28+
29+
_OPTS_WITH_VALUE = frozenset({"--output", "-o", "--api-key", "--api-url", "--project", "-p"})
30+
_FLAGS = frozenset({"--no-wait", "--verbose", "-v"})
31+
32+
def parse_args(self, ctx, args): # noqa: ANN001
33+
global_args: list[str] = []
34+
remaining: list[str] = []
35+
i = 0
36+
while i < len(args):
37+
arg = args[i]
38+
if arg == "--":
39+
remaining.extend(args[i:])
40+
break
41+
elif "=" in arg and arg.split("=", 1)[0] in self._OPTS_WITH_VALUE:
42+
global_args.append(arg)
43+
i += 1
44+
elif arg in self._OPTS_WITH_VALUE:
45+
global_args.append(arg)
46+
if i + 1 < len(args):
47+
global_args.append(args[i + 1])
48+
i += 2
49+
else:
50+
i += 1
51+
elif arg in self._FLAGS:
52+
global_args.append(arg)
53+
i += 1
54+
else:
55+
remaining.append(arg)
56+
i += 1
57+
return super().parse_args(ctx, global_args + remaining)
58+
59+
2460
app = typer.Typer(
61+
cls=_GlobalOptionsGroup,
2562
help="CLI for ZAD (Zelfservice Applicatie Deployment).",
2663
no_args_is_help=True,
2764
rich_markup_mode="rich",

tests/test_cli.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,44 @@ def test_all_subcommands_have_help():
153153
for cmd in subcommands:
154154
result = _run_help(cmd)
155155
assert result.returncode == 0, f"{cmd} --help failed: {result.stderr}"
156+
157+
158+
# --- Global options in any position ---
159+
160+
_MINIMAL_ENV = {"PATH": "/usr/bin:/bin", "NO_COLOR": "1", "TERM": "dumb"}
161+
162+
163+
def test_global_option_after_subcommand():
164+
"""Global options like --output should work after the subcommand."""
165+
result = subprocess.run(
166+
[sys.executable, "-m", "zad_cli", "metrics", "overview", "--output", "json"],
167+
capture_output=True,
168+
text=True,
169+
env=_MINIMAL_ENV,
170+
)
171+
err = _strip_ansi(result.stderr)
172+
assert "No such option" not in err
173+
174+
175+
def test_global_option_equals_form():
176+
"""--output=json form should also work after the subcommand."""
177+
result = subprocess.run(
178+
[sys.executable, "-m", "zad_cli", "deployment", "list", "--output=yaml"],
179+
capture_output=True,
180+
text=True,
181+
env=_MINIMAL_ENV,
182+
)
183+
err = _strip_ansi(result.stderr)
184+
assert "No such option" not in err
185+
186+
187+
def test_global_flag_after_subcommand():
188+
"""Global flags like --verbose should work after the subcommand."""
189+
result = subprocess.run(
190+
[sys.executable, "-m", "zad_cli", "metrics", "overview", "--verbose"],
191+
capture_output=True,
192+
text=True,
193+
env=_MINIMAL_ENV,
194+
)
195+
err = _strip_ansi(result.stderr)
196+
assert "No such option" not in err

0 commit comments

Comments
 (0)