(sigil process)
Process Management Library
Process management operations including spawning subprocesses, environment variable access, and command execution. Native functions are implemented in C with higher-level utilities defined here.
Running Commands
(import (sigil process))
;; Run and wait for completion
(process-run "ls" "-l")
;; Capture output as string
(process-output->string "date") ; => "Thu Jan 15 14:30:00 2025"
;; Capture output as lines
(process-lines "ls") ; => ("file1" "file2" ...)Spawning Processes
(let ((p (process-spawn "cat")))
(display "Hello\n" (process-stdin p))
(close-output-port (process-stdin p))
(display (read-line (process-stdout p)))
(process-wait p))Environment
(getenv "HOME") ; => "/home/user"
(setenv! "MY_VAR" "value")Exports
process-runprocedureRun a command and wait for it to complete.
Returns the exit status (0 for success; 128+N when the child died from signal N). Raises an io-error when the process cannot be started or waited on (for example, fork failing under process pressure) -- it never returns a non-integer.
(process-run "ls" "-l") ; => 0process-spawnprocedureSpawn a subprocess without waiting.
Returns a process object, or #f if the process cannot be started. Use process-wait to wait for completion and retrieve the exit status.
With die-with-parent: #t, the child is killed automatically when this (parent) process dies, even on an uncatchable SIGKILL. On Linux this uses PR_SET_PDEATHSIG; on other platforms it is a no-op.
cwd: changes only the child's working directory. environment: is a list of KEY=VALUE overrides merged into the child's inherited environment without mutating the parent. Both options use the native spawn path on Unix, macOS, and Windows.
With new-process-group: #t, the child calls setsid before exec, so it leads its own session and its own process group ((process-pgid p) equals (process-pid p)). Anything the child spawns inherits that group, so process-signal! then reaches the child's own children too; without it, a signal reaches only the child and grandchildren are left running. Use it when you need to tear down a whole process tree. Unix only: on Windows it is a no-op (process-pgid and process-signal! both raise there, so nothing can act on a group that was not created).
This is the group-and-session half of what process-spawn-pty does, without the terminal half and without merging the streams: stdout and stderr stay on separate pipes. The pty spawn goes on to claim the pty as a controlling terminal, which this does NOT — so the two are not equivalent arrangements.
Two consequences of setsid worth knowing: the child detaches from this process's controlling terminal, so a Ctrl-C typed at the terminal no longer reaches it, and it acquires no terminal of its own, so it cannot open /dev/tty to prompt.
(let ((p (process-spawn "sleep" "10")))
(process-kill! p))
;; Child dies on its own if this process is killed:
(process-spawn "my-worker" "--serve" die-with-parent: #t)
;; Signal the child AND everything it spawned, stdout/stderr
;; still separate:
(let ((p (process-spawn "build.sh" new-process-group: #t)))
(process-signal! p 'term))process-communicateprocedureConcurrently feed input and drain both output streams (Unix). Returns (wait-result stdout stderr timed-out?). Owns and closes raw pipes; call before accessing process ports. TIMEOUT bounds communication as well as execution, including descendants retaining a pipe after parent exit.
process-spawn-ptyprocedureSpawn a subprocess on a pseudo-terminal (Unix only).
Allocates a pty, forks, and runs the command as a session leader with the pty slave as its controlling terminal wired to stdin/stdout/stderr. All fork discipline is handled in C; do not attempt fork or forkpty from Scheme.
Returns a process object whose I/O rides the pty MASTER, not pipes: use process-pty-read / process-pty-write (guarded by await-readable-fd / await-writable-fd on process-pty-fd in async code), NOT process-stdout / process-stdin.
The master fd is non-blocking. process-pty-read returns an empty bytevector when no data is ready and an eof-object once the child exited and the stream drained. cwd: sets the child's working directory; term: sets its TERM (default "xterm-256color"; pass #f to inherit).
(let ((p (process-spawn-pty "sh" cols: 120 rows: 40)))
(process-pty-write p "ls\n")
...
(process-pty-close! p)
(process-wait p))process-spawn-pipeprocedureSpawn a subprocess on non-blocking pipes for a stream session (Unix only).
The pipe analog of process-spawn-pty, for a child that speaks a line/byte protocol over stdin/stdout rather than a terminal — e.g. tmux -C, which hard-fails on a tty. stdin and stdout are NON-BLOCKING pipes; the child's stderr is inherited (not captured), keeping the stdout stream clean for a strict control protocol.
Returns a process object whose I/O rides raw pipe fds: use process-pipe-read / process-pipe-write (guarded by await-readable-fd / await-writable-fd on process-stdout-fd / process-stdin-fd), NOT process-stdout / process-stdin. process-pipe-read returns an empty bytevector when no data is ready and an eof-object once the child closes stdout. cwd: sets the child's working directory.
(let ((p (process-spawn-pipe "tmux" "-C" "new-session")))
(process-pipe-write p "list-sessions\n")
...
(process-pipe-close-stdin! p)
(process-wait p))process-signal!procedureSend a signal to a spawned process.
Accepts a signal name symbol ('hup 'int 'quit 'kill 'usr1 'usr2 'term 'cont 'stop 'tstp 'winch) or a raw signal number. Returns #t if the signal was delivered.
The signal goes to the child's whole PROCESS GROUP — reaching the child's own children as well, matching terminal semantics — exactly when SIGIL created that group for it: every process-spawn-pty child, and a process-spawn child asked for with new-process-group: #t. Otherwise only the child itself is signalled, and anything it spawned is left running; a default process-spawn child shares THIS process's group, so signalling that group would signal the caller too.
The test is what was requested AT SPAWN, not what the child did afterwards. A child that runs setsid itself ((process-spawn "setsid" ...), nohup, a daemonising program) will report (= (process-pgid p) (process-pid p)) and still be signalled individually — process-pgid describes the child, this describes what we will do to it, and they can disagree.
Returns #f for a process already reaped by process-wait, group or not: after reaping, the pid may be reissued to a stranger, and refusing is the only safe answer. So when tearing down a tree, SIGNAL FIRST AND WAIT AFTER. A child that has exited but not yet been reaped is still signallable, and its group id stays valid as long as the group has members — which is what lets you sweep up grandchildren that outlived their parent.
(process-signal! p 'int) ; like Ctrl-C
(process-signal! p 'term)
;; Tearing down a tree on a timeout:
(process-signal! p 'term) ; reaches the group
(process-wait p) ; only then reapprocess-waitprocedureWait for a process to complete.
Returns the exit status.
(process-wait p) ; => 0process-kill!procedureKill a running process.
(process-kill! p)process?procedureCheck if a value is a process object.
process-pty?procedureCheck if a process was spawned with a pty (via process-spawn-pty).
process-pty-fdprocedureGet the raw pty master file descriptor of a pty-spawned process.
Feed this to await-readable-fd / await-writable-fd from (sigil async) before calling process-pty-read / process-pty-write so the single-threaded VM never blocks.
process-pty-readprocedureRead available bytes from the pty master.
Returns a bytevector (EMPTY when no data is ready — the fd is non-blocking) or an eof-object once the child exited and the stream is drained. Optional second argument caps the read size (default 4096).
process-pty-writeprocedureWrite a string or bytevector to the pty master (child input).
Returns the number of bytes actually written; may be 0 (kernel buffer full) or partial. Await writability and retry with the remainder.
process-pty-resize!procedureSet the pty window size. The kernel delivers SIGWINCH to the child's foreground process group automatically.
process-pty-close!procedureClose the pty master fd (idempotent). A still-running child sees terminal hangup.
process-stdout-fdprocedureGet the raw stdout (read) pipe fd of a pipe-spawned process.
Feed this to await-readable-fd from (sigil async) before calling process-pipe-read so the single-threaded VM never blocks. The fd is non-blocking.
process-stdin-fdprocedureGet the raw stdin (write) pipe fd of a pipe-spawned process.
Feed this to await-writable-fd from (sigil async) before calling process-pipe-write. The fd is non-blocking.
process-pipe-readprocedureRead available bytes from the child's stdout pipe.
Returns a bytevector (EMPTY when no data is ready — the fd is non-blocking) or an eof-object once the child closes its stdout. Optional second argument caps the read size (default 4096).
process-pipe-writeprocedureWrite a string or bytevector to the child's stdin pipe.
Returns the number of bytes actually written; may be 0 (kernel buffer full) or partial. Await writability and retry with the remainder. Raises an io-error if the child has closed its stdin.
%process-pipe-write-or-closedprocedureInternal non-raising EPIPE variant used by (sigil shell)'s pump. Returns #f when the child has closed its stdin; otherwise has the same partial/non-blocking integer result as process-pipe-write.
process-pipe-close-stdin!procedureClose the child's stdin pipe (idempotent), delivering EOF to the child.
process-pidprocedureGet the OS process id of a spawned process.
process-pgidprocedureGet the process group id of a spawned process (Unix only).
Read from the kernel, so it reports what is actually true even if the child changed its own group after exec. Returns #f once the process has been reaped by process-wait — deliberately, and the refusal comes BEFORE the lookup: a reaped pid can be reissued, and asking the kernel about it would answer about a stranger. Read it (and signal) BEFORE you reap.
For a child spawned with new-process-group: #t, or on a pty, this equals (process-pid p) — the child leads its own group. For a default spawn it is THIS process's own group id; do not signal it.
(= (process-pgid p) (process-pid p)) does NOT imply that process-signal! will signal the group: a child that ran setsid itself also reports that, and process-signal! keys off what was requested at spawn. See its docstring.
(let ((p (process-spawn "build.sh" new-process-group: #t)))
(= (process-pgid p) (process-pid p))) ; => #tprocess-alive?procedureCheck if a process is still running.
(process-alive? p) ; => #t or #fprocess-stdinprocedureGet the stdin port of a spawned process.
Returns an output port for writing to the process.
process-stdoutprocedureGet the stdout port of a spawned process.
Returns an input port for reading the process output.
process-stderrprocedureGet the stderr port of a spawned process.
Returns an input port for reading error output.
getenvprocedureGet an environment variable.
Returns the value as a string, or #f if not set.
(getenv "HOME") ; => "/home/user"
(getenv "MISSING") ; => #fsetenv!procedureSet an environment variable.
(setenv! "MY_VAR" "my-value")command-lineprocedureGet the command-line arguments.
Returns a list where the first element is the program name.
(command-line) ; => ("./my-program" "arg1" "arg2")%set-command-line!procedureReplace the list command-line returns (a list of strings).
sigil run uses this to give an entry point it runs in-process the argv a built binary of the same program would see.
exitprocedureExit the process with a status code.
(exit 0) ; success
(exit 1) ; failure%set-exit-hook!procedureInstall a procedure that exit runs immediately before the process terminates, or #f to clear it. The hook receives the pending exit status and may call exit again with a different status to OVERRIDE it; re-entry is guarded so the hook runs at most once.
This exists for one job: letting a command that loads and runs untrusted code survive that code calling exit out from under it. sigil test uses it so a test file ending in (exit 0) can no longer abandon the run and report success. It is deliberately exception-free — a guard built on raise is worthless in exactly the release binaries where propagation out of native frames is broken.
Prefer ordinary control flow. This is a last-ditch guard, not a general interception mechanism.
(%set-exit-hook! (lambda (status)
(display "about to exit\n" (current-error-port))))process-idprocedureGet the current process ID.
(process-id) ; => 12345command-exists?procedureCheck if a command exists in PATH.
(command-exists? "ls") ; => #t
(command-exists? "foo") ; => #fcall-with-processprocedureCall a procedure with a spawned process.
The process is automatically waited for when proc returns.
(call-with-process "ls" '("-l")
(lambda (p)
(read-line (process-stdout p))))process-output->stringprocedureRun a command and capture its stdout as a string.
Returns #f if the process cannot be started.
(process-output->string "echo" "hello") ; => "hello"
(process-output->string "date") ; => "Thu Jan 15..."read-all-stringprocedureRead all content from a port as a string. Helper for process-output->string.
process-linesprocedureRun a command and return its stdout as a list of lines.
Returns #f if the process cannot be started.
(process-lines "ls") ; => ("file1" "file2" ...)
(process-lines "ls" "-la") ; => ("total 42" "drwxr-x..." ...)read-all-linesprocedureRead all lines from a port. Helper for process-lines.
%process-spawn-redirectedvariable(No description)
%process-spawn-optsvariable(No description)
%process-communicate-stepvariable(No description)
%process-close-pipes!variable(No description)
%process-spawn-errorvariable(No description)
%process-wait-resultvariable(No description)
%process-signalvariable(No description)
%process-spawn-ptyvariable(No description)
%process-spawn-pipevariable(No description)
%set-exit-trap!variable(No description)
%last-trapped-exit-statusvariable(No description)
emergency-exitvariable(No description)
get-environment-variablesvariable(No description)
executable-pathvariable(No description)