cli-tool-design

v2026.09.25

Design command-line interfaces with clear argument parsing, subcommands, help text, output formatting, and exit codes. Covers Click, Typer, argparse, and shell completion. Triggers on CLI tool development, argument parsing, or terminal UX design requests.

GitHub
安装命令
npx skhub add organvm-iv-taxis/cli-tool-design
Markdown
SKILL.md

CLI Tool Design

Build command-line tools that are discoverable, composable, and pleasant to use.

Design Principles

The UNIX Philosophy Applied

  1. Do one thing well — Each command has a clear, singular purpose
  2. Compose through pipes — Support stdin/stdout for chaining
  3. Fail loudly — Non-zero exit codes and stderr for errors
  4. Be predictable — Consistent flags, consistent output format

Command Structure

program [global-options] command [command-options] [arguments]

Example:

organvm --verbose registry update --organ IV a-i--skills

Framework Selection

FrameworkLanguageBest For
TyperPythonModern CLIs with type hints, auto-completion
ClickPythonComplex CLIs, plugins, nested groups
argparsePythonZero-dependency, stdlib-only
clapRustHigh-performance, compiled CLIs
cobraGoGo microservice CLIs

Building with Typer (Recommended for Python)

Basic Command

import typer

app = typer.Typer(help="ORGANVM system management CLI")

@app.command()
def status(
    organ: str = typer.Argument(help="Organ number (I-VII or META)"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
):
    """Show the status of an organ's repositories."""
    # Implementation here

Subcommand Groups

app = typer.Typer()
registry_app = typer.Typer(help="Registry operations")
app.add_typer(registry_app, name="registry")

@registry_app.command("update")
def registry_update(
    organ: str = typer.Argument(help="Target organ"),
    dry_run: bool = typer.Option(False, "--dry-run", "-n"),
):
    """Update registry entries for an organ."""

Rich Output Integration

from rich.console import Console
from rich.table import Table

console = Console(stderr=True)  # Status to stderr

def show_repos(repos: list[dict]):
    table = Table(title="Repositories")
    table.add_column("Name", style="cyan")
    table.add_column("Status", style="green")
    table.add_column("Tier")
    for repo in repos:
        table.add_row(repo["name"], repo["status"], repo["tier"])
    console.print(table)

Argument Design

Positional vs Optional

Use CaseTypeExample
Required inputPositionalprogram FILE
Behavior modifierFlag--verbose, --dry-run
ConfigurationOption--output FORMAT
Multiple inputsVariadicprogram FILE...

Flag Conventions

-v, --verbose       Increase output verbosity
-q, --quiet         Suppress non-error output
-n, --dry-run       Show what would happen without doing it
-f, --force         Skip confirmation prompts
-o, --output FILE   Write output to FILE instead of stdout
    --json          Machine-readable JSON output
    --no-color      Disable colored output

Boolean Flags with Negation

@app.command()
def deploy(
    color: bool = typer.Option(True, "--color/--no-color"),
    interactive: bool = typer.Option(True, "--interactive/--no-interactive"),
):

Output Design

Human vs Machine Output

import json
import sys

def output_results(results: list[dict], json_mode: bool = False):
    if json_mode:
        # Machine output to stdout
        json.dump(results, sys.stdout, indent=2)
    else:
        # Human output with formatting
        for r in results:
            console.print(f"[cyan]{r['name']}[/] — {r['status']}")

Progress Indicators

from rich.progress import track

for item in track(items, description="Processing..."):
    process(item)

Stderr vs Stdout

  • stdout: Data output (pipeable)
  • stderr: Status messages, progress, errors

Exit Codes

CodeMeaning
0Success
1General error
2Usage error (bad arguments)
64-78BSD sysexits conventions
130Interrupted (Ctrl+C)
import sys

def main():
    try:
        result = run_command()
        if not result.success:
            console.print(f"[red]Error:[/] {result.error}", file=sys.stderr)
            raise SystemExit(1)
    except KeyboardInterrupt:
        raise SystemExit(130)

Configuration Loading

Priority order (highest to lowest):

  1. Command-line arguments
  2. Environment variables
  3. Project-level config file (.tool.yaml)
  4. User-level config (~/.config/tool/config.yaml)
  5. System defaults
def get_config(cli_value: str | None = None) -> str:
    return (
        cli_value
        or os.environ.get("TOOL_CONFIG")
        or load_project_config()
        or load_user_config()
        or DEFAULT_VALUE
    )

Shell Completion

Typer Auto-Completion

# Generate completion script
my-cli --install-completion

# Or manually
_MY_CLI_COMPLETE=bash_source my-cli > ~/.my-cli-complete.bash
source ~/.my-cli-complete.bash

Custom Completions

def complete_organ(incomplete: str) -> list[str]:
    organs = ["I", "II", "III", "IV", "V", "VI", "VII", "META"]
    return [o for o in organs if o.startswith(incomplete.upper())]

@app.command()
def status(organ: str = typer.Argument(autocompletion=complete_organ)):
    ...

Testing CLIs

from typer.testing import CliRunner

runner = CliRunner()

def test_status_command():
    result = runner.invoke(app, ["status", "IV"])
    assert result.exit_code == 0
    assert "a-i--skills" in result.stdout

def test_invalid_organ():
    result = runner.invoke(app, ["status", "INVALID"])
    assert result.exit_code == 2

Anti-Patterns

  • Requiring interactive input in scripts — Always support --yes / --no-interactive flags
  • Mixing data and status on stdout — Use stderr for progress and status messages
  • Inconsistent flag naming — Pick a convention and stick to it across all subcommands
  • No help text — Every command, argument, and option needs a help string
  • Swallowing errors — Always exit with appropriate non-zero codes on failure
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.25

发布时间

2026年9月25日

分类

未分类

许可证

Apache-2.0

源路径

skills/development/cli-tool-design

默认分支

main

最新提交

6b53d3e

Tree SHA

92a821e