RSH Script Engine v2.3

Back to Home

RSH (RadiumOS Shell Script) is a lightweight cooperative scripting language that runs directly on the bare-metal RadiumOS kernel. It has no heap allocation, no dynamic memory, and uses fixed-size static buffers throughout. Scripts are loaded from AVFS, parsed into a flat line array, and executed cooperatively by the kernel.


1. Basics and File Structure

RSH scripts are plain text files stored in AVFS. They are executed by calling script_execute_file(path) from C, or via the RSH shell command.

A script is read top-to-bottom. Blank lines and comment lines are skipped. Every non-blank, non-comment line is a command. Lines are trimmed of leading and trailing whitespace before execution.

The maximum file size is 32768 bytes (32 KB). The maximum number of lines per file is 1000. Each line may be at most 255 characters.

The file extension .rsh is automatically tried if the given path does not exist verbatim. So script_execute_file("myscript") will also try myscript.rsh.

Entrypoint Declaration

If your script defines functions and wants execution to start from a main function rather than top-to-bottom, put this at the top of the file:

RSH
^entrypoint

This causes the engine to collect all function definitions first, then call main automatically. Without this directive, all non-function lines execute immediately top to bottom.

Include

You can include another RSH file with:

RSH
^include filename
    ^include "other_script.rsh"

Includes may be nested up to 8 levels deep. The included file runs in the same script context (shares variables, maps, and functions).


2. Comments

Any line beginning with % or # is a comment and is completely ignored.

RSH
% This is a comment
    # This is also a comment
    echo Hello   % inline comments are NOT supported - only full-line comments work

The special token __~~%~~__ at the start of a line is also treated as a comment (used internally).


3. Variables

Variables are untyped. They hold byte strings. Numbers are stored as decimal strings and parsed on demand. There is no type system.

Variable names may contain letters, digits, and underscores. They are case-sensitive. There are at most 32 variables active at once per scope.

Setting Variables

RSH
set NAME value
    set NAME "value with spaces"
    let NAME value
    export NAME value

set, let, and export are identical. Calling set NAME with no value sets it to an empty string.

Variable Expansion

Use $NAME or ${NAME} anywhere in a line to substitute its value. Expansion happens before the command is parsed.

RSH
set X 42
    echo The value is $X
    echo Also works: ${X}

Unsetting / Default / Swap / Copy

RSH
unset NAME
    default NAME fallback_value
    swap A B      % exchanges values of A and B
    copy SRC DST  % copies value of SRC into DST

Listing / Debugging

RSH
vars           % print all defined variables
    dump VARNAME   % print raw bytes of a variable as hex

Special Variables Set by the Engine


4. Output Commands

echo

RSH
echo Hello World
    echo "Value is" $X

Prints all arguments separated by spaces, followed by a newline.

print

RSH
print Hello
    print $X

Prints arguments concatenated with no separator and no trailing newline.

echoln

RSH
echoln VARNAME

Prints the value of the named variable followed by a newline. Takes a variable name, not a literal value.

printf

Formatted output. Supports format specifiers:

RSH
set NAME scp_2801
    printf "Hello %s you have %d items%n" $NAME 5
    printf "Port value: %x%n" $PORTVAL

5. Math and Arithmetic

All math operates on 64-bit signed integers. Values are stored and read as decimal strings.

math

RSH
math RESULT_VAR expression
    math X $A + $B
    math Y $X * 3

Supported operators: + - * /. Evaluates left to right. For complex expressions use multiple math calls.

inc / dec

RSH
inc VARNAME         % adds 1
    inc VARNAME 5       % adds 5
    dec VARNAME         % subtracts 1
    dec VARNAME 3       % subtracts 3

mod / div / pow

RSH
mod A B RESULT      % RESULT = A mod B
    div A B RESULT      % RESULT = A / B (integer division)
    pow BASE EXP RESULT % RESULT = BASE ^ EXP

sqrt / abs / min / max / clamp / sign

RSH
sqrt N RESULT           % integer square root
    abs N RESULT            % absolute value
    min A B RESULT          % smaller of A and B
    max A B RESULT          % larger of A and B
    clamp N LO HI RESULT    % clamps N to [LO, HI]
    sign N RESULT           % -1, 0, or 1

rand / hex / dec2

RSH
rand MIN MAX RESULT     % random integer in [MIN, MAX]
    hex N RESULT            % converts decimal N to hex string (e.g. 0x1F)
    dec2 N RESULT           % parses N (may be hex) and stores decimal value

6. Bitwise Operations

RSH
band A B RESULT     % bitwise AND
    bor  A B RESULT     % bitwise OR
    bxor A B RESULT     % bitwise XOR
    bnot A RESULT       % bitwise NOT (32-bit)
    shl  A N RESULT     % shift A left by N bits
    shr  A N RESULT     % shift A right by N bits
    bit.set  A N RESULT % set bit N in A
    bit.clr  A N RESULT % clear bit N in A
    bit.test A N RESULT % result is 1 if bit N of A is set, else 0

7. String Operations

RSH
strlen   VARNAME RESULT                    % length of value
    substr   VARNAME START LEN RESULT           % extract substring
    strcat   DEST SRC1 SRC2 ...                 % append SRC values onto DEST
    strrep   VARNAME OLD NEW RESULT             % replace first occurrence
    strrep.all VARNAME OLD NEW RESULT           % replace all occurrences
    strupper VARNAME RESULT                     % uppercase copy
    strlower VARNAME RESULT                     % lowercase copy
    strfind  VARNAME NEEDLE RESULT              % index of first match, or -1
    strsplit VARNAME SEPARATOR INDEX RESULT     % token at INDEX (zero-based)
    strcount VARNAME NEEDLE RESULT              % count occurrences of NEEDLE
    strtrim  VARNAME RESULT                     % whitespace-trimmed copy
    strpad   VARNAME WIDTH PADCHAR RESULT       % left-pad to WIDTH
    strpad.r VARNAME WIDTH PADCHAR RESULT       % right-pad to WIDTH
    strhex   VARNAME RESULT                     % encode bytes as hex string
    strrev   VARNAME RESULT                     % reverse the string
    strjoin  SEPARATOR ARG1 ARG2 ... RESULT     % join args with SEPARATOR
    strstarts VARNAME PREFIX RESULT             % 1 if starts with PREFIX
    strends   VARNAME SUFFIX RESULT             % 1 if ends with SUFFIX
    strsub.r  VARNAME N RESULT                  % last N characters

8. Control Flow

if / elif / else / endif

RSH
if CONDITION
    ... body ...
    elif OTHER_CONDITION
    ... body ...
    else
    ... body ...
    endif

Conditions support:

Inline if

RSH
if CONDITION do COMMAND
    if CONDITION do COMMAND else OTHER_COMMAND
    echo hello if $X == 5

break / continue / return / exit

RSH
break       % exits current loop
    continue    % skips to next loop iteration
    return      % exits current function
    exit N      % exits script with code N

assert

RSH
assert CONDITION
    assert CONDITION "message if fails"

9. Loops

while

RSH
while CONDITION
    ... body ...
    endwhile

Maximum iterations: 100,000,000. Use break to exit early.

for (range)

RSH
for VAR in START..END
    ... body ...
    endfor

    for VAR in START..END..STEP
    ... body ...
    endfor

STEP may be negative for counting down.

for (token list)

RSH
for VAR in token1 token2 token3
    ... body ...
    endfor

repeat

RSH
repeat N COMMAND

Runs COMMAND N times. ITER holds the current zero-based index.


10. Switch / Case

RSH
switch $VARIABLE
    case value1
    ... body ...
    endcase
    case value2
    ... body ...
    endcase
    default
    ... body ...
    endcase
    endswitch

Only the first matching case runs. The default block runs if no case matched.


11. Functions

Maximum 64 functions simultaneously. Each body may have at most 128 lines. Call stack is at most 16 frames deep.

Defining a Function

RSH
function myfunction
    echo Running myfunction
    echo First arg is $1
    echo Second arg is $2
    echo Arg count is $ARG_COUNT
    endfunction

def is an alias for function. Use enddef or endfunction to close.

One-time Functions

RSH
ont function setup
    echo This only runs once ever
    endfunction

Recursive Functions

RSH
function countdown
    echo $1
    if $1 > 0 do call countdown $1 - 1
    ^allowedRecursive
    endfunction

Calling Functions

RSH
call functionname arg1 arg2 arg3
    functionname arg1 arg2 arg3

Arguments are accessible inside the function as $1, $2, etc. Functions are fully scoped — they cannot modify caller variables.

Function Management

RSH
func.list       % print all defined functions
    func.drop NAME  % undefine a function
    pool.info       % show function body pool usage

once

RSH
once TAG COMMAND

Runs COMMAND the first time this tag is encountered, then never again.


12. Maps

Up to 16 maps at once, each holding up to 32 entries. Keys up to 32 bytes, values up to 128 bytes.

Declaring a Map

RSH
const MAP mymap
    key1: value1
    key2: value2
    endMAP

    editable MAP myeditable
    setting1: off
    setting2: 100
    endMAP

Map Commands

RSH
map.new NAME [editable]
    map.set NAME KEY VALUE
    map.get NAME KEY RESULT
    map.del NAME KEY
    map.has NAME KEY RESULT
    map.clear NAME
    map.count NAME RESULT
    map.key NAME INDEX RESULT
    map.val NAME INDEX RESULT
    map.keys NAME RESULT
    map.vals NAME RESULT
    map.merge SRC DST
    map.copy SRC DST
    map.drop NAME
    map.dump NAME
    map.sum NAME RESULT
    map.max NAME RESULT [KEYRESULT]
    map.min NAME RESULT [KEYRESULT]
    map.invert SRC DST
    map.list

Map Lookup via Variable Expansion

RSH
set K mykey
    echo $mymap[$K]       % value at key mykey
    echo $mymap[mykey]    % literal key
    echo $mymap['mykey']  % quoted literal key

with Block

RSH
with MAPNAME do
    echo Key is $key Value is $val
    endwith

13. File / AVFS Commands

RSH
fexists    FILENAME RESULT    % 1 if file exists
    fsize      FILENAME RESULT    % file size in bytes, or -1
    fread      FILENAME RESULT    % read content into RESULT (up to 4096 bytes)
    fwrite     FILENAME VARNAME   % write VARNAME's value to file (overwrites)
    fappend    FILENAME VARNAME   % append VARNAME's value to file
    fdelete    FILENAME           % remove file from AVFS
    fline      FILENAME N RESULT  % read line N (zero-based)
    flinecount FILENAME RESULT    % count newlines in the file
    fgrep      FILENAME NEEDLE RESULT  % first line containing NEEDLE

The file read scratch buffer is 4096 bytes. Content longer than that is silently truncated.


14. Port I/O Commands

Direct hardware port access. Calls outb/inb directly in the kernel.

RSH
outb PORT VALUE          % write 8-bit VALUE to I/O port PORT
    inb  PORT [RESULT]       % read 8-bit value, store in RESULT (default INB)
    outw PORT VALUE          % write 16-bit value
    inw  PORT [RESULT]       % read 16-bit value (default INW)
    outl PORT VALUE          % write 32-bit value
    inl  PORT [RESULT]       % read 32-bit value (default INL)

    io.wait PORT MASK EXPECTED_VAL [TIMEOUT_MS [OUT_VAR]]
    % spins reading PORT until (value AND MASK) == EXPECTED_VAL
    % OUT_VAR (default IO_WAIT_OK) is 1 on success, 0 on timeout

You can also use inline function-call syntax:

RSH
outb(0x60, 0xFF)
    set VAL inb(0x64)

15. VGA Text Commands

Write directly to the VGA text-mode framebuffer at 0xB8000. Screen is 80 columns × 25 rows.

RSH
cls                              % clear the terminal
    color FG [BG]                    % set terminal color (0-15)
    vgaput ROW COL CHAR ATTR         % write single character at position
    vgastr ROW COL STRING ATTR       % write string starting at position
    vgabar CURRENT MAX WIDTH         % draw a text progress bar [###...]
    vgaclear ROW ATTR                % fill entire row with spaces
    vgafill ROW COL LEN CHAR ATTR    % fill LEN cells with CHAR

Color values (0–15): 0=Black, 1=Blue, 2=Green, 3=Cyan, 4=Red, 5=Magenta, 6=Brown, 7=Light Grey, 8=Dark Grey, 9=Light Blue, 10=Light Green, 11=Light Cyan, 12=Light Red, 13=Light Magenta, 14=Yellow, 15=White.


16. Timing Commands

RSH
pause MS                     % sleep for MS milliseconds
    ticks [RESULT]               % store PIT tick count in RESULT (default TICKS)
    elapsed START_VAR RESULT     % RESULT = current_ticks - START_VAR

At the default RadiumOS PIT frequency of 100 Hz, one tick equals 10ms.

RSH
ticks START
    % ... do work ...
    elapsed START DURATION
    echo Elapsed ticks: $DURATION

17. Type Check Commands

RSH
isnumber  VARNAME RESULT   % 1 if valid decimal integer
    isempty   VARNAME RESULT   % 1 if empty or undefined
    isdefined VARNAME RESULT   % 1 if variable is defined
    tobool    VARNAME RESULT   % 1 if non-zero, else 0
    bool.and  A B RESULT       % 1 if both non-zero
    bool.or   A B RESULT       % 1 if either non-zero
    bool.not  A RESULT         % 1 if A is zero
    bool.xor  A B RESULT       % 1 if exactly one is non-zero

    bool set VARNAME TRUE      % set to 1
    bool set VARNAME FALSE     % set to 0
    bool toggle VARNAME        % flip 0 to 1 or 1 to 0
    bool is VARNAME            % sets ? to 1 or 0

18. Misc / Utility

RSH
nop                  % no operation
    log LEVEL MESSAGE    % print [RSH][LEVEL] MESSAGE to terminal
    error MESSAGE        % print red error message and return -1
    warn  MESSAGE        % print yellow warning message
    info  MESSAGE        % print blue info message
RSH
log DEBUG "Value of X is $X"
    log INFO "Starting scan loop"
    log ERROR "Something went wrong"

19. Engine Limits

LimitValue
Max variables per scope32
Max functions64
Max call stack depth16
Max lines per file1000
Max line length255 bytes
Max function body lines total (pool)3072
Max lines per single function body128
Max arguments per call16
Max include depth8
Max while/for iterations100,000,000
Max maps16
Max entries per map32
Max map key length32 bytes
Max map value length128 bytes
Max file read size32768 bytes (32 KB)
Max once-tags32
Variable name max length32 bytes
Variable value max length128 bytes
Function name max length64 bytes

20. Example: Keyboard Poller

Demonstrates keyboard polling using PS/2 port I/O, state variables, conditional logic, maps, and a clean exit condition. Runs top to bottom — no main function.

Port 0x64 is the PS/2 status register. Bit 0 indicates whether a scancode is ready. Port 0x60 is the data port. Scancodes below 0x80 are key-press events; at or above 0x80 are key-release events. Scancode 0x01 is Escape.

RSH
% =============================================================
    % RSH Keyboard Poller Example - RadiumOS / scp_2801
    % Polls PS/2 port 0x60 and prints key names to the terminal.
    % Press Escape to exit.
    % =============================================================

    set PS2_STATUS 0x64
    set PS2_DATA   0x60
    set SC_ESCAPE  0x01

    cls
    color 11
    echo ==========================================
    echo  RadiumOS RSH Keyboard Poller
    echo  Press any keys. ESC to quit.
    echo ==========================================
    color 7
    echo

    set SHIFT_HELD 0
    set RUNNING 1
    set LAST_SC 0

    while $RUNNING == 1

    inb $PS2_STATUS STATUS_BYTE
    band $STATUS_BYTE 1 DATA_READY

    if $DATA_READY == 1

        inb $PS2_DATA SCANCODE

        if $SCANCODE == $LAST_SC
        pause 1
        continue
        endif
        set LAST_SC $SCANCODE

        if $SCANCODE >= 128
        math MAKE_CODE $SCANCODE - 128
        if $MAKE_CODE == 42
            set SHIFT_HELD 0
        endif
        if $MAKE_CODE == 54
            set SHIFT_HELD 0
        endif
        continue
        endif

        if $SCANCODE == $SC_ESCAPE
        color 12
        echo ESC pressed - exiting keyboard poller.
        color 7
        set RUNNING 0
        continue
        endif

        if $SCANCODE == 42
        set SHIFT_HELD 1
        echo [SHIFT LEFT held]
        continue
        endif
        if $SCANCODE == 54
        set SHIFT_HELD 1
        echo [SHIFT RIGHT held]
        continue
        endif

        if $SCANCODE == 58
        echo [CAPS LOCK toggled]
        continue
        endif

        map.new KEYMAP
        map.set KEYMAP 2  1
        map.set KEYMAP 3  2
        map.set KEYMAP 4  3
        map.set KEYMAP 5  4
        map.set KEYMAP 6  5
        map.set KEYMAP 7  6
        map.set KEYMAP 8  7
        map.set KEYMAP 9  8
        map.set KEYMAP 10 9
        map.set KEYMAP 11 0
        map.set KEYMAP 16 q
        map.set KEYMAP 17 w
        map.set KEYMAP 18 e
        map.set KEYMAP 19 r
        map.set KEYMAP 20 t
        map.set KEYMAP 21 y
        map.set KEYMAP 22 u
        map.set KEYMAP 23 i
        map.set KEYMAP 24 o
        map.set KEYMAP 25 p
        map.set KEYMAP 30 a
        map.set KEYMAP 31 s
        map.set KEYMAP 32 d
        map.set KEYMAP 33 f
        map.set KEYMAP 34 g
        map.set KEYMAP 35 h
        map.set KEYMAP 36 j
        map.set KEYMAP 37 k
        map.set KEYMAP 38 l
        map.set KEYMAP 44 z
        map.set KEYMAP 45 x
        map.set KEYMAP 46 c
        map.set KEYMAP 47 v
        map.set KEYMAP 48 b
        map.set KEYMAP 49 n
        map.set KEYMAP 50 m
        map.set KEYMAP 28 ENTER
        map.set KEYMAP 14 BACKSPACE
        map.set KEYMAP 57 SPACE
        map.set KEYMAP 15 TAB
        map.set KEYMAP 72 UP
        map.set KEYMAP 80 DOWN
        map.set KEYMAP 75 LEFT
        map.set KEYMAP 77 RIGHT
        map.set KEYMAP 59 F1
        map.set KEYMAP 60 F2
        map.set KEYMAP 61 F3
        map.set KEYMAP 62 F4
        map.set KEYMAP 63 F5
        map.set KEYMAP 64 F6

        map.has KEYMAP $SCANCODE KEY_KNOWN
        if $KEY_KNOWN == 1
        map.get KEYMAP $SCANCODE KEY_NAME
        if $SHIFT_HELD == 1
            strupper KEY_NAME KEY_NAME
        endif
        color 10
        printf "Key pressed: %s (scancode 0x%x)%n" $KEY_NAME $SCANCODE
        color 7
        else
        hex $SCANCODE SC_HEX
        color 14
        printf "Unknown scancode: %s%n" $SC_HEX
        color 7
        endif

        map.drop KEYMAP

    endif

    pause 5

    endwhile

    color 11
    echo Keyboard poller stopped.
    color 7

How It Works

Port polling: Every iteration reads port 0x64. band isolates bit 0. If set, a scancode is ready at port 0x60.

Key release filtering: PS/2 break codes are make code OR 0x80 (≥128 decimal). The script subtracts 128 to recover the make code, checks for shift release, then skips printing with continue.

Shift tracking: SHIFT_HELD is set to 1 on shift make, back to 0 on shift break. Used to call strupper on letter keys.

Map-based lookup: Scancode-to-name mappings are loaded into a map each iteration and looked up with map.has / map.get. The map is dropped and recreated each iteration due to the 32-entry limit.

Hex fallback: Unrecognized scancodes are converted with hex and printed in 0xNN format.

Exit condition: Scancode 0x01 (Escape) sets RUNNING to 0, which terminates the while loop on the next condition check.


RSH Script Engine v2.3 — RadiumOS — scp_2801

All content related to RadiumOS or TomatoOS is open source and freely distributed. Maintained in partnership with AT Products LLC. Site inspired by TempleOS, may Terry A. Davis's soul rest in peace.

2026 © AT Products LLC.