Add lyng.io.process module for process execution utilities and shell scripting sugar (e.g., sh, exec, CommandRun).

This commit is contained in:
sergeych committed 2026-05-10 15:04:32 +03:00
1 parent 3e74019d9d
commit 234d1ef02b
8 files changed
+611 -7

No files matched your search

+4
View File
@@ -88,6 +88,10 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
Requires installing `lyngio` into the import manager from host code.
- `import lyng.io.fs` (filesystem `Path` API)
- `import lyng.io.process` (process execution API)
- Shell-script sugar: `sh(command): CommandRun` and `exec(executable, args=[]): CommandRun`.
- Prefer `sh("git status --short").out` for small shell output, `sh("...").lines` for large stdout streams, and `.check()` for commands that must exit with code 0.
- Prefer `exec("git", ["add", file])` when arguments come from data, filenames, or user input; it bypasses shell parsing.
- `CommandRun` is active and owns process pipes; choose one consumption path per stream (`out` or `lines`, `err` or `errorLines`).
- `import lyng.io.console` (console capabilities, geometry, ANSI/output, events)
- `import lyng.io.http` (HTTP/HTTPS client API)
- `import lyng.io.http.server` (minimal HTTP/1.1 and WebSocket server API)
+267 -6
View File
@@ -3,6 +3,7 @@
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:** `lyngio` is a separate library module. It must be explicitly added as a dependency to your host application and initialized in your Lyng scopes.
> The reference `lyng`/`jlyng` CLI installs this module in its import manager, so CLI scripts can use `import lyng.io.process` directly.
---
@@ -44,24 +45,241 @@ suspend fun bootstrapProcess() {
---
#### Using from Lyng scripts
#### Shell-script shorthand
Most CLI scripts should start with the shorthand API:
```lyng
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:
```lyng
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:
```lyng
sh("lyng fmt --check examples/*.lyng").check()
sh("git diff --check").check()
```
Use `.wait()` when the exit code is data and should not raise:
```lyng
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:
```lyng
for (file in sh("git ls-files").lines) {
if (file.endsWith(".lyng")) {
println(file)
}
}
```
You can combine it with normal iterable helpers:
```lyng
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:
```lyng
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.
```lyng
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.
- `.out` captures stdout into memory, waits for the process to finish, and also drains stderr concurrently.
- `.err` captures 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.
- `.lines` streams stdout line by line and does not buffer the whole output.
- `.errorLines` streams 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:
```lyng
val version = sh("git describe --tags --always").out.trim()
```
For large output, stream:
```lyng
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:
```lyng
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:
```lyng
val currentCommit = exec("git", ["rev-parse", "--short", "HEAD"]).out.trim()
```
Run a command for its side effects and fail on error:
```lyng
exec("mkdir", ["-p", "build/reports"]).check()
sh("lyng fmt --check src/**/*.lyng").check()
```
Filter a long output stream:
```lyng
for (path in sh("git ls-files").lines) {
if (path.endsWith(".lyng")) {
println(path)
}
}
```
Branch on a command status:
```lyng
if (exec("git", ["diff", "--quiet"]).wait() != 0) {
println("working tree has changes")
}
```
Keep user-controlled text out of the shell:
```lyng
val file = ARGV[0] as String
exec("git", ["log", "--oneline", "--", file]).check()
```
---
#### `sh` vs `exec`
Use `sh(...)` when you intentionally want shell syntax:
```lyng
sh("git status --short | wc -l").out.trim()
sh("find . -name '*.lyng' | sort").check()
```
Use `exec(...)` when you already have structured arguments:
```lyng
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:
```lyng
import lyng.io.process
// Execute a process with arguments
val p = Process.execute("ls", ["-l", "/tmp"])
for (line in p.stdout) {
for (line in p.stdout()) {
println("OUT: " + line)
}
val exitCode = p.waitFor()
println("Process exited with: " + exitCode)
// Run a shell command
val sh = Process.shell("echo 'Hello from shell' | wc -w")
for (line in sh.stdout) {
val shellProcess = Process.shell("echo 'Hello from shell' | wc -w")
for (line in shellProcess.stdout()) {
println("Word count: " + line.trim())
}
```
Signals and termination:
```lyng
val server = Process.shell("python3 -m http.server 8080")
delay(1000)
server.signal("SIGTERM")
val code = server.waitFor()
println("server exited: " + code)
```
---
#### Platform information
```lyng
import lyng.io.process
// Platform information
val details = Platform.details()
@@ -77,15 +295,57 @@ if (Platform.isSupported()) {
---
#### Executable script example
```lyng
#!/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/sh` or `cmd.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 after `wait`, `check`, `out`, or `err`; otherwise `null`.
- `ok: Bool?` — `true` for exit code 0, `false` for non-zero, or `null` before 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.
- `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.
@@ -110,6 +370,7 @@ Example of a restricted policy in Kotlin:
```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
+10
View File
@@ -4,6 +4,7 @@ The Lyng CLI is the reference command-line tool for the Lyng language. It lets y
- Run Lyng scripts from files or inline strings (shebangs accepted)
- Use standard argument passing (`ARGV`) to your scripts.
- Import `lyng.io.process` for shell-script style process execution (`sh`, `exec`, and `CommandRun`).
- Resolve local file imports from the executed script's directory tree.
- Format Lyng source files via the built-in `fmt` subcommand.
- Register synchronous process-exit handlers with `atExit(...)`.
@@ -81,6 +82,15 @@ lyng -- -my-script.lyng arg1 arg2
lyng -x "println(\"Hello\")" more args
```
The CLI installs `lyng.io.process`, so scripts can use the shell/process shorthand after importing it:
```lyng
import lyng.io.process
val branch = sh("git branch --show-current").out.trim()
exec("git", ["status", "--short"]).check()
```
- Print version/help:
```
+1 -1
View File
@@ -14,7 +14,7 @@
- **[lyng.io.db](lyng.io.db.md):** Portable SQL database access. Provides `Database`, `SqlTransaction`, `ResultSet`, SQLite support through `lyng.io.db.sqlite`, and JVM JDBC support through `lyng.io.db.jdbc`.
- **[lyng.io.fs](lyng.io.fs.md):** Async filesystem access. Provides the `Path` class for file/directory operations, streaming, and globbing.
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides shell-script sugar (`sh`, `exec`, `CommandRun`), low-level `Process`/`RunningProcess`, and `Platform` information.
- **[lyng.io.console](lyng.io.console.md):** Rich console/TTY access. Provides `Console` capability detection, geometry, output, and iterable events.
- **[lyng.io.http](lyng.io.http.md):** HTTP/HTTPS client access. Provides `Http`, `HttpRequest`, `HttpResponse`, and `HttpHeaders`.
- **[lyng.io.http.server](lyng.io.http.server.md):** Minimal HTTP/1.1 and WebSocket server. Provides `HttpServer`, `Router`, `ServerRequest`, `RequestContext`, and `ServerWebSocket`.