Skip to content

Command-Line Interface

Snipwise looks for snipwise.md in the current directory and then in its parents. The directory containing that file is the root against which all patterns are resolved.

snipwise                              # check all files selected by the rules
snipwise check                        # the same, written out in full
snipwise check --diff                 # check and show what the differences are
snipwise --verbose                    # check, and list every snippet first
snipwise fix                          # rewrite the files that are out of date
snipwise fix --original README.md:12  # the same, saying where an ambiguous original is
snipwise init                         # create a snipwise.md file

The check subcommand is the default and may be omitted. snipwise init writes a small snipwise.md template in the current directory and refuses to overwrite a file that is there already, and snipwise --version prints the installed version.

Only -v and --verbose may be written before the subcommand. A subcommand that follows any other option is refused with exit code 2, because it would otherwise be read as a file name, and a file name that no pattern matches is ignored, which would leave snipwise --diff fix checking nothing and exiting 0.

Both check and fix accept file names, which restricts them to those files:

snipwise fix docs/installation.md

Passing snipwise.md itself means that the definitions changed, so all files selected by the rules are checked again, regardless of any other file names you pass. Passing a file that holds the source of a snippet means the same thing for that snippet, so the files that receive it are checked along with it, whether or not you named them. See Source Rules.

Snipwise reads every file the rules select on every run. Naming files therefore restricts what Snipwise checks and rewrites, not what it reads, and a malformed marker in a file you did not name still stops the run. A file that no rule selects is never read at all, so a name that no pattern matches is ignored.

snipwise check prints one line per inconsistent block, naming the file, the line of the begin marker and the snippet:

docs/install.md:8: out-of-date snippet 'python-versions'

The file is named relative to the working directory and always with forward slashes, so the same project gives the same report on every operating system.

Add --diff to print a unified diff of the affected files instead of that list. It is an option of snipwise check alone, because snipwise fix writes the change instead of showing it. snipwise fix rewrites the same blocks and reports them in the same one-line format. It rewrites nothing at all when any selected file is malformed, because the whole selection is read and checked before the first file is written. The line endings and the final newline of a file it edits are left as they are, so Snipwise never tidies up a file on the side.

Both subcommands therefore work as a pre-commit hook, one to report inconsistencies and one to repair them, and both use the same three exit codes:

Code Meaning
0 Nothing needs to change
1 A block differs from what it should hold
2 The input is refused and nothing is written

Code 2 covers a malformed snipwise.md, a malformed marker, a snippet that fewer than two places hold, and a snipwise fix that cannot work out which copy of a snippet is the original. Each of them is a decision Snipwise will not make on your behalf, so it stops and leaves every file as it found it. See The Lone-Copy Error.

-v or --verbose, which both check and fix accept on either side of the subcommand, lists every snippet Snipwise knows before it checks or rewrites anything:

snipwise: 3 snippets
  authors            definition in snipwise.md
                     README.md:31
                     pyproject.toml:14
  python-versions    source at README.md:12
                     docs/install.md:8
  tagline            no original
                     docs/index.md:5
                     package.json:/description

Every snippet is listed by name, in alphabetical order, with where its original is, when it has one, and where its copies are, which answers the two questions a reader who followed a marker came with: which names this project has, and where each of them lives. The listing goes to standard error, so that standard output stays the list of blocks that need to change, and it is printed before anything else, so that the picture survives a run that ends in an error.

The listing counts every file the patterns select, even when you name files on the command line. Naming files restricts what Snipwise checks and rewrites, never what it counts, which is what keeps a hook that passes the staged files from reporting on half a project.

There is one scenario where the two subcommands will have different exit codes for the same files. If copies of a snippet differ while snipwise.md defines none of them, snipwise check exits 1 and lists the copies that disagree instead of naming a block that is out of date, because nothing tells it which of them the others should have followed. snipwise fix works out which copy wins whenever it can, possibly falling back on a comparison with the last commit. It exits 2 without writing anything when the original cannot be identified, and reports on the ambiguity, e.g.:

snipwise: snippet 'python-versions' has 2 candidate originals
  README.md:12
    | Snipwise requires Python 3.11 or later.
  docs/install.md:8
    | Snipwise requires Python 3.12 or later.
resolve this run with one of:
  snipwise fix --original README.md:12
  snipwise fix --original docs/install.md:8
settle it for good with one of:
  [[sources]]
  patterns = ["README.md"]
  snippets = ["python-versions"]

  [[sources]]
  patterns = ["docs/install.md"]
  snippets = ["python-versions"]

snipwise check prints the same report on standard output and without that prefix, because it neither decides nor writes and only says what it found.

The report offers the decision twice, in the form that carries it out. A snipwise fix --original command settles the snippet for one run, and a [[sources]] rule settles it for good once pasted into snipwise.md, because an ambiguity that arises once will arise again. See Source Rules.

A run that stops in this way also counts the blocks it would otherwise have brought up to date, because the snippets those blocks hold are settled and only the ambiguity is keeping them as they are:

snipwise: 3 other blocks are out of date and were not rewritten

They are counted rather than listed, because snipwise check is what lists them. The line is absent when the ambiguity leaves nothing out of date.

The --original option settles the ambiguity once, by referencing the block that holds the text the other copies receive, e.g.:

snipwise fix --original docs/install.md:8

The location is the path and the line of the begin marker, as every report writes it. The path on its own is enough when the file holds a single block that may be the original:

snipwise fix --original docs/install.md

It is an option of snipwise fix alone, because check decides nothing and an original would change nothing it reports.

The option supplies a decision and does not restrict the run. The command above acts on the same files as a plain snipwise fix, and every other snippet whose original is not in doubt is brought up to date along with it. Naming files on the command line is what restricts a run to part of the project.

Give the option more than once to settle several snippets in one run, and give it for each snippet that is in doubt, because a fix left with an original it cannot identify writes no file at all, and says so on the last line of its report:

snipwise: 1 snippet is still ambiguous, and nothing is rewritten until every snippet is settled

Settling one of two ambiguous snippets therefore leaves both of them as they were.

--original chooses between texts the project already holds, and never composes one out of two. The location has to name a block of a snippet the run cannot settle on its own, and anything else is an error that rewrites nothing: a command pasted after an edit has moved the line names no block, and one pasted after an earlier fix names a block whose snippet is no longer in doubt. Both fail loudly instead of just copying the wrong text. Every location is looked up before the first file is written, so a run with one location that cannot be looked up leaves the project as it was instead of half settled.

The Lone-Copy Error

Snipwise exists to keep two texts equal, so it counts the places that hold each snippet: a definition in snipwise.md, a region a [[sources]] rule names, and every block found in a target file. A snippet with fewer than two of them is keeping nothing consistent, and is far more often a mistyped marker than an intention:

snipwise: 'python-verisons' has one copy and nothing to keep it consistent with
  docs/install.md:8
  the closest known name is 'python-versions'
snipwise: add the place that is missing, or remove the one that is there

The closest known name is printed when the project has one close enough to be worth mentioning. It names a snippet that is already there, because Snipwise suggests nothing it has not read.

A definition in snipwise.md counts as one of the two places, so a snippet defined there that no file holds a copy of is refused as well, and so is a source region that no file receives. A name in the snippets list of a rule that no definition and no marker produced is refused too, because such a rule does nothing and a typo in that list has nowhere else left to be caught.

Nothing silences this. A heading in snipwise.md says which copy of a snippet to edit, and it does not say that one copy is enough. See Snippets without a Definition.

The run stops here with exit code 2 and no file is rewritten, so a lone copy in one corner of a project holds back the repair of the rest of it. snipwise fix does not repair it either, and cannot: the two ways out are to give the snippet the second place it is missing, or to take away the place it has, and which of them you meant is not something Snipwise can work out.

A lone copy is a state a snippet passes through while you are writing it, and it is not a state that survives to a commit, which is where Snipwise is usually run.

This is how a mistyped marker is caught, because a name that no heading defines is a snippet like any other rather than an error. One mistake still gets through. A rename that leaves two or more blocks behind under the old name is missed altogether, because those blocks are a snippet with two parties that agree with each other. snipwise --verbose lists every name the project has, which is how that one is spotted.

Pre-commit Hook

Add the following to your .pre-commit-config.yaml file:

- repo: https://github.com/reproducible-reporting/snipwise
  rev: v1.0.0
  hooks:
  - id: snipwise
    args: [fix]

Leave out args if you prefer a hook that only reports inconsistencies and lets you resolve them yourself.

Three remarks on this configuration:

  • Nothing has to be set up for the comparison with the last commit. pre-commit sets your unstaged changes aside before it runs a hook, so snipwise fix compares what is about to be committed with what was committed before, which is exactly the comparison that rule wants. See The Comparison with the Last Commit.
  • Do not restrict the hook with types or files. It has to receive snipwise.md when that file is staged, because editing the definitions is what makes all other files worth checking.
  • Do not exclude files with the exclude key of the hook, because that only filters the file names being passed on, and not the files that Snipwise checks after snipwise.md has changed. Use the exclude key of a rule in snipwise.md instead.