Skip to content

OceanSprint/tesh

Repository files navigation

CI for tesh (main branch) Test coverage (main branch) Test coverage (main branch) latest version of tesh on PyPI Supported Python versions License: MIT Built by these great folks!

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.

$

Syntax

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!
```

Linking multiple code blocks into a single shell session

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!
```

Ignoring parts of the output

Parts of the inline output can be ignored with ...:

```console tesh-session="ignore"
$ echo "Hello from Space!"
Hello ... Space!
```

Multiline support

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!
```

Advanced directives

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 to true to showcase long-running commands such as docker compose up.

Let's look at all of these through examples!

Testing exit codes

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

```

Test setup

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!
```

Custom prompts

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
```

Only run on certain platforms

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...
```

Dump file to disk

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
```

Custom timeout

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

```

Long running processes

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 ...
```

Installation

The best way to install tesh is with your favorite Python package manager.

$ pip install tesh

Design decisions

  • Supports Linux / macOS.
  • Not tied to a specific markdown flavor or tooling.
  • Renders reasonably well on GitHub.

Comparison with other tools

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.

Developing tesh

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

Multiple Python versions

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.