from __future__ import annotations import sys import unittest from dataclasses import dataclass from typing import Sequence @dataclass(frozen=True) class CliOptions: name: str = "world" count: int = 1 verbose: bool = False class CliError(ValueError): """Raised when command-line arguments are invalid.""" def usage(program: str) -> str: return ( f"Usage: {program} [OPTIONS]\n\n" "Options:\n" " -n, --name TEXT Name to greet (default: world)\n" " -c, --count NUMBER Number of greetings (default: 1)\n" " -v, --verbose Print extra details\n" " -h, --help Show this help message\n" " --self-test Run built-in tests\n" ) def parse_args(argv: Sequence[str]) -> CliOptions: name = "world" count = 1 verbose = False i = 0 while i < len(argv): arg = argv[i] if arg in ("-h", "--help"): raise SystemExit(0) if arg in ("-v", "--verbose"): verbose = True i += 1 continue if arg in ("-n", "--name", "-c", "--count"): if i + 1 >= len(argv): raise CliError(f"{arg} requires a value") value = argv[i + 1] if arg in ("-n", "--name"): if not value: raise CliError("name cannot be empty") name = value else: try: count = int(value) except ValueError as exc: raise CliError("count must be an integer") from exc if count < 1: raise CliError("count must be at least 1") i += 2 continue raise CliError(f"unknown argument: {arg}") return CliOptions(name=name, count=count, verbose=verbose) def main(argv: Sequence[str] | None = None) -> int: program = sys.argv[0] args = list(sys.argv[1:] if argv is None else argv) if any(arg in ("-h", "--help") for arg in args): print(usage(program)) return 0 try: options = parse_args(args) except CliError as exc: print(f"error: {exc}", file=sys.stderr) print(usage(program), file=sys.stderr) return 2 # Keep output predictable so the CLI is easy to test and compose. if options.verbose: print(f"greeting {options.count} time(s) for {options.name!r}") for _ in range(options.count): print(f"Hello, {options.name}!") return 0 class ParseArgsTests(unittest.TestCase): def test_defaults(self) -> None: self.assertEqual(parse_args([]), CliOptions()) def test_name_count_and_verbose(self) -> None: self.assertEqual( parse_args(["--name", "Ada", "--count", "2", "--verbose"]), CliOptions(name="Ada", count=2, verbose=True), ) def test_rejects_bad_count(self) -> None: with self.assertRaises(CliError): parse_args(["--count", "0"]) if __name__ == "__main__": if "--self-test" in sys.argv[1:]: unittest.main(argv=[sys.argv[0]]) raise SystemExit(main())