Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / cli / commands / help_command.py: 99%
121 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright © 2025 Bijan Mousavi
4"""Runtime execution for the `help` command (IO + exit behavior)."""
6from __future__ import annotations
8from collections.abc import Callable
9import io
10import sys
11import time
13import typer
15from bijux_cli.cli.color import resolve_click_color
16from bijux_cli.cli.commands.help import (
17 _HUMAN,
18 _VALID_FORMATS,
19 _build_help_intent,
20 _build_help_payload,
21 _find_target_command,
22 _get_formatted_help,
23)
24from bijux_cli.cli.core.command import (
25 contains_non_ascii_env,
26 raise_exit_intent,
27 record_history,
28 validate_common_flags,
29)
30from bijux_cli.cli.core.constants import (
31 OPT_FORMAT,
32 OPT_LOG_LEVEL,
33 OPT_PRETTY,
34 OPT_QUIET,
35)
36from bijux_cli.cli.core.help_text import (
37 HELP_FORMAT_HELP,
38 HELP_LOG_LEVEL,
39 HELP_NO_PRETTY,
40 HELP_QUIET,
41)
42from bijux_cli.core.di import DIContainer
43from bijux_cli.core.enums import (
44 ErrorType,
45 ExitCode,
46 OutputFormat,
47)
48from bijux_cli.core.exit_policy import ExitIntent, ExitIntentError
49from bijux_cli.core.precedence import (
50 EffectiveConfig,
51 Flags,
52 OutputConfig,
53 default_execution_policy,
54)
55from bijux_cli.core.runtime import AsyncTyper
57typer.core.rich = None # type: ignore[attr-defined]
60def _resolve_help_config() -> tuple[EffectiveConfig, OutputConfig]:
61 """Resolve effective and output config for help handling."""
62 try:
63 effective = DIContainer.current().resolve(EffectiveConfig)
64 except Exception:
65 policy = default_execution_policy()
66 effective = EffectiveConfig(
67 flags=Flags(
68 quiet=policy.quiet,
69 log_level=policy.log_level,
70 color=policy.color,
71 format=policy.output_format,
72 )
73 )
74 try:
75 output = DIContainer.current().resolve(OutputConfig)
76 except Exception:
77 policy = default_execution_policy()
78 output = OutputConfig(
79 include_runtime=policy.log_policy.show_internal,
80 pretty=policy.log_policy.pretty_default,
81 log_level=effective.flags.log_level,
82 color=effective.flags.color,
83 format=effective.flags.format,
84 log_policy=policy.log_policy,
85 )
86 return effective, output
89def _emit_structured_help(
90 *,
91 command: str,
92 payload: dict[str, object],
93 output_format: OutputFormat,
94 pretty: bool,
95 emit_output: bool,
96) -> None:
97 """Emit structured help payload with history recording."""
98 record_history(command, ExitCode.SUCCESS)
99 if not emit_output:
100 raise ExitIntentError(
101 ExitIntent(
102 code=ExitCode.SUCCESS,
103 stream=None,
104 payload=None,
105 fmt=output_format,
106 pretty=pretty,
107 show_traceback=False,
108 )
109 )
110 raise ExitIntentError(
111 ExitIntent(
112 code=ExitCode.SUCCESS,
113 stream="stdout",
114 payload=payload,
115 fmt=output_format,
116 pretty=pretty,
117 show_traceback=False,
118 )
119 )
122def _emit_human_help(
123 *,
124 emit_output: bool,
125 color: bool,
126 help_text_provider: Callable[[], str],
127) -> None:
128 """Emit human help output without building text in quiet mode."""
129 if not emit_output:
130 raise ExitIntentError(
131 ExitIntent(
132 code=ExitCode.SUCCESS,
133 stream=None,
134 payload=None,
135 fmt=OutputFormat.JSON,
136 pretty=False,
137 show_traceback=False,
138 )
139 )
140 text = help_text_provider()
141 typer.echo(text, color=color, err=False)
142 raise ExitIntentError(
143 ExitIntent(
144 code=ExitCode.SUCCESS,
145 stream=None,
146 payload=None,
147 fmt=OutputFormat.JSON,
148 pretty=False,
149 show_traceback=False,
150 )
151 )
154def _capture_help_text(help_text_provider: Callable[[], str]) -> str:
155 """Capture help text without leaking human output to stdout."""
156 buf = io.StringIO()
157 original = sys.stdout
158 sys.stdout = buf
159 try:
160 text = help_text_provider()
161 finally:
162 sys.stdout = original
163 captured = buf.getvalue()
164 if text.strip():
165 return text
166 return captured
169def _override_fmt_from_argv(fmt: str) -> str:
170 """Prefer an explicit format flag value when provided on the CLI."""
171 if fmt.strip().lower() != _HUMAN:
172 return fmt
173 argv = sys.argv[1:]
174 if "help" in argv:
175 argv = argv[argv.index("help") + 1 :]
176 for idx, arg in enumerate(argv):
177 if arg in OPT_FORMAT and idx + 1 < len(argv):
178 return argv[idx + 1]
179 return fmt
182help_app = AsyncTyper(
183 name="help",
184 add_completion=False,
185 help="Show help for any CLI command or subcommand.",
186 context_settings={
187 "help_option_names": ["-h", "--help"],
188 "ignore_unknown_options": True,
189 "allow_extra_args": True,
190 "allow_interspersed_args": True,
191 },
192)
194ARGS = typer.Argument(None, help="Command path, e.g. 'config get'.")
197@help_app.callback(invoke_without_command=True)
198def help_callback(
199 ctx: typer.Context,
200 command_path: list[str] | None = ARGS,
201 quiet: bool = typer.Option(False, *OPT_QUIET, help=HELP_QUIET),
202 fmt: str = typer.Option(_HUMAN, *OPT_FORMAT, help=HELP_FORMAT_HELP),
203 pretty: bool = typer.Option(True, OPT_PRETTY, help=HELP_NO_PRETTY),
204 log_level: str = typer.Option("info", *OPT_LOG_LEVEL, help=HELP_LOG_LEVEL),
205) -> None:
206 """Entry point for the `bijux help` command."""
207 _ = (quiet, pretty, log_level)
208 started_at = time.perf_counter()
209 effective, output = _resolve_help_config()
210 emit_output = not effective.flags.quiet
211 fmt = _override_fmt_from_argv(fmt)
213 if "-h" in sys.argv or "--help" in sys.argv:
214 all_args = sys.argv[2:]
215 known_flags_with_args = set(OPT_FORMAT)
216 path_tokens = []
217 i = 0
218 while i < len(all_args):
219 arg = all_args[i]
220 if arg in known_flags_with_args:
221 i += 2
222 elif arg.startswith("-"):
223 i += 1
224 else:
225 path_tokens.append(arg)
226 i += 1
228 target = _find_target_command(ctx, path_tokens) or _find_target_command(ctx, [])
229 if target:
230 target_cmd, target_ctx = target
231 help_text = _get_formatted_help(target_cmd, target_ctx)
232 if emit_output: 232 ↛ 238line 232 didn't jump to line 238 because the condition on line 232 was always true
233 typer.echo(
234 help_text,
235 color=resolve_click_color(quiet=False, fmt=None),
236 err=False,
237 )
238 raise ExitIntentError(
239 ExitIntent(
240 code=ExitCode.SUCCESS,
241 stream=None,
242 payload=None,
243 fmt=OutputFormat.JSON,
244 pretty=False,
245 show_traceback=False,
246 )
247 )
248 raise ExitIntentError(
249 ExitIntent(
250 code=ExitCode.SUCCESS,
251 stream=None,
252 payload=None,
253 fmt=OutputFormat.JSON,
254 pretty=False,
255 show_traceback=False,
256 )
257 )
259 tokens = command_path or []
260 command = "help"
261 intent = _build_help_intent(tokens, fmt, effective, output)
263 if intent.fmt_lower != "human":
264 validate_common_flags(
265 intent.format_value or OutputFormat.JSON,
266 command,
267 intent.quiet,
268 include_runtime=intent.include_runtime,
269 log_level=intent.log_level,
270 )
272 if intent.fmt_lower not in _VALID_FORMATS:
273 raise_exit_intent(
274 f"Unsupported format: '{fmt}'",
275 code=2,
276 failure="format",
277 command=command,
278 fmt=intent.error_fmt,
279 quiet=intent.quiet,
280 include_runtime=intent.include_runtime,
281 log_level=intent.log_level,
282 error_type=ErrorType.USER_INPUT,
283 )
285 for token in intent.tokens:
286 if "\x00" in token:
287 raise_exit_intent(
288 "Embedded null byte in command path",
289 code=3,
290 failure="null_byte",
291 command=command,
292 fmt=intent.error_fmt,
293 quiet=intent.quiet,
294 include_runtime=intent.include_runtime,
295 log_level=intent.log_level,
296 error_type=ErrorType.ASCII,
297 )
298 try:
299 token.encode("ascii")
300 except UnicodeEncodeError:
301 raise_exit_intent(
302 f"Non-ASCII characters in command path: {token!r}",
303 code=3,
304 failure="ascii",
305 command=command,
306 fmt=intent.error_fmt,
307 quiet=intent.quiet,
308 include_runtime=intent.include_runtime,
309 log_level=intent.log_level,
310 error_type=ErrorType.ASCII,
311 )
313 if contains_non_ascii_env():
314 raise_exit_intent(
315 "Non-ASCII in environment",
316 code=3,
317 failure="ascii",
318 command=command,
319 fmt=intent.error_fmt,
320 quiet=intent.quiet,
321 include_runtime=intent.include_runtime,
322 log_level=intent.log_level,
323 error_type=ErrorType.ASCII,
324 )
326 target = _find_target_command(ctx, intent.tokens)
327 if not target:
328 raise_exit_intent(
329 f"No such command: {' '.join(intent.tokens)}",
330 code=2,
331 failure="not_found",
332 command=command,
333 fmt=intent.error_fmt,
334 quiet=intent.quiet,
335 include_runtime=intent.include_runtime,
336 log_level=intent.log_level,
337 error_type=ErrorType.USER_INPUT,
338 )
340 target_cmd, target_ctx = target
342 if intent.fmt_lower == _HUMAN:
343 _emit_human_help(
344 emit_output=emit_output,
345 color=bool(resolve_click_color(quiet=intent.quiet, fmt=None)),
346 help_text_provider=lambda: _get_formatted_help(target_cmd, target_ctx),
347 )
349 help_text = _capture_help_text(lambda: _get_formatted_help(target_cmd, target_ctx))
350 try:
351 payload = _build_help_payload(help_text, intent.include_runtime, started_at)
352 except ValueError as exc:
353 raise_exit_intent(
354 str(exc),
355 code=3,
356 failure="ascii",
357 command=command,
358 fmt=intent.error_fmt,
359 quiet=intent.quiet,
360 include_runtime=intent.include_runtime,
361 log_level=intent.log_level,
362 )
364 output_format = (
365 OutputFormat.YAML
366 if intent.format_value == OutputFormat.YAML
367 else OutputFormat.JSON
368 )
369 _emit_structured_help(
370 command=command,
371 payload=payload,
372 output_format=output_format,
373 pretty=intent.pretty,
374 emit_output=emit_output,
375 )
378__all__ = ["ARGS", "help_app", "help_callback"]