shell scripting.md


title: "Shell Scripting" canonical_url: https://www.superlogical.com/rex/docs/automate/shell-scripting


Shell Scripting

You can also use the rex CLI with typical shell scripts to automate everything in Rex:

#!/bin/sh
rex new work
rex split -s work -- htop
rex run -s work --label logs -- tail -f app.log

For anything beyond a few commands in a row, consider a Lua script instead. Lua can do everything on this page with one connection to the server and without parsing JSON.

Keeping Track of IDs

Commands that create something print its ID with --json. Save the ID and pass it to later commands, so the script keeps working when a label changes or focus moves.

$ rex split --json -- htop
{
  "anchor_block_id": "block:01a10e00-7268-78c6-8b85-86c56d6a53a9",
  "block_ids": [
    "block:01a10e00-729d-7c38-90f7-d6568611b1c4"
  ],
  "split_ids": [
    "split:01a10e00-72ab-7157-b9e0-0d4c2a8a1a68"
  ],
  "revision": 2
}

jq is the easiest way to pull the ID out:

block=$(rex split --json -- htop | jq -r '.block_ids[0]')
rex zoom -b "$block"
Command Creates ID is at
rex new --json A session with one window and one terminal .session_id
rex run --json A terminal in a new window .block_ids[0], .window_id
rex split --json A terminal beside an existing one .block_ids[0]

A label is a fine alternative when the script sets it. Give the block a label with --label and use that as the target:

rex split --label logs -- tail -f app.log
rex capture -b logs

Building a Layout

rex new creates a session, rex run adds a window to it, and rex split splits an existing block.

#!/bin/sh
# Open a project: an editor with a shell below it, and a second
# window that follows the logs.
rex new api --cwd ~/src/api -- nvim
rex split -s api --split=below --ratio 30 --cwd ~/src/api
rex run -s api --label logs --focus=false -- tail -f ~/src/api/app.log

These options are shared by rex run and rex split:

Option Does
-- COMMAND Runs a command instead of an interactive shell.
--cwd DIR Sets the working directory.
--label TEXT Labels the new block.
--split DIRECTION Places the block right, left, above, or below its anchor.
--ratio PCT Gives the new block that percentage of the split.
--focus=false Leaves focus where it was.
--keep-open Keeps the terminal open after its command exits.

A command runs with the environment of your login shell, so your PATH applies but aliases don't. It isn't parsed by a shell. To use pipes or &&, run a shell explicitly:

rex run -- sh -c 'make build && make test'

Typing Into a Terminal

rex send types text into a terminal, and rex send-key presses keys:

rex send -b logs 'make test'
rex send-key -b logs enter

rex send doesn't press Enter for you. Follow it with rex send-key enter, or include the newline in the text. It also reads from stdin:

printf 'make test\n' | rex send -b logs

rex send-key uses the same key names as keyboard shortcuts, so rex send-key ctrl+c interrupts whatever is running.

Reading a Terminal

rex capture prints a terminal's screen and its scrollback:

$ rex capture -b logs --trim
hello from logs
Option Does
--trim Removes trailing blank lines.
--unwrap Joins lines the terminal wrapped.
--format html Keeps colors and styling, as HTML.
--format vt Keeps the terminal's control sequences.

A terminal keeps about 64 MiB of scrollback. That is roughly 100,000 lines of plain text at 80 columns, and fewer in a wider terminal or with styled output. Once the limit is reached, the oldest lines are dropped. To keep everything a long command prints, redirect its output to a file.

Full-screen programs, such as editors, pagers, and many TUI apps, draw on a separate screen that has no scrollback. While one is running, rex capture returns only what that program shows right now. Any history it has, like an editor's file or a chat app's earlier messages, stays inside the program.

Running a Command and Waiting

Add --wait to rex run or rex split to wait for the command to finish. The rex command then exits with the command's own status:

if rex run --wait -- make test; then
  echo "tests passed"
fi

rex wait does the same for a terminal that is already running, given its ID or label:

block=$(rex split --json -- make test | jq -r '.block_ids[0]')
# ... do other things ...
rex wait "$block"
echo "make exited with $?"

rex wait also accepts a window or a session, and returns when it closes.

Use --for to give up after a while. A wait that times out exits with status 1:

rex wait --for 10m "$block" || echo "failed or timed out"

Performing Actions

rex do performs an action by name, with arguments as key=value pairs. It runs both the app's actions and the custom actions in your configuration file:

rex do dev_layout file=app.log
rex do pane.split direction=right

Actions such as pane.split are performed by the app, so the app must be running, with Remote Control turned on in its settings.

Watching Events

rex events prints events as they happen and runs until interrupted. With -s it follows one session:

$ rex events -s work
14:38:07  block_created         …65951c  session …8f9d5d
14:38:07  session_view_changed  …8f9d5d  revision 11
14:38:07  block_event           …65951c  com.superlogical.terminal.bell {}

With --json it prints one JSON object per line, ready for jq. This prints the ID of each terminal that rings its bell:

rex events -s work --json |
  jq -r 'select(.payload.event.payload.name == "bell")
         | .payload.event.payload.block_id'

The events reference describes the JSON and lists every event. To act on events rather than print them, Lua event handlers are much less work than a pipeline.

Example: Run Tests in a Split

This script runs a command in a split beside the current terminal. If the command fails, it zooms the split and exits with a failure.

#!/bin/sh
# tests.sh
cmd="${1:-make test}"

block=$(rex split --keep-open --focus=false --json -- sh -c "$cmd" |
  jq -r '.block_ids[0]')

if ! rex wait --for 10m "$block"; then
  rex zoom -b "$block" --on
  exit 1
fi
./tests.sh 'go test ./...'

The same script in Lua is a useful comparison.