From Patch to Playbook: A Repeatable Process for Refactoring CLI Help Text
A recent patch series for arran4/git-tag-inc (PR #54) is a good example of how
to turn a one-off code change into a repeatable method other projects can use.
The technical change was simple:
- move long CLI help text out of Go code and into an embedded template,
- keep dynamic values in code,
- shift conditional rendering into the template,
- and avoid global flag-output side effects.
The process is what matters most, because it is reusable across repos and agent-assisted workflows.
What changed in the patch (short version)
The patch evolved in three commits:
- Replace many
fmt.Fprintfcalls inUsage()with an embeddedusage.txtrendered bytext/template. - Replace
flag.PrintDefaults()output-capture hacks with manualflag.VisitAllformatting to avoid mutating global flag output. - Move mode-specific branching fully into the template (
{{if ...}}) so presentation logic lives with presentation text.
That progression is exactly the pattern to replicate.
Why this pattern is worth reusing
When help text lives inline in code, every wording update becomes risky and noisy. Templates provide:
- Review clarity: prose changes are mostly text-file diffs.
- Separation of concerns: code prepares data; template renders docs.
- Safer evolution: feature flags/modes can be made explicit in template conditionals.
- Agent-friendliness: LLM agents can edit usage copy with lower risk of breaking control flow.
A repeatable 7-step process for other projects
Use this as a playbook for any CLI project.
1) Start with a behavior snapshot
Before refactoring output code, record current behavior.
- Run
tool --helpin at least one default mode. - Save output fixtures if the repo uses golden tests.
- Note any mode-dependent lines (legacy modes, experimental flags, etc.).
2) Extract static prose into a template file
Create something like:
cmd/<tool>/usage.txt(or.gotmpl)
Keep placeholders only for truly dynamic values:
- executable name,
- generated flag listing,
- mode booleans,
- version/build metadata.
3) Embed template at compile time
In Go:
//go:embed usage.txt- parse once in
Usage()or cache if needed.
This keeps distribution simple (no runtime file lookups).
4) Build a data struct with minimal fields
Pass a narrow struct to the template, for example:
ProgramName stringFlags stringIsLegacyMode bool
Avoid passing full config objects. Small template contracts are easier to test and review.
5) Keep presentation branching in the template
If output differences are textual, put them in template conditionals:
{{if .IsLegacyMode}}...{{else}}...{{end}}
This avoids scattering output branches through Go code.
6) Avoid global side effects while collecting flag text
If a codebase currently captures flag.PrintDefaults() by mutating shared output,
prefer deterministic manual formatting over global output swapping.
That makes tests and concurrent tooling safer.
7) Validate with focused regression checks
For CLI output changes, use a compact check matrix:
go test ./...<tool> --help<tool> --mode legacy --help(or equivalent)
If possible, diff previous and new outputs for intentional-only changes.
Suggested “definition of done” for this class of refactor
You can call the work complete when:
- help text is readable as documentation in a standalone template,
- mode-specific wording is obvious from template conditionals,
- flag output generation is deterministic and side-effect free,
- output parity is confirmed by tests and manual smoke checks,
- future copy edits no longer require touching control-flow-heavy code.
Agent-ready prompt you can reuse
If you want LLM agents to apply this process in other repositories, use a prompt like:
Refactor the CLI
Usage()implementation to a compile-time embedded template. Keep dynamic values in a small data struct, move text conditionals into the template, avoid global side effects in flag formatting, and preserve current help output semantics. Provide tests or smoke checks showing parity.
That gives agents a crisp architectural target instead of just “clean this up”.
Closing thought
The interesting part of PR #54 was not only the template migration. It was the iterative tightening of boundaries:
- first extract text,
- then remove side effects,
- then move final display logic to the rendering layer.
That sequence is a durable refactoring strategy you can apply to any CLI that has
outgrown fmt.Fprintf-driven usage blocks.