Script Runner (onion)¶
The onion command compiles and executes Onion source files directly in memory, without creating .class files.
Usage¶
Options¶
-classpath <classpath>¶
Set the classpath for compilation and execution.
-encoding <encoding>¶
Specify the character encoding of source files. Default is UTF-8.
-maxErrorReport <count>¶
Limit the number of compilation errors reported.
-super <super class>¶
Specify the class a top-level script's synthesized class extends. Only meaningful when the source has no explicit class declaration.
--verbose¶
Show timing for each compilation phase (parsing, rewriting, type checking, code generation) before the script runs.
--dump-ast¶
Print the parsed AST to stderr before running the script.
--dump-typed-ast¶
Print a typed AST summary (classes, fields, methods) to stderr before running the script.
--profile-compile¶
Emit a compile profile before the script is executed.
--profile-format <text|json>¶
Choose text or JSON profile output.
--profile-output <target>¶
Write the compile profile to stderr, stdout, or a file path.
onion --profile-compile --profile-format json \
--profile-output target/script-profile.json \
MyScript.on
--warn <off|on|error>¶
Control warning reporting. error treats warnings as compilation errors.
--Wno <codes>¶
Suppress specific warning categories by code or name.
--no-check-laws¶
Do not execute a record's law / example clauses.
--law-seed <n> / --law-samples <n>¶
Control how law clauses are sampled; a falsified law reports the settings that
produced its counterexample.
--effects¶
Print each compiled method's inferred effect set (read write net exec env clock rand console unknown; empty means pure) to stderr.
--stacktrace¶
Print the raw JVM trace for an uncaught runtime error instead of the rendered diagnostic report.
--watch¶
Run the script, then re-run it automatically whenever its file changes. Compile errors and runtime exceptions are printed without stopping the watch loop; press Ctrl-C to stop.
Dependencies (//> using dep)¶
A script declares the Maven libraries it needs in its own header, in scala-cli's
syntax, instead of taking them from -classpath:
//> using dep "org.apache.poi:poi-ooxml:5.5.1"
import { org.apache.poi.xssf.usermodel.XSSFWorkbook }
tool book(dst: String): Int requires { write(dst), unknown } {
val wb = new XSSFWorkbook()
wb.createSheet("report").createRow(0).createCell(0).setCellValue("hello")
val bytes = new java.io.ByteArrayOutputStream()
wb.write(bytes)
wb.close()
Files::writeBytes(dst, bytes.toByteArray()) // the `write(dst)` the tool declares
return 0
}
A library that is not on Maven Central needs its repository too:
//> using repository "https://nexus.example.com/repository/maven-public" // optional
//> using dep "com.example.internal:ledger:2.3.0"
using deptakes one or more"group:artifact:version"coordinates, andusing repositoryone or more absolutehttp,httpsorfileURLs. Values may be quoted or bare, and a trailing// commentis allowed.depsandrepositoriesare accepted as the plural spellings.- Repositories are searched before Maven Central, in the order written, exactly as a
project's
[[repositories]]. Resolution is the same coursier resolver a project uses. - The resolved jars, transitives included, are on the classpath for compiling and for
running, including with
--effects, a tool's--plan/--help,--watch, andONION_DAEMON=1(the daemon compiles with the same jars). - Directives are read only from the leading comment block: an optional
#!line, then blank lines and comments, before any code. A//>line there must be a well-formed directive, and a//> usingline after code is an error, not an ignored comment. A malformed directive names the script, line and column and stops the run before compiling. - Versions must be exact: a range (
[1.0,2.0)),latest.*,LATEST,RELEASEor1.+is rejected, as is the same module at two versions. This is the ruleonion.toml's[dependencies]applies, from the same code, so a coordinate a project accepts a script accepts too. Scala'sgroup::artifactform is not supported; name the full artifact. - Download progress is printed to stderr.
onioncreads the same directives from the files it compiles, through the same parser and cache (see the compiler), andonionc --print-classpathprints the classpath they resolve to.- The language server reads them too: a standalone script is validated against the jars its directives resolve to (see the language server).
A script has no lock file. The directives pin each direct dependency, but transitive
versions are whatever resolution picks at the time, so two machines can run the same script
against different transitive jars. When that matters, make it a
project: [dependencies] in onion.toml plus a committed onion.lock.
Resolving through coursier costs about a second even when everything is already downloaded,
so the resolved classpath is cached per set of directives (a hash of the sorted
dependencies and the ordered repositories) in script-deps/ under Onion's cache directory,
and reused as long as every jar it lists still exists. The cache directory is
$ONION_CACHE_DIR (or -Donion.cache.dir) when set, else %LOCALAPPDATA%\onion\cache on
Windows, ~/Library/Caches/onion on macOS and $XDG_CACHE_HOME/onion (~/.cache/onion)
elsewhere. Deleting it is always safe; the next run resolves afresh.
Program Arguments¶
Arguments after the source file(s) are passed to your program:
Access them in your code:
class MyScript {
public:
static def main(args :String[]): void {
foreach arg :String in args {
println("Argument: " + arg)
}
}
}
Entry Point¶
The script runner determines the entry point automatically:
1. Explicit Main Method¶
If a class has a main method, it's used as the entry point:
class MyProgram {
public:
static def main(args :String[]): void {
println("Hello from main method")
}
}
2. First Class with Main¶
If multiple classes have main methods, the first one is used:
class First {
public:
static def main(args :String[]): void {
println("This will run")
}
}
class Second {
public:
static def main(args :String[]): void {
println("This won't run")
}
}
3. Top-Level Declarations and Expressions¶
If there's no explicit main method, the first top-level declaration or expression is the entry point:
println("Hello, World!")
val x: Int = 10
println("x = " + x)
// These block elements execute immediately
Examples¶
Simple Script¶
hello.on:
Run:
With Arguments¶
greet.on:
class Greeter {
public:
static def main(args :String[]): void {
if args.length > 0 {
println("Hello, " + args[0] + "!")
} else {
println("Hello, stranger!")
}
}
}
Run:
Quick Calculations¶
calc.on:
Run:
File Processing¶
count_lines.on:
import {
java.io.BufferedReader;
java.io.FileReader;
}
class LineCounter {
public:
static def main(args :String[]): void {
if args.length == 0 {
println("Usage: onion count_lines.on <filename>")
return
}
val filename: String = args[0]
val reader: BufferedReader = new BufferedReader(
new FileReader(filename)
)
var count: Int = 0
var line: String = null
while (line = reader.readLine()) != null {
count = count + 1
}
reader.close()
println("Lines: " + count)
}
}
Run:
In-Memory Compilation¶
The onion command:
- Compiles source files to bytecode
- Loads classes into memory
- Executes the entry point
- No
.classfiles are created
This is ideal for: - Quick scripts - Testing code snippets - Automation tasks - One-off programs
Multiple Source Files¶
Compile and run multiple files:
All files are compiled together, and the entry point is determined from the first file.
Error Handling¶
Compilation Errors¶
Runtime Errors¶
$ onion runtime_error.on
Exception in thread "main" java.lang.ArithmeticException: / by zero
at RuntimeError.main(runtime_error.on:10)
Comparison with onionc¶
| Feature | onion | onionc |
|---|---|---|
| Creates .class files | No | Yes |
| Execution | Immediate | Requires java command |
| Use case | Scripts, testing | Production, libraries |
| Speed | Fast for small programs | Better for repeated runs |
| Distribution | Requires source | Can distribute .class/.jar |
Scripting Best Practices¶
Shebang Line (Unix-like systems)¶
Make scripts executable:
hello.on:
Make executable:
Error Messages¶
Provide helpful error messages:
class Script {
public:
static def main(args :String[]): void {
if args.length < 2 {
println("Error: Missing arguments")
println("Usage: onion script.on <input> <output>")
return
}
// Process arguments...
}
}
Exit Codes¶
Return appropriate exit codes:
class Script {
public:
static def main(args :String[]): void {
if args.length == 0 {
System::exit(1) // Error
}
// Success
System::exit(0)
}
}
Next Steps¶
- Compiler (onionc) - Compile to class files
- REPL Shell - Interactive programming
- Examples - Example scripts