On this page
The single authorized surface for effectful calls: every subprocess launch and every filesystem mutation the engine makes goes through one effects handle.
#internal/effects
#internal/effects
Package effects is the single authorized surface for effectful calls in selfdoc production code.
Every subprocess launch and every filesystem mutation made by the engine packages goes through a [Handle]. Nothing else may call os/exec, os.WriteFile, os.Rename, os.MkdirAll, os.RemoveAll or their siblings directly.
Why a chokepoint: selfdoc rides strictcli's effects regime, where every mutation is declared, previewable under --dry-run, and recorded into the would-do log. selfdoc force-pushes a gh-pages branch, deploys to Cloudflare Pages, creates GitHub repositories, sets repository secrets and auto-commits to the user's working tree -- a --dry-run that executed any of that would be worse than no dry run at all. With every effect funnelled through this one package the regime is adapted in one file instead of at ~150 call sites.
#No ambient binding
There is no package-level handle and no contextvar equivalent. A command handler builds its handle with [FromContext] and passes it explicitly to every engine function that mutates or spawns; a library caller -- the build pipeline, the check helpers, the test suite -- builds an [Unbound] handle, which executes everything directly.
#The mode rule (declared, never inferred)
- Preview mode -- a handle built from a context whose --dry-run flag was passed. Every mutating operation is minted on the strictcli effects handle: recorded, never executed. A [Result] then reports Unsettled true, and the filesystem operations report no error while having changed nothing. - Live mode -- an unbound handle, or a bound handle outside --dry-run. The operations execute directly, with their full selfdoc semantics: per-call timeouts, byte captures, stdin payload streaming, [Handle.AtomicWrite]'s temp-file-plus-rename (the only way to rewrite the 0444 generated root files), and the missing-is-an-error distinctions call sites branch on. strictcli's closed method set expresses none of those, so routing a live run through it would silently drop a hang guard or a permission-preserving rename.
The split is by mode, decided before anything runs, and identical on every invocation -- it is not a fallback: nothing here ever tries the strictcli handle, fails, and retries elsewhere.
#Reads are never effects
[Read] marks a subprocess run as a declared read. A declared read executes in every mode and is never minted, never recorded and never logged -- the same treatment strictcli gives an allowlisted observe, and for the same reason: a preview that could not look at the world would have nothing to preview. It is declared per call site rather than through an app-level proc_observe_allowlist because the argv cannot classify these: gh api is both a GET of repository contents and a POST of a workflow dispatch, and git is both rev-parse and push --force.
#ModeDefault
const ModeDefault fs.FileMode = 0ModeDefault is the file mode operand meaning "no explicit mode": the file is created with 0644 as modified by the process umask, and no chmod is recorded in a preview. It is the counterpart of the Python surface's permissions=None.
#ErrTimeout
var ErrTimeout = errors.New("timed out")ErrTimeout is the sentinel every timed-out subprocess error wraps.
#ExitError
type ExitError structExitError is returned by [Handle.Run] when the caller declared [Check] and the child exited non-zero. It is the counterpart of Python's subprocess.CalledProcessError.
#Result
type Result structResult is the outcome of [Handle.Run] or [Handle.Pipeline].
#Handle
type Handle structHandle is the effects handle an engine function receives as an explicit parameter. Build one with [FromContext] inside a command handler, or with [Unbound] for a library call.
A Handle is immutable after construction and safe to share across goroutines to the extent the underlying operations are.
#Option
type Option structOption is one trailing option on a [Handle.Run] or [Handle.Pipeline] call. An option a method does not accept is a call-time error: silently ignoring one is the single outcome a declare-everything surface cannot have.
#FromContext
func FromContext(ctx *strictcli.Context) *HandleFromContext builds the handle for a command dispatch.
The returned handle previews when ctx was invoked with --dry-run and executes directly otherwise. A nil ctx yields an [Unbound] handle, so a test that dispatches nothing still gets a working handle.
#Unbound
func Unbound() *HandleUnbound builds a handle with no strictcli effects handle behind it: every operation executes directly. This is the library path -- the build pipeline, the check helpers and unit tests all call the engine outside a command dispatch, and there is nothing to mint on there.
#Cwd
func Cwd(dir string) OptionCwd sets the working directory of the child process.
#Env
func Env(env map[string]string) OptionEnv sets the complete environment of the child process, replacing the inherited one -- the semantics of Python's subprocess env argument, which every selfdoc call site was written against. Omit it to inherit.
A preview renders the entries as overrides instead, because strictcli's recorded env merges over the inherited environment; nothing executes there, so the difference is confined to the rendered line.
#Timeout
func Timeout(d time.Duration) OptionTimeout kills the child and returns an error wrapping [ErrTimeout] when it has not exited within d. Zero means no deadline.
#Check
func Check() OptionCheck makes a non-zero exit an [ExitError] instead of a [Result]. Off by default, matching the Python surface.
#CaptureOutput
func CaptureOutput() OptionCaptureOutput captures the child's stdout and stderr into the [Result] instead of letting it inherit this process's streams.
#Stdin
func Stdin(payload []byte) OptionStdin writes payload to the child's standard input and closes it.
#Read
func Read() OptionRead declares this run an observation: it changes nothing, so it executes in every mode and is never recorded.
#Resource
func Resource(token string) OptionResource declares an opaque token naming what this run produces. Preview metadata only.
#SkipIfCurrent
func SkipIfCurrent(token string) OptionSkipIfCurrent declares the token a preview annotates the recorded line with, spelling out that the handler skips this step when the resource is current.
#Grant
func Grant(name string) OptionGrant names a grant declared on the running command, whose reason is rendered beside the recorded step in the preview.
#Stream
func Stream(stream bool) OptionStream overrides whether a recorded run is rendered as inheriting this process's streams. Without it a recorded run streams unless the caller declared [CaptureOutput].
#ShellQuote
func ShellQuote(token string) stringShellQuote quotes token for a rendered /bin/sh -c line, leaving a token made only of characters no shell reinterprets unquoted.
#ExitError.Error
func (e *ExitError) Error() stringError renders the failed command and its exit status.
#Result.StdoutString
func (r Result) StdoutString() stringStdoutString is Stdout decoded as text with one trailing newline removed -- the form call sites forward into a later command's argv.
#Result.StderrString
func (r Result) StderrString() stringStderrString is Stderr decoded as text with one trailing newline removed.
#Handle.Previewing
func (h *Handle) Previewing() boolPreviewing reports whether this handle records mutations instead of performing them.
#Handle.Run
func (h *Handle) Run(argv []string, options ...Option) (Result, error)Run runs a command to completion and returns its [Result].
In preview mode a declared read ([Read]) still executes and returns a real result; anything else is recorded and returns a result whose Unsettled field is true, standing in for the run that did not happen.
#Handle.Pipeline
func (h *Handle) Pipeline(argvs [][]string, options ...Option) (Result, error)Pipeline runs the stages of argvs as one shell pipeline -- stage N's stdout feeds stage N+1's stdin -- and returns the last stage's result.
strictcli's closed method set has no pipeline, so a preview records the whole chain as the one /bin/sh -c invocation that performs it: a faithful rendering of the work, not an invented one. Live mode keeps the real multi-process pipeline, which is what streams a git archive into tar without buffering the whole tree in memory.
Only the last stage's stderr is captured; the earlier stages inherit this process's stderr.
#Handle.Write
func (h *Handle) Write(path string, content []byte, mode fs.FileMode) errorWrite writes content to path, truncating any existing file.
Pass [ModeDefault] as mode to leave the mode to the process umask; any other value is applied to the file, and recorded as a chmod in a preview.
#Handle.AtomicWrite
func (h *Handle) AtomicWrite(path string, content []byte, mode fs.FileMode) errorAtomicWrite writes content to path atomically: the bytes land in a sibling temporary file that is renamed over the target in one directory operation.
A crash mid-write can therefore never leave a truncated file. Because the rename is a directory operation it also succeeds when path itself is read-only (the 0444 generated root files) with no unlock step -- which is why live mode keeps the temp-file dance instead of routing through a plain write.
Pass [ModeDefault] as mode to leave the mode alone.
#Handle.OpenWrite
func (h *Handle) OpenWrite(path string) (io.WriteCloser, error)OpenWrite opens path for writing, truncating any existing file, and returns the stream. Use it for writers that produce their bytes incrementally; whole-content writers should prefer [Handle.Write] or [Handle.AtomicWrite].
strictcli's closed method set has no streaming write, so in preview mode the content accumulates in memory and Close mints the single resulting write, carrying the byte count the file would have had. Close must therefore be called -- and its error checked -- on every path.
#Handle.OpenAppend
func (h *Handle) OpenAppend(path string) (io.WriteCloser, error)OpenAppend opens path for appending, creating it when missing, and returns the stream.
strictcli's closed method set has no append, so in preview mode the recorded write carries the whole resulting file -- the existing bytes plus the appended ones -- and the preview's byte count is the real one.
#recordedWriter.Write
func (w *recordedWriter) Write(p []byte) (int, error)Write accumulates p in memory.
#recordedWriter.Close
func (w *recordedWriter) Close() errorClose mints the single write standing for everything that was streamed.
#Handle.Mkdir
func (h *Handle) Mkdir(path string) errorMkdir creates path and any missing parents. An already-existing path is an error, matching Python's os.makedirs default.
#Handle.MkdirAll
func (h *Handle) MkdirAll(path string) errorMkdirAll creates path and any missing parents. An already-existing path is not an error.
#Handle.Remove
func (h *Handle) Remove(path string) errorRemove deletes the file or symlink at path. A missing path is an error.
#Handle.RemoveIfExists
func (h *Handle) RemoveIfExists(path string) errorRemoveIfExists deletes the file or symlink at path. A missing path is not an error.
#Handle.Rmdir
func (h *Handle) Rmdir(path string) errorRmdir removes the empty directory at path.
#Handle.RmTree
func (h *Handle) RmTree(path string) errorRmTree recursively deletes the directory tree at path. A missing path is an error, matching Python's shutil.rmtree default.
#Handle.RmTreeIgnoreErrors
func (h *Handle) RmTreeIgnoreErrors(path string) errorRmTreeIgnoreErrors recursively deletes the directory tree at path, ignoring filesystem errors -- including a missing path. The returned error is only ever a preview-mode recording failure.
#Handle.Chmod
func (h *Handle) Chmod(path string, mode fs.FileMode) errorChmod sets the permission bits of path.
#Handle.Rename
func (h *Handle) Rename(src, dst string) errorRename moves src to dst.
#Handle.CopyFile
func (h *Handle) CopyFile(src, dst string) errorCopyFile copies src to dst, preserving src's permission bits.
#Handle.CopyTree
func (h *Handle) CopyTree(src, dst string, dirsExistOK bool) errorCopyTree recursively copies the directory tree src to dst. With dirsExistOK false an already-existing dst is an error.
A preview records one mkdir plus one write per file, so it names every path the copy would create rather than one opaque "copy tree" line.