agents.md
✅ DO:
--debug-node-info flag to get a deeper understanding of the ast.❌ DON'T:
This guide explains how to add support for a new format (encoder/decoder) to yq without modifying candidate_node.go.
The encoder/decoder architecture in yq is based on two main interfaces:
CandidateNode to output in a specific formatCandidateNodeEach format is registered in pkg/yqlib/format.go and made available through factory functions.
pkg/yqlib/encoder.go - Defines the Encoder interfacepkg/yqlib/decoder.go - Defines the Decoder interfacepkg/yqlib/format.go - Format registry and factory functionspkg/yqlib/operator_encoder_decoder.go - Encode/decode operatorspkg/yqlib/encoder_*.go - Encoder implementationspkg/yqlib/decoder_*.go - Decoder implementationsEncoder Interface:
type Encoder interface {
Encode(writer io.Writer, node *CandidateNode) error
PrintDocumentSeparator(writer io.Writer) error
PrintLeadingContent(writer io.Writer, content string) error
CanHandleAliases() bool
}
Decoder Interface:
type Decoder interface {
Init(reader io.Reader) error
Decode() (*CandidateNode, error)
}
Create pkg/yqlib/encoder_<format>.go implementing the Encoder interface:
Encode() - Convert a CandidateNode to your format and write to the output writerPrintDocumentSeparator() - Handle document separators if your format requires themPrintLeadingContent() - Handle leading content/comments if supportedCanHandleAliases() - Return whether your format supports YAML aliasesSee encoder_json.go or encoder_base64.go for examples.
Create pkg/yqlib/decoder_<format>.go implementing the Decoder interface:
Init() - Initialize the decoder with the input reader and set up any needed stateDecode() - Decode one document from the input and return a CandidateNode, or io.EOF when finishedSee decoder_json.go or decoder_base64.go for examples.
Create a test file pkg/yqlib/<format>_test.go using the formatScenario pattern:
formatScenario structs with fields: description, input, expected, scenarioTypescenarioType can be "decode" (test decoding to YAML) or "roundtrip" (encode/decode preservation)test<Format>Scenario() that switches on scenarioTypeTest<Format>FormatScenarios() that iterates over scenariosdocumentScenarios to ensure testcase documentation is generated.Test coverage must include:
See hcl_test.go for a complete example.
Edit pkg/yqlib/format.go:
Add a new format variable:
"<format>" is the formal name (e.g., "json", "yaml")[]string{...} contains short aliases (can be empty)Add the format to the Formats slice in the same file
See existing formats in format.go for the exact structure.
If your format has preferences/configuration options:
format.go to pass the configured preferencesoperator_encoder_decoder.go if special indent handling is needed (see existing formats like JSON and YAML for the pattern)This pattern is optional and only needed if your format has user-configurable options.
Use build tags to allow optional compilation of formats:
//go:build !yq_no<format> at the top of your encoder and decoder filespkg/yqlib/no_<format>.go that returns nil for encoder/decoder factoriesThis allows users to compile yq without certain formats using: go build -tags yq_no<format>
The CandidateNode struct represents a YAML node with:
Kind: The node type (ScalarNode, SequenceNode, MappingNode)Tag: The YAML tag (e.g., "!!str", "!!int", "!!map")Value: The scalar value (for ScalarNode only)Content: Child nodes (for SequenceNode and MappingNode)Key methods:
node.guessTagFromCustomType() - Infer the tag from Go typenode.AsList() - Convert to a list for processingnode.CreateReplacement() - Create a new replacement nodeNewCandidate() - Create a new CandidateNode✅ DO:
Encoder and Decoder interfacesformat.go only//go:build !yq_<format>. Be sure to also update the build_small-yq.sh and build-tinygo-yq.sh to not include the new format.❌ DON'T:
candidate_node.go to add format-specific logicCandidateNodeRefer to existing format implementations for patterns:
encoder_json.go, decoder_json.goencoder_yaml.go, decoder_yaml.goencoder_sh.go (ShFormat has nil decoder)encoder_base64.go, decoder_base64.goTests must be implemented in <format>_test.go following the formatScenario pattern:
Create test scenarios using the formatScenario struct with fields:
description: Brief description of what's being testedinput: Sample input in your formatexpected: Expected output (typically in YAML for decode tests)scenarioType: Either "decode" or "roundtrip"Test coverage must include:
Test function pattern:
test<Format>Scenario(): Helper function that switches on scenarioTypeTest<Format>FormatScenarios(): Main test function that iterates over scenariosExample from existing formats:
hcl_test.go for a complete exampleyaml_test.go for YAML-specific patternsjson_test.go for more complex scenariosUse preferences to control output formatting:
type <format>Preferences struct {
Indent int
}
func (prefs *<format>Preferences) Copy() <format>Preferences {
return *prefs
}
Decoders should support reading multiple documents:
func (dec *<format>Decoder) Decode() (*CandidateNode, error) {
if dec.finished {
return nil, io.EOF
}
// ... decode next document ...
if noMoreDocuments {
dec.finished = true
}
return candidate, nil
}
This guide explains how to add a new operator to yq. Operators are the core of yq's expression language and process CandidateNode objects without requiring modifications to candidate_node.go itself.
Operators transform data by implementing a handler function that processes a Context containing CandidateNode objects. Each operator is:
operationType in operation.golexer_participle.gooperator_<type>.go fileoperator_<type>_test.gopkg/yqlib/doc/operators/headers/<type>.mdpkg/yqlib/operation.go - Defines operationType and operator registrypkg/yqlib/lexer_participle.go - Registers operators with their syntax patternspkg/yqlib/operator_<type>.go - Operator implementationpkg/yqlib/operator_<type>_test.go - Operator tests using expressionScenariopkg/yqlib/doc/operators/headers/<type>.md - Documentation headeroperationType:
type operationType struct {
Type string // Unique operator name (e.g., "REVERSE")
NumArgs uint // Number of arguments (0 for no args)
Precedence uint // Operator precedence (higher = higher precedence)
Handler operatorHandler // The function that executes the operator
CheckForPostTraverse bool // Whether to apply post-traversal logic
ToString func(*Operation) string // Custom string representation
}
operatorHandler signature:
type operatorHandler func(*dataTreeNavigator, Context, *ExpressionNode) (Context, error)
expressionScenario for tests:
type expressionScenario struct {
description string
subdescription string
document string
expression string
expected []string
skipDoc bool
expectedError string
}
Create pkg/yqlib/operator_<type>.go implementing the operator handler function:
operatorHandler function signaturecontext.MatchingNodesContext with results using context.ChildContext()candidate.CreateReplacement() or candidate.CreateReplacementWithComments() to create new nodesSee operator_reverse.go or operator_keys.go for examples.
Add the operator type definition to pkg/yqlib/operation.go:
var <type>OpType = &operationType{
Type: "<TYPE>", // All caps, matches pattern in lexer
NumArgs: 0, // 0 for no args, 1+ for args
Precedence: 50, // Typical range: 40-55
Handler: <type>Operator, // Reference to handler function
}
Precedence guidelines:
Optional fields:
CheckForPostTraverse: true - If your operator can have another directly after it without the pipe character. Most of the time this is false.ToString: customToString - Custom string representation (rarely needed)Edit pkg/yqlib/lexer_participle.go to add the operator to the lexer rules:
simpleOp() for simple keyword patterns_? and aliases with |See existing operators in lexer_participle.go for pattern examples.
Create pkg/yqlib/operator_<type>_test.go using the expressionScenario pattern:
description, document, expression, and expected fieldsexpected is a slice of strings showing output format: "D<doc>, P[<path>], (<tag>)::<value>\n"skipDoc: true for edge cases you don't want in generated documentationsubdescription for longer test namesexpectedError if testing error casesdocumentScenarios to ensure testcase documentation is generated.Test coverage must include:
See operator_reverse_test.go for a simple example and operator_keys_test.go for complex cases.
Create pkg/yqlib/doc/operators/headers/<type>.md:
See existing headers in doc/operators/headers/ for examples.
context.ChildContext(results) - Create child context with resultscontext.GetVariable("varName") - Get variables stored in contextcontext.SetVariable("varName", value) - Set variables in contextcandidate.CreateReplacement(ScalarNode, "!!str", stringValue) - Create a replacement nodecandidate.CreateReplacementWithComments(SequenceNode, "!!seq", candidate.Style) - With style preservedcandidate.Kind - The node type (ScalarNode, SequenceNode, MappingNode)candidate.Tag - The YAML tag (!!str, !!int, etc.)candidate.Value - The scalar value (for ScalarNode only)candidate.Content - Child nodes (for SequenceNode and MappingNode)candidate.guessTagFromCustomType() - Infer the tag from Go typecandidate.AsList() - Convert to a list representation✅ DO:
operation.go with appropriate precedencelexer_participle.godoc/operators/headers/Context.ChildContext() for proper context threading❌ DON'T:
candidate_node.go (operators shouldn't need this)CandidateNodeRefer to existing operator implementations for patterns:
operator_reverse.go - Processes arrays/sequencesoperator_map.go - Takes an expression argumentoperator_keys.go - Produces multiple resultsoperator_to_number.go - Configuration optionsoperator_error.go - Control flow with errorsoperator_strings.go - Multiple related operatorsRefer to existing test files for specific patterns:
operator_reverse_test.gooperator_keys_test.gooperator_error_test.goskipDoc flag to exclude from generated documentationRefer to existing operator implementations for these patterns:
operator_reverse.gooperator_error.gooperator_map.gooperator_with.go