Quickstart

Installation

structured-tutorials is published on PyPI and thus can simply be installed via pip (or any other package management tool, such as uv):

$ pip install structured-tutorials

To render your tutorials in Sphinx, add the extension and, optionally, where your tutorials are stored (this can be outside of the Sphinx root):

extensions = [
    # ... other extensions
    "structured_tutorials.sphinx",
]

# Location where your tutorials are stored (default: same directory as conf.py).
#structured_tutorials_root = Path(__file__).parent / "tutorials"

Editor integration

The JSON schema is can be found here and can be used in IDEs for auto-completion and validation. For example:

Your first tutorial

To get started with the simplest possible tutorial, create a minimal configuration file:

docs/tutorials/quickstart/tutorial.yaml
parts:
  - commands:
      - command: structured-tutorial --help
        doc:
          output: |
            usage: structured-tutorial [-h] path
            ...
      - command: echo "Finished running example tutorial."
    doc:
      text_before: "This shows your first tutorial:"

You can run this tutorial straight away:

user@host:~/example/$ structured-tutorial docs/tutorials/quickstart/tutorial.yaml
Running part 0...
+ structured-tutorial --help
usage: structured-tutorial [-h] path
...
+ echo "Finished running example tutorial."
Finished running example tutorial.

Finally, you can render the tutorial in your Sphinx tutorial:

docs/tutorial.rst
Configure the tutorial that is being displayed - this will not show any output:

.. structured-tutorial:: quickstart/tutorial.yaml

.. structured-tutorial-part::

In fact, above lines are included below, so this is how this tutorial will render in your documentation:

Configure the tutorial that is being displayed - this will not show any output:

This shows your first tutorial:

user@host:~$ structured-tutorial --help
usage: structured-tutorial [-h] path
...
user@host:~$ echo "Finished running example tutorial."

A more advanced example

The above is nice but not very useful. Let’s show you a few new cool features next.

Commands, output, file contents and many other parts are rendered using Jinja templates. This allows you to reduce repetition of and use different values for documentation and runtime.

Tutorials can create files (that are shown appropriately in your documentation). The tutorial below shows you how to create a JSON file that shows up with proper syntax highlighting.

You can test success of a command by checking the status code, output or even if a TCP port was opened. When checking the output, you can use regular expressions for matching and even named patterns to update the context with runtime data. Below we create a temporary directory with mktemp and use it later to create the file in it.

The following example will create a directory, writes to a file in it and outputs its contents:

docs/tutorials/templates/tutorial.yaml
configuration:
  context:  # context shared between documentation and runtime
    filename: example.json
  doc:  # template context at runtime
    context:
      dest: /tmp/tmp...
      contents: '{"key": "doc"}'
  run:  # template context at runtime
    context:
      # dest is captured and added to the context by mktemp at runtime
      contents: '{"key": "run"}'
parts:
  - id: create-directory
    commands:
      - command: mktemp -d
        doc:
          output: "{{ dest }}"
        run:
          show_output: false
          test:
            # Capture the whole output and add it to the context
            - regex: (?P<dest>.*)
          cleanup:
            # Remove temporary directory after running the tutorial
            - command: rm -rf {{ dest }}
  - id: create-file
    contents: "{{ contents }}\n"
    destination: "{{ dest }}/{{ filename }}"
    doc:
      language: json
      # If you use sphinxcontrib-spelling, make sure the filename is not spell-checked
      ignore_spelling: true
  - commands:
      - command: cat {{ dest }}/{{ filename }}
        doc:
          output: "{{ contents }}"
      - command: cat {{ dest }}/{{ filename }} | python -m json.tool
        doc:
          output: ...

Render this tutorial

The code in your reStructuredText doesn’t look much different. You render three parts, and the first two reference the id you have given them in the YAML file.

docs/tutorial.rst
.. structured-tutorial:: templates/tutorial.yaml

First, create a temporary directory:

.. structured-tutorial-part:: create-directory

Then create a JSON file in it:

.. structured-tutorial-part:: create-file

You can verify the contents of the file like this:

.. structured-tutorial-part::

The above file is included below.

First, create a temporary directory:

user@host:~$ mktemp -d
/tmp/tmp...

Then create a JSON file in it:

/tmp/tmp…/example.json
{"key": "doc"}

You can verify the contents of the file like this:

user@host:~$ cat /tmp/tmp.../example.json
{"key": "doc"}
user@host:~$ cat /tmp/tmp.../example.json | python -m json.tool
...

Run this tutorial

When running this tutorial, it’ll do what you instructed the user to do: Create a temporary directory, then a JSON file in it, and then output it. Cleanup is assured through the cleanup directive, even if one of the commands would fail:

user@host:~/example/$ structured-tutorial docs/tutorials/templates/tutorial.yaml
Running part create-directory...
+ mktemp -d
Running part create-file...
Running part 2...
+ cat /tmp/tmp.6G6S9dX0MN/example.txt
{"key": "run"}
+ cat /tmp/tmp.6G6S9dX0MN/example.txt | python -m json.tool
...
INFO     | Running cleanup commands.
+ rm -r /tmp/tmp.6G6S9dX0M

Generating documentation out of the tutorial

Long commands wrap automatically

When rendering a tutorial, long commands wrap automatically. With the following YAML file:

docs/tutorials/long-commands/tutorial.yaml
parts:
  - commands:
      # This command is very short
      - command: docker run --rm -it ubuntu:24.04
      # But a longer command will wrap automatically
      - command: docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value ubuntu:24.04
      # Use '>' to break multiline strings in your YAML file, but still wrap commands
      # automatically. Any newlines in your YAML will be treated as a single space
      # instead:
      - command: >
          docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value
              ubuntu:24.04
              echo "Run a very long command"
      # You can also use a "\" to force line breaks when rendering:
      - command: >
          docker run --rm -it \
              -v `pwd`:/example \
              -e DEMO=value \
              ubuntu:24.04 \
              echo "Run a command"
    # show how single options will never split, see how "-e DEMO=..." is in one line.
  - commands:
      - command: docker run --rm -it -v `pwd`:/very/long/path -e DEMO=very-long-value ubuntu:24.04

you will get:

user@host:~$ docker run --rm -it ubuntu:24.04
user@host:~$ docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value ubuntu:24.04
user@host:~$ docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value ubuntu:24.04 echo \
>    "Run a very long command"
user@host:~$ docker run --rm -it \
>    -v `pwd`:/example \
>    -e DEMO=value \
>    ubuntu:24.04 \
>    echo "Run a command"

Note that single-value-options and respective values do not split by default, so -e DEMO=value will never split between option argument and value.

Single-character options will not be split from their respective value:

user@host:~$ docker run --rm -it -v `pwd`:/very/long/path -e DEMO=very-long-value \
>    ubuntu:24.04

Show the user alternatives

Sometimes you want to present the user with different options when following a tutorial. For example, you might want to show a user how to set up your web application using either PostgreSQL or MySQL.

structured-tutorials supports alternatives. They render as tabs in documentation, but when running a tutorial, the user has to specify an alternative. Alternatives can contain either commands or files (and you could even mix them):

docs/tutorials/alternatives/tutorial.yaml
configuration:
  doc:
    # Optional: Specify additional configuration for each alternative
    alternatives:
      mariadb:
        name: MariaDB
        context:
          package: mariadb-server
      postgresql:
        name: PostgreSQL
        context:
          package: postgresql
  # you can also update configuration at runtime
  run:
    alternatives:
      mariadb:
        context:
          dbclient: mysql
      postgresql:
        context:
          dbclient: psql
parts:
  # part 1: install database backend
  - alternatives:
      # We use YAML anchors here to reduce duplication. "{{ package }}" is the name of the
      # correct package.
      mariadb: &install-package
        commands:
          - command: sudo apt install {{ package }}
      postgresql: *install-package
    # Skip installation of packages at runtime (we don't want the tutorial
    # to require sudo)
    run: false

  # part 2: configure some application for that database
  - alternatives:
      mariadb:
        contents: "dbtype: mariadb"
        destination: "configuration.yaml"
        doc:
          language: yaml
      postgresql:
        contents: "dbtype: postgresql"
        destination: "configuration.yaml"
        doc:
          language: yaml

The first part will show the user how to install the respective database backend:

user@host:~$ sudo apt install mariadb-server
user@host:~$ sudo apt install postgresql

The second part will show the user how to configure your application for the respective database backend.

configuration.yaml
dbtype: mariadb
configuration.yaml
dbtype: postgresql

Note that this example of course omits configuring the database itself or any other details.

Running the tutorial

Verify success of commands

You can verify the success of commands by checking the status code, the output or even test if a port is opened properly. You can add multiple tests, and when testing the output, update the context for successive commands.

See Test commands for more information.

Select alternatives

If your tutorial contains alternatives (see Show the user alternatives), you must select one of them when running your tutorial. You wouldn’t normally install both PostgreSQL and MariaDB, for example:

user@host:~$ structured-tutorials -a postgresql ...

Ask the user for feedback

When running a tutorial, you can prompt the user to inspect the current state. You can ask the user to just press “enter” or even to confirm that the current state looks okay (with answering “yes” or “now”).

When rendering a tutorial, prompt parts are simply skipped.

As an example:

docs/tutorials/interactive-prompt/tutorial.yaml
configuration:
  run:
    # Switch to temporary directory before running the tutorial
    temporary_directory: true
parts:
  - commands:
      - command: echo "some content" > test.txt
      - command: echo "About to give you two prompts..."
  # Just prompt the user to hit 'enter' at this point:
  - prompt: "Hit enter to continue... "
  # Ask the user to confirm the current state - if they answer "no", the tutorial will abort.
  # In this case, we output the current working directory and ask the user to confirm:
  - prompt: |-
      Current working directory is {{ cwd }} ...
      Is the current state satisfactory (y/n)?
    response: confirm
    # Set to false to also raise an error if the user just presses enter:
    #default: true
    # You can also overwrite the error message the user will get:
    #error: Custom error encountered.

In Sphinx, you can call structured-tutorial-part only twice, as prompts are simply skipped. The first part just creates a file. Since temporary_directory: true in the configuration, this will run in a temporary directory that is removed after running the tutorial:

user@host:~$ echo "some content" > test.txt
user@host:~$ echo "About to give you two prompts..."

When running the tutorial, the user will now be prompted to confirm the current state. The prompt would contain the current working directory. Presumably, the user would check the contents of test.txt in that directory.

Prevent shell injection

You may also specify commands as lists to prevent shell injection. Note that every word of your command is still rendered as template:

docs/tutorials/command-as-list/tutorial.yaml
configuration:
  doc:
    context:
      value: example
parts:
  - commands:
    - command: [echo, "value with spaces"]
    - command: [echo, "tokens are also templates: {{ value }}"]

user@host:~$ echo 'value with spaces'
user@host:~$ echo 'tokens are also templates: example'