CLI Arguments Overriding Environment Variables
An argument parser that reads flags from the command line and environment with declared precedence, reporting which source supplied each value.
Code
import os
import sys
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence
class ArgError(ValueError):
"""Raised for unknown flags, missing values or failed conversions."""
@dataclass(frozen=True)
class FlagSpec:
name: str
env: Optional[str] = None
converter: Callable[[str], Any] = str
default: Any = None
required: bool = False
@dataclass(frozen=True)
class ParsedArgs:
values: Dict[str, Any] = field(default_factory=dict)
sources: Dict[str, str] = field(default_factory=dict)
def parse_args(
argv: Sequence[str],
specs: Sequence[FlagSpec],
env: Optional[Mapping[str, str]] = None,
) -> ParsedArgs:
"""Parse argv with env fallback; argv always beats the environment."""
environment = os.environ if env is None else env
by_flag = {spec.name: spec for spec in specs}
parsed = ParsedArgs()
index = 0
while index < len(argv):
token = argv[index]
if not token.startswith("--"):
raise ArgError("unexpected positional argument: " + token)
name = token[2:]
spec = by_flag.get(name)
if spec is None:
raise ArgError("unknown flag: " + name)
if name in parsed.values:
raise ArgError("flag repeated: " + name)
index += 1
if index >= len(argv):
raise ArgError("missing value for flag: " + name)
raw = argv[index]
index += 1
try:
parsed.values[name] = spec.converter(raw)
except (TypeError, ValueError) as exc:
raise ArgError("invalid value for %s: %s" % (name, exc)) from exc
parsed.sources[name] = "argv"
for spec in specs:
if spec.name in parsed.values:
continue
if spec.env and spec.env in environment:
try:
parsed.values[spec.name] = spec.converter(environment[spec.env])
except (TypeError, ValueError) as exc:
raise ArgError(
"invalid value for %s from %s: %s" % (spec.name, spec.env, exc)
) from exc
parsed.sources[spec.name] = "env:" + spec.env
continue
if spec.default is not None:
parsed.values[spec.name] = spec.default
parsed.sources[spec.name] = "default"
continue
if spec.required:
raise ArgError("missing required flag: " + spec.name)
return parsed
def _int(value: str) -> int:
return int(value)Report a problem with this exampleOpens a pre-filled issue on GitHub.