From a937e9a79d8ef01635c975b326fc33518cdf0f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20G=2E=20MARAND?= Date: Sun, 6 Sep 2026 12:34:13 +0200 Subject: [PATCH] docs(#58): fix non-ASCII docs/code. --- .github/workflows/tests.yml | 2 +- README.md | 14 +-- SECURITY.md | 26 ++--- doc.go | 2 +- .../adr/001-handing-control-to-the-command.md | 54 ++++----- ...-splitting-the-command-from-the-library.md | 104 +++++++++--------- docs/environment-file.md | 8 +- docs/exit-status.md | 8 +- docs/installing.md | 4 +- env/doc.go | 4 +- env/env.go | 39 +++---- env/env_test.go | 4 +- env/errors.go | 16 +-- env/errors_test.go | 4 +- env/example_test.go | 2 +- env/load_internal_test.go | 16 +-- exec_other.go | 4 +- main.go | 2 +- main_test.go | 10 +- main_unix_test.go | 6 +- 20 files changed, 166 insertions(+), 163 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 614dc8a..d15fb3c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,7 @@ jobs: # A second job rather than a matrix on the one above: # Windows runs neither the Makefile's shell recipes nor the race detector, # and reports no coverage, so a matrix would be conditionals in every step. - # What it must prove is narrower — + # What it must prove is narrower - # that the platform still builds and still runs a command. # The transparency v0.2.0 adds is *nix-only (ADR-001), # and Windows keeps the supervising path, diff --git a/README.md b/README.md index 77b18f3..2db37de 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Variables already present in the environment override the ones in the file. The command may have arguments, and it will be looked up in the `$PATH` if its name does not contain a `/`. Many programs, and most IDEs in their run configurations, can read a `.env` file themselves. -`envrun` is for everywhere else — CI/CD, a `just` or `make` target, a plain shell — +`envrun` is for everywhere else - CI/CD, a `just` or `make` target, a plain shell - where the program to be run cannot. If the program is yours and written in Go, you need no wrapper at all: @@ -27,7 +27,7 @@ On \*nix, `envrun` does not wrap the command: it **becomes** it, through `execve`, keeping the same process. Signals, exit status, standard streams and the controlling terminal are the command's own, exactly as if it had read the environment file itself. -Windows has no equivalent and keeps a supervising parent — +Windows has no equivalent and keeps a supervising parent - see [Exit status and platform scope](docs/exit-status.md). ## Installing @@ -62,7 +62,7 @@ A run with no defaults to add is a run that does not need `envrun`, so a missing file is a failure rather than a silent pass-through: configuration that was meant to be there is worth stopping for. -`envrun` reads the file, it never sources it — no expansion, no execution, +`envrun` reads the file, it never sources it - no expansion, no execution, so `DSN=postgres://${INSTANCE}/db` reaches the command exactly as written. A shell sourcing the same file would substitute instead, so keep values plain if both read it. @@ -82,7 +82,7 @@ Apart from `-h`, which prints usage on standard output and exits `0`, `envrun` has statuses only for its own failures, before the command starts, following the convention of coreutils `env`, `timeout` and `nohup`: -- `125`: `envrun` itself failed — the environment file could not be read, +- `125`: `envrun` itself failed - the environment file could not be read, a flag was not understood, or no command was given - `126`: the command exists but could not be executed - `127`: the command could not be found @@ -99,14 +99,14 @@ The rest, and the Windows divergence, is in None at build or run time: `envrun` uses only the standard library. The modules in `go.mod` belong to `staticcheck`, used for linting. Each release archive ships an SPDX SBOM built from the binary itself, -so the claim is checkable without a toolchain — +so the claim is checkable without a toolchain - see the [Installing](docs/installing.md#dependencies) document. ## Related tools `envrun` optimises for being invisible. -The command runs exactly as if it had read the file itself — -same process, same signals, same exit status, same terminal — +The command runs exactly as if it had read the file itself - +same process, same signals, same exit status, same terminal - and nothing in the file is ever expanded or executed, so a value cannot mean one thing to a shell and another to the program. No dependencies, standard library only, diff --git a/SECURITY.md b/SECURITY.md index 508c8b5..dd6890c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,11 +7,11 @@ Until 1.0 that means the newest `v0.x` tag and the binaries published with it: there are no maintenance branches, so an earlier release is superseded, never patched. -| Version | Supported | +| Version | Supported | |-----------------------|:------------------:| | latest `v0.x` release | :white_check_mark: | -| earlier releases | :x: | -| `main` at HEAD | :x: | +| earlier releases | :x: | +| `main` at HEAD | :x: | ## Verifying a release @@ -43,7 +43,6 @@ because it is a list, and the attestation is what says who wrote the list. Each release also carries one SPDX SBOM per archive, generated from the binary's own build graph rather than from `go.mod`. - ## Reporting a Vulnerability To report a vulnerability: @@ -53,6 +52,7 @@ To report a vulnerability: - the first response on that form should be within 1 day Monday to Friday If a vulnerability is: + - accepted: we can work together on a fix, and you will be credited (unless you prefer not to be) on the fix - considered not to be an actual security issue: you will get a suggestion to open it, as an issue on the github issue system for the repo @@ -64,7 +64,7 @@ If a vulnerability is: ### The file is read, never sourced -envrun invokes no shell — not to read the environment file, +envrun invokes no shell - not to read the environment file, and not to start the command. The file is parsed line by line, and the command is started through `execve` with an argument array rather than a command string. @@ -81,8 +81,8 @@ GLOB=* -> the value is one asterisk; no filenames TILDE=~ -> the value is one tilde; no home directory ``` -This is what makes a file from a semi-trusted source — a checkout, a CI -artifact, a pasted snippet — safe to load: +This is what makes a file from a semi-trusted source - a checkout, a CI +artifact, a pasted snippet - safe to load: its contents can become the value of a variable, never a command envrun runs. **Where this one stops.** @@ -100,13 +100,13 @@ See [The environment file](docs/environment-file.md). ### Secrets in diagnostics An environment file often holds secrets, -and envrun writes its diagnostics to standard error — +and envrun writes its diagnostics to standard error - which under CI is a log that is retained, searched, and often world-readable. So envrun reports **names and line numbers, never the text after the first `=`**. That guarantee holds at every point where the library builds a message: -- a rejected name is `line 5: invalid name "9LEADING"` — the name is quoted only +- a rejected name is `line 5: invalid name "9LEADING"` - the name is quoted only where the name is itself the fault; - a NUL in a value is `line 3: value contains NUL`, naming neither the value nor the variable that held it; @@ -114,7 +114,7 @@ That guarantee holds at every point where the library builds a message: - a variable that cannot be set is its name and the system's `setenv: invalid argument`. -Values do reach the command envrun runs — that is what envrun is for. +Values do reach the command envrun runs - that is what envrun is for. This is a property of the diagnostics alone. **Where this one stops.** @@ -125,13 +125,13 @@ A line that is *only* a pasted secret, and whose own bytes contain an `=`, is therefore reported as a bad name, with the secret inside it: ``` -https://user:pw@h/p?k=v → line 1: invalid name "https://user:pw@h/p?k" -aGVsbG8+d29ybGQ= → line 1: invalid name "aGVsbG8+d29ybGQ" +https://user:pw@h/p?k=v -> line 1: invalid name "https://user:pw@h/p?k" +aGVsbG8+d29ybGQ= -> line 1: invalid name "aGVsbG8+d29ybGQ" ``` Both halves are needed for this: a stray line, and an `=` within it. A stray line with no `=` at all is reported as `line 1: not a name=value pair`, quoting nothing. -If it does happen, treat that value as disclosed wherever the log is kept, and rotate it. +If it does happen, treat that value as disclosed wherever the log is kept, and rotate it. Nothing else in the file is affected, every other line being reported by number. diff --git a/doc.go b/doc.go index ac40be6..ac541f7 100644 --- a/doc.go +++ b/doc.go @@ -18,7 +18,7 @@ // Print the usage message on standard output and exit 0, // without reading the environment or running a command. // -// The file is read, never sourced — no expansion and no execution — so a value +// The file is read, never sourced - no expansion and no execution - so a value // reaches the command exactly as written. Its variables are merged under the // inherited environment, so a name already exported wins over the file. // diff --git a/docs/adr/001-handing-control-to-the-command.md b/docs/adr/001-handing-control-to-the-command.md index 83ec6aa..4ffd495 100644 --- a/docs/adr/001-handing-control-to-the-command.md +++ b/docs/adr/001-handing-control-to-the-command.md @@ -26,7 +26,7 @@ $ kill -TERM Terminal use hides this, because Ctrl-C signals the whole foreground process group and so reaches the child directly anyway. -The gap shows when a signal is directed at the envrun process alone — +The gap shows when a signal is directed at the envrun process alone - a supervisor, a `just` target, or a plain `kill `. The goal stated while preparing the fix reframes what a fix should achieve: @@ -45,11 +45,11 @@ They are recorded here because they are expensive to reproduce and easy to misremember, not because they all favour one option. -### 1. Supervise, and forward signals — the issue as filed +### 1. Supervise, and forward signals - the issue as filed Register `signal.Notify`, relay to the child, and exit `128+signo` when the child dies of a signal. -- **A blanket `signal.Notify(c)` delivers SIGURG** — 21 of them in one busy run. +- **A blanket `signal.Notify(c)` delivers SIGURG** - 21 of them in one busy run. Go's runtime uses SIGURG for asynchronous preemption, so "forward everything" forwards runtime noise into the child. - **`Notify` disables default behaviour at registration**, which makes a denylist impossible rather than merely awkward: @@ -58,7 +58,7 @@ Register `signal.Notify`, relay to the child, and exit `128+signo` when the chil - so the set of signals to handle has to be an allowlist, and the allowlist is exactly the thing that will be found incomplete later. - **Some signals cannot be relayed at all.** - - `kill -SEGV` with `Notify` registered still dies through `runtime.throw` — `sigcode=2` (`SI_USER`), + - `kill -SEGV` with `Notify` registered still dies through `runtime.throw` - `sigcode=2` (`SI_USER`), register dump, exit 2. - SIGSEGV, SIGBUS and SIGFPE therefore cannot be forwarded; - SIGKILL and SIGSTOP cannot be caught in the first place. @@ -76,7 +76,7 @@ The allowlist is unavoidable, and three signals cannot be relayed even in principle, so any version of this ships with a known-incomplete taxonomy. -### 2. Replace the process — `syscall.Exec` +### 2. Replace the process - `syscall.Exec` After `execve` there is no wrapper left: signals, exit status, stdio and the controlling terminal all belong to the command, @@ -103,7 +103,7 @@ because it *is* the process that was envrun. SigCgt: 0000000000000400 ``` - - `SigBlk` and `SigIgn` are empty, so nothing is handed to the command blocked or ignored — + - `SigBlk` and `SigIgn` are empty, so nothing is handed to the command blocked or ignored - and that held across *two* chained `execve` calls, because `sh -c` with a single command execs straight into `grep`. - The `SigCgt` bit is signal 11, SIGSEGV, installed by `grep` itself after exec: @@ -119,7 +119,7 @@ because it *is* the process that was envrun. Measured 2026-08-23 under `go test -covermode=atomic -coverprofile`: `runtime/coverage.WriteMetaDir` returns "no meta-data available (binary not built with -cover?)", - and `WriteCountersDir` reports "invoked for program built with -covermode=" — + and `WriteCountersDir` reports "invoked for program built with -covermode=" - in the helper process and the test process alike. Those APIs work only in a binary built with `go build -cover`. `make cover` passes `-covermode=atomic`, @@ -128,20 +128,20 @@ because it *is* the process that was envrun. the in-process ENOEXEC test *executes* the `syscall.Exec` statement, since exec returns on failure, and statement coverage cannot tell the success half of a statement from the failure half. - A subprocess test is still wanted for **behaviour** — - that a child's exit 42 passes through — + A subprocess test is still wanted for **behaviour** - + that a child's exit 42 passes through - but it never needed to reach the coverage profile. **Verdict: chosen, on \*nix.** See *Decision*. -### 3. Fork and reap by hand — `syscall.ForkExec` with our own `Wait4` +### 3. Fork and reap by hand - `syscall.ForkExec` with our own `Wait4` The only option that can observe job-control stops, and the only one that gives full control over process groups and death signals. It is also the largest: it takes over responsibilities `os/exec` currently handles correctly, for a capability nothing has asked for. -Recorded here so that it is visibly declined rather than overlooked — +Recorded here so that it is visibly declined rather than overlooked - it is a reasonable design for a process supervisor, but envrun is not one at this stage in its life. **Verdict: declined at this stage in the project's lifetime, not missed.** @@ -164,13 +164,13 @@ This is what the installation advice has to answer to: the transparency envrun offers is only as good as the layer it is invoked through. - **`go run` does not forward SIGTERM.** - Reproduced live: the child is orphaned to PID 1 — the very failure #35 reports. + Reproduced live: the child is orphaned to PID 1 - the very failure #35 reports. - **`go tool` forwards only `{HUP, INT, QUIT, TERM}`** (`cmd/go/internal/tool/signal.go:14`) and **remaps a tool's signal death to exit 1** (`cmd/go/internal/tool/tool.go:429-433` in Go 1.27.0). So under `go tool envrun`, exit-status transparency is lost at the `go` layer, no matter how faithful envrun is, and USR1, USR2 and WINCH still orphan. -This is not an argument against exec — supervision through the same layer is strictly worse. +This is not an argument against exec - supervision through the same layer is strictly worse. **It is an argument about installation, and one route already avoids it.** `go install github.com/fgm/envrun@latest` produces a binary in `$GOBIN` @@ -187,13 +187,13 @@ installs the version that module's `go.mod` already pins, so a consumer can hold the pin *and* get a wrapper-free binary. - **`go tool envrun`** re-resolves the pin on every run, - so the version can never drift from `go.mod` — but the `go` process interposes, + so the version can never drift from `go.mod` - but the `go` process interposes, forwarding four signals and flattening signal death to exit 1. -- **A `go install`ed binary** is fully transparent — but it pins at install time, +- **A `go install`ed binary** is fully transparent - but it pins at install time, nothing re-checks it afterward, and `GOBIN` is shared, so one project's install silently changes which envrun every other project gets. -- **A project-local build** — `go build -o bin/ github.com/fgm/envrun` - from inside the consuming module — is transparent *and* fixes the drift instead of relocating it. +- **A project-local build** - `go build -o bin/ github.com/fgm/envrun` + from inside the consuming module - is transparent *and* fixes the drift instead of relocating it. It resolves through the same `go.mod` pin, it does not touch `GOBIN`, so projects pinning different versions no longer fight, and because the output is in the project it can be a build target with `go.mod` as its prerequisite: @@ -208,7 +208,7 @@ so a consumer can hold the pin *and* get a wrapper-free binary. Verified 2026-08-23: `go build -o /` creates the directory if absent, so no `.gitkeep` is needed; and building a tool dependency into a directory from a consuming module works as expected. - **Ignore the built binary, not the directory** — + **Ignore the built binary, not the directory** - `bin/` is frequently a tracked source directory rather than a scratch one (one consuming project keeps six shell scripts and a `//go:build ignore` helper there), so `/bin/envrun` in `.gitignore`, never `bin/`. @@ -223,7 +223,7 @@ A README that says "prefer `go install`" without "and reinstall when you bump th Note also that `go tool` remaps only `ExitCode() == -1`, that is death by signal; an ordinary non-zero exit passes through untouched. So a consumer needing -exit-status fidelity for ordinary failures — not signal transparency — is served +exit-status fidelity for ordinary failures - not signal transparency - is served by `go tool` plus a current pin, and need not change invocation at all. #41 is therefore a matter of reach rather than of feasibility: signed release @@ -239,8 +239,8 @@ Process creation goes through `CreateProcess`, which always produces a *new* process with a new PID, so "replace yourself with the command" cannot be expressed. -Its signal and exit-status model differ too — console control events rather than POSIX signals, -and plain exit codes with no signal encoding — so `128+signo` has no meaning there either. +Its signal and exit-status model differ too - console control events rather than POSIX signals, +and plain exit codes with no signal encoding - so `128+signo` has no meaning there either. ### Classifying a failure to start @@ -259,7 +259,7 @@ where execve reports ENOEXEC and envrun stops at 126. envrun becomes the command rather than supervising it, so the transparency it offers is the process's own rather than something it emulates. -**Windows keeps `os/exec`**, the shape not being expressible there — +**Windows keeps `os/exec`**, the shape not being expressible there - see *Windows has no fork/exec pair*. Supervision is not a fallback on that platform, it is the only available shape, and it will not become available later. @@ -269,7 +269,7 @@ Only `run`, the one step that starts the command, has two implementations, selected by build tag; the program itself carries no constraint. Tagging the program instead, with a stub `main` refusing to run off \*nix, would turn a working platform into a hard failure -in order to improve a different one — +in order to improve a different one - envrun builds and works on windows/amd64 today, `os/exec` being portable. **This release is therefore \*nix-only in what it guarantees**, @@ -304,11 +304,11 @@ the platform is supported, the transparency is not. - `tests.yml` runs a Windows job, so a regression in any of it fails visibly. - **Testability.** Exec never returns on success, so the tests split in two, and the split follows the statuses. - - The pre-exec failures — 125, 126, 127 — stay **in process**, + - The pre-exec failures - 125, 126, 127 - stay **in process**, and are the ones #18 lists as uncovered. ENOEXEC reaches the `syscall.Exec` call in process; so do EINVAL (a NUL in a value, demonstrated) and, on linux, - ETXTBSY and E2BIG — ENOEXEC is not the only one. + ETXTBSY and E2BIG - ENOEXEC is not the only one. - Everything past the point where the command starts needs a **subprocess**, and the test binary is its own: it re-enters as envrun, or as a helper reporting one fact about the process it runs in. @@ -319,7 +319,7 @@ the platform is supported, the transparency is not. - The subprocess half contributes nothing to the coverage profile, and cannot: the flush APIs need a `go build -cover` binary, as measured above. It is there for behaviour, not for the percentage. -- **`exitStatus` needs three explicit arms** — `ErrNotFound`, `fs.ErrPermission`, +- **`exitStatus` needs three explicit arms** - `ErrNotFound`, `fs.ErrPermission`, and `EISDIR` and `ENOEXEC` each on its own, for the reason measured under *Classifying a failure to start*. @@ -327,7 +327,7 @@ the platform is supported, the transparency is not. - **envrun stops being observable once the command starts.** - No wrapper means no place to log, count, time, or retry. - - Anything of that kind must happen before `execve` or not at all — + - Anything of that kind must happen before `execve` or not at all - which also means #3's verbose flag can only ever report on the environment, never on the command's behaviour. - **The README gets shorter, not longer.** The reasoning moves here, diff --git a/docs/adr/002-splitting-the-command-from-the-library.md b/docs/adr/002-splitting-the-command-from-the-library.md index b0b6b73..f8815b0 100644 --- a/docs/adr/002-splitting-the-command-from-the-library.md +++ b/docs/adr/002-splitting-the-command-from-the-library.md @@ -2,14 +2,14 @@ - Status: Accepted - Date: 2026-08-23 -- Ticket: #43 — split out of #35 on 2026-08-23 +- Ticket: #43 - split out of #35 on 2026-08-23 - Scope: the package layout and the shape of the library API. How the command is handed over is ADR-001. ## Context envrun is a command. -Its goal, recorded in ADR-001 — run a command "as if it had the environment internally" — +Its goal, recorded in ADR-001 - run a command "as if it had the environment internally" - has a second half that the command form cannot serve: a Go program should be able to obtain the same environment *internally*, with no wrapper process at all. ```go @@ -18,7 +18,7 @@ res, err := env.Apply() // that is the whole of it **Why not simply reach for `joho/godotenv` in the Go code, and envrun in CI?** Because the two read the same file differently. -godotenv *accepts* what envrun rejects — interpolation, quoted multiline values, lax names — +godotenv *accepts* what envrun rejects - interpolation, quoted multiline values, lax names - so a team using both would have its CI and its tests reading different dialects of one file, and finding out only when they disagreed. The single core is what makes *the same file means the same thing* a promise rather than a slogan, @@ -34,9 +34,9 @@ The split is what makes the second delivery possible; it is not a tidying exerci ### Layout -- `github.com/fgm/envrun` — the CLI stays at the root, +- `github.com/fgm/envrun` - the CLI stays at the root, because "the CLI version is likely to be typed more times than the import path". -- `github.com/fgm/envrun/env` — load, parse, merge, apply. +- `github.com/fgm/envrun/env` - load, parse, merge, apply. A third package, `github.com/fgm/envrun/autoload`, was examined and declined: see *Rejected: an `autoload` package*. @@ -48,7 +48,7 @@ see *Rejected: an `autoload` package*. Problems are *returned rather than logged*: a library cannot know what its caller's output has to look like, so presenting them is the caller's job. -Returning them flattened into one string is not enough either — +Returning them flattened into one string is not enough either - a caller must be able to count them, locate them in the file, and act on one without matching on a message. @@ -65,22 +65,22 @@ so the argument has to be made three times. A slog application can pass `slog.NewLogLogger(handler, level)` and get clean, correctly framed records. What it loses is the structure it wanted: the whole message arrives as one `msg` string, - so the JSON consumer — the very caller the first objection was about — + so the JSON consumer - the very caller the first objection was about - is worse off than if it had been handed the data. `slog.NewLogLogger(handler, level)` also takes its level *once*, at - construction, and a `*log.Logger` has no notion of level to vary it with — + construction, and a `*log.Logger` has no notion of level to vary it with - so every message the library sends arrives at the one severity the caller chose when building the bridge, and filtering cannot separate one kind of finding from another. That costs nothing while the only output is Notes; it is the shape foreclosing something rather than present harm. -- **`*slog.Logger`** answers both of those — - structure is preserved, levels are per-call — and fails on reach instead. +- **`*slog.Logger`** answers both of those - + structure is preserved, levels are per-call - and fails on reach instead. It is a concrete struct, not an interface, and it cannot be derived from an existing logger: the bridge runs the other way, `slog.NewLogLogger` producing a `*log.Logger` from a handler, with nothing coming back. So a caller already on zap, logrus or apex must either adopt `slog` - or write a `slog.Handler` wrapping their own logger — a dependency, + or write a `slog.Handler` wrapping their own logger - a dependency, and a decision envrun would be imposing on them. The standard library declines this itself: `http.Server.ErrorLog` and `httputil.ReverseProxy.ErrorLog` are still @@ -93,7 +93,7 @@ and none of them dominates: - `*slog.Logger` carries everything and accepts almost no one, - `*log.Logger` sits in between. -That is the shape of a false choice, and returning the data escapes it — +That is the shape of a false choice, and returning the data escapes it - the library already hands back everything it observed. Hence `Result.Notes` carries the non-fatal findings and `ParseError` the fatal ones, @@ -105,22 +105,22 @@ which is never the library. #### Chosen shape: return the data, and let the caller present it -- `Result.Path` — which file was actually used. -- `Result.Env` — what the file declared, never the merge. +- `Result.Path` - which file was actually used. +- `Result.Env` - what the file declared, never the merge. The merged view would be redundant: the file's set is merged *under* the inherited environment, which therefore **overrides it**, and that result is what the command is given. After applying, `os.Environ()` *is* the merge. - The file-only set is the part that cannot be recovered — once applied, + The file-only set is the part that cannot be recovered - once applied, a file-sourced variable is indistinguishable from an inherited one. That distinction is issue #39: `-clean` needs the file's set alone as `cmd.Env`, the default needs it merged. A library returning only the merge cannot serve it, and issue #39 alone justifies the field. -- `Result.Notes` — non-fatal findings. +- `Result.Notes` - non-fatal findings. A failed close of the file is the only one raised so far, and is a Note rather than an error because the file has already given everything it had: refusing to run over it would withhold a working environment for a problem that no longer affects the command. - A repeated name is the next, once the parser detects one at all — see *Semantics #3 needs*. + A repeated name is the next, once the parser detects one at all - see *Semantics #3 needs*. - Rejections stay fatal but become inspectable in two ways because callers want two different things. Each problem wraps one of `ErrNotAPair`, `ErrInvalidName` or `ErrNUL`, and the file's `ParseError` wraps every problem it collected through `Unwrap() []error`, @@ -134,7 +134,7 @@ which is never the library. It is the obvious way to wrap several errors, and `Unwrap() []error` is exactly what it produces, so the reach it buys is kept above. What it cannot carry is the rest: `Path` has nowhere to live, counting the problems means walking the multi-error interface -by hand, and `errors.Join` renders its errors one per line — where these leave +by hand, and `errors.Join` renders its errors one per line - where these leave through a= single `envrun failed:` line, which would put every problem after the first outside the attribution contract the README documents. @@ -171,14 +171,14 @@ which is why the command's entry point must apply nothing. Two entry points, then, because envrun genuinely has two callers: -- `env.L=oad(paths ...string) (Result, error)` — resolve and read, apply nothing. +- `env.L=oad(paths ...string) (Result, error)` - resolve and read, apply nothing. The CLI's entry point. -- `env.Apply(paths ...string) (Result, error)` — `Load`, then merge, mutating the environment. +- `env.Apply(paths ...string) (Result, error)` - `Load`, then merge, mutating the environment. One line for an importer, and still enough returned for issues #3 and #39. `Apply` mutates process state, so it belongs at the top of `main`, or in `TestMain`, before anything concurrent starts. -The Go side is safe on its own — `syscall.Setenv` and `Getenv` share an`envLock` mutex — +The Go side is safe on its own - `syscall.Setenv` and `Getenv` share an`envLock` mutex - so the hazard is not Go against Go. It is twofold: - **cgo.** With cgo linked in, setting a variable also calls C `setenv` through @@ -195,7 +195,7 @@ the process, leaving the caller to pass the values where they are wanted. That is a second reason for `Load` to exist, independent of the command's needs. For a caller following the convention envrun itself uses, the constraint costs nothing. -`main` is the locus of global access — here it sets the log flags and does nothing else — +`main` is the locus of global access - here it sets the log flags and does nothing else - and calls `realMain` with everything injected, environment included. Such a caller already has a place for `Apply` before it has anything to race with, @@ -215,15 +215,15 @@ It is declined **as currently imagined**. The idea may return in another shape; this one does not survive its own arguments. - **Blank or useful, never both.** The moment a program reads `autoload.Err()`, - the import stops being blank — + the import stops being blank - and at that point `env.Apply()` is shorter and carries no package-level state. The one-line form exists only for a program that checks nothing. - **Checking nothing contradicts the parser.** envrun fails a whole file over one bad line, on the grounds that a line in the file would have reached nothing without envrun, so running without the operator's configuration is worse than not running at all. An unchecked autoload does precisely that, silently. -- **Panicking does not rescue it.** The obvious answer — panic in `init`, - so failure is loud without anyone checking — borrows a precedent that does not transfer. +- **Panicking does not rescue it.** The obvious answer - panic in `init`, + so failure is loud without anyone checking - borrows a precedent that does not transfer. `sql.Register`, `regexp.MustCompile` and `template.Must` panic on *programmer* error: values the program supplied as constants. A malformed `.env` is *input*, and the convention for input is `regexp.Compile`, @@ -231,7 +231,7 @@ this one does not survive its own arguments. A panicking autoload would abort a program in production over a stray file the operator may not know is read, inside an `init` no importer can guard. - **Its one structural advantage does not work.** - Running before other packages' `init` functions is the only thing `env.Apply` in `main` cannot do — + Running before other packages' `init` functions is the only thing `env.Apply` in `main` cannot do - but relative init order follows the import graph, which the importer does not control, so the advantage is unreliable rather than real. - **Its search rule is the one silent break in this ADR.** @@ -240,8 +240,8 @@ this one does not survive its own arguments. Everything else here breaks loudly if revised, which is why this is the question decided by not shipping rather than by choosing. -The habitat the idea is strongest in — a blank import in a `_test.go`, -one line per package instead of a `TestMain` — is already served better from both sides. +The habitat the idea is strongest in - a blank import in a `_test.go`, +one line per package instead of a `TestMain` - is already served better from both sides. `envrun go test ./...` applies the same file with the same semantics, from a directory the operator chose, failing before any test runs; and `env.Apply` in a three-line `TestMain` keeps a real error path for a single package. @@ -255,7 +255,7 @@ and it is recorded so that a future proposal answers these five arguments rather ### Who prints, and how -The library never writes output — it returns what it observed instead — +The library never writes output - it returns what it observed instead - which leaves the question for the CLI alone. It reports through two package-level helpers, `fail(err)` and `note(format, args...)`, @@ -263,7 +263,7 @@ writing to `log`'s default logger with the flags cleared. Two shapes, one destination, no levels. **That covers diagnostics, not everything envrun writes.** -Output the user *asked for* — `-h`'s usage today, `-version` later — +Output the user *asked for* - `-h`'s usage today, `-version` later - goes to standard output and carries no prefix, because it is the command's product rather than a report about it, and a caller redirecting it wants it apart from the diagnostics. @@ -282,7 +282,7 @@ The decisive argument is about when envrun writes at all, not about convention: Every problem `slog` exists to solve is one this path cannot have. - **Where a parent survives, the one moment envrun writes after the command is the moment structure reads worst.** On Windows `run` reports the command's - own exit status after it has produced all its output, on the same handle — + own exit status after it has produced all its output, on the same handle - Windows has a genuine separate stderr, `STD_ERROR_HANDLE`, which `cmd.Stderr = os.Stderr` binds. A bare `envrun: mytool exited with status 1` belongs in that stream; `msg="mytool exited with status 1"` does not. @@ -292,12 +292,12 @@ The decisive argument is about when envrun writes at all, not about convention: line beginning `envrun failed:` means the status is envrun's rather than the command's, and `envrun:` reports what the command did. A `slog.Handler` choosing between them must key off `Record.Level`, - which makes a routing fact depend on a severity — so a future non-fatal error + which makes a routing fact depend on a severity - so a future non-fatal error would have to be logged at `Info` to avoid claiming envrun failed. - **The formats are reachable, at a price worth naming.** `log` needs no help: cleared flags give a bare line and `SetPrefix` gives the other shape. `slog` can also produce them, but not through `HandlerOptions`: - `ReplaceAttr` drops the time and level, leaving `msg="…"`, + `ReplaceAttr` drops the time and level, leaving `msg="..."`, because key=value is what `TextHandler` *is*. Reproducing the two shapes takes a custom `slog.Handler` of about twenty lines, against four for the helpers it would replace. @@ -308,9 +308,9 @@ The decisive argument is about when envrun writes at all, not about convention: **The package-level logger is not a defect here.** It would be one in a program with goroutines; a command that starts none has a single destination for its whole life, and a global models that. -The one cost is in the tests. They assert on the exact lines envrun prints — +The one cost is in the tests. They assert on the exact lines envrun prints - the `envrun failed:` prefix is what makes a status attributable, so it is worth -pinning — which means capturing the output in a buffer. +pinning - which means capturing the output in a buffer. With a global destination that means `log.SetOutput`: five sites across three files, and no `t.Parallel()`. The `testing` package is no help, never touching `log` at all: @@ -330,13 +330,13 @@ The library path serves that same goal from inside a Go program. It is **not** an attempt to provide a general `.env` package, and features are not adopted on the grounds that other `.env` libraries have them. ADR-001 states the goal as running a command as if it had the environment internally, -rather than adding features around that — +rather than adding features around that - and a feature adopted because a neighbouring project has one is exactly the second thing. This is why the parser is deliberately narrower than the alternatives, rejecting multiline quoted values and names outside the accepted character set, rather than interpreting them. -Variable interpolation inside values — `${OTHER}` — falls on the same side of that line: +Variable interpolation inside values - `${OTHER}` - falls on the same side of that line: it is a feature of the file format, not of running a command with an environment, and it is declined here for that reason rather than for lack of precedent. See PR #6 for the request, and ADR-001 for the goal statement. @@ -358,20 +358,20 @@ The table describes what is built: both ADRs are accepted, and all three of this project's cells name code that exists. It was written down while two of them were still intent, on purpose, because a stated goal is what a proposed feature gets measured against. -Without one, whatever library came up most recently in discussion becomes the default answer — +Without one, whatever library came up most recently in discussion becomes the default answer - which is how interpolation was nearly adopted above, on no stronger ground than that another project had it. ### Discovery, not composition -`paths ...string` reads two ways — files to search, or files to merge — +`paths ...string` reads two ways - files to search, or files to merge - and they are different features. It has to mean one of them: two behaviors on one variadic is how an API becomes ambiguous. Four things decide it, and none needs looking outside this repository: - **`-f` already exists, and it names one exact file.** - Under discovery that is an override — the search with a single candidate. + Under discovery that is an override - the search with a single candidate. Under composition it has no coherent meaning: - it would either be a one-element merge, which is the same as discovery, or a contradiction of it. - **`Result.Path` only makes sense under discovery.** @@ -379,21 +379,21 @@ Four things decide it, and none needs looking outside this repository: - and none when several are layered. Reporting it is a requirement of #3, so composition would cost the field. - **Composition needs a precedence rule between files**, - and envrun's precedence between the *file* and the *inherited environment* is itself unsettled — + and envrun's precedence between the *file* and the *inherited environment* is itself unsettled - PR #6 asked for it to be reversed and it never was. Adding a second, independent ordering question on top of an open one is not a trade worth making now. - **The two are not symmetric in cost.** - Composition can be added later without disturbing discovery — + Composition can be added later without disturbing discovery - a separate parameter, or a separate call. Discovery cannot be retrofitted onto a parameter that already merges, because programs will by then depend on every listed file being read. Conventional usage agrees: -a config search path in Go — viper's `AddConfigPath` with `ReadInConfig`, -for one — takes the first match, and layering is a distinct call that a caller opts into. +a config search path in Go - viper's `AddConfigPath` with `ReadInConfig`, +for one - takes the first match, and layering is a distinct call that a caller opts into. So `paths...` is a search path, first match wins, and `-f` overrides it. -Composition — `.env` then `.env.local` — stays available as a later additive feature, +Composition - `.env` then `.env.local` - stays available as a later additive feature, but must not share this parameter. ### Semantics issue #3 needs, which do not exist yet @@ -401,7 +401,7 @@ but must not share this parameter. - **A repeated name keeps the last value.** Deliberate, and the rule `Merge` already follows, so the semantics need no deciding here. - What is missing is only the detection needed to raise a `Note` for one — see #3. + What is missing is only the detection needed to raise a `Note` for one - see #3. - **The ordering problem was imported, not found.** Earlier drafts of this ADR said `Env` needed a stable order, because issue #3 would "dump the computed environment", and that a map could not give one. @@ -416,8 +416,8 @@ but must not share this parameter. whether the parser change raising a `Note` for a repeated name lands with this ADR or with #3. The `Note` it will raise wants more than a string. -A duplicate is inherently *two* positions — declared at one line, overridden at -another — and it needs the name besides. +A duplicate is inherently *two* positions - declared at one line, overridden at +another - and it needs the name besides. The file is not among them: it is already `Result.Path`, named once for the whole result rather than repeated on every finding. So it arrives as a further implementation of `Note`, carrying those fields. @@ -428,8 +428,8 @@ because nothing consumes it until #3 exists. Most notes report nothing that failed: a file with a repeated name is valid, which is why its fixture is `pass-duplicate-name.env`, so typing every note as an error would have the API contradict the parser. -The notes that _do_ report a failure implement `error` as well — -`CloseError` wraps what `Close` returned, keeping an `*io/fs.PathError` reachable — +The notes that _do_ report a failure implement `error` as well - +`CloseError` wraps what `Close` returned, keeping an `*io/fs.PathError` reachable - so nothing is lost by the narrower contract. Callers discriminate notes by type, which is why the implementations are exported. @@ -451,7 +451,7 @@ it may name the variable and the lines, never either value. one file means one thing whether it is read by the command or imported. Symmetry with the CLI is not a reason. - The library returns data and never writes output. -- **Every logger parameter is rejected** — `io.Writer`, `*log.Logger` and `*slog.Logger` — +- **Every logger parameter is rejected** - `io.Writer`, `*log.Logger` and `*slog.Logger` - because `Result` already carries everything observed, and a logger would be a second channel for the same findings. - **The library offers both entry points; the CLI may use only the non-applying one.** @@ -460,7 +460,7 @@ it may name the variable and the lines, never either value. - **No value leaves the package inside a message.** Names and line numbers only. - **This is not a general `.env` package**, and a feature is not adopted because other `.env` libraries carry it. - Variable interpolation is declined on that basis — see PR #6. + Variable interpolation is declined on that basis - see PR #6. - `paths...` is discovery, not composition. - **`Env` is `map[string]string`.** The ordering argument that questioned it rested on a dump issue #3 never asks for. diff --git a/docs/environment-file.md b/docs/environment-file.md index c83e360..31ab867 100644 --- a/docs/environment-file.md +++ b/docs/environment-file.md @@ -17,7 +17,7 @@ override the ones the file defines. and any name outside `[_A-Za-z][-._A-Za-z0-9]*`. - One bad line fails the whole file, because running a command on configuration the operator meant to set is worse than not running it. - - This half is loud — `envrun` exits `125` before the command starts, + - This half is loud - `envrun` exits `125` before the command starts, naming the line at fault. A file it cannot read at all, such as one holding a line over 64 KiB, fails the same way but without a position. @@ -32,12 +32,12 @@ DSN=postgres://${INSTANCE}/db - A shell sourcing the same file would substitute, or execute, and hand the command something else entirely. - **This half is silent**: both readers succeed, and the values simply differ. - If a file is read both ways — sourced by a launcher, + If a file is read both ways - sourced by a launcher, and passed to `envrun` elsewhere, the two diverge invisibly. Keep values plain, or read the file one way only. A line without `=` is refused here even though the *inherited* environment may hold one, -and it is carried through — see below. +and it is carried through - see below. The rule for the file is simply that every line must declare a variable; a line that declares none is refused rather than passed on, because passing it on would leave the variable it was meant to set absent, @@ -66,6 +66,6 @@ The same shape of row in the *file* fails it instead, which is not an inconsistency: what differs is the baseline. An inherited row would have reached the command with or without `envrun`, so carrying it is faithful. -A file line would have reached nothing at all, since nothing else reads the file — +A file line would have reached nothing at all, since nothing else reads the file - so there is no behaviour to preserve, only an intent that did not take effect, and saying so is more useful than passing on a row no name can match. diff --git a/docs/exit-status.md b/docs/exit-status.md index dea3678..8e30f64 100644 --- a/docs/exit-status.md +++ b/docs/exit-status.md @@ -17,7 +17,7 @@ without reading the environment or running anything, They follow the convention used by coreutils `env`, `timeout` and `nohup`, which keeps them out of the range a command is likely to use: -- `125`: `envrun` itself failed — the environment file could not be read, +- `125`: `envrun` itself failed - the environment file could not be read, a flag was not understood, or no command was given - `126`: the command exists but could not be executed - `127`: the command could not be found @@ -45,8 +45,8 @@ Choosing an interpreter is not `envrun`'s job. ## Platform scope The transparency above is \*nix-only, and permanently so. -Windows has no fork/exec pair — -`CreateProcess` always makes a new process with a new PID — +Windows has no fork/exec pair - +`CreateProcess` always makes a new process with a new PID - so "replace yourself with the command" cannot be expressed there. Windows is supported and tested: `envrun` builds and runs commands, standard input included, @@ -61,7 +61,7 @@ Being still there, it also reports what it saw the command do: envrun: sh exited with status 42 ``` -Prefixed `envrun:` and never `envrun failed:` — +Prefixed `envrun:` and never `envrun failed:` - `envrun` did not fail, and the status is the command's. Why `execve` rather than forwarding signals from a wrapper is recorded in diff --git a/docs/installing.md b/docs/installing.md index 6d640fd..54c60bf 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -81,7 +81,7 @@ which is often a tracked source directory. Both work, and both cost something the recipe above does not: - **`go tool github.com/fgm/envrun`** re-resolves `go.mod` on every run, - so the version can never drift — + so the version can never drift - but the `go` process stays between the caller and the command. It forwards four signals (`HUP`, `INT`, `QUIT`, `TERM`), and reports a command killed by any signal as exit 1. @@ -95,7 +95,7 @@ Both work, and both cost something the recipe above does not: installs that module's pinned version, which fixes the pin but not the sharing. - **`go run`** interposes and does not forward `SIGTERM` at all. -A download shares `go install`'s weakness — nothing re-checks it — and avoids +A download shares `go install`'s weakness - nothing re-checks it - and avoids its sharing problem, since you decide where the binary lands. ## Dependencies diff --git a/env/doc.go b/env/doc.go index 1873d79..6a052c8 100644 --- a/env/doc.go +++ b/env/doc.go @@ -4,7 +4,7 @@ // The command must not put the variables into its own process: // what it needs is the set to hand to the command it runs, // which under ADR-001 becomes that command's environment across execve. -// A Go program importing this package wants the opposite — +// A Go program importing this package wants the opposite - // the variables in its own process, with no wrapper process at all. // // So there are two entry points over one core: @@ -19,7 +19,7 @@ // would interleave non-JSON lines into a JSON stream. // // It is deliberately not a general .env package. -// The file format is narrow — see [ParseError] for what it refuses and why — +// The file format is narrow - see [ParseError] for what it refuses and why - // and a feature is not adopted here on the grounds that other .env libraries carry it. // See docs/adr/002-splitting-the-command-from-the-library.md. package env diff --git a/env/env.go b/env/env.go index 812c78b..45766e1 100644 --- a/env/env.go +++ b/env/env.go @@ -37,7 +37,7 @@ const DefaultPath = ".env" // // It is a map because nothing here needs an order: // execve takes an array the kernel does not order, -// and the one place order matters — reporting — can sort at presentation time, +// and the one place order matters - reporting - can sort at presentation time, // where the caller knows what it is sorting for. type Vars map[string]string @@ -86,7 +86,7 @@ type Result struct { // // If names overlap, the argument wins over the receiver, as in PHP array_merge. // The command's merge is fileEnv.Merge(inherited), so the inherited environment -// overrides the file — see PR #6 for the request to reverse that. +// overrides the file - see PR #6 for the request to reverse that. func (v Vars) Merge(w Vars) Vars { res := make(Vars, len(v)+len(w)) maps.Copy(res, v) @@ -247,7 +247,7 @@ func parseReader(r io.Reader) (Vars, []Problem, error) { // An importer wants [Apply] instead. // // paths is a search path, not a list to merge: the first candidate that exists -// wins, the rest are never read, and composition — .env then .env.local — stays +// wins, the rest are never read, and composition - .env then .env.local - stays // a separate feature rather than a second meaning for this parameter. With no // path at all it looks for [DefaultPath] in the working directory. The command's // -f flag overrides the search by naming its one candidate. @@ -257,8 +257,8 @@ func parseReader(r io.Reader) (Vars, []Problem, error) { // - the file's contents, as a [*ParseError], // whose problems are reachable one by one with errors.AsType; // - reaching the file at all, as an *io/fs.PathError. -// errors.Is against fs.ErrNotExist then separates "no file" — -// which may be no error at all for a caller with an optional one — +// errors.Is against fs.ErrNotExist then separates "no file" - +// which may be no error at all for a caller with an optional one - // from a file that is there but could not be read. // // A failing Result is not empty: [Result.Path] and [Result.Notes] are returned @@ -272,8 +272,8 @@ func Load(paths ...string) (Result, error) { return loadFile(file, path) } -// openFirst opens the first candidate that exists, and reports which one that -// was: with a search path, the caller cannot otherwise tell. +// openFirst opens the first candidate that exists, and reports which one that was: +// with just a search path, the caller could not tell. // // The file is returned open, and closing it is the caller's job from here. func openFirst(paths []string) (*os.File, string, error) { @@ -287,10 +287,10 @@ func openFirst(paths []string) (*os.File, string, error) { if err == nil { return f, candidate, nil } - // Only a missing file is a miss. A candidate that exists but cannot be - // read — no permission, a directory — is a problem to report rather than - // a reason to look further: silently falling through to the next - // candidate would hide it. + // Only a missing file is a miss. + // A candidate that exists but cannot be read - no permission, a directory - + // is a problem to report rather than a reason to look further: + // silently falling through to the next candidate would hide it. if !errors.Is(err, iofs.ErrNotExist) { return nil, "", fmt.Errorf("reading %s: %w", candidate, err) } @@ -299,12 +299,13 @@ func openFirst(paths []string) (*os.File, string, error) { return nil, "", fmt.Errorf("reading %s: %w", strings.Join(paths, ", "), lastErr) } -// loadFile reads an opened environment file, closes it, and reports what it -// declared. path names the file, for the Result and any [ParseError] to carry. +// loadFile reads an opened environment file, closes it, +// and reports what it declared. +// path names the file, for the Result and any [ParseError] to carry. // -// It takes an interface where its only caller holds an *os.File, because the -// close it has to report on is the one thing a real file will not do: a -// descriptor opened read-only has nothing left to fail at. +// It takes an interface where its only caller holds an *os.File, +// because the close it has to report on is the one thing a real file will not do: +// a descriptor opened read-only has nothing left to fail at. func loadFile(rc io.ReadCloser, path string) (Result, error) { v, problems, err := parseReader(rc) // Closed here rather than deferred: the command hands over with syscall.Exec, @@ -337,13 +338,13 @@ func loadFile(rc io.ReadCloser, path string) (Result, error) { // spare them: an API that returned the merge-and-apply loop to its caller would // not be simplifying anything. // -// It mutates process state, so it belongs at the top of main, or in TestMain, +// It mutates the process state, so it belongs at the top of main, or in TestMain, // before anything concurrent starts. // Go-to-Go access is safe on its own, since syscall.Setenv and Getenv share a // mutex. What that mutex does not reach is: // -// - a C library calling getenv on another thread; -// - any reader that captured a value before the change, and never learns of it. +// - A C library calling getenv on another thread; +// - Any reader that captured a value before the change and never learns of it. // // There is no concurrency-safe way to mutate a process environment. // Callers who need one want [Load], which touches nothing diff --git a/env/env_test.go b/env/env_test.go index 4d304a7..9504294 100644 --- a/env/env_test.go +++ b/env/env_test.go @@ -18,7 +18,7 @@ import ( // // Each fixture isolates one behaviour, so that a failure names one cause. // The parser does accumulate problems and reports them together, -// so a fixture could carry several — it just would not say which one broke. +// so a fixture could carry several - it just would not say which one broke. func TestLoad(t *testing.T) { tests := []struct { name string @@ -289,7 +289,7 @@ func TestLoadDiscovery(t *testing.T) { // Only a missing candidate is a miss. One that exists but cannot be read is a // problem to report: falling through to the next would leave the operator // running against a file they did not mean to use, with nothing said about the -// one they did. So the assertion is that an error comes back at all — reaching +// one they did. So the assertion is that an error comes back at all - reaching // the fallback is exactly what returns nil. // // Neither case uses a mode: root ignores the permission bits, and CI images run diff --git a/env/errors.go b/env/errors.go index f9a3873..df789a7 100644 --- a/env/errors.go +++ b/env/errors.go @@ -8,8 +8,8 @@ import ( // Why a line can be rejected. // -// Sentinels rather than an enum, so a caller asks the question it actually has — -// errors.Is(err, env.ErrInvalidName) — +// Sentinels rather than an enum, so a caller asks the question it actually has - +// errors.Is(err, env.ErrInvalidName) - // without needing to know that [Problem] or [ParseError] exist, // let alone how to walk them. // [ParseError] wraps every problem it collected, @@ -39,9 +39,9 @@ var ( // withhold a working environment for a problem that no longer affects it. // // It is also an error, the one note that genuinely reports a failure, so the -// cause — an *io/fs.PathError, which names the file itself — stays reachable: +// cause - an *io/fs.PathError, which names the file itself - stays reachable: // -// if e, ok := n.(error); ok && errors.Is(e, fs.ErrPermission) { … } +// if e, ok := n.(error); ok && errors.Is(e, fs.ErrPermission) { ... } type CloseError struct { // Err is what Close returned. Err error @@ -57,7 +57,7 @@ func (e CloseError) Unwrap() error { return e.Err } // Problem locates one rejected line. // -// It carries the line number and, where there was one to read, the name — +// It carries the line number and, where there was one to read, the name - // and never the value. // A malformed line may hold a secret, and a caller printing what it is handed // must not be able to print that secret by accident. @@ -93,7 +93,7 @@ func (p Problem) Unwrap() error { return p.Err } // because envrun is the only thing that reads the file: // a line it cannot honour would have reached nothing without it, // so passing that line over leaves the command running without the -// configuration the operator meant to set — worse than not running it at all. +// configuration the operator meant to set - worse than not running it at all. // // [FromEnviron] does the opposite with the same shape of row, // and the difference is the baseline rather than the format: @@ -102,12 +102,12 @@ func (p Problem) Unwrap() error { return p.Err } // Two ways in, because callers want two different things. To ask whether a kind // of problem occurred at all: // -// if errors.Is(err, env.ErrInvalidName) { … } +// if errors.Is(err, env.ErrInvalidName) { ... } // // To count them, or to locate them in the file: // // if perr, ok := errors.AsType[*env.ParseError](err); ok { -// for _, p := range perr.Problems { … } +// for _, p := range perr.Problems { ... } // } // // The second is what [Unwrap] cannot serve: errors.As stops at the first match diff --git a/env/errors_test.go b/env/errors_test.go index 5a08633..6388753 100644 --- a/env/errors_test.go +++ b/env/errors_test.go @@ -11,8 +11,8 @@ import ( // TestProblemError pins the rendered form of every kind. // -// The messages are an output contract — the command prints them and the README -// quotes their shape — so a change to the wording should fail here first and say +// The messages are an output contract - the command prints them and the README +// quotes their shape - so a change to the wording should fail here first and say // which one it broke, rather than surfacing as a mismatched substring elsewhere. func TestProblemError(t *testing.T) { tests := []struct { diff --git a/env/example_test.go b/env/example_test.go index 7c44c81..c2717f7 100644 --- a/env/example_test.go +++ b/env/example_test.go @@ -18,7 +18,7 @@ import ( // // They write their own file rather than reading testdata, because only go test // runs a binary with the package directory as its working directory. An example -// is also compiled as a standalone program — by pkg.go.dev's Run button, say — +// is also compiled as a standalone program - by pkg.go.dev's Run button, say - // where a relative path would resolve against somewhere else entirely. func writeEnv(body string) (path string, remove func()) { dir, err := os.MkdirTemp("", env.AppName) diff --git a/env/load_internal_test.go b/env/load_internal_test.go index 0d9ac10..8c93908 100644 --- a/env/load_internal_test.go +++ b/env/load_internal_test.go @@ -1,7 +1,8 @@ // This file is in package env, where every other test file is in env_test, -// because the property it covers is unreachable from outside: Load opens the -// file itself, read-only, and a descriptor with no write-back has nothing left -// to fail at on Close. loadFile takes a reader precisely so a fake can fail. +// because the property it covers is unreachable from outside: +// Load opens the file itself, read-only, +// and a descriptor with no write-back has nothing left to fail at on Close. +// loadFile takes a reader precisely so a fake can fail. package env import ( @@ -21,8 +22,8 @@ type failingCloser struct { func (f failingCloser) Close() error { return f.err } // TestLoadFileCloseError covers what a failed close must not cost the caller: -// everything the file had to give has already been read, so the close failure -// travels as a Note beside the result rather than replacing it. +// everything the file had to give has already been read, +// so the close failure travels as a Note beside the result rather than replacing it. func TestLoadFileCloseError(t *testing.T) { const path = "x.env" cause := &fs.PathError{Op: "close", Path: path, Err: fs.ErrPermission} @@ -54,8 +55,9 @@ func TestLoadFileCloseError(t *testing.T) { if res.Env != nil { t.Errorf("expected no environment beside an error, got %v", res.Env) } - // The property this case exists for: the parse failure does not bury the - // unrelated finding the caller could still act on. + // The property this case exists for: + // the parse failure does not bury the unrelated finding + // the caller could still act on. assertCloseNote(t, res, path, cause) }) } diff --git a/exec_other.go b/exec_other.go index d84e49f..121bf51 100644 --- a/exec_other.go +++ b/exec_other.go @@ -24,8 +24,8 @@ import ( // so the shape of exec_unix.go cannot be expressed there and never will be. // Supervision is the only available shape rather than a fallback, // which is why the transparency v0.2.0 promises is *nix-only. -// What supervision could still gain on Windows — -// an allowlist of console control events, and job objects with kill-on-close — +// What supervision could still gain on Windows - +// an allowlist of console control events, and job objects with kill-on-close - // is issue #44. // // The remaining platforms it covers, js/wasm and wasip1, cannot start a process at all. diff --git a/main.go b/main.go index f119b2e..24cb592 100644 --- a/main.go +++ b/main.go @@ -38,7 +38,7 @@ func parseArgs(args []string, stdW, errW io.Writer) (string, []string, error) { // Declared rather than left to flag. // Declaring keeps flag from writing usage of its own, which it would send to one destination, - // for both a help request and a parse failure — and those need different ones. + // for both a help request and a parse failure - and those need different ones. showHelp := fs.Bool("h", false, "Print this message on standard output and exit") fs.BoolVar(showHelp, "help", false, "Alias for -h") diff --git a/main_test.go b/main_test.go index cac2fea..9afe861 100644 --- a/main_test.go +++ b/main_test.go @@ -31,7 +31,7 @@ import ( // helperVar reaches the helper through the environment *file*, never the inherited environment: // envrun passes its own environment on, // so a helper role set on the envrun process would be taken by that process instead of by its command. -// mainVar takes the opposite route for the same reason — +// mainVar takes the opposite route for the same reason - // it must reach envrun and, were the helper switch not first, it would reach the helper too. const ( helperVar = "ENVRUN_TEST_HELPER" @@ -140,8 +140,8 @@ func TestPrefixesMatchTheDocumentation(t *testing.T) { // where a diagnostic could be missing for any number of reasons; // this one checks the two lines themselves, // so a change to their wording fails here first and says which contract it broke. -// note() is the half with no *nix caller in the tests — -// its only one is a close failure — +// note() is the half with no *nix caller in the tests - +// its only one is a close failure - // and the half a Windows regression would show up in. func TestDiagnosticsNameEnvrun(t *testing.T) { var buf bytes.Buffer @@ -197,8 +197,8 @@ func TestCommandExitStatus(t *testing.T) { var stderr bytes.Buffer cmd.Stderr = &stderr // A non-zero status is the point here, - // so only a failure to start at all — - // leaving no ProcessState behind — is a test failure. + // so only a failure to start at all - + // leaving no ProcessState behind - is a test failure. if err := cmd.Run(); err != nil && cmd.ProcessState == nil { t.Fatalf("failed running envrun: %v", err) } diff --git a/main_unix_test.go b/main_unix_test.go index 6862717..e4013dd 100644 --- a/main_unix_test.go +++ b/main_unix_test.go @@ -19,7 +19,7 @@ import ( // TestExecKeepsThePID covers the process replacement itself, // which is what issue #35 asks for. // -// The repro in that issue — kill envrun and watch the command survive — +// The repro in that issue - kill envrun and watch the command survive - // cannot be asserted directly without racing on whether an orphan is still around. // Identity can: // after execve the command *is* the process that was envrun, @@ -48,7 +48,7 @@ func TestExecKeepsThePID(t *testing.T) { t.Fatalf("parsing the reported PID: %v", err) } if want := cmd.Process.Pid; got != want { - t.Errorf("the command ran as PID %d, expected %d — envrun is still a wrapper", got, want) + t.Errorf("the command ran as PID %d, expected %d - envrun is still a wrapper", got, want) } } @@ -182,7 +182,7 @@ func TestRealMainErrnoClassification(t *testing.T) { // The shebang-less script is the deliberate divergence, and is asserted as one: // bash retries such a file under /bin/sh and runs it, // where execve reports ENOEXEC and envrun stops at 126. -// envrun transports an environment, it does not choose an interpreter — +// envrun transports an environment, it does not choose an interpreter - // but a change on either side should fail here rather than silently. func TestBashOnNotInvocable(t *testing.T) { bash, err := exec.LookPath("bash")