/typst-shell-escape

Shell Escape for Typst typesetting system. Linux Only.

Primary LanguageRust

ARCHIVED

Starting from Typst v0.something, this no longer works due to forcing the --root option. Not that it should be used anyway.

I wrote this a long time ago, mostly as a joke, yet a few people thanked me in DMs in discord for implementing it. I am totally fine with people using my stuff, but I know that what I made can be significantly improved upon, at least ported to the latest version of Typst.

And at the moment, I am not in the mood to improve it.

Shell Escape for Typst

This is a simple shell escape for Typst. It allows you to run shell commands directly from Typst compiler.

That said, it does not mean that you should run shell commands from Typst. In fact, I would highly recommend against it. This is a very dangerous feature and should be used with extreme caution, and, if possible (it is possible), not at all.

Usage

You don't.

Usage

Please, don't. I beg you.

Usage

Fine. But be aware that you aren't just playing with fire anymore. You are planning with fire, in a forest, during a drought, with a flamethrower, near the lake of gasoline, with fifteen nuclear power plants nearby.

Clone the repo and make sure you have cargo and Typst CLI installed.

Run cargo build. This will create a binary in /target. Get the shell-escape.typ file and #import it in your Typst project. Run the built executable before compiling your project.

A note of caution

This is a very dangerous feature. It's not just dangerous, it's extremely dangerous. There is a reason shell-escape will never be implemented in Typst Not only is it very bad for security, you can ruin your workspace. You are literally opening a window to an undefined behaviour from a safe environment of the Typst virtual machine.

And don't even think of running this along with typst-lsp, or any other compiler instance. There will be no guarantees on the order of execution of commands. This can result in the deadlock, and you will be lucky if only your compiler deadlocks.

You have been warned.

High-level API

To run a command, use #exec-command.

Argument Type Description Kind Default
command string Command to run. positional
method-stdout function Function to call when the command writes to stdout, used to interpret stdout. For example, if command returns .json, pass json. named read
method-stderr function Function to call when the command writes to stderr. named read
format-stdout string File extension of stdout. For example, if you want to read svg image, you should use image function with ".svg" format named ""
format-stderr string File extension of stderr. named ""
custom-hash string Discriminator which helps defeat the limitation of function purity. Can be any string. If your command is pure, it's not needed. named ""
allow-non-zero-error-code bool If false, the function will panic if command finishes execution with non-zero error code. named true

It returns a dictionary with three entries:

Key Type Description
stdout any (most likely string) Stdout of the command, read with the given method-stdout
stderr any (most likely string) Stderr of the command, read with the given method-stdout
error-code int Exit code of command

Example:

Calculate 2 + 2 using Python:
#exec-command("python -c \"print(2 + 2)\"")

Returns #(stdout: "4\n", stderr: "", error-code: 0)

See example-*.typ files for more.

HTTP API (curl wrapper)

To make it easier to use, there is a wrapper around curl command for making get-requests. It's called #http-get.

Argument Type Description Kind Default
url string URL to make a request to. positional
method function Function to interpret output with. named read
format string File extension of the response. named ""

There is also #encode-url function for URL parameter encoding.

Low-level API

I will not document everything, but here is an overview:

  • #exec-command-async queries a command for execution. It does not return anything.

  • #wait-one waits for one command to finish execution. It returns a dictionary with two entries: command and result. There are no guarantees on the order of commands, so you need to check the command field to see which command finished execution.

  • #get-stdout and #get-stderr return stdout and stderr of a last executed (and waited for) command respectively.

  • #reset-and-terminate-all terminates all running commands. You should run it before exiting your program.

In theory, this API allows you to run multiple commands in parallel, but I wouldn't recommend it. It's not tested, just like everything else here, and I'm not sure if it works.

How it works

It mounds a custom userspace filesystem. The only way Typst can interact with the outer world is by reading from files, and we are using this to our advantage.

The filesystem is build in a way that allows us to build commands piece by piece and execute them. For example, you ran #exec-command("ls -la /"), Typst does the following (approximately):

Stop all running commands:
#read("<...>/reset")

Send hex-encoded command to the buffer:
#read("<...>/6c73202d6c61202f")

Request an execution of the command in the buffer:
#read("<...>/exec")

Wait for the command to finish execution:
#read("<...>/wait")

Check that command executed successfully:
#read("<...>/diagnostics")

Get the stdout of the command:
#read("<...>/stdout")

Get the stderr of the command:
#read("<...>/stderr")

Except, this won't quite work, because every function in Typst is cached, so subsequent executions may not actually read the file. To fix this, we need to add a "random" string at the start of every file path. This is what discriminator parameters are for. You should not care much about those, unless you work with low-level API.

Limitations

Linux only. Might work on other Unix-like systems or MacOS, but I haven't tested it. Windows is not supported, do not ask.

Uses fuse. Make sure you have user_allow_other option enabled in /etc/fuse.conf.

Currently, the filesystem is hardcoded to be mounted at /tmp/typst-shell-escape/shell-escape. I probably should have made it configurable, but I didn't. Change it in main.rs, and in shell-escape.typ if you need to.

If the command you are running touches /tmp/typst-shell-escape/shell-escape in any way, it will deadlock. This can be fixed, but I won't bother for now.