From c0bff56dca1f18ef6b027b4842f20a0eca2846f5 Mon Sep 17 00:00:00 2001 From: Manjunath Patil Date: Wed, 1 Oct 2025 19:22:34 +0530 Subject: [PATCH 1/4] global --verbose flag #326 done --- cli/README.md | 25 +++++++++++- cli/cli.py | 23 ++++++++++- cli/commands/hello.py | 22 ++++++++++- cli/commands/listing.py | 74 +++++++++++++++++++++++++++++++++++ cli/state.py | 29 ++++++++++++++ cli/test_cli.py | 87 ++++++++++++++++++++++++++++++----------- 6 files changed, 234 insertions(+), 26 deletions(-) create mode 100644 cli/commands/listing.py create mode 100644 cli/state.py diff --git a/cli/README.md b/cli/README.md index 87b8584e..810e4e03 100644 --- a/cli/README.md +++ b/cli/README.md @@ -25,34 +25,56 @@ python -m cli.cli --help ## Commands ### Hello Command + ```bash linux-cli hello greet linux-cli hello greet "Linux User" +linux-cli hello greet --verbose +``` + +### List Command + +```bash +linux-cli list +linux-cli list --limit 5 +linux-cli list --verbose +``` + +### Global Verbose Flag + +```bash +linux-cli --verbose hello greet +linux-cli --verbose list --limit 5 ``` ## Development ### Running Tests + ```bash pytest ``` ### Code Formatting + ```bash black cli/ ``` ### Import Sorting + ```bash isort cli/ ``` ### Linting + ```bash flake8 cli/ ``` ### Type Checking + ```bash mypy cli/ ``` @@ -60,8 +82,9 @@ mypy cli/ ## GitHub Actions The CLI has its own workflow that runs: + - Code formatting checks (Black) -- Import sorting checks (isort) +- Import sorting checks (isort) - Linting (Flake8) - Type checking (MyPy) - Tests (pytest) diff --git a/cli/cli.py b/cli/cli.py index 4a52975d..e7f0b9c8 100644 --- a/cli/cli.py +++ b/cli/cli.py @@ -1,12 +1,33 @@ import typer -from commands import hello +from commands import hello, listing +from state import set_verbose # Create the root CLI app app = typer.Typer(help="101 Linux Commands CLI 🚀") + +@app.callback() +def main( + ctx: typer.Context, + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + is_flag=True, + help="Enable verbose debug output for all commands.", + ), +) -> None: + """Configure application-wide options before subcommands run.""" + + set_verbose(ctx, verbose) + if verbose: + typer.echo("[verbose] Verbose mode enabled", err=True) + + # Register subcommands app.add_typer(hello.app, name="hello") +app.add_typer(listing.app, name="list") if __name__ == "__main__": app() diff --git a/cli/commands/hello.py b/cli/commands/hello.py index ebe8ca0c..b12280df 100644 --- a/cli/commands/hello.py +++ b/cli/commands/hello.py @@ -1,9 +1,29 @@ import typer +from state import set_verbose, verbose_active + + app = typer.Typer(help="Hello command group") @app.command() -def greet(name: str = "World"): +def greet( + ctx: typer.Context, + name: str = typer.Option("World", "--name", "-n", help="Name to greet."), + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + is_flag=True, + help="Enable verbose output for this command.", + ), +) -> None: """Say hello to someone.""" + + if verbose: + set_verbose(ctx, True) + + if verbose_active(ctx): + typer.echo(f"[verbose] Preparing greeting for {name}", err=True) + typer.echo(f"Hello, {name}!") diff --git a/cli/commands/listing.py b/cli/commands/listing.py new file mode 100644 index 00000000..412f33e8 --- /dev/null +++ b/cli/commands/listing.py @@ -0,0 +1,74 @@ +"""Command utilities for listing available lessons.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Iterable, Optional + +import typer + +from state import set_verbose, verbose_active + +app = typer.Typer(help="List available Linux command lessons.") + +_CONTENT_DIR = Path(__file__).resolve().parents[2] / "ebook" / "en" / "content" + + +def _get_lessons() -> Iterable[Path]: + if not _CONTENT_DIR.exists(): + return [] + return sorted(_CONTENT_DIR.glob("*.md")) + + +def _format_title(path: Path) -> str: + stem = path.stem + prefix, _, slug = stem.partition("-") + title = slug.replace("-", " ").strip().title() if slug else prefix.replace("-", " ") + if prefix.isdigit(): + return f"{prefix} {title}".strip() + return title + + +@app.callback(invoke_without_command=True) +def list_commands( + ctx: typer.Context, + limit: Optional[int] = typer.Option( + None, + "--limit", + "-l", + min=1, + help="Limit the number of commands displayed. Shows all when omitted.", + ), + verbose: bool = typer.Option( + False, + "--verbose", + "-v", + is_flag=True, + help="Enable verbose output for this command.", + ), +) -> None: + """Display the available Linux command lessons.""" + + if verbose: + set_verbose(ctx, True) + + lessons = list(_get_lessons()) + total = len(lessons) + + if verbose_active(ctx): + typer.echo(f"[verbose] Located {total} command lessons", err=True) + + if total == 0: + typer.echo("No command lessons found.") + return + + limit_value = total if limit is None else min(limit, total) + + if verbose_active(ctx) and limit is not None: + typer.echo(f"[verbose] Limiting output to {limit_value} entries", err=True) + + for path in lessons[:limit_value]: + typer.echo(_format_title(path)) + + +__all__ = ["app"] diff --git a/cli/state.py b/cli/state.py new file mode 100644 index 00000000..3895adfb --- /dev/null +++ b/cli/state.py @@ -0,0 +1,29 @@ +"""Shared CLI state helpers for global flags.""" + +from __future__ import annotations + +import typer + +_VERBOSE_KEY = "verbose" + + +def set_verbose(ctx: typer.Context, value: bool) -> None: + """Persist the verbose flag on this context and all parents.""" + current = ctx + while current is not None: + current.ensure_object(dict) + current.obj[_VERBOSE_KEY] = value + current = current.parent + + +def verbose_active(ctx: typer.Context) -> bool: + """Check whether verbose mode is enabled anywhere up the chain.""" + current = ctx + while current is not None: + if current.obj and current.obj.get(_VERBOSE_KEY): + return True + current = current.parent + return False + + +__all__ = ["set_verbose", "verbose_active"] diff --git a/cli/test_cli.py b/cli/test_cli.py index f48f17e3..944abae7 100644 --- a/cli/test_cli.py +++ b/cli/test_cli.py @@ -4,59 +4,100 @@ import os import subprocess import sys +from pathlib import Path -def test_cli_help(): - """Test that the CLI shows help.""" - result = subprocess.run( - [sys.executable, "cli.py", "--help"], +CLI_DIR = Path(__file__).parent +CLI_ENV = {**os.environ, "PYTHONIOENCODING": "utf-8"} + + +def run_cli(*args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "cli.py", *args], capture_output=True, text=True, - cwd=os.path.dirname(__file__), + encoding="utf-8", + cwd=CLI_DIR, + env=CLI_ENV, ) + + +def test_cli_help(): + """Test that the CLI shows help.""" + result = run_cli("--help") assert result.returncode == 0 assert "101 Linux Commands CLI" in result.stdout def test_hello_command(): """Test the hello command.""" - result = subprocess.run( - [sys.executable, "cli.py", "hello", "greet"], - capture_output=True, - text=True, - cwd=os.path.dirname(__file__), - ) + result = run_cli("hello", "greet") assert result.returncode == 0 assert "Hello, World!" in result.stdout def test_hello_command_with_name(): """Test the hello command with a custom name.""" - result = subprocess.run( - [sys.executable, "cli.py", "hello", "greet", "--name", "Linux"], - capture_output=True, - text=True, - cwd=os.path.dirname(__file__), - ) + result = run_cli("hello", "greet", "--name", "Linux") assert result.returncode == 0 assert "Hello, Linux!" in result.stdout +def test_hello_command_verbose_option(): + """Command-level verbose flag should emit debug output.""" + result = run_cli("hello", "greet", "--verbose") + assert result.returncode == 0 + assert "Hello, World!" in result.stdout + assert "[verbose]" in result.stderr + + +def test_global_verbose_flag(): + """Global verbose flag should cascade to subcommands.""" + result = run_cli("--verbose", "hello", "greet", "--name", "Tester") + assert result.returncode == 0 + assert "Hello, Tester!" in result.stdout + assert "[verbose] Verbose mode enabled" in result.stderr + assert "[verbose] Preparing greeting for Tester" in result.stderr + + def test_hello_help(): """Test the hello command help.""" - result = subprocess.run( - [sys.executable, "cli.py", "hello", "--help"], - capture_output=True, - text=True, - cwd=os.path.dirname(__file__), - ) + result = run_cli("hello", "--help") assert result.returncode == 0 assert "Hello command group" in result.stdout +def test_list_command_basic(): + """List command should show lesson titles.""" + result = run_cli("list", "--limit", "3") + assert result.returncode == 0 + assert "000" in result.stdout + assert "Introduction" in result.stdout + + +def test_list_command_verbose_option(): + """Command-level verbose flag should emit debug output.""" + result = run_cli("list", "--limit", "2", "--verbose") + assert result.returncode == 0 + assert "[verbose] Located" in result.stderr + + +def test_list_command_global_verbose(): + """Global verbose flag should cascade to list command.""" + result = run_cli("--verbose", "list", "--limit", "1") + assert result.returncode == 0 + assert "[verbose] Verbose mode enabled" in result.stderr + assert "[verbose] Located" in result.stderr + + if __name__ == "__main__": test_cli_help() test_hello_command() test_hello_command_with_name() + test_hello_command_verbose_option() + test_global_verbose_flag() test_hello_help() + test_list_command_basic() + test_list_command_verbose_option() + test_list_command_global_verbose() print("✅ All tests passed!") From 8edb27164315bd325bcaba2931bf08ab131bfd51 Mon Sep 17 00:00:00 2001 From: Manjunath Patil <65996426+Manjunath3155@users.noreply.github.com> Date: Wed, 1 Oct 2025 20:36:24 +0530 Subject: [PATCH 2/4] Clean up whitespace in hello.py Remove unnecessary blank lines in hello.py --- cli/commands/hello.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cli/commands/hello.py b/cli/commands/hello.py index b12280df..448902a9 100644 --- a/cli/commands/hello.py +++ b/cli/commands/hello.py @@ -1,8 +1,6 @@ import typer - from state import set_verbose, verbose_active - app = typer.Typer(help="Hello command group") From db3651e10baf66b2c191d931bba25369a216392a Mon Sep 17 00:00:00 2001 From: Manjunath Patil <65996426+Manjunath3155@users.noreply.github.com> Date: Wed, 1 Oct 2025 20:41:44 +0530 Subject: [PATCH 3/4] Remove unnecessary blank line in test_cli.py --- cli/test_cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/test_cli.py b/cli/test_cli.py index 944abae7..636e4133 100644 --- a/cli/test_cli.py +++ b/cli/test_cli.py @@ -6,7 +6,6 @@ import sys from pathlib import Path - CLI_DIR = Path(__file__).parent CLI_ENV = {**os.environ, "PYTHONIOENCODING": "utf-8"} From 403add3c5b967c77d5b3d7d7ab1def2467e2188e Mon Sep 17 00:00:00 2001 From: Manjunath Patil <65996426+Manjunath3155@users.noreply.github.com> Date: Wed, 1 Oct 2025 20:42:08 +0530 Subject: [PATCH 4/4] Add import statement for additional functionality --- cli/commands/hello.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/commands/hello.py b/cli/commands/hello.py index 448902a9..b9ff37e9 100644 --- a/cli/commands/hello.py +++ b/cli/commands/hello.py @@ -1,4 +1,5 @@ import typer + from state import set_verbose, verbose_active app = typer.Typer(help="Hello command group")