Current source: 1.0.1  |  Requires Ruby 4.0 or newer  |  Unix systems

SRSH 1.0

SRSH is a Unix shell written in Ruby. RSH is the language built into it.

Ordinary commands keep ordinary shell syntax. When a script needs lists, maps, functions, errors, workers, modules, or objects, it can use RSH without leaving the shell.

A small RSH program
branch := $(git branch --show-current)

files := glob("src/**/*.rb")
  |> reject(::path => contains(path, "/vendor/"))
  |> sort

? files |> len > 20 => = "#{branch}: plenty of Ruby"

Two kinds of pipe

SRSH keeps process pipelines and value pipelines separate.

FormWhat moves
command | commandBytes between Unix processes
value |> functionRSH values between functions
$(command)Command output into an RSH string
cmd("git", "status")Arguments kept as structured data

What is implemented

  • Unix pipelines, redirection, connectors, and background jobs
  • Local and environment bindings
  • Lists, maps, ranges, strings, numbers, booleans, and void
  • Functions, lambdas, closures, and value pipelines
  • Pattern matching and safe access
  • Prototypes, traits, namespaces, and modules
  • Structured errors and deferred cleanup
  • Tasks, atoms, channels, thread pools, and process workers
  • Parsed code values and reflection
  • Direct C ABI calls with bridge

Find a manual entry

RSH language manual

This manual describes the 1.0 source tree. Examples use the readable syntax unless the short form is the point.

1. Getting started

Start the interactive shell with srsh, run a script by path, or evaluate a single expression.

srsh
srsh script.rsh one two
srsh -e "[1,2,3] |> sum"
srsh --check script.rsh
srsh -c "printf 'hello\n'"
srsh --norc

At the prompt, assignments, expressions, functions, and blocks are parsed as RSH. Other input is handled as a shell command. The editor understands multiline blocks, collections, command continuations, and bracketed paste.

2. Shell commands and jobs

These forms run commands through the shell executor:

cat *.log | grep ERROR | sort -u
make -j8 && put built
generate > output.txt 2> errors.txt
long_job &
jobs
fg %1

Implemented operators are |, &&, ||, ;, and background &. Redirections include <, >, >>, 2>, and 2>>. The shell supports process groups, jobs, fg, bg, wait %N, aliases, globbing, tilde expansion, and nested command substitution.

No-match globs stay literal. SRSH is not a Bash parser, so existing Bash scripts should still be run with Bash.

3. Values and literals

42  -12  0xff  0b1010  0o755  1_000_000
3.14159  2.5e6
yes  no  void

"hello #{name}"
'no interpolation #{name}'
[[raw text #{name}]]

[1, 2, 3]
%[name: "srsh", ready: yes]
1 .. 10
0 ..< 10

Double quoted strings interpolate full RSH expressions. Single quoted and raw strings do not. Maps use %[...]. Ranges can include or exclude the upper bound.

Member access uses value.name and indexed access uses value[index]. Safe forms return void when access cannot be completed:

host := cfg?.server?.host ?? "localhost"
first := cfg?.hosts?[0] ?? host

4. Bindings and assignment

name := "Robert"
count := 4
count += 1
name ++= "!"

$EDITOR := "vim"
home := $HOME
first_arg := $1
last_status := $?

:= creates a local in the current lexical scope. Compound assignment updates the nearest existing local. A leading $ on assignment writes the process environment. Shell commands can expand locals, environment variables, positional arguments, $?, and $!.

Tasks receive isolated copies of ordinary lists and maps. Shared mutation must use a prototype object, atom, or channel.

5. Operators and value pipelines

Operator groups, from loose to tight, are approximately:

??
or  ||
and &&
== != === !== =~ !~ in
< <= > >=
|>
.. ..<
+ - ++
* / %
**

++ concatenates strings. + is numeric unless its operands support a sensible arithmetic addition. Strict equality uses ===.

The value pipeline inserts its left side as the first argument to the call on its right:

names := glob("src/**/*.rb")
  |> map(::path => basename(path))
  |> uniq
  |> sort

6. Functions, lambdas, and tasks

fn scale(x, by := 2)
  return x * by
end

fn square(x) => x * x

double := ::x => x * 2
sum_rest := ::(head, *tail) => tail |> sum

task fetch(url)
  return cmd("curl", "-fsS", url).check().out
end

Functions are first-class values and closures capture lexical values. Parameters may have defaults and one trailing rest parameter. A task has the same parameter rules, but calling it starts work immediately and returns a task handle.

Short named functions use :: name(args) ... .::. A caret is accepted as the short spelling of return.

7. Control flow

if score >= 90
  emit "great"
else
  emit "keep going"
end

each users -> user
  = user.name
end

while pending
  work()
end

Integers are iterable, so each 3 -> i visits 0, 1, and 2. Maps yield key and value pairs. Lists and pairs can be destructured.

head, second, *rest := [10, 20, 30, 40]

? ready => emit "go"
@ users -> user => = user.name
@? pending
  work()
.@

The short block markers are ? ... .? for conditions and @ ... .@ for loops. Use break or ^!, and continue or ^>.

The original times N ... end loop and fn name arg ... end function spelling remain available for older scripts.

8. Pattern matching

match status
| 200..299 => = "ok"
| [401, 403] => = "auth"
| ? it >= 500 => = "server"
| _ => = "other"
end

Patterns may be values, ranges, lists, partial maps, prototype references, or guard expressions. The matched value is available as it in a guard. The short form begins with ?? and ends with .??.

9. Collection functions

The collection toolbox works in call form, pipeline form, and usually method form.

map filter reject fold find any all count sum each sort uniq flat zip enumerate take drop chunk group tap partial compose
total := [1,2,3,4,5,6]
  .filter(::x => x % 2 == 0)
  .map(::x => x ** 2)
  .sum()

by_ext := files |> group(::path => ext(path))

File, path, string, and JSON helpers include:

readfile writefile appendfile exists file dir glob stat mkdirp rmfile cpfile mvfile basename dirname ext json json_dump lines words replace upper lower trim split join shellquote

10. Prototypes, traits, and objects

RSH uses prototypes with composable traits instead of class inheritance.

trait Printable
  fn show() => "#{self.name}=#{self.value}"
end

proto Counter(name, start := 0) with Printable
  slot name := name
  slot value := start

  fn inc(by := 1)
    self.value += by
    return self
  end
end

counter := Counter("requests", 10)
counter.inc()

Slot compound updates are synchronized. Reflection helpers include fields(), methods(), protoof(), is(), and clone().

11. Namespaces and modules

space build
  root := "out"
  fn artifact(name) => root ++ "/" ++ name
end

use "./lib/net.rsh" as net

= build.artifact("app")
= net.fetch(url)

A space may contain bindings, functions, tasks, prototypes, traits, nested spaces, bridges, and code declarations. use runs a file inside its own namespace. Relative paths are resolved against the importing script, and import cycles are rejected.

12. Errors and cleanup

try
  cfg := json(readfile("config.json"))
catch err
  = err.message
  cfg := %[]
finally
  audit("attempted")
end

defer rmfile(tmp)
result := attempt(:: => risky())

Caught errors provide at least .type and .message. Deferred calls and blocks run last-in, first-out when the current script or function scope exits, including on return and error. fail() raises an error and assert() checks a condition.

13. Concurrency

jobs := urls |> map(fetch)
pages := await_all(jobs)

job := &:: => expensive_io()
= job.await(2.0)

hits := atom(0)
hits.swap(::n => n + 1)

queue := chan(8)
queue.send("hello")
= queue.recv(1.0)

Task methods are await, done, status, and cancel. race(tasks) cancels losers. await_all(tasks) preserves order and cancels unfinished siblings when one fails.

parallel(values, fn, workers) uses Ruby threads and suits files, networks, and subprocess waits. pmap(values, fn, workers) uses Unix fork workers for CPU work and preserves input order.

14. Structured command values

job := cmd("git", "rev-parse", "--verify", ref)
result := job.check()
sha := result.out.trim()
.argv()Return a copy of the argument vector.
.run()Inherit the terminal and return status.
.result()Capture standard output, standard error, and status.
.capture()Return standard output as a string.
.check()Return the result or raise on nonzero status.
.task()Run the command in an RSH task.

Use cmd() when filenames, URLs, or other data must stay separate arguments and must not be parsed as shell text.

15. C ABI bridges

Warning: A wrong native signature can crash the SRSH process. Read the security notes before using pointers.
bridge libc from "libc.so.6"
  getpid() -> i32
  strlen(cstr) -> usize
  gethostname(ptr, usize) -> i32
end

buf := cbuf(256)
libc.gethostname(buf, buf.size())
= buf.string()

@self loads symbols from the current process. Supported ABI names are:

void bool i8 u8 i16 u16 i32 u32 i64 u64 isize usize f32 f64 cstr ptr

Buffer methods include size, address, ptr, read, write, string, and clear.

16. Code values and reflection

code cleanup
  rm build/tmp/cache
end

= sourceof(cleanup)
run(cleanup)

formula := "x * 3"
= eval(formula)

code ... end parses source without running it. Dynamic helpers are eval, code, run, valid, and sourceof. Introspection functions include locals, fns, protos, and traits.

17. Built-in function reference

Conversionint float str bool type
Numbers and choiceround floor ceil sqrt clamp abs min max rand pick
Stringslen empty contains starts ends starts_with ends_with split join upper lower trim replace lines words
Mapskeys values fields
Environmentenv cwd clock status cpu_count
Processescapture sh cmd shellquote
Concurrencyspawn await await_all race parallel pmap chan atom sleep
Errorsattempt fail assert
Objectsclone fields methods protoof is
Filesreadfile writefile appendfile exists file dir glob stat mkdirp rmfile cpfile mvfile basename dirname ext
Datajson json_dump
Codeeval code run sourceof valid locals fns protos traits
Nativecbuf

18. Shell configuration

Shell strictness is controlled with regular shell commands:

option pipefail yes
option nounset yes
option noclobber yes
option strict yes

strict enables pipefail and nounset. It does not copy Bash set -e.

~/.srshrcStartup commands
~/.srsh_historyInteractive history
~/.srsh/themesPrivate .theme and .json theme files
~/.srsh/pluginsPrivate .rsh and trusted .rb plugins

Use scheme --list, scheme NAME, plugins, and reload to manage the interactive environment. Ruby plugins are trusted code. Plugin and theme files must pass owner and mode checks before automatic loading.

Core shell commands are:

cd pwd put echo printf ls alias unalias set export unset read true false sleep source . exit quit help hist clearhist scheme theme themes plugins reload jobs wait fg bg exec systemfetch which type dirs pushd popd umask kill option

A simple theme file uses ANSI SGR values:

# ~/.srsh/themes/amber.theme
border=1;33
title=1;37
key=33
value=0;37
ok=32
warn=33
error=31
dim=90
path=33
host=36
mark=35

Ruby plugins receive the SRSH API and may register builtins, hooks, aliases, and themes. RSH plugin files run as ordinary RSH scripts.

Back to the top of the manual

Download SRSH

Release artifacts are produced from a matching version tag after the test suite and release checks pass.

Latest release: 1.0.1

The site will read the latest published release when GitHub is available.

Download gem Download source SHA256SUMS

Open the latest release on GitHub

Install the gem file

Ruby 4.0 or newer is required. RubyGems installs Fiddle 1.1.8 or newer for native bridges. Building the optional native extension also needs a C compiler and Ruby development headers.

gem install ./srsh.gem
srsh --version
srsh

Run from the source archive

The shell can run as Ruby-only code. The native helper is optional when running from a checkout or source archive.

tar -xzf srsh-source.tar.gz
cd srsh-1.0.1
make test
./bin/srsh

Install from a checkout

git clone https://github.com/RobertFlexx/RSH.git
cd RSH
make test
make PREFIX="$HOME/.local" install

The direct branch snapshot is also available as a zip file.

Verify a download

sha256sum -c SHA256SUMS

The checksum file covers the stable and versioned artifacts attached to the release.

RSH examples

These examples correspond to programs in the repository and use features present in the 1.0 tree.

Filter files with a value pipeline

root := $1
? root == "" => root := "."

ruby := glob(root ++ "/**/*.rb")
  |> reject(::path => contains(path, "/vendor/"))
  |> map(::path => %[path: path, bytes: len(readfile(path))])
  |> sort(::item => 0 - item.bytes)

@ ruby -> item => = "#{item.bytes}  #{item.path}"

View examples/hot.rsh

Use a module

use "./modules/text.rsh" as text

rows := [" hello ", "", "world"] |> text.clean
@ rows -> row => = text.tag(row)

View examples/modules.rsh

Run tasks together

space sys
  task kernel() =>
    cmd("uname", "-srmo").check().out.trim()

  task uptime() =>
    cmd("uptime", "-p").check().out.trim()
end

jobs := [sys.kernel(), sys.uptime()]
= await_all(jobs)

View examples/paste.rsh

Call the C ABI

bridge c from "@self"
  strlen(cstr) -> usize
end

= c.strlen("simple ruby shell")

View examples/bridge.rsh

More programs

Project information

SRSH is developed in the open and distributed under the MIT License.

Source and issues

Repository layout

bin/srsh                 command entry point
lib/srsh/                shell and language implementation
lib/srsh/language/       RSH lexer, parser, values, evaluator
lib/srsh/shell/          command lexer, executor, jobs, terminal
ext/srsh_native/         optional C helper
examples/                sample RSH programs
language-docs/           long-form language and security notes
docs/                    this static site
test/                    language and shell tests

Release policy

A tag such as v1.0.1 must match Srsh::VERSION. The release workflow runs syntax checks, the test suite, example validation, smoke tests, and the native build. It then creates gem and source artifacts, writes SHA-256 checksums, and creates the GitHub release at that tag.

Scope

SRSH is a shell and a language implementation under active development. It is not a secure sandbox and it is not a drop-in Bash interpreter. Ruby plugins and C bridges are trusted, process-level extension points.