Shell scripting
Run in scripting mode with sigil script.sgl args…, or make it executable with #!/usr/bin/env sigil. Direct scripts load (sigil shell prelude) and print only explicit output. An uncaught error exits nonzero; (exit n) returns that status. Libraries retain their explicit import boundaries.
#!/usr/bin/env sigil
(requires (tools git))
(define root (realpath (path (script-dir) "..")))
(println "Revision: ~a" (out git rev-parse HEAD cwd: ,root))sigil eval -f file.sgl evaluates ordinary Sigil with explicit imports and no script context. sigil eval '<forms>' prints the final value; add --quiet (-q) to suppress that value while retaining explicit output and failures. These evaluation commands do not build a native executable.
Uncaught script and eval errors go to stderr with their message, structured details, and immediate source location when available. Request expanded diagnostics with sigil --error-history script.sgl or sigil eval --error-history '<forms>'. This is captured call history: it can include completed calls and macro expansion, so it is not necessarily a live stack. --quiet controls only implicit expression results.
Prelude
The prelude exposes the public shell, filesystem, path, string, I/O, time, math, iteration, array, and process APIs. Core is implicit. JSON command capture is provided by (sigil shell); import (sigil json) for direct encoding/decoding. Reusable modules can explicitly import (sigil shell prelude) when they want the same vocabulary, or import the individual libraries they use.
| Short name | Original name |
|---|---|
path | path-join |
dirname, basename | path-dirname, path-basename |
read-text, write-text | read-file-string, write-file-string |
trim, split | string-trim, string-split |
contains?, starts-with?, ends-with? | Corresponding string- predicates |
Aliases preserve argument order and behavior. The original names also remain available. println already supports formatting, and str concatenates values.
(define action (arg 1 default: "status"))
(define state (path (script-dir) "server.command"))
(define expected (read-text state missing: #f trim: #t))arg indexes positional script arguments starting at one. An absent argument raises unless default: is supplied; an empty string is a present argument. script-args, script-file, and script-dir expose the original invocation. script-dir follows the supplied path, so use realpath when an absolute path is required. For flag parsing and generated help, use (sigil args).
read-text/read-file-string accept missing: and trim:. The missing-file default applies only to absence; permission errors, invalid paths, and read failures still raise. Trimming applies to successfully read text, not the fallback.
Commands and captures
(requires (tools git printf))
($ git status --short) ; print output, raise on failure
(define probe ($? git diff --quiet)) ; return an outcome, including failure
(define revision (out git rev-parse HEAD))
(define files (lines git ls-files))out captures stdout and trims trailing CR/LF characters. lines returns a list of lines; empty output gives an empty list. json decodes captured JSON: objects have keyword keys and JSON arrays are Sigil arrays.
Bare symbols are literal arguments; numeric tokens pass through Sigil's reader before becoming arguments. ,value inserts exactly one argument; ,@values splices a list. There is no implicit word splitting, globbing, or shell expansion. Use glob explicitly. Quote reader-sensitive literals:
($? kill "-0" ,pid)
($ ps -p ,pid -o "pid,etime,pcpu,args")
($ git show "9820f0e")
($ printf "%s" "2026.9.7")Always quote numeric-looking options such as "-0". In ($? kill -0 ,pid), the reader turns -0 into numeric zero, so the command receives kill 0 PID. On Unix this can send SIGTERM to the current process group instead of checking whether the process exists. It can execute without a reader or shell error. Use ($? kill "-0" ,pid) for the existence probe.
The same rule applies whenever the spelling matters: bare 007 becomes 7, and bare +7 becomes 7. Quote numeric-looking options, leading-zero identifiers, versions, and digit-leading hashes. Ordinary numeric arguments such as head -n 10 remain useful; use strings when you need the exact original characters. Commas are interpolation syntax, so comma-containing arguments also need quotes.
These harmless probes show the difference without sending any signals:
(out printf "<%s>" -0) ; => "<0>"
(out printf "<%s>" "-0") ; => "<-0>"
(out printf "<%s>" "007") ; => "<007>"Command options are cwd:, env:, in:, out:, err:, timeout: (seconds), and ansi:. For synchronous commands, in: is text; out: and a path-valued err: write captured output after completion. err: 'merge combines captured stdout and stderr; it does not preserve their interleaving. ANSI escapes are removed unless ansi: #t is supplied.
($ converter input.svg
env: ,(dict "FONTCONFIG_FILE" config)
timeout: 20)env: accepts an alist or dictionary of string keys and values. Synchronous commands apply cwd/environment in the child without changing the parent. On Unix, capture concurrently feeds stdin and drains both output streams, so output larger than pipe capacity can complete. A timeout terminates the owned process group with KILL, drains available evidence, and returns a timed-out outcome.
Use ok?, exit-status, cause, stdout-of, stderr-of, and format-outcome to inspect results. A spawn failure may have no output: format-outcome includes its cause. $ and captures demand success; $? lets the script decide what a failure means. An ignored $? result does not set the script's exit status automatically.
&& and || short-circuit on outcomes. (pipe (producer ...) (consumer ...)) connects commands; pipelines currently accept cwd/environment and input on the first stage. Unsupported options fail before spawning. sh explicitly invokes a shell; pass variable data as separate arguments instead of constructing code. SSH itself joins remote argv into shell text, so local argument quoting alone does not provide remote quoting.
Iteration, polling, and text
(each file (glob "src/**/*.sgl")
(println "Checking ~a" file)
($ sigil format ,file))
(wait-until (listening?)
timeout: 10
interval: 0.1
message: "Server did not open its listener; inspect the log.")each evaluates its collection once and visits a list or array in order. wait-until reevaluates the condition, returning its first true value. It uses a monotonic deadline and propagates condition errors. A zero timeout probes once. Both helpers also live in (sigil iteration) and (sigil time).
The deadline is checked after an unsuccessful condition evaluation. wait-until cannot interrupt a blocking condition, and a successful condition can return after the deadline. Bound each blocking operation separately:
(wait-until (ok? ($? curl --silent --fail ,health-url timeout: 1))
timeout: 5
interval: 0.1
message: "Server did not become ready")The five-second polling deadline is therefore not a strict wall-clock limit for the whole expression; the last request and its cleanup can finish later.
(string-truncate text 80) limits text to 80 Unicode characters, including the default … marker. Use marker: "..." or marker: "" to change it. Short text is unchanged; zero yields an empty string; a marker longer than the limit is itself clipped. This counts Unicode characters, not terminal columns or grapheme clusters. (string-contains-any? text '("worker" "compiler")) tests literal alternatives. Both helpers belong to (sigil string).
Ordinary strings can contain physical newlines. Quotes and backslashes still need escaping; raw heredoc syntax is not introduced by script mode.
Background processes and resource scopes
On Unix, spawn supports child-side file sinks with output visible while the process runs. in: names an input file for spawn; it is text for synchronous commands. append: #t appends to output files, and err: 'merge directs stderr to the same file descriptor as stdout, preserving their write order.
(define server
(spawn ,binary serve
cwd: ,root
detached: #t
out: ,log-file
append: #t
err: 'merge))
(write-text pid-file (str (process-pid server) "\n"))Ordinary shell children request parent-death cleanup (Linux) and retain the parent's session, allowing tools to access /dev/tty. Timed commands own a process group so their deadline can terminate descendants (Unix). detached: #t explicitly disables parent-death cleanup and starts a separate session. It requires a log path, defaults stdin to /dev/null, and defaults stderr to the stdout log. New log files have mode 0600; existing file permissions are preserved. Relative redirection paths resolve against the child's cwd. File sinks and detached shell spawning currently require Unix.
spawn returns a process handle and raises immediately on launch failure. Use process-pid, process-alive?, process-signal!, and process-wait to manage it. process-wait reaps a child but does not drain its pipes. The Unix process-communicate operation owns and drains raw pipes, returning (wait-result stdout stderr timed-out?); call it before accessing process ports. An independently running service still needs application-specific readiness and identity checks. Recording a PID alone does not make later signaling safe.
(with-temp-dir work
(write-text (path work "input.txt") "example\n")
($ converter ,(path work "input.txt")
out: ,(path work "output.txt")))with-temp-dir removes its disposable tree on normal or exceptional scope exit, without following symlinks. call-with-temp-file, call-with-temp-directory, call-with-input-file, call-with-output-file, and with-current-directory also clean up on unwinding. These scopes cannot run cleanup after an OS-level kill or an immediate process exit. The older call-with-temp-directory retains its empty-directory contract unless recursive: #t is supplied.
An HTTP harness that retains evidence
Use make-temp-directory when logs and database fixtures must survive a failed test. Put setup, spawning, readiness checks, and assertions inside one dynamic-wind; its cleanup stops and reaps the child while leaving the evidence directory available. with-temp-dir would remove those diagnostic files.
This complete recipe runs a disposable Python HTTP server on loopback and checks readiness and a missing-resource response. It requires python3 and curl. Choose an unused port, for example sigil examples/scripts/http-check.sgl 18080. The runnable example contains this code:
#!/usr/bin/env sigil
(requires (tools python3 curl))
(define port (arg 1 default: "18080"))
(define base (str "http://127.0.0.1:" port))
(define work (make-temp-directory))
(unless work
(error "Could not create the HTTP evidence directory"))
(define public (path work "public"))
(define log-file (path work "server.log"))
(define response-file (path work "response.txt"))
(define server #f)
(dynamic-wind
(lambda () #f)
(lambda ()
(ensure-directory public)
(write-text (path public "health") work)
(set! server
(spawn python3 -u -m http.server ,port
--bind "127.0.0.1" --directory ,public
in: "/dev/null" out: ,log-file err: 'merge))
(wait-until
(begin
(unless (process-alive? server)
(error "HTTP server exited; inspect the retained log"))
(let ((probe ($? curl --silent --show-error --fail
,(str base "/health") timeout: 1)))
(and (ok? probe) (equal? work (stdout-of probe)))))
timeout: 5
interval: 0.1
message: "HTTP server did not become ready")
(let ((status (out curl --silent --show-error
--output ,response-file --write-out "%{http_code}"
,(str base "/missing") timeout: 2)))
(unless (equal? "404" status)
(error (str "Expected HTTP 404, received " status))))
(println "PASS: readiness response and missing-resource status"))
(lambda ()
(eprintln "HTTP evidence: ~a" work)
(when server
(when (process-alive? server)
(process-kill! server))
(process-wait server))))The unique readiness body prevents an unrelated listener from passing the test. The child stays attached to the harness's lifetime; detached: #t is for services that must survive their launcher. Evidence is retained on success as well as failure, and the directory path is printed to stderr. Remove it after inspection. Cleanup runs on normal return and exceptions, with the OS-level exit limitations described above.
For an application harness, replace the Python command with your server and pass its loopback address, port, synthetic database path, and disabled-mail setting through env:. Keep fixtures under work and retain explicit business assertions in the body. Adapt readiness to verify that the responding service is the instance launched by this test.