mdrip
mdrip
turns a directory hierarchy of markdown files into a testable, tutorial web app.
- Navigate with keys (mouse usage optional).
- Track progress with check marks.
- Immediately execute code blocks in tmux without copy/paste.
Or just use mdrip
to place markdown-based tutorials under CI/CD testing,
to avoid disappointing users.

Installation
Assuming Go installed just:
go get github.com/monopole/mdrip
or put it in your tmp dir
GOBIN=$TMPDIR go install github.com/monopole/mdrip
Execution
mdrip {filePath}
This searches the given path for files named
*.md
(ignoring everything else), and parses
the markdown into memory.
The filePath
argument can be
- a single local file,
- a local directory,
- a github URL in the style
gh:{user}/{repoName}
,
- or a particular file or a directory in the repo, e.g.
gh:{user}/{repoName}/foo/bar
.
What happens next depends on the --mode
flag.
Demo Mode: make a tutorial web app
mdrip --mode demo {filePath}
This serves rendered markdown at
http://localhost:8000
. Change the endpoint using
--port
and --hostname
.
Clicking on a code block in your browser (or navigating
to it using keys only) will copy its contents to
your clipboard. Hit '?' in the browser to see key controls.
If you have a local instance of tmux
running, the mdrip
server sends the code
block directly to active tmux
pane for immediate execution.
Example:
Render the content you are now reading locally:
$TMPDIR/mdrip --mode demo gh:monopole/mdrip/README.md
Visit localhost:8000.
If you have it, start tmux. Click on command
blocks in your browser to send them directly
to your active tmux window.
In this default mode, the command
eval "$(mdrip file.md)"
runs extracted blocks in the current
terminal, while
mdrip file.md | source /dev/stdin
runs extracted blocks in a piped shell that exits with
extracted code status. The difference between these
two compositions is the same as the difference between
eval "$(exit)"
and
echo exit | source /dev/stdin
The former affects your current shell, the latter does
not. To stop on error, pipe mdrip
output to bash -e
.
Example:
Print all the fenced code blocks from this README
:
$TMPDIR/mdrip gh:monopole/mdrip/README.md
Test Mode: place markdown code under test
mdrip --mode test /path/to/tutorial.md
runs extracted blocks in an mdrip
subshell,
leaving the executing shell unchanged.
In this mode, mdrip
captures the stdout and stderr of
the subprocess, reporting only blocks that fail,
facilitating error diagnosis. Normally, mdrip exits
with non-zero status only when used incorrectly,
e.g. file not found, bad flags, etc. In test mode,
mdrip will exit with the status of any failing code
block.
This mode is an instance of literate programming in
that code (shell commands) are embedded in explanatory
content (markdown). One can use here documents to
incorporate any programming language into the tests
(see the example below).
Labels
Fenced code blocks can be preceeded in the markdown by
a one-line HTML comment with embedded labels in this form:
<!-- @initializeCluster @tutorial03 @test -->
```
echo hello
```
Then the --label
flag can be used to extract only
code blocks with the given label, e.g.
mdrip --label test {filePath}
discards all code blocks other than those with a
preceding @test
label.
This can be used, for example, to gather blocks that
should be placed under test, and ignore those that
shouldn't. An example of the latter would be commands
that prompt an interactive user to login (test
frameworks typically have their own notion of an
authenticated user).
Special labels
-
The first label on a block is slightly special, in
that it's reported as the block name for logging in
test mode. So its useful to treat it as a method
name for the block, e.g. @initializeCluster
or
@createWebServer
.
-
The label @sleep
causes mdrip to insert a sleep 2
command after the block. Appropriate if one is
starting a server in the background in that block,
and want to impose a short wait (which you'd get
implicitly if a human were executing the blocks more
slowly as part of a demo).
Example:
This Go tutorial has code blocks that write, compile
and run a Go program.
Use this to extract blocks to stdout
:
alias mdrip=$TMPDIR/mdrip
mdrip --label lesson1 \
gh:monopole/mdrip/data/example_tutorial.md
Test the code from the markdown in a subshell:
clear
mdrip --mode test --label lesson1 \
gh:monopole/mdrip/data/example_tutorial.md
echo $?
The above command should show an error, and exit with non-zero status,
because that example tutorial has several baked-in errors.
To see success, download the example and confirm
that it fails locally:
tmp=$(mktemp -d)
git clone https://github.com/monopole/mdrip.git $tmp
file=$tmp/data/example_tutorial.md
mdrip --mode test --label lesson1 $file
Fix the problems:
sed -i 's|comment this|// comment this|' $file
sed -i 's|intended to fail|intended to succeed|' $file
sed -i 's|badCommandToTriggerTestFailure|echo Hello|' $file
Run the test again:
mdrip --mode test --label lesson1 $file
echo $?
The return code should be zero.
So, adding a line like
mdrip --mode test --label {someLabel} {filePath}
to your CI/CD test framework covers
the execution path determined by that label.
Tips for writing markdown tutorials
-
Place commands that the reader should copy/paste/execute in
fenced code blocks.
-
Eschew preceding commands with redundant prompts (e.g. $
).
-
Code-style text not intended for immediate execution, e.g. alternative
commands or example output, should be in a fenced code block indented via a
block quote. This makes them invisible to mdrip
.