tesh [tɛʃ] - TEstable SHell sessions in Markdown
Showing shell interactions how to run a tool is useful for teaching and explaining.
Making sure that example still works over the years is painfully hard.
Not anymore.
$ tesh demo/
📄 Checking demo/happy.md
✨ Running foo ✅ Passed
✨ Running bar ✅ Passed
📄 Checking demo/sad.md
✨ Running foo ❌ Failed
Command: echo "foo"
Expected:
sad panda
Got:
foo
Taking you into the shell ...
Enter `!!` to rerun the last command.
$
To mark a code block as testable, append tesh-session="NAME"
to the header line.
You can use any syntax highlighting directive, such as bash
, shell
, shell-session
, console
or others.
```console tesh-session="hello"
$ echo "Hello World!"
Hello World!
```
Besides marking a code block as testable, tesh-session
is a unique identifier that allows for multiple code blocks to share the same session.
```console tesh-session="multiple_blocks"
$ export NAME=Earth
```
```console tesh-session="multiple_blocks"
$ echo "Hello $NAME!"
Hello Earth!
```
Parts of the inline output can be ignored with ...
:
```console tesh-session="ignore"
$ echo "Hello from Space!"
Hello ... Space!
```
The same can be done for multiple lines of output. Note that trailing whitespace in every line is trimmed.
```console tesh-session="ignore"
$ printf "Hello \nthere \nfrom \nSpace!"
Hello
...
Space!
```
Commands can continue across multiple lines by prefixing lines with >
.
```console tesh-session="multiline"
$ echo "Hello from" \
> "another" \
> "line!"
Hello from another line!
```
You can set a few other optional directives in the header line:
tesh-exitcodes
: a list of exit codes in the order of commands executed inside the code block,tesh-setup
: a filename of a script to run before running the commands in the code block,tesh-ps1
: allow an additional PS1 prompt besides the default$
,tesh-platform
: specify on which platforms this session block should be tested (linux
,darwin
,windows
),tesh-fixture
: a filename to save the current snippet,tesh-timeout
: number of seconds before a command timeouts (defaults to 30s),tesh-long-running
: set totrue
to showcase long-running commands such asdocker compose up
.
Let's look at all of these through examples!
tesh-exitcodes
accepts a list of integers, which represent the exit code for every command in the block.
```console tesh-session="exitcodes" tesh-exitcodes="1 0"
$ false
$ true
```
Sometimes you need to do some test setup before running the examples in your code blocks. Put those in a file and point to it with the tesh-setup
directive.
```console tesh-session="setup" tesh-setup="readme.sh"
$ echo "Hello $NAME!"
Hello Gaea!
```
Every so often you need to drop into a virtualenv or similar shell that changes the prompt. tesh
supports this via test-ps1
directive.
```console tesh-session="prompt" tesh-ps1="(foo) $"
$ PS1="(foo) $ "
(foo) $ echo "hello"
hello
```
Some examples should only run on certain platforms, use tesh-platform
to declare them as such.
```console tesh-session="platform" tesh-platform="linux"
$ uname
...Linux...
```
```console tesh-session="platform" tesh-platform="darwin"
$ uname
...Darwin...
```
Occasionally your examples consist of first showing contents of a file, then executing a command that uses said file. This is supported, use the tesh-fixture
directive.
```bash tesh-session="fixture" tesh-fixture="foo.sh"
echo "foo"
```
```console tesh-session="fixture"
$ chmod +x foo.sh
$ ./foo.sh
foo
```
By default, tesh
will fail if an example command does not finish in 30 seconds. This number can be modified using the tesh-timeout
directive.
```console tesh-session="timeout" tesh-timeout="3"
$ sleep 1
```
Some processes that you want to show examples for are long-running processes, like docker compose up
. They are supported in tesh
blocks using the tesh-long-running
directive. Note that they need to be the last command in the block.
```console tesh-session="long-running" tesh-timeout="1" tesh-long-running="true"
$ nmap 1.1.1.1
Starting Nmap ...
```
The best way to install tesh
is with your favorite Python package manager.
$ pip install tesh
- Supports Linux / macOS.
- Not tied to a specific markdown flavor or tooling.
- Renders reasonably well on GitHub.
tesh | mdsh | pandoc filters | |
---|---|---|---|
Execute shell session | ✔️ | ✔️ | ✔️ |
Modify markdown file with the new output | 🚧[1] | ✔️ | ✔️ |
Shared session between code blocks | ✔️ | ✖️ | ✖️ |
Custom PS1 prompts | ✔️ | ✖️ | ✖️ |
Assert non-zero exit codes | ✔️ | ✖️ | ✖️ |
Setup the shell environment | ✔️ | ✖️ | ✖️ |
Reference fixtures from other snippets | ✔️ | ✖️ | ✖️ |
Wildcard matching of the command output | ✔️ | ✖️ | ✖️ |
Starts the shell in debugging mode | ✔️ | ✖️ | ✖️ |
Specify timeout | ✔️ | ✖️ | ✖️ |
Support long-running commands | ✔️ | ✖️ | ✖️ |
- ✔️: Supported
- C: Possible but you have to write some code yourself
- 🚧: Under development
- ✖️: Not supported
- ?: I don't know.
We provide two development environments for people working on this project, one based on Nix and one based on Docker.
For Nix, run nix develop
to enter the development environment, where everything is ready for use.
For Docker, run docker build -t tesh . && docker run --rm -v .:/tesh -it tesh
to enter the development environment, where everything is ready for use.
Then you can run make tests
to run all tests & checks.
Additional make
commands are available to run just a subset of tests or checks.
# run tesh on all Markdown files
$ make tesh
# run flake8 linters on changed files only
$ make lint
# run flake8 linters on all files
$ make lint all=true
# run mypy type checker
$ make types
# run unit tests
$ make unit
# run a subset of unit tests (regex find)
$ make unit filter=foo
By default, the development environment uses the latest supported Python version. This is how you drop into an environment with an older Python
On Linux:
$ nix develop .#devShells.x86_64-linux.default-python39
On macOS:
$ nix develop .#devShells.aarch64-darwin.default-python39
On CI, all supported versions of Python are tested.