Expand description
The shell integration: how the game runs inside a wrapped bash or fish and changes it.
The game is a short-lived process, but the player lives in a long-running shell. A child
process cannot change the directory or the environment of its parent, so the game starts a
game shell of its own, and a small script sourced into that shell (bash.sh, fish.fish)
connects the two. This module owns both halves of the connection: the shell scripts and the
Rust code that starts the shell and speaks the protocol.
| module | responsibility |
|---|---|
detect | which shell to start (ShellKind, --shell, parent processes, $SHELL) |
render | the integration script text (render_bash, render_fish) |
launch | starting the shell with the right environment (launch()) |
protocol | the directives the binary writes for the shell (Directives) |
§Contract between the binary and the shell scripts
§Environment
The launcher sets these with Command::env. They are never pasted into script text, so no
path is ever quoted.
| variable | value |
|---|---|
SHELLGAME_WRAPPER | 1: the binary runs inside the game shell |
SHELLGAME_BIN | absolute path of the binary |
SHELLGAME_INIT | path of the rendered integration script |
SHELLGAME_FD_HOOK | path of the fd-hook launcher |
BASH_SILENCE_DEPRECATION_WARNING | 1: no zsh banner from the macOS system bash 3.2 |
An inherited SHELLGAME_AUTO_STARTED is removed, so the new shell autostarts. The scripts
export SHELLGAME_AUTO_STARTED=1 when they autostart. The binary itself exports
SHELLGAME_WORKSPACE (where evidence markers live; used by pwd and the deleted-cwd
relocation) and SHELLGAME_LEVEL through the protocol.
§The shellgame function
shellgame ARGS... runs "$SHELLGAME_BIN" ARGS... with stdin and stdout on the terminal and
stderr captured in a temporary file, then replays that file line by line. A line that starts
with __SHELLGAME_EXEC__ is a directive and is executed, in order; every other
line is written to stderr unchanged; a marker anywhere else in a line is plain text. The
function returns the binary’s exit status. If the shell’s current directory no longer exists,
the function first moves to $SHELLGAME_WORKSPACE (or /tmp).
There is no special casing of command names. In particular shellgame remove closes the shell
because the binary emits the exit directive (Directives::exit).
A directive cd moves with the shell’s own cd implementation and never runs the cd hook
below, so teleports are not subject to level rules. In bash that is builtin cd --, in fish
the stock cd that the integration copies to __shellgame_cd before it defines its hook, so
cd -, prevd and nextd keep working after a teleport.
§The cd hook
Both scripts define one static cd function; there is no per-level list. Before moving it
runs, directly and not through the shellgame function (so stderr is live and nothing is
relocated):
"$SHELLGAME_BIN" cd-hook pre <pwd> <prev> -- <cd args...><pwd>is$PWDbefore the move.<prev>is the directorycd -would move to:$OLDPWDin bash, and in fish (which never setsOLDPWD) the last entry of$dirprev, or of$dirnextright after acd -. It is an empty argument when there is none.<cd args...>are all arguments of thecdcall, verbatim: possibly none,-,-P,--, an empty string, or a directory that is literally namedpre. The binary must parse this command line by hand, before any option parser.
Exit status 3 means blocked: the function returns 1 without moving. The binary prints
the explanation itself, on stderr. Any other status (0, a crash, a missing binary) allows the
move, so a broken hook can never lock the player in. Then the function performs the real move
(bash: builtin cd "$@", fish: the stock cd copy) and, only if that succeeded, runs
"$SHELLGAME_BIN" cd-hook post <pwd before the move> <new pwd> -- <cd args...> >/dev/null 2>&1where <new pwd> is $PWD after the move (never re-resolved). Its output and status are
ignored, and a failed move is never reported. The function returns the status of the move.
The bash functions are written to survive set -e in the player’s shell: a failing hook or a
failing binary must not end the shell before the move, the replay or the cleanup.
§The fd hook
Level scripts execute "$SHELLGAME_FD_HOOK" ARGS.... The launcher file is written by
write_fd_launcher into the state directory (mode 0700) and consists of
#!/bin/sh
exec "$SHELLGAME_BIN" fd-hook "$@"exec keeps the descriptors the script was started with, so fd-hook sees whether stdout and
stderr were redirected to /dev/null.
§pwd
Both scripts define pwd as builtin pwd "$@" (the logical path, in both shells) followed
by touch "$SHELLGAME_WORKSPACE/.pwd_used", only when pwd succeeded and that variable is set
and names an existing directory. The workspace is never guessed.
§Dependencies and supported shells
The scripts need mktemp without arguments, base64 --decode (GNU and macOS), touch and
rm. bash.sh runs on bash 3.2, the system bash of macOS, and exports shellgame with its
two helper functions so that child bash processes can call it too. fish.fish runs on fish
3.3 and later and relies on nothing from fish 3.4 or 4 (for example $(...)). tests/shell.rs
runs the scripts in real shells; it was checked with bash 3.2.57 and 5.3, fish 3.3.1, 3.7.0 and
4.8.1, and shellcheck 0.9.0 and 0.11. To try another version, put it first in PATH.
Re-exports§
pub use detect::ShellKind;pub use detect::detect;pub use launch::LaunchOptions;pub use launch::launch;pub use launch::launch_with;pub use launch::write_fd_launcher;pub use protocol::Directives;pub use protocol::InvalidName;pub use render::ScriptOptions;pub use render::render_bash;pub use render::render_fish;
Modules§
- detect
- Choosing which shell the game runs in.
- launch
- Starting the wrapped game shell.
- protocol
- The directive protocol: how the binary changes the state of the wrapped shell.
- render
- Rendering of the shell integration scripts.
Constants§
- ENV_
AUTO_ STARTED - Set by the scripts once the game autostarted; removed by the launcher.
- ENV_BIN
- Absolute path of the binary, set by the launcher.
- ENV_
FD_ HOOK - Path of the fd-hook launcher, set by the launcher.
- ENV_
INIT - Path of the rendered integration script, set by the launcher.
- ENV_
LEVEL - The id of the current level, exported by the binary through the protocol.
- ENV_
WORKSPACE - The workspace directory, exported by the binary through the protocol.
- ENV_
WRAPPER 1inside the game shell; the binary only runs the game (and emits directives) when it is set.