feat(inspect): evaluate -f/--format as a Go-template across all inspect commands#42
Conversation
…ct commands
`inspect` -- and the dedicated `container inspect` / `image inspect` subcommands
-- declared `-f/--format` mirroring `docker inspect`, but parsed and then ignored
it: the full JSON array was always printed. Docker callers drive inspect with a
template and expect a bare scalar, e.g. dev stands and CI health gates:
docker inspect -f '{{.State.Running}}' <container> | grep -q true
Against the unevaluated flag that grep is a coin-flip on the substring "true"
appearing somewhere in the JSON dump.
Add a small Go text/template evaluator (`GoTemplate`) for the `{{ .Dotted.Path }}`
field-access subset real inspect callers use, and route every inspect entry point
through a shared `InspectFormat` helper that renders `--format` (one line per
record) and otherwise prints the JSON exactly as before. This also wires
`--format` into `container inspect` and `image inspect`, which previously accepted
the flag for Docker surface parity but never applied it.
Behavior:
- Paths resolve against the Docker-shaped inspect JSON from us#40 (`.State.Running`,
`.State.Status`, `.Id`, `.Name`, `.Config.Image`, `.NetworkSettings.IPAddress`,
...). Scalars render the Go way: bare `true`/`false`, integers without a decimal
point, strings verbatim; an unknown/null path and array/object leaves render empty.
- Substitution is a single left-to-right pass by match range, so a resolved value
that itself contains `{{...}}` is emitted verbatim and never re-triggers
substitution.
- `--format json` is treated as Docker's documented special value (emit the JSON),
not as a template.
- Any `{{...}}` block outside the supported field-access subset (`if`, `range`,
`json`, `index`, a bare `{{.}}`, ...) throws rather than passing through as
misleading literal text -- a silently-unevaluated template would feed false data
to the grep/jq pipelines these outputs drive.
Add GoTemplateTests covering the dev-stand health-gate paths, integer-vs-bool
scalars (incl. Pid 0 on a stopped container), whitespace, multi-token / repeated
tokens, no cross-token / re-entrant substitution, array-leaf-empty, the
JSONSerialization bool->NSNumber bridging path, and fail-loud rejection of
unsupported actions.
GoTemplateError only conformed to CustomStringConvertible, but ArgumentParser prints thrown errors through localizedDescription, which honors LocalizedError only. Without this the helpful "unsupported template action" message was replaced by a generic "The operation couldn't be completed" string. Conform to LocalizedError (matching MockerError) so the message surfaces. Fail-loud behaviour (nonzero exit) was already correct.
|
Thanks @alex-mextner — excellent work. 🙌 The scope call is spot on: a minimal Verified locally (the full graph builds against I pushed one small follow-up commit: The documented scope limits (unknown path → empty, array/object leaf → empty) are reasonable for a first cut. Merging now — thanks for the great contribution! |
What
mocker inspect-- and the dedicatedmocker container inspect/mocker image inspectsubcommands -- declare-f/--format(mirroringdocker inspect) but parse then ignore it, always printing the full JSON array.This adds Go text/template evaluation for the
{{ .Dotted.Path }}field-accesssubset and routes every inspect entry point through one shared formatter.
Why
Docker-shaped callers drive
inspectwith a template and expect a barescalar, e.g. dev stands and CI health gates:
With the flag unevaluated, that
grepis a coin-flip on the substringtrueappearing somewhere in the JSON dump. Every templated
inspectcaller is brokenuntil the template is actually evaluated. (
ContainerInspect.swiftalreadycarried a
// ... will be wired up in the follow-up (PR 2)note for exactlythis -- this is that wiring.)
How
GoTemplate(Formatters/GoTemplate.swift): evaluates the{{ .Dotted.Path }}subset only. Substitution is a single left-to-right passby match range, so a resolved value that itself contains
{{...}}is emittedverbatim and can never re-trigger substitution (repeated tokens can't
contaminate each other either).
{{...}}block that is not a supportedfield access (
{{if}},{{range}},{{json .X}},{{index …}}, a bare{{.}}, …) throwsGoTemplateErrorrather than passing through as misleadingliteral text.
--format jsonis handled as Docker's documented special value(emit the JSON), not as a template.
InspectFormat(Formatters/InspectFormat.swift): one sharedemitOne/emitArraypath used byinspect,container inspect, andimage inspect--renders
--formatwhen set (one line per record), otherwise prints the JSONexactly as before. No duplicated formatting logic.
(
.State.Running,.State.Status,.Id,.Name,.Config.Image,.NetworkSettings.IPAddress, …). Scalars render the Go way: baretrue/false, integers without a decimal point, strings verbatim; anunknown/null path and array/object leaves render empty (documented scope limit).
Tests
Tests/MockerTests/GoTemplateTests.swiftcovers the dev-stand health-gate paths,integer-vs-bool scalars (including
Pid0 on a stopped container, the case theCFBooleantype-id check guards), whitespace, multi-token / repeated tokens,no cross-token / re-entrant substitution, array-leaf-empty (pinned), the
JSONSerializationbool->NSNumberbridging path, and fail-loud rejection ofif/range/json/index/ bare{{.}}.Verification
GoTemplateis pure Foundation; it was compiled and run standalone under Swift6.3.2 with the full test matrix above -- all pass, including
{{.State.Running}}->true/false,{{.State.Pid}}->1234/0(integer,not
true/false), the previously-buggy cross-token case{{.A}}-{{.B}}withA == "{{.B}}"->{{.B}}-x, and the unsupported-action throws.InspectFormattypechecks against the real
TableFormatter+GoTemplate. End to end, adocker inspect -f '{{.State.Running}}' <container> | grep -q truehealth gateagainst a real Apple-
container-backed Postgres/Redis now passes.