effects

package
v0.43.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 19, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

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.

Index

Constants

View Source
const ModeDefault fs.FileMode = 0

ModeDefault 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.

Variables

View Source
var ErrTimeout = errors.New("timed out")

ErrTimeout is the sentinel every timed-out subprocess error wraps.

Functions

func ShellQuote

func ShellQuote(token string) string

ShellQuote quotes token for a rendered /bin/sh -c line, leaving a token made only of characters no shell reinterprets unquoted.

Types

type ExitError

type ExitError struct {
	// Argv is the command that failed.
	Argv []string
	// ExitCode is the child's exit status.
	ExitCode int
	// Stdout and Stderr are the captured streams, nil when the run did not
	// capture them.
	Stdout []byte
	Stderr []byte
}

ExitError 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.

func (*ExitError) Error

func (e *ExitError) Error() string

Error renders the failed command and its exit status.

type Handle

type Handle struct {
	// contains filtered or unexported fields
}

Handle 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.

func FromContext

func FromContext(ctx *strictcli.Context) *Handle

FromContext 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.

func Unbound

func Unbound() *Handle

Unbound 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.

func (*Handle) AtomicWrite

func (h *Handle) AtomicWrite(path string, content []byte, mode fs.FileMode) error

AtomicWrite 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.

func (*Handle) Chmod

func (h *Handle) Chmod(path string, mode fs.FileMode) error

Chmod sets the permission bits of path.

func (*Handle) CopyFile

func (h *Handle) CopyFile(src, dst string) error

CopyFile copies src to dst, preserving src's permission bits.

func (*Handle) CopyTree

func (h *Handle) CopyTree(src, dst string, dirsExistOK bool) error

CopyTree 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.

func (*Handle) Mkdir

func (h *Handle) Mkdir(path string) error

Mkdir creates path and any missing parents. An already-existing path is an error, matching Python's os.makedirs default.

func (*Handle) MkdirAll

func (h *Handle) MkdirAll(path string) error

MkdirAll creates path and any missing parents. An already-existing path is not an error.

func (*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.

func (*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.

func (*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.

func (*Handle) Previewing

func (h *Handle) Previewing() bool

Previewing reports whether this handle records mutations instead of performing them.

func (*Handle) Remove

func (h *Handle) Remove(path string) error

Remove deletes the file or symlink at path. A missing path is an error.

func (*Handle) RemoveIfExists

func (h *Handle) RemoveIfExists(path string) error

RemoveIfExists deletes the file or symlink at path. A missing path is not an error.

func (*Handle) Rename

func (h *Handle) Rename(src, dst string) error

Rename moves src to dst.

func (*Handle) RmTree

func (h *Handle) RmTree(path string) error

RmTree recursively deletes the directory tree at path. A missing path is an error, matching Python's shutil.rmtree default.

func (*Handle) RmTreeIgnoreErrors

func (h *Handle) RmTreeIgnoreErrors(path string) error

RmTreeIgnoreErrors 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.

func (*Handle) Rmdir

func (h *Handle) Rmdir(path string) error

Rmdir removes the empty directory at path.

func (*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.

func (*Handle) Write

func (h *Handle) Write(path string, content []byte, mode fs.FileMode) error

Write 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.

type Option

type Option struct {
	// contains filtered or unexported fields
}

Option 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.

func CaptureOutput

func CaptureOutput() Option

CaptureOutput captures the child's stdout and stderr into the Result instead of letting it inherit this process's streams.

func Check

func Check() Option

Check makes a non-zero exit an ExitError instead of a Result. Off by default, matching the Python surface.

func Cwd

func Cwd(dir string) Option

Cwd sets the working directory of the child process.

func Env

func Env(env map[string]string) Option

Env 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.

func Grant

func Grant(name string) Option

Grant names a grant declared on the running command, whose reason is rendered beside the recorded step in the preview.

func Read

func Read() Option

Read declares this run an observation: it changes nothing, so it executes in every mode and is never recorded.

func Resource

func Resource(token string) Option

Resource declares an opaque token naming what this run produces. Preview metadata only.

func SkipIfCurrent

func SkipIfCurrent(token string) Option

SkipIfCurrent declares the token a preview annotates the recorded line with, spelling out that the handler skips this step when the resource is current.

func Stdin

func Stdin(payload []byte) Option

Stdin writes payload to the child's standard input and closes it.

func Stream

func Stream(stream bool) Option

Stream overrides whether a recorded run is rendered as inheriting this process's streams. Without it a recorded run streams unless the caller declared CaptureOutput.

func Timeout

func Timeout(d time.Duration) Option

Timeout kills the child and returns an error wrapping ErrTimeout when it has not exited within d. Zero means no deadline.

type Result

type Result struct {
	// Argv is the command that was run, or -- for a pipeline -- the first
	// stage's command.
	Argv []string
	// ExitCode is the child's exit status. Meaningless when Unsettled.
	ExitCode int
	// Stdout and Stderr are the captured streams. Both are nil unless the
	// caller declared [CaptureOutput], and always nil when Unsettled.
	Stdout []byte
	Stderr []byte
	// Unsettled is true when the run was recorded rather than performed. The
	// exit code and the streams then stand for nothing: a caller that needs
	// them must check this field and decline, which is the honest outcome when
	// nothing ran.
	Unsettled bool
}

Result is the outcome of Handle.Run or Handle.Pipeline.

func (Result) StderrString

func (r Result) StderrString() string

StderrString is Stderr decoded as text with one trailing newline removed.

func (Result) StdoutString

func (r Result) StdoutString() string

StdoutString is Stdout decoded as text with one trailing newline removed -- the form call sites forward into a later command's argv.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL