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
\ralone 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-widemdtablefix --git --in-placetherefore rewrites a mixed file to whichever style dominates it, whatever atextoreolattribute 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.
Symbolic links
A read follows a symbolic link, so --check and --diff report a link's
target like any other file. A write declines the link rather than replacing it,
because the rename would turn the link into a regular file; the run reports the
declined link, fails that file, and exits 2. A link to a file that needs no
changes is not written at all, so it succeeds. See
In-place editing for the full replacement contract.
A --git selection never reaches either case for a link: a link is not a
regular file, so it is skipped during selection, before it is analysed. The
same skip covers a candidate reached through a symlinked directory, so if
docs is a link to a directory outside the working tree, docs/guide.md is a
regular file but not one inside the selection, and it is skipped rather than
written through the link. See
Selecting files from Git.