12 KiB
lyng.io.process — Process execution and control for Lyng scripts
This module provides a way to run external processes and shell commands from Lyng scripts. It is designed to be multiplatform and uses coroutines for non-blocking execution.
Note:
lyngiois a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes. The referencelyng/jlyngCLI installs this module in its import manager, so CLI scripts can useimport lyng.io.processdirectly.
Add the library to your project (Gradle)
If you use this repository as a multi-module project, add a dependency on :lyngio:
dependencies {
implementation("net.sergeych:lyngio:0.0.1-SNAPSHOT")
}
For external projects, ensure you have the appropriate Maven repository configured (see lyng.io.fs documentation).
Install the module into a Lyng session
The process module is not installed automatically. The preferred host runtime is EvalSession: create the session, get its underlying scope, install the module there, and execute scripts through the session. You can customize access control via ProcessAccessPolicy.
Kotlin (host) bootstrap example:
import net.sergeych.lyng.Scope
import net.sergeych.lyng.EvalSession
import net.sergeych.lyng.io.process.createProcessModule
import net.sergeych.lyngio.process.security.PermitAllProcessAccessPolicy
suspend fun bootstrapProcess() {
val session = EvalSession()
val scope: Scope = session.getScope()
createProcessModule(PermitAllProcessAccessPolicy, scope)
// In scripts (or via session.eval), import the module:
session.eval("import lyng.io.process")
}
Shell-script shorthand
Most CLI scripts should start with the shorthand API:
import lyng.io.process
val branch = sh("git branch --show-current").out.trim()
println("Branch: " + branch)
sh(command) starts command through the platform shell and returns an active CommandRun.
The process is already running when the handle is returned. The handle gives you both convenient capture properties and streaming line flows:
val run = sh("git status --short")
println(run.out) // capture all stdout as String
println(run.err) // capture all stderr as String
println(run.code) // known after out/err/wait/check, otherwise null
println(run.ok) // true, false, or null
Use .check() when a non-zero exit code should fail the script:
sh("lyng fmt --check examples/*.lyng").check()
sh("git diff --check").check()
Use .wait() when the exit code is data and should not raise:
val code = sh("git diff --quiet").wait()
if (code == 0) {
println("clean")
} else {
println("changed")
}
Use .lines for output that may be large. It streams stdout instead of buffering the entire output:
for (file in sh("git ls-files").lines) {
if (file.endsWith(".lyng")) {
println(file)
}
}
You can combine it with normal iterable helpers:
val count = sh("git ls-files").lines.count {
it.endsWith(".lyng")
}
println("Lyng files: " + count)
Use exec(executable, args) when arguments come from data, filenames, or user input:
val file = ARGV[0] as String
exec("git", ["add", file]).check()
exec(...) bypasses the shell, so arguments are passed as argv entries rather than parsed by shell quoting rules.
val status = exec("git", ["status", "--short"]).out
println(status)
Capture vs streaming
CommandRun is active: it owns a running process. Choose the consumption style intentionally.
.outcaptures stdout into memory, waits for the process to finish, and also drains stderr concurrently..errcaptures stderr into memory, waits for the process to finish, and also drains stdout concurrently..check()captures both streams, waits, and fails if the exit code is non-zero..linesstreams stdout line by line and does not buffer the whole output..errorLinesstreams stderr line by line and does not buffer the whole error output..wait()only waits and returns the exit code.
Treat a CommandRun as a one-shot stream owner. Pick one stdout consumption path (out or lines) and one stderr consumption path (err or errorLines). Do not collect the same stream twice; it is backed by the process pipe.
For small output, capture is simplest:
val version = sh("git describe --tags --always").out.trim()
For large output, stream:
for (line in sh("find . -type f").lines) {
if (line.endsWith(".lyng")) {
println(line)
}
}
For commands that may write a lot to both stdout and stderr, consume both streams. One practical pattern is to collect stderr in a background task while processing stdout:
val r = sh("some-command-that-writes-a-lot")
val errors: List<String> = []
val stderrTask = launch {
for (line in r.errorLines) {
errors.add(line)
}
}
for (line in r.lines) {
println("OUT: " + line)
}
val code = r.wait()
stderrTask.await()
if (code != 0) {
println(errors.joinToString("\n"))
exit(code)
}
As with ordinary shell programming, avoid using .out for unbounded output. It is meant for command results that comfortably fit in memory.
Common script patterns
Capture one small value:
val currentCommit = exec("git", ["rev-parse", "--short", "HEAD"]).out.trim()
Run a command for its side effects and fail on error:
exec("mkdir", ["-p", "build/reports"]).check()
sh("lyng fmt --check src/**/*.lyng").check()
Filter a long output stream:
for (path in sh("git ls-files").lines) {
if (path.endsWith(".lyng")) {
println(path)
}
}
Branch on a command status:
if (exec("git", ["diff", "--quiet"]).wait() != 0) {
println("working tree has changes")
}
Keep user-controlled text out of the shell:
val file = ARGV[0] as String
exec("git", ["log", "--oneline", "--", file]).check()
sh vs exec
Use sh(...) when you intentionally want shell syntax:
sh("git status --short | wc -l").out.trim()
sh("find . -name '*.lyng' | sort").check()
Use exec(...) when you already have structured arguments:
exec("git", ["commit", "-m", message]).check()
exec("cp", [sourcePath, targetPath]).check()
This distinction matters for safety. Shell commands are parsed by /bin/sh -c on Unix-like systems or the platform shell on supported platforms. If you interpolate untrusted text into a shell string, it can change the command being run. Prefer exec(...) for user input and filenames.
Low-level process API
The shorthand API is built on top of Process.execute(...) and Process.shell(...). Use the low-level API when you need direct process control, including explicit signals or forceful termination:
import lyng.io.process
// Execute a process with arguments
val p = Process.execute("ls", ["-l", "/tmp"])
for (line in p.stdout()) {
println("OUT: " + line)
}
val exitCode = p.waitFor()
println("Process exited with: " + exitCode)
// Run a shell command
val shellProcess = Process.shell("echo 'Hello from shell' | wc -w")
for (line in shellProcess.stdout()) {
println("Word count: " + line.trim())
}
Signals and termination:
val server = Process.shell("python3 -m http.server 8080")
delay(1000)
server.signal("SIGTERM")
val code = server.waitFor()
println("server exited: " + code)
Platform information
import lyng.io.process
// Platform information
val details = Platform.details()
println("OS: " + details.name + " " + details.version + " (" + details.arch + ")")
if (details.kernelVersion != null) {
println("Kernel: " + details.kernelVersion)
}
if (Platform.isSupported()) {
println("Processes are supported!")
}
Executable script example
#!/usr/bin/env lyng
import lyng.io.process
if (ARGV.size == 0) {
println("usage: changed-files.lyng <extension>")
exit(2)
}
val ext = ARGV[0] as String
var matches = 0
for (file in sh("git status --short").lines) {
if (file.endsWith(ext)) {
println(file)
matches++
}
}
println("matched: " + matches)
API Reference
Process (static methods)
execute(executable: String, args: List<String>): RunningProcess— Start an external process.shell(command: String): RunningProcess— Run a command through the system shell (e.g.,/bin/shorcmd.exe).
Shorthand functions
sh(command: String): CommandRun— Run a command through the system shell and return an active handle.exec(executable: String, args: List<String> = []): CommandRun— Run an executable with argv-style arguments and return an active handle.
CommandRun
command: String— Original command display text.lines: Flow<String>— Streaming stdout lines. Use for large output.errorLines: Flow<String>— Streaming stderr lines. Use for large error output.out: String— Captured stdout. Captures both stdout and stderr concurrently and waits for completion.err: String— Captured stderr. Captures both stdout and stderr concurrently and waits for completion.wait(): Int— Wait for the command to exit and return the exit code.check(): CommandRun— Capture output, wait, and fail if the exit code is non-zero.code: Int?— Known exit code afterwait,check,out, orerr; otherwisenull.ok: Bool?—truefor exit code 0,falsefor non-zero, ornullbefore the exit code is known.
RunningProcess (instance methods)
stdout(): Flow— Standard output stream as a Lyng Flow of lines.stderr(): Flow— Standard error stream as a Lyng Flow of lines.waitFor(): Int— Wait for the process to exit and return the exit code.signal(name: String)— Send a signal to the process (e.g.,"SIGINT","SIGTERM","SIGKILL").destroy()— Forcefully terminate the process.
Platform (static methods)
details(): Map— Get platform details. Returned map keys:name,version,arch,kernelVersion.isSupported(): Bool— True if process execution is supported on the current platform.
Security Policy
Process execution is a sensitive operation. lyngio uses ProcessAccessPolicy to control access to execute and shell operations.
ProcessAccessPolicy— Interface for custom policies.PermitAllProcessAccessPolicy— Allows all operations.ProcessAccessOp(sealed) — Operations to check:Execute(executable, args)Shell(command)
Example of a restricted policy in Kotlin:
import net.sergeych.lyngio.fs.security.AccessDecision
import net.sergeych.lyngio.fs.security.AccessContext
import net.sergeych.lyngio.fs.security.Decision
import net.sergeych.lyngio.process.security.ProcessAccessOp
import net.sergeych.lyngio.process.security.ProcessAccessPolicy
val restrictedPolicy = object : ProcessAccessPolicy {
override suspend fun check(op: ProcessAccessOp, ctx: AccessContext): AccessDecision {
return when (op) {
is ProcessAccessOp.Execute -> {
if (op.executable == "ls") AccessDecision(Decision.Allow)
else AccessDecision(Decision.Deny, "Only 'ls' is allowed")
}
is ProcessAccessOp.Shell -> AccessDecision(Decision.Deny, "Shell is forbidden")
}
}
}
createProcessModule(restrictedPolicy, scope)
Platform Support
- JVM: Full support using
ProcessBuilder. - Native (Linux/macOS): Support via POSIX.
- Windows: Support planned.
- Android/JS/iOS/Wasm: Currently not supported;
isSupported()returnsfalseand attempts to run processes will throwUnsupportedOperationException.