mdsh runs the code blocks in a markdown file, like a notebook whose file format is plain markdown. The documentation lives at gdey.github.io/markdownshell. See docs/spec.md for the design and docs/plan.md for the roadmap.
Here is examples/hello.md, written the literate way: prose
explains each block, and the output under each block was written back by
mdsh -w.
#!/usr/bin/env mdsh
---
title: Hello, mdsh
---
# Hello, mdsh
First we decide what to say and who to say it to. `export` makes
`GREETING` visible to any sub-env we start later; `NAME` is a plain shell
variable, so it stays in this shell.
```sh {#setup}
export GREETING=hello
NAME=world
echo "$GREETING, $NAME"
```
```output {for=setup exit=0}
hello, world
```
Blocks without `env=` all run in the same shell, like lines of one
script, so this block still sees `NAME`. It counts the letters in it:
`printf` avoids a trailing newline, `wc -c` counts bytes, and `tr`
strips the padding some `wc`s add.
```sh {#count}
printf '%s' "$NAME" | wc -c | tr -d ' '
```
```output {for=count exit=0}
5
```
A sub-env is a separate shell that only receives the exported variables
named in `share=`. `GREETING` comes through. `NAME` doesn't, because it
was never exported, so `${NAME:-unset}` falls back to `unset`.
```sh {#isolated env=sub share=GREETING}
echo "GREETING=$GREETING NAME=${NAME:-unset}"
```
```output {for=isolated exit=0}
GREETING=hello NAME=unset
```
A block can also write markdown. `out=markdown` puts its output into the
document as-is, and `md_table` turns tab-separated lines (header first)
into a table. `md_table -F ' '` splits on runs of blanks instead (any
`awk -F` separator works), and `-s N` skips the first N lines.
```sh {#table out=markdown}
printf 'variable\tvalue\nGREETING\t%s\nNAME\t%s\n' "$GREETING" "$NAME" | md_table
```
<!-- mdsh:output for="table" -->
| variable | value |
| --- | --- |
| GREETING | hello |
| NAME | world |
<!-- /mdsh:output -->
A block's output can also be an image. With `out=image`, the block
prints image bytes (PNG, JPEG, GIF or SVG), and mdsh saves them next to
this file in `hello.md.assets/` and links them here. This block draws an SVG banner with plain `printf`:
the bar is 12 pixels wide per letter of the greeting, using shell
arithmetic `$(( ))`.
```sh {#banner out=image}
w=$(( ${#GREETING} * 12 ))
printf '<svg xmlns="http://www.w3.org/2000/svg" width="240" height="40">'
printf '<rect width="%d" height="40" rx="6" fill="#4f7cff"/>' "$w"
printf '<text x="%d" y="26" font-family="sans-serif" font-size="16" fill="#888">%s, %s</text>' "$(( w + 8 ))" "$GREETING" "$NAME"
printf '</svg>\n'
```
<!-- mdsh:output for="banner" -->

<!-- /mdsh:output -->
To take a picture of plain text output instead, use `out=screenshot`:
whatever the block prints is drawn as a terminal-style image. It's an
SVG by default; ending the path in `.png` asks for a PNG. Here
`printf '%-10s'` pads each name to 10 columns so the values line up.
```sh {#screenshot out=screenshot:hello.md.assets/screenshot.png}
printf '%-10s %s\n' GREETING "$GREETING" NAME "$NAME"
```
<!-- mdsh:output for="screenshot" -->

<!-- /mdsh:output -->
Some parts are only for debugging. Everything between the
`mdsh:if` and `mdsh:end` comments below is printed only when `DEBUG` is
set and exposed (try `DEBUG=1 MDSH_EXPOSE=DEBUG ./hello.md`), and `run={{$DEBUG}}` keeps the block
from running otherwise. Its stderr is shown too, because of
`show-stderr`; normally stderr only appears when a block fails.
<!-- mdsh:if {{$DEBUG}} -->
**Debug:** the shell is **{{#shell/env(SHELL_NAME):"sh"}}**.
```sh {#shell run={{$DEBUG}} show-stderr}
SHELL_NAME=$(ps -p $$ -o comm= 2>/dev/null || echo unknown)
echo "running in $SHELL_NAME" >&2
```
```output {for=shell stream=stderr}
running in sh
```
<!-- mdsh:end -->
Prose can quote what a block left behind: `setup` set the greeting to
**{{#setup/env(GREETING):"hello"}}** and greeted **{{#setup/env(NAME):"world"}}**.
It can also quote the environment mdsh was started in: this ran as
**{{$USER}}** (the value isn't saved in the file).The two images it produced:
examples/README.md is a guided tour of the rest:
quoting files, guard rails for runbooks, inputs, terminal recordings,
background blocks, reports and interactive sessions, ending with the
complete runbooks in examples/runbooks: deploying a
site, cutting a release, a system report, Linux and macOS host triage,
and a test coverage report for mdsh itself. go test ./cmd/mdsh runs
each runbook; a name ending in _linux.md or _darwin.md runs only on
that OS.
With Homebrew, on macOS or Linux:
brew install gdey/tap/mdsh # the command line and mdsh -i
brew install --cask gdey/tap/mdsh # the desktop app and the command line, macOS onlyThe cask's app carries mdsh and links it for you, so it conflicts with
the formula: on a Mac, install one or the other.
On Debian, Ubuntu, Fedora and the like, each release has a .deb and an
.rpm for mdsh.
Or download an archive for your system from the
releases page, unpack it,
and put mdsh on your PATH. Builds cover Linux, macOS and Windows on
amd64 and arm64; checksums.txt lists each archive's SHA-256. For example,
on Linux amd64:
VERSION=1.0.0
curl -LO https://github.com/gdey/markdownshell/releases/download/v$VERSION/mdsh_${VERSION}_linux_amd64.tar.gz
tar xzf mdsh_${VERSION}_linux_amd64.tar.gz mdsh
sudo mv mdsh /usr/local/bin/
mdsh --versionOn macOS, a downloaded binary may be quarantined; xattr -d com.apple.quarantine mdsh clears it. Blocks run in /bin/sh by default;
on Windows, set shell: under mdsh: in the front matter to a POSIX shell
such as Git Bash's bash.exe. The Windows builds are not well tested yet.
The releases also have mdsh-gui, a desktop app for macOS (a universal
.dmg) and Linux (an AppImage), each carrying the same mdsh, which
Settings › Shell puts on your PATH; see cmd/mdsh-gui.
With Go 1.27 or newer you can build it instead:
go install github.com/gdey/markdownshell/cmd/mdsh@latestmdsh notes.md # run, print markdown with output under each block
mdsh -w notes.md # run, write output back into the file
mdsh -o out.md notes.md # same, but into out.md; notes.md is untouched
mdsh --hide-code notes.md # prose and output only
mdsh --hide-output notes.md
mdsh --hide-code --hide-output notes.md # prose only, nothing runs
mdsh -n notes.md # print with the output saved by -w; nothing runs
mdsh -n -o view.md notes.md # save that printout to view.md instead
mdsh --pdf notes.pdf notes.md # what it would print, as a PDF
mdsh --only greet notes.md # one block, plus what it depends on
mdsh --yes notes.md # run confirm blocks without asking
mdsh --step notes.md # ask before every block: run, skip or stop
mdsh --source defs.zsh notes.md # source defs.zsh into every shell first
mdsh -i notes.md # open it in a terminal UI and run blocks as you pick them
mdsh -i ~/notes # pick a file under ~/notes first (mdsh -i alone: this folder)
Shell completion covers flags, .md files and, after --only, the
file's block ids:
source <(mdsh completion bash) # bash, e.g. in ~/.bashrc
mdsh completion zsh > "${fpath[1]}/_mdsh" # zsh, then restart the shell
source <(mdsh completion zsh) # or zsh, after compinit in ~/.zshrc
mdsh lsp is a language server for editors: on stdin and stdout, it
completes the attributes in a fence's {…} and the block names in
depends=, explains an attribute on hover and flags mistakes as
diagnostics. In Neovim, for markdown files:
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function(ev)
vim.lsp.start({ name = "mdsh", cmd = { "mdsh", "lsp" } }, { bufnr = ev.buf })
end,
})Printed markdown is styled when stdout is a terminal; --color=never,
--no-color or NO_COLOR turn that off.
mdsh -i notes.md opens the document full screen, with its prose styled
and each block in a box. Pick a block and run it; its output streams in
under it. A • in the status line marks output that isn't saved; w
saves it, and --autosave saves after every block that runs.
If the file is read-only, w opens save as instead; with --autosave,
save as opens before the first run, and esc there lets the next run
go ahead without saving. mdsh-gui does the same with its Save dialog.
mdsh -w on a read-only file stops before running anything; -o saves
the output to another file.
Without a file, or with a folder, mdsh -i starts on a list of the
markdown files under the folder, as mdsh-gui does: the files opened
recently first, then the rest grouped by folder, each with its block
count, when it changed and the description (else summary, else
abstract) from its front matter. Typing filters the list by path and
description, and enter opens the file picked; q goes back to the list.
Recent files, and the view the last file was left in, are kept in
mdsh/tui.json in the user's config folder. Blocks keep
their envs between runs, the same as in one mdsh run, so running a
block again runs it in the env as the earlier runs left it, and replaces
its output.
| Key | Does |
|---|---|
j/k, arrows, tab |
next or previous block |
g/G |
first or last block |
| space, pgup/pgdn, ctrl+u/ctrl+d | scroll |
enter or r |
run the block |
a |
run the block and every one after it, like a normal run |
s |
run the section the block is in, its subsections included, as a would: from the heading above it to the next heading of the same or a higher level |
i or : |
type a command; enter adds it to the end of the document as an sh block and runs it, esc cancels |
h |
open the History pane: a shell along the bottom whose commands are added to the end of the document as #history-N skip links blocks with their output (skip keeps a whole-file run from running them again; links makes web addresses in it OSC 8 links), # lines as prose; tab completes, ↑/↓ or ctrl+p/n recall, the emacs line keys edit (ctrl+a/e, ctrl+w, ctrl+u/k, ctrl+y, alt+b/f/d, ctrl+l clears the pane), esc goes back to the document, ctrl+d closes it |
V |
select lines of the History pane's output (shift+↑ in the pane does too): ↑/↓ or j/k move the selection's end, g/G to the first or last line, o to its other end, y or enter copies them as text, without colors, the way y copies a block; esc cancels |
e |
edit the whole block, fences and attributes included, in $VISUAL, $EDITOR or vi; it is saved to the file, and enter runs it |
y |
copy the block's code, without its fences, to the clipboard. It is sent to the terminal as OSC 52, which works over ssh in terminals that allow it, and run locally it also goes to the system clipboard (pbcopy, wl-copy, xclip or xsel), for terminals that don't, such as macOS's Terminal or tmux by default |
E |
edit the whole file in $VISUAL, $EDITOR or vi, at the block under the cursor, with output not saved yet in it. Blocks whose code didn't change keep their output, as in mdsh-gui's source view; save with w |
alt+↑/↓, or K/J |
move the block, with its output and tape, up or down past the paragraph, heading or block next to it, as alt+↑/↓ does in mdsh-gui. Prose stays put. What the move breaks that depends on block order, such as a run= that now reads a block below it, shows in the footer. Not while a block runs |
u |
undo the last move |
v |
show the block's image or recording full screen; enter goes back |
o |
show or hide the outline: the headings and blocks down the left, with each block's state (✓ ran, ✗ failed, ? waiting for an answer, … running) |
C / O |
hide or show the code / the output of blocks |
p |
create a report, as mdsh-gui's Create Report: PDF, HTML or markdown, with or without code and output, of this run, of a run of every block, or of the file as saved. It shows the mdsh command that makes the same report |
, |
run options for this file: the arguments $1 and on, --only, --step and --stop-on-error. Setting them starts new shells, and the file keeps them when opened again |
w or ctrl+s |
save now |
W |
save as: type a path (relative to the file's folder); mdsh follows the new file. In another folder it opens it again, with fresh shells |
R |
reload the file from disk, dropping output not saved |
M |
when the file changed on disk, keep what is shown; the next save overwrites the file |
| ctrl+c | stop the running block; its env starts over with a fresh shell. Quits when nothing runs |
q |
go back to the list of files, or close the tab; the last tab quits |
| ctrl+t | open a new tab with the list of files; picking a file already open in a tab goes to that tab |
] / [ |
the next or previous tab. The tab bar marks a file with • when its output isn't saved, … while a block runs and ? while one waits for an answer. Blocks go on running in the tabs behind |
| ctrl+w | close the tab (esc on the list of files closes its tab). Quitting stops at a tab with output not saved first |
? |
the keys, each with what it does. Keys that do nothing now, such as u with no move to undo, are greyed with the reason. ↑/↓ pick one and enter does it; esc closes |
A block picked by hand runs even if it is marked skip or its run=
condition is false; a honors both. confirm blocks, and every block
under --step or step: true when running with a, ask in the
block's card, under its code, with the same choices as on the command line. --yes, --stop-on-error
and block arguments work as usual. A tty block runs in a terminal drawn
in its card, as in mdsh-gui: up to 24 rows, as wide as the card. While
it runs every key goes to it, ctrl+c included, and ctrl+] stops it. A
taped block's run is shown the same way. Images and recordings
show under their block as a preview in colored half blocks (any terminal
with 24-bit color; off with NO_COLOR). v shows one full size, through
the kitty, iTerm2 or sixel protocol when mdsh recognizes the terminal
(kitty, Ghostty, iTerm2, WezTerm, foot, mlterm, contour, or sixel in
TERM), and in half blocks otherwise. SVG images show only as a link. Include blocks show the file's lines,
numbered when they have number_on. out=markdown output is drawn as
markdown, and placeholders in the prose show their current values. If the file changes on disk while it
is open, a banner offers to reload it (R) or keep yours (M), and mdsh
won't save over it until you pick one or press w twice.
--pdf FILE lays out what mdsh would print as a PDF, so a run can be
shared as a report. The hide flags and --only apply, and with -w or
-o the markdown is written too:
mdsh --pdf report.pdf report.md # run, write the PDF
mdsh --hide-code --pdf report.pdf report.md # prose and output only
mdsh -w --pdf report.pdf report.md # update the file and the PDF
mdsh -n --pdf report.pdf report.md # the output saved by -w; nothing runs--html FILE writes the same view as a standalone HTML page instead, with
local images inside it, so it can be sent on its own.
It's pure Go, with no external tools. Output shows in a terminal-style box,
stderr in red, out=screenshot output as text, and number_on includes
with a line-number gutter. examples/report.md shows
each of these, and examples/pdf holds the PDFs of it,
hello.md, include.md,
terminal.md and a run of the
test coverage report.
Every sh and zsh block runs, top to bottom. Blocks without env= share
one shell, so variables, functions and the working directory carry over.
Blocks run in /bin/sh unless shell: under mdsh: in the front matter
says otherwise; a document with any zsh block runs in zsh by default.
mdsh hands each block to the shell as written. In zsh a lone } ends a
{ } group wherever it is on the line, so echo } is a parse error
there; quote it, as in echo '}'. mdsh checks a block's syntax before
running it, so setopt ignore_close_braces in the block can't help.
| Attribute | Meaning |
|---|---|
#id |
Block name. An unnamed block gets block-<uuid>, written into its fence when mdsh saves the file |
skip |
Don't run |
run=COND |
Run only if the condition holds |
show=COND |
Print the code only if the condition holds |
show-output=COND |
Print the output only if the condition holds |
show-stderr[=COND] |
Show stderr (by default it only shows when the block fails) |
collapse[=COND] |
In mdsh-gui, open with the code folded to one line |
collapse-output[=COND] |
In mdsh-gui, open with the output folded to one line |
nosave |
Keep the value out of the file: the block's output, or what is typed in a textbox or picked in a choice, lives in the session, and saving leaves what the file holds. On an output fence, keeps just that fence |
secret |
The code and output hold a secret: mdsh-gui masks them until you show them, and as with nosave the output isn't saved to the file |
env=name |
Run in a named sub-env; a/b stacks b on a |
share=A,B |
Exported vars a new sub-env gets from its parent, besides PATH, HOME, USER and the rest of the base set (default all; empty for just the base set; -PATH drops one) |
on_error=stop |
Stop the run if this block fails |
depends=A,B |
Blocks to run first: each one that hasn't run yet is asked about |
always |
As a dependency, run again each time a block that depends on it runs, even if it already ran |
confirm[="question"] |
Show the block and ask whether to run it, skip it or stop the run |
timeout=30s |
Kill the block's shell after this long (stops the run) |
tty |
Run on the terminal, for commands like docker run -it; the session is saved as an asciinema cast, which mdsh-gui plays |
frame=N |
With tty, the frame a PDF shows (default the last) |
out=markdown |
Insert output as raw markdown |
out=image[:path] |
Output is image bytes (PNG, JPEG, GIF, SVG): save it and link it |
out=screenshot[:path] |
Draw the text output as a terminal-style image: SVG, or PNG if the path ends in .png |
embed=inline |
With out=image or out=screenshot, embed a data URI instead of a file |
The output right under a block is its own. To show that output again
elsewhere, say in a summary at the end, put an output fence naming the
block anywhere else in the document; mdsh refills it on every run, the
way it fills in a placeholder:
## Environment
```output {for=env exit=0}
HOME is set
GITHUB_TOKEN is not set
```stream=stderr copies the block's stderr fence, and an
<!-- mdsh:output for="id" --> region copies out=markdown or image
output. A copy keeps what it holds when its block doesn't run, and mdsh
warns about a copy for a block that doesn't exist.
A block marked <include holds lines from another file, so a doc can quote
real code without a copy that goes stale:
```go {<include file=cmd/main.go from=10 to=24 number_on}
```Each run writes the file's lines into the block, so it reads well on
GitHub too. file= is relative to the document, and file=res://notes.md
reads notes.md from the document's assets folder, <doc>.assets/.
out=image:res://plot.png saves an image there the same way. from= and to= are line
numbers counted from 1 and inclusive; from defaults to the start of the
file and to to the end. The fence's language is the language, and
number_on shows line numbers when the document is printed, in the
TUI, and in --html and --pdf. mdsh-gui numbers every code block, so
it numbers an include's lines from from= with or without number_on.
An include block never runs and has no name. examples/include.md
shows each option.
A block marked confirm pauses the run: mdsh shows its code and asks
Run this block? [r]un / s[k]ip / [o]nly run blocks without saved output / [s]top,
or your own question from confirm="Deploy to prod?".
rruns the block.kskips it and goes on. A skipped block keeps any output already saved in the file.oskips this block and every later one that already has saved output, and runs the rest. Blocks without saved output are still asked about. The prompt says(has saved output)when a block has some.s, or just enter, stops the run there. This is the safe choice.
The prompt goes to the terminal, so it works even when output is piped.
With no terminal, the run stops there with an error unless you pass --yes.
To step through a whole document this way, pass --step or set
step: true under mdsh: in the front matter. Every block that would run
is then asked about, as if it were marked confirm.
When a run can ask, because of --step, step: true or a confirm
block, mdsh prints the document as it goes rather than all at the end:
the prose and code up to each block before asking about it, and the
block's output, stderr included if it failed, as soon as it finishes.
This happens with -w too, so you can follow a run as it is written
back. Without asking, -w prints only a status line per block, followed
by the output and stderr of a block that failed.
depends= lists blocks, by id, that a block needs to have run first:
```sh {#build}
make
```
```sh {#deploy depends=build,login}
./deploy.sh
```When deploy is about to run, mdsh first works out everything it needs:
build and then login, in that order, each after the blocks its own
depends= names, all the way up the chain. A block that has already run
in this session (in mdsh -i or mdsh-gui; in a plain mdsh run, earlier
in the run) and didn't fail is passed over, and so is what it needs,
unless it is marked always or is stale. Then
mdsh walks back down from the top of the chain, asking
Run build first? [r]un / s[k]ip / [s]top about each block right before
it would run, so each answer is followed by the run it allowed, and every
block a block needs has run (or been skipped) before it is asked about:
rruns it now. Itsskip,run=andconfirmdon't apply, since you just said to run it.kgoes on without it; the blocks below it are still asked about.s, or just enter, runs neither it nor anything below it, nordeploy.
If deploy is itself marked confirm, it is confirmed last, after the
blocks it depends on have run.
If a dependency fails, deploy doesn't run. In a whole run, the run then
goes on or stops as the dependency's on_error says, and a block already
run as a dependency isn't run again when the run reaches it. With no
terminal the run stops with an error, unless you pass --yes, which runs
dependencies without asking. An id that names no block is a warning
and is ignored, so a typo doesn't stop the file opening; mdsh-gui
underlines it in the source and notes it in the Console. A block that depends on itself and a cycle
are errors when the file is read. mdsh-gui and mdsh -i ask the same
question in their own way.
A dependency with nothing to run, a body of only blank lines and #
comments, runs without asking, since running it does nothing; its own
depends= are still asked about, so an empty block can name a group of
steps. Mark it confirm to have it asked about anyway, as a checkpoint.
Mark a dependency always when what it does doesn't last, such as
refreshing a token or pulling the latest code:
```sh {#login always}
./get-token.sh > .token
```Each time a block that depends on login runs, login is asked about
again, even if it already ran. It runs once on the way to that block,
even when several of its dependencies need it. always doesn't make it
run without asking; --yes still does that.
A block that ran is stale once a block it depends on runs again, or
has its output cleared in mdsh-gui, or is stale itself. A stale
dependency is asked about again, like one that never ran. Say c
depends on a, d on a, b and c, and e on a and d, and
all have run. Run a again, and c, d and e are stale; run e,
and mdsh asks about d, then, on the way to d, c, but not b.
mdsh-gui marks a stale block's card with a stale badge, and mdsh -i
shows ✓ stale in its header.
Some commands need a terminal: docker run -it, ssh, sudo, an
editor, a REPL. Mark their block tty and mdsh runs it on a terminal,
passing your keys through:
```sh {#shell tty}
docker run -it --rm alpine sh
```The block still runs in its env's shell, so variables and the working directory carry over as usual. Ctrl+c goes to the command, not to mdsh, and window resizes are passed on.
mdsh records the session as an asciinema cast,
<doc>.assets/<block-id>.cast (or the path out=recording:path names),
and links it below the block. mdsh-gui plays it in the block's card,
where you can pause, scrub, and step back and forth a frame at a time.
GitHub can't play a cast, so for a document read there, give a path
ending in .gif, as in out=recording:res://demo.gif: mdsh links an
animated GIF made from the cast instead, and keeps the cast beside it.
An HTML report plays a GIF made from the cast. A PDF can't animate, so
it shows the last frame; set frame=N to show frame N instead,
counting from 1.
A tape fence types the input for a tty block, in the commands of a
VHS tape, so the block runs with
nobody at the keyboard, in CI or under -w:
```sh {#repl tty}
python3 -q
```
```tape {for=repl}
Wait />>>$/
Type "print(6 * 7)"
Enter
Wait />>>$/
Type "exit()"
Enter
```Or read it from a file: ```tape {for=repl <include file=demo.tape}.
mdsh supports Type, the named keys (Enter, Tab, Up, …) with a
count, Ctrl+X and Alt+X, Sleep, Wait /regex/ and
Set TypingSpeed, WaitTimeout and WaitPattern, and skips the rest of
VHS's commands. Double-quoted Type strings can hold placeholders,
{{$VAR}} or {{#block/env(VAR)}}, filled in as the tape plays, for
passwords and tokens that shouldn't be written into the document
(--expose VAR or $MDSH_EXPOSE must name a {{$VAR}} first). Single
quotes or backticks type the text as it is. A taped block runs on an 80x24 terminal so its recording
is the same everywhere. Start with a Wait or Sleep, since keys go out
as soon as the block starts. examples/terminal.md shows each of
these, with the recordings they make.
With no terminal (cron, CI) or with mdsh in the background, a tty block
without a tape fails with an error instead of hanging.
A condition is a placeholder without the :"..." part, optionally
compared with == or !=, or negated with !:
| Condition | True when |
|---|---|
{{$DEBUG}} |
DEBUG in mdsh's environment is exposed, set and not 0, false, no or off |
!{{$QUIET}} |
the opposite |
{{#detect/env(OS)}}==Linux |
block detect left OS set to Linux |
{{$?}}==0 |
the last block that ran exited 0 (see below) |
{{#build/?}}!=0 |
block build ran and failed |
{{$err?}}, {{#build/err?}} |
the same statuses: true when non-zero, so when the block failed |
{{$ran?}}, {{#build/ran?}} |
the last runnable block, or block build, ran (not skipped) |
true, false |
always, never |
once |
in run= only: the block has no output yet |
Use them in run=, show=, show-output= and show-stderr=, and to
show or hide whole stretches of the document:
<!-- mdsh:if {{$DEBUG}} -->
Prose and blocks here are printed only when DEBUG is set.
<!-- mdsh:end -->{{$?}} is the exit status of the last block that ran before it. In
run= that is the previous block, so run={{$?}}==0 runs a block only
if the one before it succeeded. show=, show-output= and show-stderr=
are checked after the block runs, so there it is the block's own status:
show-output={{$?}}!=0 shows output only on failure. {{#build/?}}
names the block instead. {{$ran?}} works the same way, so
run=!{{$ran?}} is a fallback for a block that was skipped, and
{{$err?}} is another name for {{$?}} that reads well as a condition.
run=once runs a block only while it has no output: none saved after it
in the file, and none from running it earlier in the same mdsh -i or
mdsh-gui session. Use it for a slow setup or a one-time fetch; delete
its output to run it again. Picking the block by hand still runs it.
Sections nest, and their comment lines never appear in printed output.
A <!-- mdsh:collapse --> line, optionally with a condition, makes
mdsh-gui open with the heading section it is in folded. With an input's
var=, <!-- mdsh:collapse {{$MDSH_VAR_details}}!=yes --> keeps a
section folded until someone picks yes. Folding a block or section by
hand in mdsh-gui writes these for you, and unfolding takes them out.
show=, show-output= and sections only change what is printed: -w
keeps everything in the file. Use run= to keep a block from running.
Checks that only apply to some setups are ordinary blocks gated with
run=. Turn a check on with an environment variable, or let an earlier
block detect what is there:
```sh {#detect}
command -v docker >/dev/null && HAS_DOCKER=1
```
```sh {#docker run={{#detect/env(HAS_DOCKER)}} show={{$ran?}}}
docker info >/dev/null && echo "docker is up"
```
```sh {#vpn run={{$CHECK_VPN}} show={{$ran?}}}
ping -c1 -W2 intranet.example.com
```mdsh doc.md checks Docker only where it is installed, and the VPN
only with CHECK_VPN=1. show={{$ran?}} leaves a skipped check out of
the printed output. An mdsh:if section around a block hides it but
does not stop it running, so gate checks with run=.
mdsh doesn't run blocks that a block prints: fences in out=markdown
output are plain code. See
Not pursued for why.
{{#setup/env(NAME)}} in the prose stands for the value NAME had right
after block setup ran (exported or not), and {{$NAME}} for NAME in
the environment mdsh was started in, if you exposed it: --expose NAME,
or MDSH_EXPOSE=NAME,OTHER in the environment (mdsh's own MDSH_
variables need nothing). A document you didn't write can't read your
$GITHUB_TOKEN this way, whatever its front matter says; an unexposed
{{$NAME}} prints as written and reads as unset in conditions, and the
value is never saved in the file. {{#setup/?}}, {{$?}}, err? and
ran? stand for exit statuses and whether a block ran, as in conditions. Printed output shows the value;
-w stores it in the file as {{#setup/env(NAME):"world"}}, so the
markdown reads well without running it and keeps the last value when the
block doesn't run. Placeholders in code blocks and inline code are left
alone. Turn them off with interpolate: false under mdsh: in the front
matter.
Every front matter option is listed on the Front matter page.
Blocks get a base set of variables from the environment mdsh was
started in: PATH, HOME, USER, LOGNAME, SHELL, TERM,
COLORTERM, LANG, LANGUAGE, LC_*, TZ, TMPDIR and, on macOS,
__CF_USER_TEXT_ENCODING. None of them is a secret. Name more under
env: in the front matter, or bring in everything with ..., and drop
any with -NAME:
mdsh:
env:
share: [..., -GITHUB_TOKEN]mdsh -i and mdsh-gui show a banner over a file that brings in
everything, so you know before you run it.
A block's stderr is captured separately. It is shown only when the block
fails, unless show-stderr (or show_stderr: true under mdsh: in the
front matter) says otherwise.
An explicit exit ends the shell and stops the run. Blocks can use
$MDSH_BLOCK, $MDSH_FILE, $MDSH_DIR, $MDSH_MODE (running,
interactive or report, also readable in conditions as
{{$MDSH_MODE}}), $MDSH_VAR_name for each textbox or choice block
with var=name (the same control can appear more than once with one
var=, and changing one copy changes them all), the script's arguments as $1…,
and the helpers md_h1, md_h2, md_h3, md_table [-F SEP] [-s N] (TSV
on stdin) and md_code.
Blocks don't see the aliases and functions of the shell mdsh was started
from, since a child process only inherits exported variables. To use them,
source them into every shell (sub-envs too) with --source FILE or with
source: under env: in the front matter. This wrapper in ~/.zshrc
passes along whatever your shell has defined when you run mdsh:
mdsh() { command mdsh --source <(alias -L; typeset -f) "$@"; }In bash, use <(unset -f mdsh _mdsh; alias -p; declare -f); leaving out
the wrapper and its completion function keeps their bash-only syntax from
stopping a /bin/sh that is dash. Settings › Shell in mdsh-gui can add the
wrapper to ~/.zshrc and ~/.bashrc, with a line that loads completion.
