Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .seal/guided_tour.luau
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ type SpawningChildProcesses = {}; local back: TableOfContents do
args = { "--version" }
}

print(if result.ok then result.stdout
else result.stderr)
print(if result.ok then result.stdout else result.stderr)
-- if you want to assume your process run attempt succeeded (and error otherwise), use :unwrap()
local stdout = result:unwrap()

Expand All @@ -142,7 +141,7 @@ type SpawningChildProcesses = {}; local back: TableOfContents do
local handle = process.spawn {
program = `seal eval '{waiting_src}'`,
shell = if env.os == "Windows" then "pwsh" else "sh"
}
} :: process.PipedChild

local text = ""
while handle:alive() do
Expand Down
200 changes: 173 additions & 27 deletions .seal/typedefs/std/process.luau
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
local child = process.spawn({
program = "someutil --watch",
shell = "sh",
})
}) :: process.PipedChild

for line in child.stdout:lines() do
local thing_changed = line:match("([%w]+) changed!")
Expand Down Expand Up @@ -97,25 +97,30 @@ export type process = {
shell: (command: string) -> RunResult,
--[=[
Spawns a long-running process in a non-blocking manner, returns a `ChildProcess` that contains handles to the spawned process' stdout, stderr, and stdin.

There are three primary usecases for `process.spawn`:
1. Listening to the output of a long-running process.
2. Running another program in the background while your program does work.
3. Launching multiple instances of the same program in parallel.

## Usage

A long-running program you want to listen to.
Listen to the output of a long-running process:

```luau
local process = require("@std/process")
local child = process.spawn({
program = "someutil --watch",
shell = "sh",
})
}) :: process.PipedChild

for line in child.stdout:lines() do
local thing_changed = line:match("([%w]+) changed!")
print(`Change detected: {thing_changed}`)
end
```

Run multiple processes at the same time to finish faster.
Run multiple processes at the same time to finish faster:

For example, `markdownlint-cli2` takes about 0.3 seconds to fix a file, so if you have 20+ files, and you
fix each file one-at-a-time with `process.run`, it'll take 6+ seconds to do all files.
Expand All @@ -125,14 +130,14 @@ export type process = {

```luau
local paths: { string } = get_files()
-- make a threadpool to keep track of what files are finished
local handles: { [string]: process.ThreadHandle } = {}
-- make a childpool to keep track of what files are finished
local handles: { [string]: process.PipedChild } = {}

for _, path in paths do
local handle = process.spawn {
program = "markdownlint-cli2",
args = { "--fix", path }
}
} :: process.PipedChild
handles[path] = handle
end

Expand All @@ -148,10 +153,6 @@ export type process = {
```
]=]
spawn: (options: SpawnOptions) -> ChildProcess,
--[=[
Doesn't work.
]=]
setexitcallback: ((number) -> ()) -> (),
--[=[
Immediately terminate the current program with exit `code`.

Expand Down Expand Up @@ -193,26 +194,97 @@ export type Err = {
stderr: string,
}

export type Stdio = "Pipe" | "Inherit" | "Ignore"

--[=[
Options for `process.run`; implicitly added to `process.shell` as well.
]=]
export type RunOptions = {
--- the program you want to run, must be available in $PATH or be an absolute path to an executable
program: string,
--- an optional list of arguments to pass into the program; on Windows, you should use this instead of trying to pass whitespace-separated arguments in `program`
args: { string }?,
--- specify a shell to run the program with; otherwise runs it as a bare process with no shell
shell: string?,
--[=[
Specify a shell to run `program` with; if unspecified or nil,
defaults to running it as a bare process with no shell (behavior may differ on Windows vs. Unix-like).

If `shell == true`, performs some magic (like checking `$SHELL`) to try and figure out
what your current shell is, defaulting to WindowsPowerShell on Windows and `sh` on Unix-like.

Be careful with shell escapes w/ user-provided values; can be a security risk.
]=]
shell: (true | string)?,
--- path to the the working directory you want your command to execute in, defaults to your shell's cwd
cwd: string?,
--[=[
Control whether the child process reads and writes to its own terminal (`"Pipe"`) or
to the terminal you're running *seal* in (`"Inherit"`).

By default, stdio is *piped*; this means the process reads and writes from separate
stdin, stdout, and stderr streams that you can read once the process completes.

If you want everything the process says to be displayed on your terminal (and not
read from the process programmatically), use `"Inherit"`.

If you want anything the process says to get ignored (redirected to `/dev/null`), use `"Ignore"`.

This field `stdio` can be either a string (if you want all streams to share the same behavior)
or a table (if you want different per-stream behavior).
]=]
stdio: {
stdout: Stdio?,
stderr: Stdio?,
stdin: Stdio?,
}? | Stdio?,
--[=[
Override environment variables of the spawned `ChildProcess`.
By default, the spawned process inherits its environment from the current process;
so all environment variables visible to *seal* will be visible to it unless removed or cleared.

- To clear *all* environment variables for the spawned `ChildProcess` so
that the spawned process does not inherit from *seal*'s environment, set `env.clear = true`.
- To add additional environment variables, define a map of them (`{ [string]: string }`)
in `env.add`.
- To prevent specific environment variables from being inherited
by the child process, list them in `env.remove`.

The order is clear, add, remove (so clearing the environment doesn't prevent variables from being
added).

## Errors

- if you pass a table to `env` without keys `clear`, `add`, and/or `remove`.
- if `env.add` is present and isn't `{ [string]: string }`
- if `env.remove` is present and isn't `{ string }`.
]=]
env: {
clear: boolean?,
add: {
[string]: string
}?,
remove: { string }?,
}?
}

export type SpawnOptions = {
--- the program you want to run, must be available in $PATH or be an absolute path to an executable
program: string,
--- an optional list of arguments to pass into the program; on Windows, you should use this instead of trying to pass whitespace-separated arguments in `program`
args: { string }?,
shell: string?,
--[=[
Specify a shell to run `program` with; if unspecified or nil,
defaults to running it as a bare process with no shell (behavior may differ on Windows vs. Unix-like).

If `shell == true`, performs some magic (like checking `$SHELL`) to try and figure out
what your current shell is, defaulting to WindowsPowerShell on Windows and `sh` on Unix-like.

Be careful with shell escapes w/ user-provided values; can be a security risk.
]=]
shell: (true | string)?,
--- path to the the working directory you want your command to execute in, defaults to your shell's cwd
cwd: string?,
--[=[
A `ChildProcessStream` captures incoming bytes from your `ChildProcess`' output streams (either stdout or stderr),
A `ChildProcessStream` captures incoming bytes from a `ChildProcess`' output stream (either stdout or stderr),
and caches them in its `inner` buffer. Each stream is spawned in a separate Rust thread to facilitate
consistently nonblocking, dependable reads, allowing most `ChildProcess.stream:read` methods to be fully nonblocking unless
specifically requested otherwise.
Expand Down Expand Up @@ -243,11 +315,60 @@ export type SpawnOptions = {
stdout_capacity: number?,
--- inner buffer capacity of `ChildProcess.stderr`, default 1024
stderr_capacity: number?,
--- what side of stdout should be truncated when full? defaults to "front"
stdout_truncate: ("front" | "back")?,
--- what side of stderr should be truncated when full? defaults to "front"
stderr_truncate: ("front" | "back")?,
--- what side of stdout should be truncated when full? defaults to "Front"
stdout_truncate: ("Front" | "Back")?,
--- what side of stderr should be truncated when full? defaults to "Front"
stderr_truncate: ("Front" | "Back")?,
}?,
--[=[
Control whether the child process reads and writes to its own terminal (`"Pipe"`) or
to the terminal you're running *seal* in (`"Inherit"`).

By default, the child process's stdio is *piped*; this means the process
reads and writes from separate stdin, stdout, and stderr streams that you
can access through the `ChildProcessStream` API.

If you want everything the process says to be displayed on your terminal (and not
read from the process programmatically), use `"Inherit"`.

If you want anything the `ChildProcess` says to get ignored (redirected to `/dev/null`), use `"Ignore"`.

This field `stdio` can be either a string (if you want all streams to share the same behavior)
or a table (if you want different per-stream behavior).
]=]
stdio: {
stdout: Stdio?,
stderr: Stdio?,
stdin: Stdio?,
}? | Stdio?,
--[=[
Override environment variables of the spawned `ChildProcess`.
By default, the spawned process inherits its environment from the current process;
so all environment variables visible to *seal* will be visible to it unless removed or cleared.

- To clear *all* environment variables for the spawned `ChildProcess` so
that the spawned process does not inherit from *seal*'s environment, set `env.clear = true`.
- To add additional environment variables, define a map of them (`{ [string]: string }`)
in `env.add`.
- To prevent specific environment variables from being inherited
by the child process, list them in `env.remove`.

The order is clear, add, remove (so clearing the environment doesn't prevent variables from being
added).

## Errors

- if you pass a table to `env` without keys `clear`, `add`, and/or `remove`.
- if `env.add` is present and isn't `{ [string]: string }`
- if `env.remove` is present and isn't `{ string }`.
]=]
env: {
clear: boolean?,
add: {
[string]: string
}?,
remove: { string }?,
}?
}
--- Represents the stdout and stderr streams of a `ChildProcess`, both ran in parallel threads
--- and streamed for nonblocking behavior.
Expand Down Expand Up @@ -433,7 +554,7 @@ export type ChildProcessStream = setmetatable<{
local child = process.spawn({
program = "someutil --watch",
shell = "sh",
})
}) :: process.PipedChild

for line in child.stdout:lines() do
local thing_changed = line:match("([%w]+) changed!")
Expand All @@ -450,9 +571,11 @@ export type ChildProcessStream = setmetatable<{
shell = "sh",
}

local next_line = child.stdout:lines()
local first_line = next_line()
local second_line = next_line()
if child.stdout then
local next_line = child.stdout:lines()
local first_line = next_line()
local second_line = next_line()
end
```
]=]
lines: (self: ChildProcessStream, timeout: number?) -> (() -> string),
Expand Down Expand Up @@ -495,19 +618,42 @@ type ChildProcessStdin = {
```luau
local child = process.spawn {
program = "python3",
args = { "-" },
args = { "-" }, -- allows python3 to accept source code from stdin
}
assert(child.stdin ~= nil, "stdin is piped")

child.stdin:write(PYTHON_SRC)
child.stdin:close()
child.stdin:close() -- send EOF to python so it evaluates the source code
```
]=]
close: (self: ChildProcessStdin) -> (),
}

--[=[
A handle to a child process created by `process.spawn`, with optional handles
to the child's stdout, stderr, and stdin output/input streams.

By default, stdout, stderr, and stdin are piped; cast the result of `process.spawn` to
`process.PipedChild` to reflect this.
]=]
export type ChildProcess = {
--- The process id (`pid`) of the spawned `ChildProcess`
id: number,
alive: (self: ChildProcess | PipedChild) -> boolean,
kill: (self: ChildProcess | PipedChild) -> (),
stdout: ChildProcessStream?,
stderr: ChildProcessStream?,
stdin: ChildProcessStdin?,
}

--[=[
Default kind of `ChildProcess` created by `process.spawn` pipes for stdout, stderr, and stdin.
]=]
export type PipedChild = {
--- The process id (`pid`) of the spawned `ChildProcess`
id: number,
alive: (self: ChildProcess) -> boolean,
kill: (self: ChildProcess) -> (),
alive: (self: ChildProcess | PipedChild) -> boolean,
kill: (self: ChildProcess | PipedChild) -> (),
stdout: ChildProcessStream,
stderr: ChildProcessStream,
stdin: ChildProcessStdin,
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@
},
"luau-lsp.inlayHints.variableTypes": false,
"luau-lsp.plugin.enabled": false,
"stylua.disableVersionCheck": true,
}
Loading