Skip to content

Commit 8e1df65

Browse files
authored
process.spawn: add stdio options and environment variables (#146) (breaking typedef changes)
Breaking changes to the type definitions of `process.ChildProcess`; now you need to nilcheck `child.stdout/stdin/stderr` or cast to `process.PipedChild`
1 parent 58581af commit 8e1df65

File tree

23 files changed

+1249
-431
lines changed

23 files changed

+1249
-431
lines changed

.seal/guided_tour.luau

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,7 @@ type SpawningChildProcesses = {}; local back: TableOfContents do
127127
args = { "--version" }
128128
}
129129

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

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

147146
local text = ""
148147
while handle:alive() do

.seal/typedefs/std/process.luau

Lines changed: 173 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
local child = process.spawn({
3030
program = "someutil --watch",
3131
shell = "sh",
32-
})
32+
}) :: process.PipedChild
3333
3434
for line in child.stdout:lines() do
3535
local thing_changed = line:match("([%w]+) changed!")
@@ -97,25 +97,30 @@ export type process = {
9797
shell: (command: string) -> RunResult,
9898
--[=[
9999
Spawns a long-running process in a non-blocking manner, returns a `ChildProcess` that contains handles to the spawned process' stdout, stderr, and stdin.
100+
101+
There are three primary usecases for `process.spawn`:
102+
1. Listening to the output of a long-running process.
103+
2. Running another program in the background while your program does work.
104+
3. Launching multiple instances of the same program in parallel.
100105
101106
## Usage
102107
103-
A long-running program you want to listen to.
108+
Listen to the output of a long-running process:
104109
105110
```luau
106111
local process = require("@std/process")
107112
local child = process.spawn({
108113
program = "someutil --watch",
109114
shell = "sh",
110-
})
115+
}) :: process.PipedChild
111116
112117
for line in child.stdout:lines() do
113118
local thing_changed = line:match("([%w]+) changed!")
114119
print(`Change detected: {thing_changed}`)
115120
end
116121
```
117122
118-
Run multiple processes at the same time to finish faster.
123+
Run multiple processes at the same time to finish faster:
119124
120125
For example, `markdownlint-cli2` takes about 0.3 seconds to fix a file, so if you have 20+ files, and you
121126
fix each file one-at-a-time with `process.run`, it'll take 6+ seconds to do all files.
@@ -125,14 +130,14 @@ export type process = {
125130
126131
```luau
127132
local paths: { string } = get_files()
128-
-- make a threadpool to keep track of what files are finished
129-
local handles: { [string]: process.ThreadHandle } = {}
133+
-- make a childpool to keep track of what files are finished
134+
local handles: { [string]: process.PipedChild } = {}
130135
131136
for _, path in paths do
132137
local handle = process.spawn {
133138
program = "markdownlint-cli2",
134139
args = { "--fix", path }
135-
}
140+
} :: process.PipedChild
136141
handles[path] = handle
137142
end
138143
@@ -148,10 +153,6 @@ export type process = {
148153
```
149154
]=]
150155
spawn: (options: SpawnOptions) -> ChildProcess,
151-
--[=[
152-
Doesn't work.
153-
]=]
154-
setexitcallback: ((number) -> ()) -> (),
155156
--[=[
156157
Immediately terminate the current program with exit `code`.
157158
@@ -193,26 +194,97 @@ export type Err = {
193194
stderr: string,
194195
}
195196

197+
export type Stdio = "Pipe" | "Inherit" | "Ignore"
198+
199+
--[=[
200+
Options for `process.run`; implicitly added to `process.shell` as well.
201+
]=]
196202
export type RunOptions = {
203+
--- the program you want to run, must be available in $PATH or be an absolute path to an executable
197204
program: string,
198205
--- 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`
199206
args: { string }?,
200-
--- specify a shell to run the program with; otherwise runs it as a bare process with no shell
201-
shell: string?,
207+
--[=[
208+
Specify a shell to run `program` with; if unspecified or nil,
209+
defaults to running it as a bare process with no shell (behavior may differ on Windows vs. Unix-like).
210+
211+
If `shell == true`, performs some magic (like checking `$SHELL`) to try and figure out
212+
what your current shell is, defaulting to WindowsPowerShell on Windows and `sh` on Unix-like.
213+
214+
Be careful with shell escapes w/ user-provided values; can be a security risk.
215+
]=]
216+
shell: (true | string)?,
202217
--- path to the the working directory you want your command to execute in, defaults to your shell's cwd
203218
cwd: string?,
219+
--[=[
220+
Control whether the child process reads and writes to its own terminal (`"Pipe"`) or
221+
to the terminal you're running *seal* in (`"Inherit"`).
222+
223+
By default, stdio is *piped*; this means the process reads and writes from separate
224+
stdin, stdout, and stderr streams that you can read once the process completes.
225+
226+
If you want everything the process says to be displayed on your terminal (and not
227+
read from the process programmatically), use `"Inherit"`.
228+
229+
If you want anything the process says to get ignored (redirected to `/dev/null`), use `"Ignore"`.
230+
231+
This field `stdio` can be either a string (if you want all streams to share the same behavior)
232+
or a table (if you want different per-stream behavior).
233+
]=]
234+
stdio: {
235+
stdout: Stdio?,
236+
stderr: Stdio?,
237+
stdin: Stdio?,
238+
}? | Stdio?,
239+
--[=[
240+
Override environment variables of the spawned `ChildProcess`.
241+
By default, the spawned process inherits its environment from the current process;
242+
so all environment variables visible to *seal* will be visible to it unless removed or cleared.
243+
244+
- To clear *all* environment variables for the spawned `ChildProcess` so
245+
that the spawned process does not inherit from *seal*'s environment, set `env.clear = true`.
246+
- To add additional environment variables, define a map of them (`{ [string]: string }`)
247+
in `env.add`.
248+
- To prevent specific environment variables from being inherited
249+
by the child process, list them in `env.remove`.
250+
251+
The order is clear, add, remove (so clearing the environment doesn't prevent variables from being
252+
added).
253+
254+
## Errors
255+
256+
- if you pass a table to `env` without keys `clear`, `add`, and/or `remove`.
257+
- if `env.add` is present and isn't `{ [string]: string }`
258+
- if `env.remove` is present and isn't `{ string }`.
259+
]=]
260+
env: {
261+
clear: boolean?,
262+
add: {
263+
[string]: string
264+
}?,
265+
remove: { string }?,
266+
}?
204267
}
205268

206269
export type SpawnOptions = {
207270
--- the program you want to run, must be available in $PATH or be an absolute path to an executable
208271
program: string,
209272
--- 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`
210273
args: { string }?,
211-
shell: string?,
274+
--[=[
275+
Specify a shell to run `program` with; if unspecified or nil,
276+
defaults to running it as a bare process with no shell (behavior may differ on Windows vs. Unix-like).
277+
278+
If `shell == true`, performs some magic (like checking `$SHELL`) to try and figure out
279+
what your current shell is, defaulting to WindowsPowerShell on Windows and `sh` on Unix-like.
280+
281+
Be careful with shell escapes w/ user-provided values; can be a security risk.
282+
]=]
283+
shell: (true | string)?,
212284
--- path to the the working directory you want your command to execute in, defaults to your shell's cwd
213285
cwd: string?,
214286
--[=[
215-
A `ChildProcessStream` captures incoming bytes from your `ChildProcess`' output streams (either stdout or stderr),
287+
A `ChildProcessStream` captures incoming bytes from a `ChildProcess`' output stream (either stdout or stderr),
216288
and caches them in its `inner` buffer. Each stream is spawned in a separate Rust thread to facilitate
217289
consistently nonblocking, dependable reads, allowing most `ChildProcess.stream:read` methods to be fully nonblocking unless
218290
specifically requested otherwise.
@@ -243,11 +315,60 @@ export type SpawnOptions = {
243315
stdout_capacity: number?,
244316
--- inner buffer capacity of `ChildProcess.stderr`, default 1024
245317
stderr_capacity: number?,
246-
--- what side of stdout should be truncated when full? defaults to "front"
247-
stdout_truncate: ("front" | "back")?,
248-
--- what side of stderr should be truncated when full? defaults to "front"
249-
stderr_truncate: ("front" | "back")?,
318+
--- what side of stdout should be truncated when full? defaults to "Front"
319+
stdout_truncate: ("Front" | "Back")?,
320+
--- what side of stderr should be truncated when full? defaults to "Front"
321+
stderr_truncate: ("Front" | "Back")?,
250322
}?,
323+
--[=[
324+
Control whether the child process reads and writes to its own terminal (`"Pipe"`) or
325+
to the terminal you're running *seal* in (`"Inherit"`).
326+
327+
By default, the child process's stdio is *piped*; this means the process
328+
reads and writes from separate stdin, stdout, and stderr streams that you
329+
can access through the `ChildProcessStream` API.
330+
331+
If you want everything the process says to be displayed on your terminal (and not
332+
read from the process programmatically), use `"Inherit"`.
333+
334+
If you want anything the `ChildProcess` says to get ignored (redirected to `/dev/null`), use `"Ignore"`.
335+
336+
This field `stdio` can be either a string (if you want all streams to share the same behavior)
337+
or a table (if you want different per-stream behavior).
338+
]=]
339+
stdio: {
340+
stdout: Stdio?,
341+
stderr: Stdio?,
342+
stdin: Stdio?,
343+
}? | Stdio?,
344+
--[=[
345+
Override environment variables of the spawned `ChildProcess`.
346+
By default, the spawned process inherits its environment from the current process;
347+
so all environment variables visible to *seal* will be visible to it unless removed or cleared.
348+
349+
- To clear *all* environment variables for the spawned `ChildProcess` so
350+
that the spawned process does not inherit from *seal*'s environment, set `env.clear = true`.
351+
- To add additional environment variables, define a map of them (`{ [string]: string }`)
352+
in `env.add`.
353+
- To prevent specific environment variables from being inherited
354+
by the child process, list them in `env.remove`.
355+
356+
The order is clear, add, remove (so clearing the environment doesn't prevent variables from being
357+
added).
358+
359+
## Errors
360+
361+
- if you pass a table to `env` without keys `clear`, `add`, and/or `remove`.
362+
- if `env.add` is present and isn't `{ [string]: string }`
363+
- if `env.remove` is present and isn't `{ string }`.
364+
]=]
365+
env: {
366+
clear: boolean?,
367+
add: {
368+
[string]: string
369+
}?,
370+
remove: { string }?,
371+
}?
251372
}
252373
--- Represents the stdout and stderr streams of a `ChildProcess`, both ran in parallel threads
253374
--- and streamed for nonblocking behavior.
@@ -433,7 +554,7 @@ export type ChildProcessStream = setmetatable<{
433554
local child = process.spawn({
434555
program = "someutil --watch",
435556
shell = "sh",
436-
})
557+
}) :: process.PipedChild
437558
438559
for line in child.stdout:lines() do
439560
local thing_changed = line:match("([%w]+) changed!")
@@ -450,9 +571,11 @@ export type ChildProcessStream = setmetatable<{
450571
shell = "sh",
451572
}
452573
453-
local next_line = child.stdout:lines()
454-
local first_line = next_line()
455-
local second_line = next_line()
574+
if child.stdout then
575+
local next_line = child.stdout:lines()
576+
local first_line = next_line()
577+
local second_line = next_line()
578+
end
456579
```
457580
]=]
458581
lines: (self: ChildProcessStream, timeout: number?) -> (() -> string),
@@ -495,19 +618,42 @@ type ChildProcessStdin = {
495618
```luau
496619
local child = process.spawn {
497620
program = "python3",
498-
args = { "-" },
621+
args = { "-" }, -- allows python3 to accept source code from stdin
499622
}
623+
assert(child.stdin ~= nil, "stdin is piped")
624+
500625
child.stdin:write(PYTHON_SRC)
501-
child.stdin:close()
626+
child.stdin:close() -- send EOF to python so it evaluates the source code
502627
```
503628
]=]
504629
close: (self: ChildProcessStdin) -> (),
505630
}
506631

632+
--[=[
633+
A handle to a child process created by `process.spawn`, with optional handles
634+
to the child's stdout, stderr, and stdin output/input streams.
635+
636+
By default, stdout, stderr, and stdin are piped; cast the result of `process.spawn` to
637+
`process.PipedChild` to reflect this.
638+
]=]
507639
export type ChildProcess = {
640+
--- The process id (`pid`) of the spawned `ChildProcess`
641+
id: number,
642+
alive: (self: ChildProcess | PipedChild) -> boolean,
643+
kill: (self: ChildProcess | PipedChild) -> (),
644+
stdout: ChildProcessStream?,
645+
stderr: ChildProcessStream?,
646+
stdin: ChildProcessStdin?,
647+
}
648+
649+
--[=[
650+
Default kind of `ChildProcess` created by `process.spawn` pipes for stdout, stderr, and stdin.
651+
]=]
652+
export type PipedChild = {
653+
--- The process id (`pid`) of the spawned `ChildProcess`
508654
id: number,
509-
alive: (self: ChildProcess) -> boolean,
510-
kill: (self: ChildProcess) -> (),
655+
alive: (self: ChildProcess | PipedChild) -> boolean,
656+
kill: (self: ChildProcess | PipedChild) -> (),
511657
stdout: ChildProcessStream,
512658
stderr: ChildProcessStream,
513659
stdin: ChildProcessStdin,

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@
3434
},
3535
"luau-lsp.inlayHints.variableTypes": false,
3636
"luau-lsp.plugin.enabled": false,
37+
"stylua.disableVersionCheck": true,
3738
}

0 commit comments

Comments
 (0)