Diagnostics¶
When the arguments don't match, docopt2 does not just reprint the usage. It points at the offending token, in the argument vector and in the usage that rejected it:
A failed parse raises DocoptExit carrying that diagnostic. The block above
is exactly what str(exc) prints for the run below, minus the usage message that DocoptExit always
appends beneath it:
from docopt2 import docopt
doc = """Git push.
Usage:
git push [--force] <remote>
Options:
--force Force the push.
"""
docopt(doc, "push --forcce origin", suggest=True) # raises the diagnostic above
Read the two snippets top to bottom:
in the arguments:- your argv, joined with spaces, with a caret under--forcce, the token that had no place in the usage.in the usage:- the usage line that declares the option you probably meant, with a caret under--force. This is the cross-reference: it ties the mistake in argv to the exact spot in the spec that rejected it, so you are not left scanning the whole usage yourself.
The help: line carries the "did you mean" hint. It appears only with suggest=True. See
Opting into hints below.
One shape for every error¶
Every error the usage produces is lowered to the shape above, whether the user mistyped an argument or the developer wrote a malformed usage:
- a bold
error:heading with a one-line summary - captioned source snippets with carets
- optional
note:andhelp:lines
Those last two follow rustc's split and are not interchangeable. A note: gives context: why the input was
rejected, or why this usage line is the one shown. A help: proposes a concrete fix, such as a "did you
mean". A diagnostic with nothing to suggest carries no help: at all.
The static usage linter reuses the same grammar with a yellow warning: heading, so a warning
and a hard error read the same way.
A schema that does not fit its usage is a different animal
A field naming no element, or an annotation nothing can coerce to, is a programming error rather than a
usage error. It raises a plain DocoptLanguageError with no source to point at.
Both places, not only typos¶
The second snippet is not exclusive to typos.
Any time the offending argv token is an option the usage actually declares, the diagnostic carets both places. Two branches of a mutually exclusive group is the common case:
doc = "Usage: prog (--fast | --slow)\n\nOptions:\n --fast Fast.\n --slow Slow."
docopt(doc, "--fast --slow")
Note
str(exc) is always plain text, so a caught diagnostic inspects and logs cleanly. When an error goes
unhandled and the interpreter prints it, the copy sent to the terminal is colored - automatically when
stderr is a TTY, and never when the NO_COLOR environment variable is set.
Piped or redirected output stays plain, so color never leaks into a file or a captured test.
The usage line you were closest to¶
When the arguments fall short of a multi-line usage, most parsers - docopt included - meet the mismatch by reprinting the whole usage and leaving you to work out which line you meant and what is missing:
docopt2 does better: it finds the line you got furthest into and points a caret at the one element still missing. A matched command is strong evidence of intent, so the line whose command you typed wins even when a later positional or option is absent:
doc = """Usage:
git push [--force] <remote>
git commit --message=<msg>
git add <path>...
Options:
--force Force.
--message=<msg> Message."""
docopt(doc, "push")
The snippet shows only the closest line - git push, not commit or add - because the caret is placed
in that line. Typing git commit instead carets --message=<msg> on the second line. git deploy prod
on a deploy <env> <version> line carets the missing <version>.
The ranking fires only with real evidence. A leading word that matches no line's command (git clone) is
not treated as a near-miss - there is nothing to be "closest" to - so docopt2 falls back to the plain
mismatch message rather than guess a line at random. A single-line usage has no alternatives to rank, so
it reports its missing element directly.
A value that does not fit its type¶
A parse can succeed and still fail when a value cannot be coerced to its schema field
type. That raises DocoptExit too, rendered with the same two-span caret:
the offending value in the argv, tied to the usage element that declared its type.
import dataclasses
from docopt2 import docopt
@dataclasses.dataclass
class Args:
port: int
doc = "Usage: prog --port=<n>"
docopt(doc, "--port=abc", schema=Args) # raises the diagnostic below
Only a user value gets this two-span treatment. A schema that disagrees with the usage - a field with
no matching element, an unsupported annotation - is the developer's error, not the user's, and raises
DocoptLanguageError with no argv caret. See
Typed results.
A malformed usage at import time¶
A usage message that cannot be parsed raises DocoptLanguageError with
the same caret treatment, so a broken spec fails loudly rather than silently. Because docopt2 parses the
usage on the first docopt() call, this is a developer error surfaced as soon as the interface is
exercised, not a user error.
An unclosed group points its single caret at the opener that was never closed:
Mismatched delimiters draw two carets in one snippet, the opener and the closer that cannot match it:
A closing bracket with nothing open is caught the same way, with the summary unexpected closing.
Tip
To catch these before any argv is ever parsed, run the usage linter. It renders the same diagnostics ahead of time, as warnings, so a malformed spec is reported in a test rather than on a user's first bad command.
Opting into hints¶
The "did you mean" hint is opt-in. Without it, the same typo is reported by the generic mismatch path:
doc = "Usage:\n git push [--force] <remote>\n\nOptions:\n --force Force.\n"
docopt(doc, "push --forcce origin") # suggest defaults to False
Pass suggest=True and docopt2 upgrades the message to the unknown option form shown at the top of this
page: the "did you mean --force?" help line, plus the usage cross-reference. That fires when a mistyped
long option resembles a known one within an edit-distance threshold.
The hint fires only on a genuine typo:
--foris an unambiguous prefix, so withallow_abbrev=True(the default) it already de-abbreviates to--force. It is accepted, never flagged.--forcceis a prefix of nothing. That is what marks it as a typo worth a suggestion.
Note
suggest only affects long options (--name). Short flags and positional arguments fall through to
the standard unexpected argument and missing or mismatched arguments diagnostics.
See also¶
- Exceptions -
DocoptExitandDocoptLanguageError. - Usage linting to catch defects before any argv is parsed.
- Usage DSL - the grammar these diagnostics point back into.