| name | rugo-quickstart |
| description | Rugo language quickstart guide. Load when writing .rugo scripts, learning Rugo syntax, or helping users with Rugo language features. |
Rugo Quickstart
Get up and running with Rugo in minutes.
Install
go install github.com/rubiojr/rugo@latest
Run your first script
rugo run script.rugo
rugo build script.rugo
rugo emit script.rugo
rugo doc http
Hello World
Create hello.rugo:
puts "Hello, World!"
Run it:
rugo run hello.rugo
Or compile to a native binary:
rugo build hello.rugo
./hello
puts prints a line. print does the same without a newline.
print "Hello, "
puts "World!"
Comments start with #:
puts "not a comment"
Variables
Variables are dynamically typed. No declarations needed.
name = "Rugo"
age = 1
pi = 3.14
cool = true
nothing = nil
Reassignment works freely:
x = 10
x = "now a string"
Compound Assignment
x = 10
x += 5
x -= 3
x *= 2
x /= 4
x %= 4
Works with strings too:
msg = "Hello"
msg += ", World!"
puts msg
Constants
Names starting with an uppercase letter are constants — they can only be assigned once:
PI = 3.14
MAX_RETRIES = 5
AppName = "MyApp"
PI = 99
Lowercase names remain freely reassignable. This follows Ruby convention.
Scoping
Different blocks have different scoping rules:
Functions have their own scope — can read top-level variables but assigning creates a local variable:
name = "Rugo"
def greet()
name = "World"
return name
end
puts greet()
puts name
if blocks share the parent scope — variables created inside are visible after:
if true
msg = "hello"
end
puts msg
Loops create their own scope — can read/modify outer variables, but new variables stay local:
total = 0
for x in [1, 2, 3]
total += x
end
puts total
Lambdas capture the outer scope (unlike functions):
prefix = "Hello"
greet = fn(name) prefix + ", " + name end
puts greet("Rugo")
Constants are scoped per function — a constant in a function is independent from one at top level.
rats blocks are fully isolated — cannot see top-level variables or constants. Use environment variables to share state.
Strings
Double-quoted strings support escape sequences and interpolation with #{}:
name = "World"
puts "Hello, #{name}!"
Expressions work inside interpolation:
x = 10
puts "#{x} squared is #{x * x}"
Note: Nested double quotes inside interpolation are not supported.
Use a variable instead: x = h["key"]; puts "#{x}"
Raw Strings
Single-quoted strings are raw — no escape processing and no interpolation:
puts 'hello\nworld'
puts '\x1b[32mgreen'
puts 'no #{interpolation}'
Only \\ (literal backslash) and \' (literal single quote) are recognized:
puts 'it\'s raw'
puts 'back\\slash'
Raw strings are useful for regex patterns, Windows paths, and test assertions where you need exact literal text.
Heredoc Strings
Heredocs are multiline string literals. Delimiters must be uppercase ([A-Z_][A-Z0-9_]*).
name = "World"
html = <<HTML
<h1>Hello #{name}</h1>
<p>Welcome!</p>
HTML
Squiggly heredoc (<<~) strips common leading whitespace:
page = <<~HTML
<h1>Hello #{name}</h1>
<p>Welcome!</p>
HTML
Raw heredoc (<<'DELIM') — no interpolation:
template = <<'CODE'
def #{method_name}
puts "hello"
end
CODE
Raw squiggly heredoc (<<~'DELIM') combines both.
Slicing
Extract a substring with text[start, length] — same syntax as array slicing:
text = "hello world"
puts text[0, 5]
puts text[6, 5]
Out-of-bounds slices are clamped silently.
Concatenation
greeting = "Hello" + ", " + "World!"
Raw and double-quoted strings can be concatenated:
puts 'raw\n' + "escaped\n"
String Comparison
Strings support all comparison operators with lexicographic ordering: ==, !=, <, >, <=, >=.
String Module
use "str"
puts str.upper("hello")
puts str.lower("HELLO")
puts str.trim(" hello ")
puts str.contains("hello", "ell")
puts str.starts_with("hello", "he")
puts str.ends_with("hello", "lo")
puts str.replace("hello", "l", "r")
puts str.index("hello", "ll")
parts = str.split("a,b,c", ",")
puts str.join(parts, " | ")
Arrays
fruits = ["apple", "banana", "cherry"]
puts fruits[0]
puts len(fruits)
Append
append fruits, "date"
The explicit assignment form also works:
fruits = append(fruits, "date")
Index Assignment
fruits[1] = "blueberry"
Nested Arrays
matrix = [[1, 2], [3, 4]]
puts matrix[0]
Slicing
numbers = [10, 20, 30, 40, 50]
first_two = numbers[0, 2]
middle = numbers[1, 3]
Out-of-bounds slices are clamped silently.
Negative Indexing
arr = [10, 20, 30, 40, 50]
puts arr[-1]
puts arr[-2]
arr[-1] = 99
Iterating
for fruit in fruits
puts fruit
end
Destructuring
Unpack an array into individual variables:
a, b, c = [10, 20, 30]
puts a
puts b
puts c
Works with any expression returning an array, including Go bridge multi-return:
import "strings"
before, after, found = strings.cut("key=value", "=")
puts before
puts after
puts found
Hashes
Colon syntax for string keys — clean and concise:
person = {name: "Alice", age: 30, city: "NYC"}
puts person["name"]
puts person.name
Arrow syntax for expression keys (variables, integers, booleans):
codes = {404 => "Not Found", 500 => "Server Error"}
key = "greeting"
h = {key => "hello"}
Both syntaxes can be mixed:
h = {name: "Alice", 42 => "answer"}
Mutation
person["age"] = 31
person["email"] = "alice@example.com"
Empty Hash
counts = {}
counts["hello"] = 1
Iterating
for key, value in person
puts "#{key} => #{value}"
end
Calling Lambdas via Dot Access
Lambdas stored in hashes can be called with dot syntax:
ops = {
add: fn(a, b) a + b end,
mul: fn(a, b) a * b end
}
puts ops.add(2, 3)
puts ops.mul(4, 5)
Control Flow
If / Elsif / Else
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
end
Comparison & Logic
Operators: ==, !=, <, >, <=, >=, &&, ||, !
if x > 0 && x < 100
puts "in range"
end
if !done
puts "still working"
end
While
i = 0
while i < 5
puts i
i += 1
end
For Loops
Array Iteration
colors = ["red", "green", "blue"]
for color in colors
puts color
end
With Index
Two-variable form gives index, value:
for i, color in colors
puts "#{i}: #{color}"
end
Hash Iteration
Single-variable form gives keys:
config = {"host" => "localhost", "port" => 3000}
for k in config
puts k
end
Two-variable form gives key, value:
for k, v in config
puts "#{k} = #{v}"
end
Break
for n in [1, 2, 3, 4, 5]
if n == 4
break
end
puts n
end
Next
for n in [1, 2, 3, 4, 5]
if n % 2 == 0
next
end
puts n
end
break and next work in while loops too.
Collection Methods
Arrays and hashes have built-in methods for transforming, filtering, and querying data. No imports needed.
Transforming
nums = [1, 2, 3, 4, 5]
doubled = nums.map(fn(x) x * 2 end)
puts doubled
pairs = [1, 2, 3].flat_map(fn(x) [x, x * 10] end)
puts pairs
Filtering
nums = [1, 2, 3, 4, 5]
big = nums.filter(fn(x) x > 3 end)
puts big
small = nums.reject(fn(x) x > 3 end)
puts small
Reducing
nums = [1, 2, 3, 4, 5]
sum = nums.reduce(0, fn(acc, x) acc + x end)
puts sum
puts nums.sum()
Searching
nums = [1, 2, 3, 4, 5]
found = nums.find(fn(x) x > 3 end)
puts found
puts nums.any(fn(x) x > 4 end)
puts nums.all(fn(x) x > 0 end)
puts nums.count(fn(x) x > 2 end)
Utilities
words = ["hello", "world", "rugo"]
puts words.join(", ")
puts words.first()
puts words.last()
puts [3, 1, 4, 1, 5].min()
puts [3, 1, 4, 1, 5].max()
puts [1, 2, 2, 3, 1].uniq()
puts [[1, 2], [3, 4]].flatten()
puts ["banana", "fig", "apple"].sort_by(fn(s) len(s) end)
Slicing
nums = [1, 2, 3, 4, 5]
puts nums.take(3)
puts nums.drop(3)
puts nums.chunk(2)
puts [1, 2, 3].zip(["a", "b", "c"])
Chaining
Methods return arrays, so they chain naturally:
result = [1, 2, 3, 4, 5]
.filter(fn(x) x > 2 end)
.map(fn(x) x * 10 end)
.join(" + ")
puts result
Hash Methods
Hash methods pass (key, value) to lambdas:
person = {name: "Alice", age: 30, city: "NYC"}
puts person.map(fn(k, v) "#{k}=#{v}" end)
adults = {alice: 30, bob: 17, carol: 25}
.filter(fn(k, v) v >= 18 end)
puts adults
found = person.find(fn(k, v) v == 30 end)
puts found
puts person.keys()
puts person.values()
merged = person.merge({email: "alice@test.com"})
total = {a: 10, b: 20}.reduce(0, fn(acc, k, v) acc + v end)
puts total
puts person.any(fn(k, v) v == 30 end)
puts person.count(fn(k, v) type_of(v) == "String" end)
Each
Use each for iteration with side effects:
items = []
[1, 2, 3].each(fn(x)
items = append(items, x * 10)
end)
puts items
Note: for..in is the primary loop form. Use each when you need
a functional style or want to pass iteration as a callback.
Functions
Define and Call
def greet(name)
puts "Hello, #{name}!"
end
greet("World")
No-Argument Functions
Functions with no parameters can omit the parentheses:
def say_hello
puts "Hello!"
end
say_hello
Both def say_hello and def say_hello() are valid.
Return Values
def add(a, b)
return a + b
end
puts add(2, 3)
Parenthesis-Free Calls
puts "hello"
greet "World"
Recursion
def factorial(n)
if n <= 1
return 1
end
return n * factorial(n - 1)
end
puts factorial(5)
Lambdas (First-Class Functions)
Anonymous functions using fn...end syntax. Can be stored in variables, passed to functions, returned, and stored in data structures.
double = fn(x) x * 2 end
puts double(5)
Multi-line:
classify = fn(x)
if x > 0
return "positive"
end
return "non-positive"
end
Passing to functions:
def my_map(f, arr)
result = []
for item in arr
result = append(result, f(item))
end
return result
end
nums = my_map(fn(x) x * 2 end, [1, 2, 3])
puts nums
Closures capture by reference — changes to the outer variable are visible:
x = 10
f = fn() x end
x = 20
puts f()
Closures can also mutate captured variables:
def make_counter()
count = 0
inc = fn()
count = count + 1
return count
end
return inc
end
counter = make_counter()
puts counter()
puts counter()
Returning closures:
def make_adder(n)
return fn(x) x + n end
end
add5 = make_adder(5)
puts add5(10)
Lambdas in data structures:
ops = {
"add" => fn(a, b) a + b end,
"mul" => fn(a, b) a * b end
}
puts ops["add"](2, 3)
Calling via dot access:
ops = {
add: fn(a, b) a + b end,
mul: fn(a, b) a * b end
}
puts ops.add(2, 3)
puts ops.mul(4, 5)
Composing lambdas:
compose = fn(f, g)
return fn(x) f(g(x)) end
end
double = fn(x) x * 2 end
inc = fn(x) x + 1 end
double_then_inc = compose(inc, double)
puts double_then_inc(5)
Shell Commands
Unknown commands run as shell commands automatically.
ls -la
whoami
date
Pipes
echo "hello world" | tr a-z A-Z
ls -la | head -5
Redirects
echo "test" > /tmp/output.txt
echo "more" >> /tmp/output.txt
ls /nonexistent 2>/dev/null
Capture Output
Use backticks to capture command output into a variable:
hostname = `hostname`
puts "Running on #{hostname}"
Backticks run the command and return stdout as a trimmed string. String interpolation works inside backticks:
name = "world"
greeting = `echo hello #{name}`
puts greeting
Mix Shell and Rugo
name = "World"
puts "Hello, #{name}!"
echo "this runs in the shell"
result = `uname -s`
puts "OS: #{result}"
Shell Exit Codes
Failed shell commands exit the script immediately. Use try to catch failures:
try rm /tmp/nonexistent-file
puts "still running"
Pipe Operator
The | pipe operator connects shell commands with Rugo functions:
use "str"
echo "hello" | str.upper | puts
"hello" | tr a-z A-Z | puts
name = echo "rugo" | str.upper
Note: The pipe passes return values, not stdout. puts and print return nil, so using them in the middle of a chain is a compile error — always put them at the end:
ls | puts | head
ls | head | puts
Known Limitations
# comments: Rugo strips # comments before shell fallback detection, so unquoted # in shell commands is treated as a comment. Use quotes: echo "issue #123".
- Shell variable syntax:
FOO=bar is interpreted as a Rugo assignment. Use bash -c "FOO=bar command" instead.
Modules
Rugo has three module systems:
| Keyword | Purpose | Example |
|---|
use | Rugo stdlib modules | use "http" |
import | Go stdlib bridge | import "strings" |
require | User .rugo files | require "helpers" |
Rugo Stdlib Modules
use "http"
use "conv"
use "str"
body = http.get("https://example.com")
n = conv.to_i("42")
parts = str.split("a,b,c", ",")
Go Stdlib Bridge
import "strings"
import "math"
puts strings.to_upper("hello")
puts math.sqrt(144.0)
Function names use snake_case in Rugo, auto-converted to Go's PascalCase.
Use as to alias: import "strings" as str_go.
Global Builtins
Available without any import: puts, print, len, append, exit, raise, type_of.
exit terminates the program with an optional exit code (defaults to 0):
exit
exit(1)
User Modules
def double(n)
return n * 2
end
require "math_helpers"
puts math_helpers.double(21)
Functions are namespaced by filename. User modules can use Rugo stdlib modules — imports are auto-propagated. Paths are resolved relative to the calling file.
Directory Modules
If the require path points to a directory, Rugo resolves an entry point:
<dirname>.rugo (e.g., mylib/mylib.rugo)
main.rugo
- The sole
.rugo file (if there's exactly one)
If a file mylib.rugo exists alongside a directory mylib/, the file takes precedence.
Multi-File Libraries with with
Use with to selectively load specific .rugo files from a local directory:
require "mylib" with greet, math
puts greet.greet("world")
puts math.double(21)
Each name in the with list loads <name>.rugo from the directory root, or from lib/<name>.rugo as a fallback. The filename becomes the namespace. Works with both local directories and remote repositories.
Rules:
use, import, and require must be at the top level
- Namespaces must be unique — alias with
as if needed
- Each module can only be imported/used once
Error Handling
Rugo uses try / or for error handling. Three levels of control.
Silent Recovery
result = try `nonexistent_command`
Fire and forget:
try nonexistent_command
puts "still running"
Default Value
hostname = try `hostname` or "localhost"
use "conv"
port = try conv.to_i("not_a_number") or 8080
Error Handler Block
data = try `cat /missing/file` or err
puts "Error: #{err}"
"fallback"
end
Raising Errors
Use raise to signal errors. Works like Go's panic() — caught with try/or:
raise("something went wrong")
raise "something went wrong"
Use in functions to validate inputs:
def greet(name)
if name == nil
raise "name is required"
end
return "Hello, " + name
end
msg = try greet(nil) or err
puts "Error: " + err
"Hello, stranger"
end
Called without arguments, raise uses a default message ("runtime error").
Concurrency
Fire and Forget
spawn
puts "working in background"
end
puts "main continues immediately"
Getting Results
use "http"
task = spawn
http.get("https://httpbin.org/get")
end
body = task.value
puts body
One-Liner Form
task = spawn http.get("https://httpbin.org/get")
puts task.value
Task API
task.value
task.done
task.wait(5)
Error Handling with spawn
task = spawn
http.get("https://doesnotexist.invalid")
end
body = try task.value or "request failed"
parallel — fan-out, wait for all
use "http"
results = parallel
http.get("https://api.example.com/users")
http.get("https://api.example.com/posts")
end
puts results[0]
puts results[1]
Each expression runs in its own goroutine. Results are returned in order. If any panics, parallel re-raises the first error — compose with try/or.
Timeouts
task = spawn `sleep 10`
result = try task.wait(2) or "timed out"
Queues
For producer-consumer patterns, use the queue module:
use "queue"
use "conv"
q = queue.new()
spawn
for i in [1, 2, 3]
q.push(i)
end
q.close()
end
q.each(fn(item)
puts conv.to_s(item)
end)
Queues support bounded capacity (queue.new(10)), pop with timeout (try q.pop(5) or "timeout"), and properties (q.size, q.closed).
Testing with RATS
RATS (Rugo Automated Testing System) uses _test.rugo files and the test module.
Writing Tests
use "test"
rats "prints hello"
result = test.run("rugo run greet.rugo")
test.assert_eq(result["status"], 0)
test.assert_contains(result["output"], "Hello")
end
Running Tests
rugo rats
rugo rats test/greet_test.rugo
rugo rats --filter "hello"
rugo rats --timing
rugo rats --recap
Capturing Command Output
test.run(cmd) returns a hash with:
"status" — exit code (integer)
"output" — combined stdout+stderr (string)
"lines" — output split by newlines (array)
Assertions
| Function | Description |
|---|
test.assert_eq(a, b) | Equal |
test.assert_neq(a, b) | Not equal |
test.assert_true(val) | Truthy |
test.assert_false(val) | Falsy |
test.assert_contains(s, sub) | String contains substring |
test.assert_nil(val) | Value is nil |
test.fail(msg) | Explicitly fail |
Skipping Tests
rats "not ready yet"
test.skip("pending feature")
end
Testing a Built Binary
use "test"
rats "binary works"
test.run("rugo build greet.rugo -o /tmp/greet")
result = test.run("/tmp/greet")
test.assert_eq(result["status"], 0)
test.assert_contains(result["output"], "Hello")
test.run("rm -f /tmp/greet")
end
Setup and Teardown
| Hook | Scope | When it runs |
|---|
def setup_file() | Per file | Once before all tests in the file |
def teardown_file() | Per file | Once after all tests in the file |
def setup() | Per test | Before each individual test |
def teardown() | Per test | After each individual test |
use "test"
use "os"
def setup_file()
os.exec("mkdir -p /tmp/myapp_test")
end
def teardown_file()
os.exec("rm -rf /tmp/myapp_test")
end
def setup()
test.write_file(test.tmpdir() + "/input.txt", "default")
end
teardown_file() always runs, even if tests fail.
Inline Tests
Embed rats blocks in regular .rugo files. rugo run ignores them; rugo rats executes them.
use "test"
def greet(name)
return "Hello, " + name + "!"
end
puts greet("World")
rats "greet formats a greeting"
test.assert_eq(greet("Rugo"), "Hello, Rugo!")
test.assert_contains(greet("World"), "World")
end
rugo run greet.rugo
rugo rats greet.rugo
When scanning a directory, rugo rats discovers both _test.rugo files and regular .rugo files containing rats blocks (directories named fixtures are skipped).
Custom Modules (Advanced)
Create your own Rugo modules in Go and build a custom Rugo binary.
runtime.go — the Go implementation:
package hello
type Hello struct{}
func (*Hello) Greet(name string) interface{} {
return "hello, " + name
}
hello.go — module registration:
package hello
import (
_ "embed"
"github.com/rubiojr/rugo/modules"
)
var runtime string
func init() {
modules.Register(&modules.Module{
Name: "hello",
Type: "Hello",
Funcs: []modules.FuncDef{
{Name: "greet", Args: []modules.ArgType{modules.String}},
},
Runtime: modules.CleanRuntime(runtime),
})
}
Build a custom Rugo binary:
package main
import (
"github.com/rubiojr/rugo/cmd"
_ "github.com/rubiojr/rugo/modules/conv"
_ "github.com/rubiojr/rugo/modules/http"
_ "github.com/yourorg/rugo-hello"
)
func main() { cmd.Execute("v1.0.0-custom") }
Use in scripts:
use "hello"
puts hello.greet("developer")
Modules can wrap external Go libraries via GoDeps:
modules.Register(&modules.Module{
Name: "slug",
Type: "Slug",
Funcs: []modules.FuncDef{{Name: "make", Args: []modules.ArgType{modules.String}}},
GoImports: []string{`gosimpleslug "github.com/gosimple/slug"`},
GoDeps: []string{"github.com/gosimple/slug v1.15.0"},
Runtime: modules.CleanRuntime(runtime),
})
Benchmarking
use "bench"
def fib(n)
if n <= 1
return n
end
return fib(n - 1) + fib(n - 2)
end
bench "fib(20)"
fib(20)
end
rugo run benchmarks.rugo
rugo bench
rugo bench bench/
The framework auto-calibrates iterations (scales until ≥1s elapsed), reports ns/op and run count.
Go Bridge
Call Go standard library functions directly with import:
import "strings"
import "math"
puts strings.to_upper("hello")
puts strings.contains("hello world", "world")
puts math.sqrt(144.0)
Type Conversions
import "strconv"
n = strconv.atoi("42")
s = strconv.itoa(42)
f = strconv.parse_float("3.14")
Error Handling
Go (T, error) returns auto-panic on error. Use try/or:
import "strconv"
n = try strconv.atoi("not a number") or 0
Aliasing
use "os"
import "os" as go_os
go_os.setenv("APP", "rugo")
puts go_os.getenv("APP")
Multi-Return Functions
Go functions returning multiple values are bridged as arrays. Use destructuring:
import "strings"
before, after, found = strings.cut("key=value", "=")
puts before
puts after
puts found
Available Packages
| Package | Key Functions |
|---|
strings | contains, has_prefix, has_suffix, to_upper, to_lower, trim_space, split, join, replace, repeat, index, count, fields, contains_func, index_func, map |
strconv | atoi, itoa, format_float, parse_float, format_bool, parse_bool |
math | abs, ceil, floor, round, sqrt, pow, log, max, min, sin, cos, tan |
path | base, clean, dir, ext, is_abs, join, match, split |
path/filepath | join, base, dir, ext, clean, is_abs, rel, split |
sort | strings, ints |
os | getenv, setenv, read_file, write_file, mkdir_all, remove, getwd |
time | now_unix, now_nano, sleep |
encoding/json | marshal, unmarshal, marshal_indent |
encoding/base64 | encode, decode, url_encode, url_decode |
encoding/hex | encode, decode |
crypto/sha256 | sum256 |
crypto/md5 | sum |
net/url | parse, path_escape, path_unescape, query_escape, query_unescape |
unicode | is_letter, is_digit, is_space, is_upper, is_lower, is_punct, to_upper, to_lower |
html | escape_string, unescape_string |
slices | contains, index, reverse, compact |
maps | keys, values, clone, equal |
JSON
import "encoding/json"
data = {name: "Rugo", version: 1}
text = json.marshal(data)
puts text
parsed = json.unmarshal(text)
puts parsed.name
Encoding (Base64 & Hex)
import "encoding/base64"
import "encoding/hex"
b64 = base64.encode("Hello!")
puts base64.decode(b64)
h = hex.encode("Hello!")
puts hex.decode(h)
Hashing
import "crypto/sha256"
import "crypto/md5"
import "encoding/hex"
puts hex.encode(sha256.sum256("hello"))
puts hex.encode(md5.sum("hello"))
URL Parsing
import "net/url"
u = url.parse("https://example.com:8080/path?q=hello#top")
puts u.scheme
puts u.hostname
puts u.port
puts u.path
puts u.query
puts u.fragment
Collections (Slices & Maps)
import "slices"
import "maps"
puts slices.contains(["a", "b", "c"], "b")
puts slices.reverse([1, 2, 3])
h = {name: "Rugo", lang: "go"}
puts maps.keys(h)
copy = maps.clone(h)
puts maps.equal(h, copy)
Use rugo doc <package> to see all functions with typed signatures and documentation.
Structs
Lightweight object-oriented programming using hashes with dot access.
Defining a Struct
struct Dog
name
breed
end
Creates a constructor Dog(name, breed) plus a new() alias for namespaces.
Dot Access on Hashes
person = {"name" => "Alice", "age" => 30}
puts person.name
person.name = "Bob"
Nested dot access:
data = {"user" => {"name" => "Alice"}}
puts data.user.name
Methods
struct Dog
name
breed
end
def Dog.bark()
return self.name + " says woof!"
end
def Dog.rename(new_name)
self.name = new_name
end
require "dog"
rex = dog.new("Rex", "Labrador")
puts dog.bark(rex)
dog.rename(rex, "Rexy")
puts dog.bark(rex)
Type Introspection
Use type_of() to get the type name of any value. For structs, it returns the struct name:
rex = Dog("Rex", "Lab")
puts type_of(rex)
puts type_of("hello")
puts type_of(42)
puts type_of([1, 2])
puts type_of({a: 1})
Web Server
Build web servers and REST APIs with the web module.
use "web"
web.get("/", "home")
def home(req)
return web.text("Hello, World!")
end
web.listen(3000)
Routes and URL Parameters
Use :name to capture path segments:
use "web"
web.get("/users/:id", "show_user")
web.post("/users", "create_user")
def show_user(req)
id = req.params["id"]
return web.json({"id" => id})
end
def create_user(req)
return web.json({"created" => true}, 201)
end
web.listen(3000)
All five HTTP methods: web.get, web.post, web.put, web.delete, web.patch.
The Request Object
def my_handler(req)
req.method
req.path
req.body
req.params["id"]
req.query["page"]
req.header["Authorization"]
req.remote_addr
end
Response Helpers
web.text("hello")
web.text("not found", 404)
web.html("<h1>Hi</h1>")
web.json({"key" => "val"})
web.json({"key" => "val"}, 201)
web.redirect("/login")
web.redirect("/new", 301)
web.status(204)
Middleware
Return nil to continue, or a response to stop:
use "web"
web.middleware("require_auth")
web.get("/secret", "secret_handler")
def require_auth(req)
if req.header["Authorization"] == nil
return web.json({"error" => "unauthorized"}, 401)
end
return nil
end
def secret_handler(req)
return web.text("secret data")
end
web.listen(3000)
Built-in middleware: "logger", "real_ip", "rate_limiter".
real_ip resolves client IP from proxy headers (X-Forwarded-For, X-Real-Ip).
Rate limiting:
web.rate_limit(100)
web.middleware("rate_limiter")
Route-level middleware:
web.get("/admin", "admin_panel", "require_auth", "require_admin")
Route Groups
web.group("/api", "require_auth")
web.get("/users", "list_users")
web.post("/users", "create_user")
web.end_group()
Remote Modules
Load .rugo modules directly from git repositories — no package registry needed.
Basic Usage
require "github.com/user/my-utils@v1.0.0" as "utils"
puts utils.slugify("Hello World")
Version Pinning
| Syntax | Meaning |
|---|
@v1.2.0 | Git tag (cached forever) |
@main | Branch (re-fetched each build) |
@abc1234 | Commit SHA (cached forever) |
| (none) | Default branch (re-fetched) |
Multi-File Libraries with with
Works with both remote repositories and local directories:
require "github.com/rubiojr/rugh@v1.0.0" with client, issue
gh = client.from_env()
issues = issue.list(gh, "rubiojr", "rugo")
Each name loads <name>.rugo from the repo root (or lib/<name>.rugo as fallback). Without with, Rugo looks for <repo-name>.rugo, then main.rugo, then the sole .rugo file.
Publishing a Multi-File Module
Publishing is just pushing a git repo. No registry, no manifest.
my-lib/
client.rugo # → client namespace
helpers.rugo # → helpers namespace
main.rugo # (optional) entry point for bare require
Rules for module authors:
- Each
.rugo file at the repo root becomes a loadable module
- Functions prefixed with
_ are private — compiler rejects external calls
- Add a
main.rugo if you want require "..." (without with) to work
Inter-Module Dependencies
If one module calls functions from another, the consumer must load both:
require "github.com/user/lib@v1.0.0" with client, issue
Order matters — load dependencies before modules that use them.
Subpath Requires
require "github.com/user/lib/client@v1.0.0"
Cache
Remote modules are cached in ~/.rugo/modules/. Override with RUGO_MODULE_DIR.
Lock File (rugo.lock)
Use rugo mod tidy to generate a lock file that pins exact commit SHAs:
rugo mod tidy
rugo mod update
rugo mod update github.com/user/repo
rugo build --frozen app.rugo -o app
Format: <module-path> <version-label> <resolved-sha> per line.
Best practices:
- Commit
rugo.lock for reproducible builds
- Use
--frozen in CI to catch unintentional dependency changes
- Run
rugo mod tidy in each directory with remote dependencies
Doc Comments
Rugo uses # comments for documentation. Write doc comments immediately before def or struct declarations with no blank line gap.
Convention
def factorial(n)
if n <= 1
return 1
end
return n * factorial(n - 1)
end
struct Dog
name
breed
end
Rules:
- Consecutive
# lines immediately before def/struct (no blank line gap) = doc comment
- First
# block at top of file before any code = file-level doc
# inside function bodies, after a blank line gap, or inline = regular comment
rugo doc Command
rugo doc file.rugo
rugo doc file.rugo factorial
rugo doc http
rugo doc strings
rugo doc use:os
rugo doc import:os
rugo doc github.com/user/repo
rugo doc --all
When bat is installed, output is syntax-highlighted automatically. Set NO_COLOR=1 to disable.
Sandbox
Opt-in process sandboxing using Linux Landlock. Restrict filesystem paths and network ports.
Basic Usage
sandbox
sandbox ro: ["/etc"], rw: ["/tmp"], rox: ["/usr/bin"]
sandbox connect: [80, 443], bind: 8080
Permission Types
| Keyword | Access | Example |
|---|
ro | Read-only | Config files |
rw | Read + write | Temp/output dirs |
rox | Read + execute | Binary dirs |
rwx | Read + write + execute | Plugin dirs |
connect | TCP connect | HTTP clients |
bind | TCP bind | Servers |
env | Env var allowlist | Restrict env access |
CLI Flags
Apply sandbox restrictions without modifying the script:
rugo run --sandbox --ro /etc --rox /usr --connect 443 --env PATH script.rugo
CLI flags override any sandbox directive in the script.
Important Notes
- Linux only: On other platforms, the directive is a no-op with a warning.
- No auto-allows: You must explicitly allow every path.
- Shell commands typically need
rox: ["/usr", "/lib"] and rw: ["/dev/null"].
stat() is not restricted: os.file_exists() always works regardless of sandbox.
Environment Variable Filtering
Restrict which env vars are visible (opt-in, works on all platforms):
sandbox env: ["PATH", "HOME"]
import "os"
puts(os.getenv("HOME"))
puts(os.getenv("SECRET"))
Go Modules via Require (Advanced)
Rugo can require Go packages directly — the compiler introspects Go source, discovers exported functions, and bridges them automatically. No manifest, no registration boilerplate.
Creating a Go Module
greeter/
go.mod
greeter.go
greeter.go:
package greeter
import "strings"
func Hello(name string) string {
return "Hello, " + name + "!"
}
func Shout(text string) string {
return strings.ToUpper(text) + "!"
}
Using It from Rugo
require "greeter"
puts(greeter.hello("World"))
puts(greeter.shout("hello"))
Function names are auto-converted: Hello → hello, IsEmpty → is_empty.
Module Naming
The namespace is derived from the last segment of the require path. Use as to override:
require "greeter" as g
puts(g.hello("Alias"))
Remote Go Modules
require "github.com/user/rugo-greeter@v1.0.0" as greeter
puts(greeter.hello("Remote"))
Supported Types
| Go type | Rugo type | Notes |
|---|
string | string | |
int | integer | |
float64 | float | |
bool | boolean | |
error | — | auto-panics on non-nil |
[]string | array | |
[]byte | string | cast |
Functions with non-bridgeable types (pointers, interfaces, channels, maps, structs, generics) are automatically excluded.
Limitations
- Only exported package-level functions are bridged (no methods, structs, variables)
- Only top-level
.go files are inspected (use with for sub-packages)
- Use
require for wrapping Go libraries; use custom builds for stateful modules