Skip to content

phpboyscout · go toolkit

Edit YAML without destroying it

Parse bytes, change one key, write bytes back. Every byte you did not touch comes back exactly as it went in, every comment keeps the key it belongs to, every value keeps its meaning, and nothing commits until the result has been checked. A YAML engine of its own, with no parser underneath.

go get gitlab.com/phpboyscout/go/yamldoc
yamldoc

The thirty-second version

A user changes one setting. Here is what that does to their file.

# Which port the public listener binds to.
# Changing it needs a firewall change too.
server:
  host: localhost   # loopback only in dev
  port: 9090

# Feature flags. Keep alphabetical.
features:
  beta_ui: false
# Which port the public listener binds to.
# Changing it needs a firewall change too.
server:
  host: localhost # loopback only in dev
  port: 9090
# Feature flags. Keep alphabetical.
features:
  beta_ui: false
features:
  beta_ui: false
server:
  host: localhost
  port: 9090

One line changed. Or the comment alignment gone and the blank line with it (goccy keeps the blank line and collapses the alignment). Or everything gone: comments, order, the lot. Those outputs are real, from the libraries named, and the rest of the comparison is measured the same way in Should you use yamldoc?

That is not a bug in any of them. A tree that is re-emitted cannot leave alone what it does not model. yamldoc edits the source instead: the bytes of the edit's footprint are replaced, and every other byte is returned untouched.

What you get

Every untouched byte, back

Comments and their alignment, blank lines, indentation width, key order, quoting and block styles, multi-line flow, anchors, aliases, merge keys, directives, CRLF, a missing final newline, a BOM, UTF-16. No list of exceptions, because nothing is re-emitted.

Comments keep their owners

A comment above a key or on its line goes when the key goes; one separated by a blank line, or after the last entry, stays. Decided by rule, written down, tested, and enforced on every commit. The rules.

Nothing commits unchecked

Every transaction re-parses its own output and compares it with an independent statement of the edit, untouched bytes and comment ownership included. A batch lands whole or not at all. How an edit is made.

Values keep their meaning

Read 0.1, write it back, nothing changes. A string "true" stays a string; a float stays a float; a 30-digit integer reads exactly. New values are spelt the way a person writes them, multiline strings as literal blocks. Every rule.

Edits that reach everything

Set, insert, replace and remove entries and items; replace a subtree with the keys you name keeping their comments; turn a scalar into a collection; fill an empty file; detach a subtree from one file and write it into another. Dotted paths with [n] indexes for one change, a transaction for several. Edit a value.

YAML as the document declares it

YAML 1.1 and 1.2 chosen per document from its %YAML directive, nineteen schema profiles, %TAG handles, explicit and verbatim tags, keys matched by value so 8080 finds 0x1F90. No other Go library reads the directive.

The spelling, the type and the value, apart

Three questions with three answers on every node: Raw gives 0x1F90, TagIdentity gives tag:yaml.org,2002:int, Value gives 8080. Syntax adds kind, style, tag, anchor and line. A 30-digit integer is a *big.Int, a decimal is a Rat. Reading a document.

Every value knows where it came from

Project a subtree to map[string]any and get, beside it, the line and column of every value and every key, the alias hops it arrived through, and for a << merge which entry was inherited from which anchor. Error messages that say server.timeout at config.yaml:5 after the value is a plain Go map. Reading a document.

Comments are data, not debris

Ask any entry, item, container, document or stream for its comments and get each one's text, location and owner. The comment above a key is the key's documentation; a linter can require one, a generator can render them, a tool can answer "what does this setting do" from the file itself. The comment views.

Tags as an extension point

Register a handler for !secret, !env or a full tag URI and resolve a node when you want its value. Providers with configuration and cleanup, nested resolution, cancellation, limits, provenance. The file keeps its literal text. Unique to yamldoc.

Its own engine, one dependency

No parser underneath. A grammar of its own checked against every valid and invalid case in the official YAML test suite, a composer that follows the standard's own rules, and a single dependency, go/errors. Behaviour is decided here, not inherited from a library with other priorities. The owned engine.

Safe with hostile input

Immutable snapshots that survive later edits. Bounded source size, depth, nodes, comments, alias expansion and work per transaction. Invisible and bidirectional characters escaped in what it writes. Validation that names a dangling alias or a duplicate key with both locations, walks what it can of a file it cannot prove, and says which checks it could not run. Validate a document.

At a glance

f, err := yamldoc.Parse(src, yamldoc.Options{})

d, _ := f.Snapshot().Document(0)
port, _ := d.Get("server.port")          // typed reads from an immutable snapshot
n, _ := port.Int64()

_ = d.Set("server.port", 9090)           // one change, one transaction

err = f.Edit(func(tx *yamldoc.Transaction) error {   // several changes, one transaction
    s, _ := tx.Snapshot()
    root, _ := s.Document(0)
    server, _ := root.Get("server")
    return tx.Set(server, yamldoc.StringStep("port"), 9090)
})

out, _ := f.Snapshot().Bytes()

The library moves text and interprets YAML; what the YAML is for belongs to whoever is using it. No file I/O, no merging or precedence, no application schema, no meaning to the order of documents. See Mechanism, not policy.

It is the editing half of the split every other ecosystem made; pair it with a value library for decoding into structs. It is strict where the standard is strict, an edit costs several times what a value library's does (why, and what the time buys), and a dotted path has no quoting for a key with a dot in it. All of that, with numbers, is in Should you use yamldoc?, and the rest in What yamldoc does not do.

Getting started Change a config file end to end, in fifteen minutes
Edit a value Scalars, new keys, subtrees, sequence items, kind changes
API overview The types, what each is for, where the detail lives
Reading a document Typed reads, spelling, provenance, merge keys, comments and partial validation, worked on one file
How a written value is spelt Every rule for the text a write produces, pinned by a test
What is preserved The guarantee, stated precisely

Further reading

The blog carries a curated route through this subject: Building a command-line tool in Go collects everything written about it, ordered so you can start at the beginning rather than newest-first.

Ask phpbotscout

phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.

Join the Discord