Getting started: change a config file without wrecking it¶
By the end of this you'll have a small Go program that opens a hand-written YAML config file, changes a port, adds a key, deletes another, and writes the file back with every comment still where its author put it.
Allow about fifteen minutes.
Before you start¶
You'll need Go 1.26 or later — go version will tell you. Everything else you
need arrives with one go get.
Work in a scratch directory. The last step overwrites the file you create, so don't point it at a config you care about until you've seen it work.
mkdir yamldoc-tour && cd yamldoc-tour
go mod init example.com/tour
go get gitlab.com/phpboyscout/go/yamldoc
That pulls in one dependency, goccy/go-yaml. Nothing else.
Create a file worth protecting¶
Save this as config.yaml. It has the things a re-marshal would destroy: a
header, an inline comment, a trailing comment on a block, and blank lines that
group it.
# Service configuration.
# Edited by hand; keep the comments.
server:
host: localhost # the interface we bind to
port: 8080
# end of the server block
logging:
level: info
debug: true
Read the file and look around¶
yamldoc works on bytes and never touches the filesystem, so you do the reading
and writing. Start with a program that only looks:
package main
import (
"fmt"
"os"
"gitlab.com/phpboyscout/go/yamldoc"
)
func main() {
src, err := os.ReadFile("config.yaml")
if err != nil {
panic(err)
}
f, err := yamldoc.Parse(src)
if err != nil {
panic(err)
}
doc := f.Documents()[0]
keys, _ := doc.Keys("")
fmt.Println("top-level keys:", keys)
n, ok := doc.Get("server.port")
fmt.Println("found:", ok)
fmt.Println("value:", n.String())
fmt.Println("line:", n.Position().Line)
host, _ := doc.Get("server.host")
fmt.Printf("host comment: %q\n", host.Comments().Line)
}
top-level keys: [server logging]
found: true
value: 8080
line: 6
host comment: "the interface we bind to"
Paths are dotted mapping keys — server.port walks from the root to server and
then to port. There's no index syntax, so nothing inside a YAML list can be
addressed this way; if your file has lists, read
Path syntax before you go much further.
Check the document is safe to edit¶
A few YAML constructs can't be re-emitted without damage. Parse finds them and
reports them, and leaves the decision to you. Check before you make any changes —
failing after the user has edited three fields looks arbitrary.
Add this straight after Parse:
Your file is clean, so nothing prints. To see it fire, temporarily add a multi-line flow collection with a comment inside it:
cannot edit safely: line 12: multi-line flow collection with interior comments (collection spans lines 12-15)
Take that back out before continuing. The full catalogue is in Unsupported constructs.
Change a value¶
An existing scalar is mutated where it sits, so its inline comment and its
quoting style stay with it. Always check the error — Set refuses several things
rather than guessing, and a silent failure here means a config change that never
happened.
Add a key that isn't there yet¶
Setting a path that doesn't exist creates it, and missing intermediate mappings
are created on the way — tls appears so that cert has somewhere to live. New
keys are appended to the end of their parent rather than inserted, which is what
keeps the existing keys' order and comments untouched. It does mean a new key can
land after a comment that closes the block; you'll see that in the output below.
One thing Set won't do is create a path that runs through a scalar:
err := doc.Set("logging.level.verbose", true)
// yamldoc: path not found: "logging.level" is not a mapping, so the remaining
// path cannot be created
Creating it would mean deleting logging.level, which you didn't ask for.
Remove a key and watch the comments¶
Removing a key takes the comments that describe it and leaves the ones that describe something else — a comment directly above the key goes with it, a section heading separated by a blank line stays, a trailing comment is re-homed onto the key above. That's the part a naive editor gets wrong; the rules are in Comment ownership.
Removing something that isn't there is an error rather than a no-op:
That's deliberate — silence there hides a typo in a key name.
Write it back¶
out, err := f.Bytes()
if err != nil {
panic(err)
}
if err := os.WriteFile("config.yaml", out, 0o644); err != nil {
panic(err)
}
Bytes re-parses and fully decodes its own output before handing it to you. If
anything went wrong you get an error and no bytes, so a structurally broken
document can't reach the file.
Run it, then look at config.yaml:
# Service configuration.
# Edited by hand; keep the comments.
server:
host: localhost # the interface we bind to
port: 9090
# end of the server block
tls:
cert: /etc/ssl/service.pem
logging:
level: info
Both comments survived, the blank line between the sections survived, debug is
gone and port changed. Two things did move: the inline comment lost its column
padding — alignment collapses to a single space, and that is not preserved — and
tls landed after the block's closing comment, because new keys append.
Feed that output straight back through Parse and Bytes and you get the same
bytes: emission converges immediately, so a tool that saves on every keystroke
doesn't accumulate drift. Re-running this program is not the way to check that,
though — the second run panics on Remove("logging.debug"), because debug is
already gone and removing an absent key is an error rather than a no-op.
One thing to know before you use this on real input¶
If the value you assign comes from a user rather than from your own code, read
Assigning a string that needs quoting
first. Overwriting a plain scalar with a string that YAML would need quoted — one
containing #, or a leading space, or the text 123 — currently writes it
unquoted, and the file then means something different. Values you control are
fine; values you don't need vetting.
Where to go next¶
- Edit a value — replacing subtrees, and what that costs
- Remove a key safely — the comment rules in detail
- API reference — every method, and what it returns when it fails
- What is preserved, and what is not — the guarantee, precisely