Round-trip: results back to argv¶
Sometimes you have the parsed result and need the command back: to log the exact invocation behind a run, or to hand an argv to a subprocess. Rebuilding it by gluing strings together is where the quoting bugs live.
docopt turns an argv into a result. format_argv does the inverse, so one usage
message drives both directions. The same spec that parses ./data ./backups --compress also synthesizes
it from the parsed values.
from docopt2 import docopt, format_argv
doc = """Usage: prog <source> <dest> [--compress] [--keep=<n>]
Options:
--keep=<n> Keep [default: 5]."""
args = docopt(doc, "./data ./backups --compress --keep=10", complete=False)
format_argv(args, doc) # ['./data', './backups', '--compress', '--keep=10']
The contract¶
format_argv(result, doc) returns an argv token list (no program name) that parses back to result:
That equality is the whole point, and it is a guarantee, not a hope. Internally format_argv generates a
candidate argv from each usage line and re-parses it, returning the first that reproduces the result.
So the output is always a valid argv, though not necessarily the shortest one, nor the exact string the user typed. The canonical form is fixed and predictable:
- Everything that carries a value. What the user supplied, plus whatever
[env:]or[config:]resolved. Only elements left at their[default: ...]are omitted, so a defaulted--keepwould not appear and an explicit one would. - Env and config values are written out, never skipped, so the argv stands on its own. A persisted command that silently depended on an unrecorded variable would not reproduce the run.
- Usage order. Tokens follow the order of the matched usage line.
- Long form. An option is written
--name=valuewhen it has a long form,-x valuewhen it is short only. A counted flag repeats (-v -v -v), a repeatable option repeats (--x=1 --x=2). - The line the result took. For a multi-pattern usage, the alternative the result matched is chosen (the
commitline for a commit result), verified by the re-parse.
A result that no usage pattern can reproduce raises ValueError rather than return a wrong argv.
That means a hand-built or inconsistent mapping, or a degenerate grammar in which one value is reachable
through differently-shaped positions ((-a | -b)..., [<name>] <path> <name>). Such a shape has a genuinely
ambiguous argv, so there is no single canonical form to return.
What it is for¶
- Reproducible commands. Log or persist the exact invocation behind a run, the "copy as command" button,
rebuilt from the typed values rather than by string-mangling the original input. Because env- and
config-sourced values are written out rather than skipped, the logged command still reproduces the run on a
machine where
APP_PORTwas never set. - Safe subprocess construction.
format_argvreturns a token list, which is exactly the shapesubprocess.runwants. Build the child's argv from a validated result instead of interpolating strings and hoping the quoting survives a path with a space in it. - Diffing invocations. Two runs typed differently normalize to the same canonical argv, since
--keep 10and--keep=10both come back as--keep=10. A diff then shows what actually differed, not how it was typed. - Property testing. Combined with the Hypothesis strategy,
docopt(format_argv(x)) == xis a round-trip invariant your own CLI inherits for free.
from hypothesis import given
from docopt2 import docopt, format_argv
from docopt2.hypothesis import argv_strategy
DOC = "Usage:\n git push [--force] <remote>\n git commit --message=<msg>"
@given(argv_strategy(DOC))
def test_every_result_round_trips(argv):
result = docopt(DOC, argv, help=False)
assert docopt(DOC, format_argv(result, DOC), help=False) == result
See also¶
docoptandformat_argvin the API reference.- Example generation - synthesize sample argvs from the grammar, rather than one from a result.
- Typed results - the
Argumentsmappingformat_argvreads, includingprovided.