RSH Script Engine v2.3
Back to HomeRSH (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.
Table of Contents
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:
^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:
^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.
% 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
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.
set X 42
echo The value is $X
echo Also works: ${X} Unsetting / Default / Swap / Copy
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
vars % print all defined variables
dump VARNAME % print raw bytes of a variable as hex Special Variables Set by the Engine
ARG_COUNT— number of arguments passed to the current function1,2,3, ... — positional arguments inside a functionTICKS— set by thetickscommandINB/INW/INL— set by the respective port read commandsITER— current iteration index insiderepeatkeyandval— set automatically inside awithblock
4. Output Commands
echo
echo Hello World
echo "Value is" $X Prints all arguments separated by spaces, followed by a newline.
print Hello
print $X Prints arguments concatenated with no separator and no trailing newline.
echoln
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:
%s— string argument%d— decimal integer argument%x— hex integer argument (prints as 0xNNNN)%%— literal percent sign%n— newline%t— typewriter effect: prints one character at a time with 100ms delay
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
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
inc VARNAME % adds 1
inc VARNAME 5 % adds 5
dec VARNAME % subtracts 1
dec VARNAME 3 % subtracts 3 mod / div / pow
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
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
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
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
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
if CONDITION
... body ...
elif OTHER_CONDITION
... body ...
else
... body ...
endif Conditions support:
- Comparison:
==!=<><=>= - Logical:
&&|| - Negation:
!CONDITION - Keyword checks:
defined VARNAME,empty VARNAME,exists FILENAME,has MAPNAME KEY - Bare values:
truefalse10yesno
Inline if
if CONDITION do COMMAND
if CONDITION do COMMAND else OTHER_COMMAND
echo hello if $X == 5 break / continue / return / exit
break % exits current loop
continue % skips to next loop iteration
return % exits current function
exit N % exits script with code N assert
assert CONDITION
assert CONDITION "message if fails" 9. Loops
while
while CONDITION
... body ...
endwhile Maximum iterations: 100,000,000. Use break to exit early.
for (range)
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)
for VAR in token1 token2 token3
... body ...
endfor repeat
repeat N COMMAND
Runs COMMAND N times. ITER holds the current zero-based index.
10. Switch / Case
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
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
ont function setup
echo This only runs once ever
endfunction Recursive Functions
function countdown
echo $1
if $1 > 0 do call countdown $1 - 1
^allowedRecursive
endfunction Calling Functions
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
func.list % print all defined functions
func.drop NAME % undefine a function
pool.info % show function body pool usage once
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
const MAP mymap
key1: value1
key2: value2
endMAP
editable MAP myeditable
setting1: off
setting2: 100
endMAP Map Commands
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
set K mykey
echo $mymap[$K] % value at key mykey
echo $mymap[mykey] % literal key
echo $mymap['mykey'] % quoted literal key with Block
with MAPNAME do
echo Key is $key Value is $val
endwith 13. File / AVFS Commands
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.
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:
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.
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
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.
ticks START
% ... do work ...
elapsed START DURATION
echo Elapsed ticks: $DURATION 17. Type Check Commands
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
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 log DEBUG "Value of X is $X"
log INFO "Starting scan loop"
log ERROR "Something went wrong" 19. Engine Limits
| Limit | Value |
|---|---|
| Max variables per scope | 32 |
| Max functions | 64 |
| Max call stack depth | 16 |
| Max lines per file | 1000 |
| Max line length | 255 bytes |
| Max function body lines total (pool) | 3072 |
| Max lines per single function body | 128 |
| Max arguments per call | 16 |
| Max include depth | 8 |
| Max while/for iterations | 100,000,000 |
| Max maps | 16 |
| Max entries per map | 32 |
| Max map key length | 32 bytes |
| Max map value length | 128 bytes |
| Max file read size | 32768 bytes (32 KB) |
| Max once-tags | 32 |
| Variable name max length | 32 bytes |
| Variable value max length | 128 bytes |
| Function name max length | 64 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 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.