Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ linux-cli --help

### Examples

### 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
```
Here are some examples of how to use the CLI:

> Note: The `list`, `search`, and `show` commands are still under development.
Expand All @@ -45,35 +67,41 @@ Here are some examples of how to use the CLI:
## Development

### Running Tests

```bash
pytest
```

### Code Formatting

```bash
black cli/
```

### Import Sorting

```bash
isort cli/
```

### Linting

```bash
flake8 cli/
```

### Type Checking

```bash
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)
Expand Down
29 changes: 21 additions & 8 deletions cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,33 @@ def resolve_command(self, ctx: click.Context, args: List[str]):
raise click.exceptions.UsageError(new_message, ctx=ctx) from e

raise


app = typer.Typer(help="101 Linux Commands CLI 🚀", cls=CustomTyper)

@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(list.app, name="list")
app.add_typer(listing.app, name="list")
app.add_typer(version.app, name="version")
app.command()(show.show)
app.add_typer(search.app, name="search")


def main() -> None:
"""CLI entry point."""
app()


if __name__ == "__main__":
main()
main()
21 changes: 20 additions & 1 deletion cli/commands/hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,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}!")
74 changes: 74 additions & 0 deletions cli/commands/listing.py
Original file line number Diff line number Diff line change
@@ -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("-", " ")
Comment on lines +23 to +26
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line contains duplicated logic for replacing dashes with spaces. Extract this transformation into a variable or helper function to avoid repetition.

Suggested change
def _format_title(path: Path) -> str:
stem = path.stem
prefix, _, slug = stem.partition("-")
title = slug.replace("-", " ").strip().title() if slug else prefix.replace("-", " ")
def _dashes_to_spaces(s: str) -> str:
return s.replace("-", " ")
def _format_title(path: Path) -> str:
stem = path.stem
prefix, _, slug = stem.partition("-")
title = _dashes_to_spaces(slug).strip().title() if slug else _dashes_to_spaces(prefix)

Copilot uses AI. Check for mistakes.

if prefix.isdigit():
return f"{prefix} {title}".strip()
return title


Comment on lines +23 to +31
Copy link

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The title formatting logic is complex and handles multiple cases in a single function. Consider splitting this into separate functions for prefix extraction and title formatting to improve readability and maintainability.

Suggested change
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
def _extract_prefix_and_slug(stem: str) -> tuple[str, str]:
prefix, _, slug = stem.partition("-")
return prefix, slug
def _format_title_from_parts(prefix: str, slug: str) -> str:
title = slug.replace("-", " ").strip().title() if slug else prefix.replace("-", " ")
if prefix.isdigit():
return f"{prefix} {title}".strip()
return title
def _format_title(path: Path) -> str:
stem = path.stem
prefix, slug = _extract_prefix_and_slug(stem)
return _format_title_from_parts(prefix, slug)

Copilot uses AI. Check for mistakes.

@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"]
29 changes: 29 additions & 0 deletions cli/state.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading
Loading