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
- Variables
- func ShellQuote(token string) string
- type ExitError
- type Handle
- func (h *Handle) AtomicWrite(path string, content []byte, mode fs.FileMode) error
- func (h *Handle) Chmod(path string, mode fs.FileMode) error
- func (h *Handle) CopyFile(src, dst string) error
- func (h *Handle) CopyTree(src, dst string, dirsExistOK bool) error
- func (h *Handle) Mkdir(path string) error
- func (h *Handle) MkdirAll(path string) error
- func (h *Handle) OpenAppend(path string) (io.WriteCloser, error)
- func (h *Handle) OpenWrite(path string) (io.WriteCloser, error)
- func (h *Handle) Pipeline(argvs [][]string, options ...Option) (Result, error)
- func (h *Handle) Previewing() bool
- func (h *Handle) Remove(path string) error
- func (h *Handle) RemoveIfExists(path string) error
- func (h *Handle) Rename(src, dst string) error
- func (h *Handle) RmTree(path string) error
- func (h *Handle) RmTreeIgnoreErrors(path string) error
- func (h *Handle) Rmdir(path string) error
- func (h *Handle) Run(argv []string, options ...Option) (Result, error)
- func (h *Handle) Write(path string, content []byte, mode fs.FileMode) error
- type Option
- func CaptureOutput() Option
- func Check() Option
- func Cwd(dir string) Option
- func Env(env map[string]string) Option
- func Grant(name string) Option
- func Read() Option
- func Resource(token string) Option
- func SkipIfCurrent(token string) Option
- func Stdin(payload []byte) Option
- func Stream(stream bool) Option
- func Timeout(d time.Duration) Option
- type Result
Constants ¶
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 ¶
var ErrTimeout = errors.New("timed out")
ErrTimeout is the sentinel every timed-out subprocess error wraps.
Functions ¶
func ShellQuote ¶
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.
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 ¶
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 ¶
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) CopyTree ¶
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 ¶
Mkdir creates path and any missing parents. An already-existing path is an error, matching Python's os.makedirs default.
func (*Handle) MkdirAll ¶
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 ¶
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 ¶
Previewing reports whether this handle records mutations instead of performing them.
func (*Handle) RemoveIfExists ¶
RemoveIfExists deletes the file or symlink at path. A missing path is not an error.
func (*Handle) RmTree ¶
RmTree recursively deletes the directory tree at path. A missing path is an error, matching Python's shutil.rmtree default.
func (*Handle) RmTreeIgnoreErrors ¶
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) Run ¶
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 ¶
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 Env ¶
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 ¶
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 ¶
Resource declares an opaque token naming what this run produces. Preview metadata only.
func SkipIfCurrent ¶
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 Stream ¶
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.
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 ¶
StderrString is Stderr decoded as text with one trailing newline removed.
func (Result) StdoutString ¶
StdoutString is Stdout decoded as text with one trailing newline removed -- the form call sites forward into a later command's argv.