A configurable command-line tool written in Rust that parses Markdown files, extracts fenced code blocks, executes them via external arbitrary commands, and optionally replaces the content of the blocks with the command output.
Useful for:
- Validating Markdown tutorials with executable code
- Auto-updating examples and documentation
- Code formatting code blocks (e.g.
black,nixfmt,shfmt,php-cs-fixer, etc) - Linting code blocks (e.g.
ruff,php -l,prettier, etc)
- Clean configuration via a TOML file
- Fast and dependency-free thanks to Rust
- Scans fenced Markdown code blocks by language
- Configurable per-language command execution
- Optional block replacement based on command output
--checkmode for CI/linting use cases- Markdown code blocks can opt-out using the
mdcr-skipflag - Placeholder support (
{file},{lang}, etc.)
git clone https://github.com/drupol/markdown-code-runner
cd markdown-code-runner
cargo build --release
./target/release/mdcr --helpAvailable soon through markdown-code-runner package, the binary is called mdcr.
mdcr --config config.toml path/to/file.mdmdcr --config config.toml --check path/to/file.mdThis will:
- Execute configured commands for each code block
- Fail with exit code
1if output differs from original (like a linter) - Do not modify files
The configuration file defines which commands to run for which Markdown block languages.
Save this file as config.toml:
[presets.ruff-format]
languages = ["python", "py"]
command = ["ruff", "format", "-"]
[presets.nixfmt]
language = "nix"
command = ["nixfmt"]
[presets.php]
language = "php"
# php-cs-fixer does not support STDIN, so we use a temporary file
command = [
"sh",
"-c",
"php-cs-fixer fix -q --rules=@PSR12 {file}; cat {file}"
]
input_mode = "file"
[presets.rust]
language = "rust"
command = ["rustfmt"]
[presets.typstyle]
language = "typst"
command = ["typstyle"]
[presets.latex]
language = "latex"
command = ["tex-fmt", "--stdin"]Each preset supports an optional input_mode, which defines how the code block is passed to the command:
stdin(default): The code is passed via standard input (STDIN)file: The code is written to a temporary file and its path is passed, the temporary file is deleted immediately after execution
Each preset also supports an optional output_mode, which defines how the command output is used:
replace(default): Replace the code block content with the command's outputcheck: Check the command's exit code, if it is different from0, the command failed, and the tool will return a non-zero exit code
If not specified, both input_mode and output_mode default to stdin and replace, respectively.
The tool scans for fenced code blocks like:
```python
print( "hello" )
```
It will execute all matching commands whose language is python.
To exclude a block from processing, add mdcr-skip after the language:
```python mdcr-skip
print("don't touch this")
```
You can use placeholders in the command field:
| Placeholder | Description |
|---|---|
{file} |
Path to the temporary code file |
{lang} |
Language of the block (python) |
{suffix} |
File suffix (e.g. .py) |
{tmpdir} |
Temporary directory path |
- Blocks with unsupported languages are skipped with a warning.
{file}placeholder is only available ininput_mode: "file"mode.
Recommended usage in continuous integration:
mdcr --config config.toml --check docs/This runs all configured checks and returns a non-zero exit code if:
- Output differs from the original
- A command fails
The --check mode will not modify any files.
The CLI option --log allows you to control the verbosity and destination of log messages emitted during execution.
mdcr --config config.toml --log debug path/to/file.mdThe logging system uses standard log levels, from most verbose to least:
| Level | Description |
|---|---|
trace |
Highly detailed, useful for debugging internal issues |
debug |
General debugging information |
info |
Informational messages about execution progress |
warn |
Non-critical issues that deserve attention |
error |
Critical problems encountered during execution |
By default, if no --log option is provided, the logging level defaults to warn.