Command-line usage

Version 0.6.1 Updated Sep 29, 2026
mdtablefix [--wrap] [--renumber] [--breaks] [--ellipsis] [--fences]
          [--footnotes] [--code-emphasis] [--headings]
          [--in-place | --check | --diff | --list-files] [FILE...]
          [--git [--include-untracked] [--md-exts EXT[,EXT...]]
                 [--allow-conflicted]]

Every named file is formatted, and the result is printed to standard output. If no file is named, the document is read from standard input and the formatted text is written to standard output. Alternatively --git selects the files from the Git repository that contains the current directory. The behaviour of each formatting flag is described in the sections that follow.

Flag Effect
--wrap Reflow paragraphs and list items to 80 columns.
--renumber Renumber ordered lists sequentially.
--breaks Rewrite thematic breaks as a line of 70 underscores.
--ellipsis Replace ... with the ellipsis character.
--fences Normalize fenced code blocks where compression is safe.
--footnotes Convert bare numeric references into footnote links.
--code-emphasis Repair emphasis markers that adjoin inline code.
--headings Convert Setext headings to hash-prefixed headings.
--in-place Rewrite each selected file instead of printing it.
--check Report each file that would be reformatted, without
writing.
--diff Print a unified diff for each file that would be
reformatted.
--list-files Print the paths a selection resolves to, without reading
or writing them.
--git Select Markdown files tracked by Git beneath the current
directory.
--include-untracked With --git, also select untracked files Git does not
ignore.
--md-exts EXT[,EXT...] With --git, the extensions to select, replacing the
default md, mdc, and markdown.
--allow-conflicted With --git --in-place, rewrite files that carry
conflict markers during a merge, rebase, revert, or
cherry-pick.
--version Print the version and exit.

Table 1: The command-line flags.

With --breaks, a thematic break inside a blockquote keeps its quote prefix; for example, > --- becomes a > marker, a space, and 70 underscores. A break indented to a list item's content column keeps that indentation, so it stays inside the item; elsewhere the break is written at column 0. A --- line immediately below compatible paragraph text is a Setext heading underline and remains unchanged, even when only --breaks is selected.

The four file modes

--in-place, --check, --diff, and --list-files act on the files a run selected, and at most one of them may be given. Each requires a source of files: mdtablefix --check on its own is a usage error, because there is no file for the mode to act on. With no mode flag the tool prints the formatted text, which is what makes mdtablefix FILE and cat FILE | mdtablefix interchangeable.

--check prints one line per file that would be reformatted, and --diff prints a unified diff per file that would be reformatted. Neither writes anything: a clean file prints nothing at all. --list-files prints the paths themselves and nothing else, which is how a selection is inspected without being acted on. --in-place rewrites only the files whose bytes would change, so a file that is already formatted keeps its inode and its modification time. See In-place editing for the replacement guarantees.

Standard output is the machine-readable half of the contract: report lines for --check, diffs for --diff, paths for --list-files, formatted text otherwise. The summary (2 files would be reformatted, 1 file left unchanged.) and every error go to standard error, so standard output can be piped or captured on its own.

Selecting files from Git

--git takes the file list from Git rather than from the command line. It may be combined with any of the four modes and with any formatting flag, and it may not be combined with file arguments: a run takes its files from one source or the other, never both. --git --in-place reformats a repository's Markdown in a single invocation, and --git --check is the matching gate for a pipeline.

The candidate set is what git ls-files --cached reports, which is to say the index. --include-untracked adds the untracked files that Git does not ignore. Because Git answers the question, .gitignore is respected exactly as Git respects it, and a file added with git add --force is selected even though .gitignore names it. --md-exts replaces the default extension set, md, mdc, and markdown; a leading dot and surrounding whitespace are optional, so --md-exts .markdown,.MD selects both. A candidate matches on its last extension, which is folded to lower case before it is compared, so a value carrying any further dot — mdc. or tar.gz — is refused where the command line is parsed rather than accepted and matching nothing.

Candidates that no longer exist in the working tree — a file deleted but not yet staged, for instance — and candidates that are not regular files, symbolic links among them, are skipped without comment. Each is an ordinary repository state rather than a mistake, and a link is skipped rather than followed for the reason given under Symbolic links. A candidate whose classification cannot be read at all — one behind a directory the run may not traverse, or one reached through a link loop — is not skipped: the selection stops, the path is named on standard error, and the run exits 2 without formatting anything, because a run that could not classify one candidate cannot say which set it would have formatted. A selected path that is not valid UTF-8 cannot be shown, so such paths are counted and the count is reported on standard error.

The selection is ordered by path, byte-wise, so it does not follow Git's own order and does not change between runs. --list-files prints it, one path per line, and exits without reading or writing any file content: it is how a selection is inspected before it is acted on, and how the same set can be handed to another tool. A selected path that itself holds a line terminator, or a backslash, cannot be printed as it stands without becoming two lines, so --list-files writes \n, \r, and \\ for the line feed, the carriage return, and the backslash itself (literally backslash-n, backslash-r, and backslash-backslash in the output); every other path is printed unchanged, so the one-line-per-path contract holds for every name Git's index can hold.

The stream is escaped rather than plain, and default xargs does not decode it: it splits on spaces as well as newlines, and a quote or a backslash in a path is its own syntax to it. The pipeline below therefore suits a repository whose paths hold none of those characters; a consumer that needs every name exactly — including one holding a space, a quote, or a line terminator — must decode the escapes itself rather than hand this text to xargs.

mdtablefix --git --list-files | xargs wc -l

Selecting nothing is a success. The run exits 0, prints nothing, and does not read standard input.

Mid-merge safety

Rewriting a file that carries conflict markers restructures the text on both sides of the boundary, so a later resolution would be made against corrupted content. --git --in-place therefore refuses a selected file that carries conflict markers while a merge, rebase, revert, or cherry-pick is paused in the repository: the file is named on standard error, left untouched, and counted as a failure, so the run exits 2. Every other selected file is still rewritten. --allow-conflicted overrides the refusal.

The repository is asked immediately before each file is replaced rather than once per run, so an operation that begins while a long run is still analysing files is seen by the writes that follow it. A file that carries no conflict markers is never the reason for asking, so an ordinary repository is not consulted at all.

The check is deliberately narrow. A file is refused only if it holds all three marker forms — a line of at least seven <, a line of at least seven =, and a line of at least seven > — and only while an operation is in progress. Seven is Git's default marker length rather than its only one: the conflict-marker-size attribute makes Git write a run of exactly the configured length, and the guard does not read .gitattributes, so it accepts any run of seven or more. The cost is a false positive on a line of eight or more marker characters, which refuses a rewrite that --allow-conflicted overrides, and that is the safer direction: a guard that demanded exactly seven would read a longer marker as ordinary Markdown and rewrite an unresolved file. A document that merely discusses conflict markers inside a fenced example is therefore rewritten, while one that quotes all three forms during a real merge is refused; --allow-conflicted is the answer to that case. The guard is a heuristic, not a substitute for git status.

Only --in-place consults the repository's state, because only --in-place writes. --check, --diff, and --list-files never pay for the second git process.

When the selection fails

Outside a repository, and whenever git cannot be run or exits non-zero, the run fails as a whole before any file is analysed, exits 2, and prints one line to standard error: this tool's own wording with Git's diagnostic appended. The process is always git ls-files, run without a shell, so nothing a user typed is ever interpreted as a command.

Exit status

Status Meaning
0 The selection and any required processing succeeded, and no
reporting mode found drift.
1 --check or --diff found a file that would be reformatted.
2 A file could not be read or rewritten, the selection of files
failed, or the command line was rejected.

Table 2: The exit statuses.

An error outranks drift: a run that could not read one file exits 2 even when another file drifted, so a gate never reports a clean tree from an incomplete analysis. Drift is a failure only under --check and --diff; --in-place over drifting files exits 0, because the drift was the work it was asked to do. A run whose standard output is closed early — the shape produced by mdtablefix --check *.md | head — exits 0 rather than failing, because the reader stopped early and that says nothing about the files.

Reading a report line

A report line is the file's path followed by the line delta of the rewrite, as two further space-separated fields:

docs/users-guide.md +25 -25

Read it from the right: the last field is the deletion count with a - prefix, the field before it is the insertion count with a + prefix, and the path is everything before those two, so a path containing spaces needs no quoting. The counts come from a line diff between the file's bytes and the text a rewrite would write, so they count lines replaced rather than edits a reviewer would tally by hand.

Line endings and byte-order marks

mdtablefix writes the line-ending style that holds the majority of the document's line endings, and preserves a leading byte-order mark (BOM) only when the file already has one. Both rules apply in every mode, so --check, --diff, and --in-place agree on the bytes a file would end up with. See Line endings for the detection rule and its tie-breaks.

Three consequences are worth knowing before running the tool over a repository.

  • Detection covers the whole document, including fenced code blocks. A predominantly CRLF document whose code samples use LF endings has those samples rewritten to CRLF, which is a change to the content of the code rather than to its formatting.
  • A lone carriage return is content, not a line ending. A file that separates its lines with \r alone is therefore treated as a single line.
  • Neither rule consults .gitattributes, because neither is Git's decision to make: the extension decides selection and the document decides its ending. A repository-wide mdtablefix --git --in-place therefore rewrites a mixed file to whichever style dominates it, whatever a text or eol attribute says.

Trailing newlines

A non-empty result always ends with one line ending, so a file whose last line is unterminated gains a terminator and is reported as drift, as +1 -1. The rule applies to every mode, including standard input, which prints the bare terminator even when the input produces no lines; an empty file stays empty.

Paths that match nothing

A path named on the command line is a path and not a pattern, and mdtablefix does no discovery of its own unless --git is given (see Selecting files from Git). A shell expands a glob before the tool sees it, so mdtablefix --check *.md in a directory with no Markdown files passes the literal pattern *.md, which is then reported as an unreadable path and exits 2. Shells differ in whether that happens: sh and bash pass an unmatched pattern through literally, while zsh — and bash under failglob — refuse to run the command at all, so the exit 2 above belongs to the shells that let the pattern through. The behaviour is deliberate either way: a run asked to check a set of files and checking none of them has not earned a clean tree. A gate over a tree that is not a repository should expand the list in a way that runs the tool only when there is something to check:

fd -e md -e mdc -e markdown -X mdtablefix --check
find . \( -name '*.md' -o -name '*.mdc' -o -name '*.markdown' \) -print0 | xargs -0 -r mdtablefix --check

Both run the tool once with every match, and run nothing at all — exiting 0 — when there are no matches. Both name the same three extensions the default --git selection covers, so a gate outside a repository checks the file types a --git run would.