一键导入
rugo-quickstart
Rugo language quickstart guide. Load when writing .rugo scripts, learning Rugo syntax, or helping users with Rugo language features.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Rugo language quickstart guide. Load when writing .rugo scripts, learning Rugo syntax, or helping users with Rugo language features.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Expert in RATS (Rugo Automated Testing System), a BATS-like end-to-end testing framework for Rugo. Load when working on rats tests, the test runner, the test module, or writing _test.rugo files.
Expert in developing Rugo, a Ruby-inspired language that compiles to native binaries via Go. Load when working on the rugo compiler, parser, modules, or writing .rugo scripts.
Expert in RunMD, a documentation verifier for Rugo code blocks in markdown files. Load when verifying docs, running runmd, or working on the runmd tool itself.
Expert in writing and using Go modules from Rugo via `require`. Load when creating Go packages that bridge to Rugo, debugging Go module imports, or helping users expose Go functions to Rugo scripts.
Guide to writing clean, idiomatic Rugo code. Load when writing .rugo scripts, reviewing Rugo code style, or learning Rugo patterns and best practices.
Expert in writing native Rugo modules (.rugo libraries). Load when creating multi-file Rugo libraries, designing module APIs, or structuring require/with patterns.
| name | rugo-quickstart |
| description | Rugo language quickstart guide. Load when writing .rugo scripts, learning Rugo syntax, or helping users with Rugo language features. |
Get up and running with Rugo in minutes.
go install github.com/rubiojr/rugo@latest
rugo run script.rugo # compile and run
rugo build script.rugo # compile to native binary
rugo emit script.rugo # print generated Go code
rugo doc http # show module documentation
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 #:
# This is a comment
puts "not a comment"
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"
x = 10
x += 5 # 15
x -= 3 # 12
x *= 2 # 24
x /= 4 # 6
x %= 4 # 2
Works with strings too:
msg = "Hello"
msg += ", World!"
puts msg
Names starting with an uppercase letter are constants — they can only be assigned once:
PI = 3.14
MAX_RETRIES = 5
AppName = "MyApp"
PI = 99 # compile error: cannot reassign constant PI
Lowercase names remain freely reassignable. This follows Ruby convention.
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" # separate local variable
return name
end
puts greet() # World
puts name # Rugo (unchanged)
if blocks share the parent scope — variables created inside are visible after:
if true
msg = "hello"
end
puts msg # hello
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 # 6
# puts x # compile error: undefined: x
Lambdas capture the outer scope (unlike functions):
prefix = "Hello"
greet = fn(name) prefix + ", " + name end
puts greet("Rugo") # Hello, 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.
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}"
Single-quoted strings are raw — no escape processing and no interpolation:
puts 'hello\nworld' # prints: hello\nworld (literal, no newline)
puts '\x1b[32mgreen' # prints: \x1b[32mgreen (literal, no ANSI)
puts 'no #{interpolation}' # prints: no #{interpolation}
Only \\ (literal backslash) and \' (literal single quote) are recognized:
puts 'it\'s raw' # prints: it's raw
puts 'back\\slash' # prints: back\slash
Raw strings are useful for regex patterns, Windows paths, and test assertions where you need exact literal text.
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.
Extract a substring with text[start, length] — same syntax as array slicing:
text = "hello world"
puts text[0, 5] # hello
puts text[6, 5] # world
Out-of-bounds slices are clamped silently.
greeting = "Hello" + ", " + "World!"
Raw and double-quoted strings can be concatenated:
puts 'raw\n' + "escaped\n" # raw\nescaped<newline>
Strings support all comparison operators with lexicographic ordering: ==, !=, <, >, <=, >=.
use "str"
puts str.upper("hello") # HELLO
puts str.lower("HELLO") # hello
puts str.trim(" hello ") # hello
puts str.contains("hello", "ell") # true
puts str.starts_with("hello", "he") # true
puts str.ends_with("hello", "lo") # true
puts str.replace("hello", "l", "r") # herro
puts str.index("hello", "ll") # 2
parts = str.split("a,b,c", ",")
puts str.join(parts, " | ") # a | b | c
fruits = ["apple", "banana", "cherry"]
puts fruits[0] # apple
puts len(fruits) # 3
append fruits, "date"
The explicit assignment form also works:
fruits = append(fruits, "date")
fruits[1] = "blueberry"
matrix = [[1, 2], [3, 4]]
puts matrix[0] # [1, 2]
numbers = [10, 20, 30, 40, 50]
first_two = numbers[0, 2] # [10, 20]
middle = numbers[1, 3] # [20, 30, 40]
Out-of-bounds slices are clamped silently.
arr = [10, 20, 30, 40, 50]
puts arr[-1] # 50 (last element)
puts arr[-2] # 40 (second-to-last)
arr[-1] = 99
for fruit in fruits
puts fruit
end
Unpack an array into individual variables:
a, b, c = [10, 20, 30]
puts a # 10
puts b # 20
puts c # 30
Works with any expression returning an array, including Go bridge multi-return:
import "strings"
before, after, found = strings.cut("key=value", "=")
puts before # key
puts after # value
puts found # true
Colon syntax for string keys — clean and concise:
person = {name: "Alice", age: 30, city: "NYC"}
puts person["name"] # Alice
puts person.name # Alice
Arrow syntax for expression keys (variables, integers, booleans):
codes = {404 => "Not Found", 500 => "Server Error"}
key = "greeting"
h = {key => "hello"} # key is the variable value, not the string "key"
Both syntaxes can be mixed:
h = {name: "Alice", 42 => "answer"}
person["age"] = 31
person["email"] = "alice@example.com"
counts = {}
counts["hello"] = 1
for key, value in person
puts "#{key} => #{value}"
end
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) # 5
puts ops.mul(4, 5) # 20
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
end
Operators: ==, !=, <, >, <=, >=, &&, ||, !
if x > 0 && x < 100
puts "in range"
end
if !done
puts "still working"
end
i = 0
while i < 5
puts i
i += 1
end
colors = ["red", "green", "blue"]
for color in colors
puts color
end
Two-variable form gives index, value:
for i, color in colors
puts "#{i}: #{color}"
end
Single-variable form gives keys:
config = {"host" => "localhost", "port" => 3000}
for k in config
puts k
end
# prints host, port
Two-variable form gives key, value:
for k, v in config
puts "#{k} = #{v}"
end
for n in [1, 2, 3, 4, 5]
if n == 4
break
end
puts n
end
# prints 1, 2, 3
for n in [1, 2, 3, 4, 5]
if n % 2 == 0
next
end
puts n
end
# prints 1, 3, 5
break and next work in while loops too.
Arrays and hashes have built-in methods for transforming, filtering, and querying data. No imports needed.
nums = [1, 2, 3, 4, 5]
doubled = nums.map(fn(x) x * 2 end)
puts doubled # [2, 4, 6, 8, 10]
pairs = [1, 2, 3].flat_map(fn(x) [x, x * 10] end)
puts pairs # [1, 10, 2, 20, 3, 30]
nums = [1, 2, 3, 4, 5]
big = nums.filter(fn(x) x > 3 end)
puts big # [4, 5]
small = nums.reject(fn(x) x > 3 end)
puts small # [1, 2, 3]
nums = [1, 2, 3, 4, 5]
sum = nums.reduce(0, fn(acc, x) acc + x end)
puts sum # 15
puts nums.sum() # 15
nums = [1, 2, 3, 4, 5]
found = nums.find(fn(x) x > 3 end)
puts found # 4
puts nums.any(fn(x) x > 4 end) # true
puts nums.all(fn(x) x > 0 end) # true
puts nums.count(fn(x) x > 2 end) # 3
words = ["hello", "world", "rugo"]
puts words.join(", ") # hello, world, rugo
puts words.first() # hello
puts words.last() # rugo
puts [3, 1, 4, 1, 5].min() # 1
puts [3, 1, 4, 1, 5].max() # 5
puts [1, 2, 2, 3, 1].uniq() # [1, 2, 3]
puts [[1, 2], [3, 4]].flatten() # [1, 2, 3, 4]
puts ["banana", "fig", "apple"].sort_by(fn(s) len(s) end)
# [fig, apple, banana]
nums = [1, 2, 3, 4, 5]
puts nums.take(3) # [1, 2, 3]
puts nums.drop(3) # [4, 5]
puts nums.chunk(2) # [[1, 2], [3, 4], [5]]
puts [1, 2, 3].zip(["a", "b", "c"]) # [[1, a], [2, b], [3, c]]
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 # 30 + 40 + 50
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 # {alice: 30, carol: 25}
found = person.find(fn(k, v) v == 30 end)
puts found # [age, 30]
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 # 30
puts person.any(fn(k, v) v == 30 end) # true
puts person.count(fn(k, v) type_of(v) == "String" end) # 2
Use each for iteration with side effects:
items = []
[1, 2, 3].each(fn(x)
items = append(items, x * 10)
end)
puts items # [10, 20, 30]
Note:
for..inis the primary loop form. Useeachwhen you need a functional style or want to pass iteration as a callback.
def greet(name)
puts "Hello, #{name}!"
end
greet("World")
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.
def add(a, b)
return a + b
end
puts add(2, 3) # 5
puts "hello"
greet "World"
def factorial(n)
if n <= 1
return 1
end
return n * factorial(n - 1)
end
puts factorial(5) # 120
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) # 10
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 # [2, 4, 6]
Closures capture by reference — changes to the outer variable are visible:
x = 10
f = fn() x end
x = 20
puts f() # 20
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() # 1
puts counter() # 2
Returning closures:
def make_adder(n)
return fn(x) x + n end
end
add5 = make_adder(5)
puts add5(10) # 15
Lambdas in data structures:
ops = {
"add" => fn(a, b) a + b end,
"mul" => fn(a, b) a * b end
}
puts ops["add"](2, 3) # 5
Calling via dot access:
ops = {
add: fn(a, b) a + b end,
mul: fn(a, b) a * b end
}
puts ops.add(2, 3) # 5
puts ops.mul(4, 5) # 20
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) # 11
Unknown commands run as shell commands automatically.
ls -la
whoami
date
echo "hello world" | tr a-z A-Z
ls -la | head -5
echo "test" > /tmp/output.txt
echo "more" >> /tmp/output.txt
ls /nonexistent 2>/dev/null
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 # hello world
name = "World"
puts "Hello, #{name}!"
echo "this runs in the shell"
result = `uname -s`
puts "OS: #{result}"
Failed shell commands exit the script immediately. Use try to catch failures:
try rm /tmp/nonexistent-file
puts "still running"
The | pipe operator connects shell commands with Rugo functions:
use "str"
echo "hello" | str.upper | puts # HELLO
"hello" | tr a-z A-Z | puts # HELLO
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 # ✗ compile error
ls | head | puts # ✓ puts at the end
# comments: Rugo strips # comments before shell fallback detection, so unquoted # in shell commands is treated as a comment. Use quotes: echo "issue #123".FOO=bar is interpreted as a Rugo assignment. Use bash -c "FOO=bar command" instead.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" |
use "http"
use "conv"
use "str"
body = http.get("https://example.com")
n = conv.to_i("42")
parts = str.split("a,b,c", ",")
import "strings"
import "math"
puts strings.to_upper("hello") # HELLO
puts math.sqrt(144.0) # 12
Function names use snake_case in Rugo, auto-converted to Go's PascalCase.
Use as to alias: import "strings" as str_go.
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 with code 0
exit(1) # exit with code 1
# math_helpers.rugo
def double(n)
return n * 2
end
# main.rugo
require "math_helpers"
puts math_helpers.double(21) # 42
Functions are namespaced by filename. User modules can use Rugo stdlib modules — imports are auto-propagated. Paths are resolved relative to the calling file.
If the require path points to a directory, Rugo resolves an entry point:
<dirname>.rugo (e.g., mylib/mylib.rugo)main.rugo.rugo file (if there's exactly one)If a file mylib.rugo exists alongside a directory mylib/, the file takes precedence.
withUse with to selectively load specific .rugo files from a local directory:
require "mylib" with greet, math
puts greet.greet("world")
puts math.double(21) # 42
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 levelas if neededRugo uses try / or for error handling. Three levels of control.
result = try `nonexistent_command`
# result is nil — script continues
Fire and forget:
try nonexistent_command
puts "still running"
hostname = try `hostname` or "localhost"
use "conv"
port = try conv.to_i("not_a_number") or 8080
data = try `cat /missing/file` or err
puts "Error: #{err}"
"fallback"
end
Use raise to signal errors. Works like Go's panic() — caught with try/or:
raise("something went wrong")
raise "something went wrong" # paren-free
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").
spawn
puts "working in background"
end
puts "main continues immediately"
use "http"
task = spawn
http.get("https://httpbin.org/get")
end
body = task.value
puts body
task = spawn http.get("https://httpbin.org/get")
puts task.value
task.value # block until done, return result
task.done # non-blocking: true if finished
task.wait(5) # block with timeout, panics on timeout
task = spawn
http.get("https://doesnotexist.invalid")
end
body = try task.value or "request failed"
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.
task = spawn `sleep 10`
result = try task.wait(2) or "timed out"
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).
RATS (Rugo Automated Testing System) uses _test.rugo files and the test module.
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
rugo rats # run all _test.rugo files in rats/ (or current dir)
rugo rats test/greet_test.rugo # run a specific file
rugo rats --filter "hello" # filter by test name
rugo rats --timing # show per-test and total elapsed time
rugo rats --recap # print all failures with details at the end
test.run(cmd) returns a hash with:
"status" — exit code (integer)"output" — combined stdout+stderr (string)"lines" — output split by newlines (array)| 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 |
rats "not ready yet"
test.skip("pending feature")
end
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
| 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.
Embed rats blocks in regular .rugo files. rugo run ignores them; rugo rats executes them.
# greet.rugo
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 # prints "Hello, World!" — tests ignored
rugo rats greet.rugo # runs the inline tests
When scanning a directory, rugo rats discovers both _test.rugo files and regular .rugo files containing rats blocks (directories named fixtures are skipped).
Create your own Rugo modules in Go and build a custom Rugo binary.
runtime.go — the Go implementation:
//go:build ignore
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"
)
//go:embed runtime.go
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"
// ... other standard modules ...
_ "github.com/yourorg/rugo-hello" // your custom module
)
func main() { cmd.Execute("v1.0.0-custom") }
Use in scripts:
use "hello"
puts hello.greet("developer") # hello, 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),
})
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 # run a single benchmark file
rugo bench # run all _bench.rugo files in current dir
rugo bench bench/ # run all _bench.rugo in a directory
The framework auto-calibrates iterations (scales until ≥1s elapsed), reports ns/op and run count.
Call Go standard library functions directly with import:
import "strings"
import "math"
puts strings.to_upper("hello") # HELLO
puts strings.contains("hello world", "world") # true
puts math.sqrt(144.0) # 12
import "strconv"
n = strconv.atoi("42") # string → int
s = strconv.itoa(42) # int → string
f = strconv.parse_float("3.14") # string → float
Go (T, error) returns auto-panic on error. Use try/or:
import "strconv"
n = try strconv.atoi("not a number") or 0
use "os"
import "os" as go_os
go_os.setenv("APP", "rugo")
puts go_os.getenv("APP")
Go functions returning multiple values are bridged as arrays. Use destructuring:
import "strings"
before, after, found = strings.cut("key=value", "=")
puts before # key
puts after # value
puts found # true
| 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 |
import "encoding/json"
data = {name: "Rugo", version: 1}
text = json.marshal(data)
puts text # {"name":"Rugo","version":1}
parsed = json.unmarshal(text)
puts parsed.name # Rugo
import "encoding/base64"
import "encoding/hex"
b64 = base64.encode("Hello!")
puts base64.decode(b64) # Hello!
h = hex.encode("Hello!")
puts hex.decode(h) # Hello!
import "crypto/sha256"
import "crypto/md5"
import "encoding/hex"
puts hex.encode(sha256.sum256("hello")) # SHA-256 hex digest
puts hex.encode(md5.sum("hello")) # MD5 hex digest
import "net/url"
u = url.parse("https://example.com:8080/path?q=hello#top")
puts u.scheme # https
puts u.hostname # example.com
puts u.port # 8080
puts u.path # /path
puts u.query # q=hello
puts u.fragment # top
import "slices"
import "maps"
puts slices.contains(["a", "b", "c"], "b") # true
puts slices.reverse([1, 2, 3]) # [3, 2, 1]
h = {name: "Rugo", lang: "go"}
puts maps.keys(h) # [lang, name]
copy = maps.clone(h)
puts maps.equal(h, copy) # true
Use rugo doc <package> to see all functions with typed signatures and documentation.
Lightweight object-oriented programming using hashes with dot access.
struct Dog
name
breed
end
Creates a constructor Dog(name, breed) plus a new() alias for namespaces.
person = {"name" => "Alice", "age" => 30}
puts person.name # Alice
person.name = "Bob"
Nested dot access:
data = {"user" => {"name" => "Alice"}}
puts data.user.name # Alice
# dog.rugo
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) # Rex says woof!
dog.rename(rex, "Rexy")
puts dog.bark(rex) # Rexy says woof!
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) # Dog
puts type_of("hello") # String
puts type_of(42) # Integer
puts type_of([1, 2]) # Array
puts type_of({a: 1}) # Hash
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)
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.
def my_handler(req)
req.method # "GET", "POST", etc.
req.path # "/users/42"
req.body # raw request body
req.params["id"] # URL parameters
req.query["page"] # query string parameters
req.header["Authorization"] # request headers
req.remote_addr # client address
end
web.text("hello") # 200 text/plain
web.text("not found", 404) # 404 text/plain
web.html("<h1>Hi</h1>") # 200 text/html
web.json({"key" => "val"}) # 200 application/json
web.json({"key" => "val"}, 201) # with status code
web.redirect("/login") # 302 redirect
web.redirect("/new", 301) # 301 permanent
web.status(204) # empty response
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) # 100 requests/second per IP
web.middleware("rate_limiter") # returns 429 when exceeded
Route-level middleware:
web.get("/admin", "admin_panel", "require_auth", "require_admin")
web.group("/api", "require_auth")
web.get("/users", "list_users")
web.post("/users", "create_user")
web.end_group()
Load .rugo modules directly from git repositories — no package registry needed.
require "github.com/user/my-utils@v1.0.0" as "utils"
puts utils.slugify("Hello World")
| 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) |
withWorks 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 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:
.rugo file at the repo root becomes a loadable module_ are private — compiler rejects external callsmain.rugo if you want require "..." (without with) to workIf one module calls functions from another, the consumer must load both:
require "github.com/user/lib@v1.0.0" with client, issue
# client is loaded first, so issue can call client.get()
Order matters — load dependencies before modules that use them.
require "github.com/user/lib/client@v1.0.0"
Remote modules are cached in ~/.rugo/modules/. Override with RUGO_MODULE_DIR.
rugo.lock)Use rugo mod tidy to generate a lock file that pins exact commit SHAs:
rugo mod tidy # resolve and write rugo.lock
rugo mod update # re-resolve all mutable deps
rugo mod update github.com/user/repo # re-resolve a specific module
rugo build --frozen app.rugo -o app # fail if lock is missing/stale
Format: <module-path> <version-label> <resolved-sha> per line.
Best practices:
rugo.lock for reproducible builds--frozen in CI to catch unintentional dependency changesrugo mod tidy in each directory with remote dependenciesRugo uses # comments for documentation. Write doc comments immediately before def or struct declarations with no blank line gap.
# File-level documentation goes here.
# Calculates the factorial of n.
# Returns 1 when n <= 1.
def factorial(n)
# This is a regular comment — NOT shown by rugo doc
if n <= 1
return 1
end
return n * factorial(n - 1)
end
# A Dog with a name and breed.
struct Dog
name
breed
end
Rules:
# lines immediately before def/struct (no blank line gap) = doc comment# block at top of file before any code = file-level doc# inside function bodies, after a blank line gap, or inline = regular commentrugo doc Commandrugo doc file.rugo # all docs in a file
rugo doc file.rugo factorial # specific function or struct
rugo doc http # stdlib module
rugo doc strings # bridge package
rugo doc use:os # disambiguate: force stdlib module
rugo doc import:os # disambiguate: force bridge package
rugo doc github.com/user/repo # remote module
rugo doc --all # list all modules and packages
When bat is installed, output is syntax-highlighted automatically. Set NO_COLOR=1 to disable.
Opt-in process sandboxing using Linux Landlock. Restrict filesystem paths and network ports.
# Deny everything (maximum restriction)
sandbox
# Allow specific paths
sandbox ro: ["/etc"], rw: ["/tmp"], rox: ["/usr/bin"]
# Allow network
sandbox connect: [80, 443], bind: 8080
| 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 |
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.
rox: ["/usr", "/lib"] and rw: ["/dev/null"].stat() is not restricted: os.file_exists() always works regardless of sandbox.Restrict which env vars are visible (opt-in, works on all platforms):
sandbox env: ["PATH", "HOME"]
import "os"
puts(os.getenv("HOME")) # works
puts(os.getenv("SECRET")) # empty string
Rugo can require Go packages directly — the compiler introspects Go source, discovers exported functions, and bridges them automatically. No manifest, no registration boilerplate.
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) + "!"
}
require "greeter"
puts(greeter.hello("World")) # Hello, World!
puts(greeter.shout("hello")) # HELLO!
Function names are auto-converted: Hello → hello, IsEmpty → is_empty.
The namespace is derived from the last segment of the require path. Use as to override:
require "greeter" as g
puts(g.hello("Alias"))
require "github.com/user/rugo-greeter@v1.0.0" as greeter
puts(greeter.hello("Remote"))
| 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.
.go files are inspected (use with for sub-packages)require for wrapping Go libraries; use custom builds for stateful modules