Compare commits

...
25 Commits
Author SHA1 Message Date
b60eb7d22a Add build tag to hcl_test.go to skip tests when HCL is disabled
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
2025-12-08 13:32:47 +11:00
86f3f77b8c Update pkg/yqlib/format.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 12:50:08 +11:00
Mike Farah bc92673d6f Fixed comment parsing, added generated docs 2025-12-08 11:42:07 +11:00
Mike Farah 87374edce5 colorise output 2025-12-08 10:57:20 +11:00
Mike Farah 5c166e3038 wip - comments 2025-12-08 10:33:06 +11:00
Mike Farah 491ab8e70f wip - comments 2025-12-08 10:18:05 +11:00
Mike Farah bf48626ad8 cleanup 2025-12-08 09:50:32 +11:00
Mike Farah 42f2436e1f Fixing formatting 2025-12-08 09:42:48 +11:00
6c4d1bd066 Update pkg/yqlib/decoder_hcl.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 09:19:30 +11:00
a3150396b5 Update pkg/yqlib/encoder_hcl.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 09:19:21 +11:00
c7c5a478c6 Update agents.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 08:38:36 +11:00
Mike Farah 30974679ae wip 2025-12-07 21:44:03 +11:00
Mike Farah d4a11521c3 Fixed processing of block labels 2025-12-07 20:29:26 +11:00
Mike Farah beaebf7790 Fixed processing of nested data 2025-12-07 20:26:01 +11:00
Mike Farah 360de4f7af Refining 2025-12-07 20:17:37 +11:00
Mike Farah 7923d04bdd Refining 2025-12-07 20:08:29 +11:00
Mike Farah bd3a647650 Adding HCL no build directive 2025-12-07 19:29:38 +11:00
Mike Farah fa42080148 Use HCL encoder 2025-12-07 19:22:00 +11:00
Mike Farah 82100a82a4 Use HCL encoder 2025-12-07 19:21:25 +11:00
Mike Farah 3d768bab73 adding agents file 2025-12-07 15:17:07 +11:00
Mike Farah 7b1c52a588 wip 2025-12-07 15:04:40 +11:00
Mike Farah 48e63a0386 hcl - sorted decoding 2025-12-07 14:52:30 +11:00
Mike Farah 0cff8c234a maps and strings 2025-12-07 10:04:47 +11:00
Mike Farah 2be7e904be wip 2025-12-07 10:00:47 +11:00
Mike Farah d405ab6c10 First cut 2025-12-07 09:38:10 +11:00
19 changed files with 2289 additions and 23 deletions
+413
View File
@@ -0,0 +1,413 @@
# Adding a New Encoder/Decoder
This guide explains how to add support for a new format (encoder/decoder) to yq without modifying `candidate_node.go`.
## Overview
The encoder/decoder architecture in yq is based on two main interfaces:
- **Encoder**: Converts a `CandidateNode` to output in a specific format
- **Decoder**: Reads input in a specific format and creates a `CandidateNode`
Each format is registered in `pkg/yqlib/format.go` and made available through factory functions.
## Architecture
### Key Files
- `pkg/yqlib/encoder.go` - Defines the `Encoder` interface
- `pkg/yqlib/decoder.go` - Defines the `Decoder` interface
- `pkg/yqlib/format.go` - Format registry and factory functions
- `pkg/yqlib/operator_encoder_decoder.go` - Encode/decode operators
- `pkg/yqlib/encoder_*.go` - Encoder implementations
- `pkg/yqlib/decoder_*.go` - Decoder implementations
### Interfaces
**Encoder Interface:**
```go
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:**
```go
type Decoder interface {
Init(reader io.Reader) error
Decode() (*CandidateNode, error)
}
```
## Step-by-Step: Adding a New Encoder/Decoder
### Step 1: Create the Encoder File
Create `pkg/yqlib/encoder_<format>.go` implementing the `Encoder` interface:
- `Encode()` - Convert a `CandidateNode` to your format and write to the output writer
- `PrintDocumentSeparator()` - Handle document separators if your format requires them
- `PrintLeadingContent()` - Handle leading content/comments if supported
- `CanHandleAliases()` - Return whether your format supports YAML aliases
See `encoder_json.go` or `encoder_base64.go` for examples.
### Step 2: Create the Decoder File
Create `pkg/yqlib/decoder_<format>.go` implementing the `Decoder` interface:
- `Init()` - Initialize the decoder with the input reader and set up any needed state
- `Decode()` - Decode one document from the input and return a `CandidateNode`, or `io.EOF` when finished
See `decoder_json.go` or `decoder_base64.go` for examples.
### Step 3: Create Tests (Mandatory)
Create a test file `pkg/yqlib/<format>_test.go` using the `formatScenario` pattern:
- Define test scenarios as `formatScenario` structs with fields: `description`, `input`, `expected`, `scenarioType`
- `scenarioType` can be `"decode"` (test decoding to YAML) or `"roundtrip"` (encode/decode preservation)
- Create a helper function `test<Format>Scenario()` that switches on `scenarioType`
- Create main test function `Test<Format>FormatScenarios()` that iterates over scenarios
Test coverage must include:
- Basic data types (scalars, arrays, objects/maps)
- Nested structures
- Edge cases (empty inputs, special characters, escape sequences)
- Format-specific features or syntax
- Round-trip tests: decode → encode → decode should preserve data
See `hcl_test.go` for a complete example.
### Step 4: Register the Format in format.go
Edit `pkg/yqlib/format.go`:
1. Add a new format variable:
- `"<format>"` is the formal name (e.g., "json", "yaml")
- `[]string{...}` contains short aliases (can be empty)
- The first function creates an encoder (can be nil for encode-only formats)
- The second function creates a decoder (can be nil for decode-only formats)
2. Add the format to the `Formats` slice in the same file
See existing formats in `format.go` for the exact structure.
### Step 5: Handle Encoder Configuration (if needed)
If your format has preferences/configuration options:
1. Create a preferences struct with your configuration fields
2. Update the encoder to accept preferences in its factory function
3. Update `format.go` to pass the configured preferences
4. Update `operator_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.
## Build Tags
Use build tags to allow optional compilation of formats:
- Add `//go:build !yq_no<format>` at the top of your encoder and decoder files
- Create a no-build version in `pkg/yqlib/no_<format>.go` that returns nil for encoder/decoder factories
This allows users to compile yq without certain formats using: `go build -tags yq_no<format>`
## Working with CandidateNode
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 type
- `node.AsList()` - Convert to a list for processing
- `node.CreateReplacement()` - Create a new replacement node
- `NewCandidate()` - Create a new CandidateNode
## Key Points
**DO:**
- Implement only the `Encoder` and `Decoder` interfaces
- Register your format in `format.go` only
- Keep format-specific logic in your encoder/decoder files
- Use the candidate_node style attribute to store style information for round-trip. Ask if this needs to be updated with new styles.
- Use build tags for optional compilation
- Add comprehensive tests
- Handle errors gracefully
- Add the no build directive, like the xml encoder and decoder, that enables a minimal yq builds. e.g. `//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:**
- Modify `candidate_node.go` to add format-specific logic
- Add format-specific fields to `CandidateNode`
- Create special cases in core navigation or evaluation logic
- Bypass the encoder/decoder interfaces
- Use candidate_node tag attribute for anything other than indicate the data type
## Examples
Refer to existing format implementations for patterns:
- **Simple encoder/decoder**: `encoder_json.go`, `decoder_json.go`
- **Complex with preferences**: `encoder_yaml.go`, `decoder_yaml.go`
- **Encoder-only**: `encoder_sh.go` (ShFormat has nil decoder)
- **String-only operations**: `encoder_base64.go`, `decoder_base64.go`
## Testing Your Implementation (Mandatory)
Tests must be implemented in `<format>_test.go` following the `formatScenario` pattern:
1. **Create test scenarios** using the `formatScenario` struct with fields:
- `description`: Brief description of what's being tested
- `input`: Sample input in your format
- `expected`: Expected output (typically in YAML for decode tests)
- `scenarioType`: Either `"decode"` or `"roundtrip"`
2. **Test coverage must include:**
- Basic data types (scalars, arrays, objects/maps)
- Nested structures
- Edge cases (empty inputs, special characters, escape sequences)
- Format-specific features or syntax
- Round-trip tests: decode → encode → decode should preserve data
3. **Test function pattern:**
- `test<Format>Scenario()`: Helper function that switches on `scenarioType`
- `Test<Format>FormatScenarios()`: Main test function that iterates over scenarios
4. **Example from existing formats:**
- See `hcl_test.go` for a complete example
- See `yaml_test.go` for YAML-specific patterns
- See `json_test.go` for more complex scenarios
## Common Patterns
### Scalar-Only Formats
Some formats only work with scalars (like base64, uri):
```go
if node.guessTagFromCustomType() != "!!str" {
return fmt.Errorf("cannot encode %v as <format>, can only operate on strings", node.Tag)
}
```
### Format with Indentation
Use preferences to control output formatting:
```go
type <format>Preferences struct {
Indent int
}
func (prefs *<format>Preferences) Copy() <format>Preferences {
return *prefs
}
```
### Multiple Documents
Decoders should support reading multiple documents:
```go
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
}
```
---
# Adding a New Operator
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.
## Overview
Operators transform data by implementing a handler function that processes a `Context` containing `CandidateNode` objects. Each operator is:
1. Defined as an `operationType` in `operation.go`
2. Registered in the lexer in `lexer_participle.go`
3. Implemented in its own `operator_<type>.go` file
4. Tested in `operator_<type>_test.go`
5. Documented in `pkg/yqlib/doc/operators/headers/<type>.md`
## Architecture
### Key Files
- `pkg/yqlib/operation.go` - Defines `operationType` and operator registry
- `pkg/yqlib/lexer_participle.go` - Registers operators with their syntax patterns
- `pkg/yqlib/operator_<type>.go` - Operator implementation
- `pkg/yqlib/operator_<type>_test.go` - Operator tests using `expressionScenario`
- `pkg/yqlib/doc/operators/headers/<type>.md` - Documentation header
### Core Types
**operationType:**
```go
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:**
```go
type operatorHandler func(*dataTreeNavigator, Context, *ExpressionNode) (Context, error)
```
**expressionScenario for tests:**
```go
type expressionScenario struct {
description string
subdescription string
document string
expression string
expected []string
skipDoc bool
expectedError string
}
```
## Step-by-Step: Adding a New Operator
### Step 1: Create the Operator Implementation File
Create `pkg/yqlib/operator_<type>.go` implementing the operator handler function:
- Implement the `operatorHandler` function signature
- Process nodes from `context.MatchingNodes`
- Return a new `Context` with results using `context.ChildContext()`
- Use `candidate.CreateReplacement()` or `candidate.CreateReplacementWithComments()` to create new nodes
- Handle errors gracefully with meaningful error messages
See `operator_reverse.go` or `operator_keys.go` for examples.
### Step 2: Register the Operator in operation.go
Add the operator type definition to `pkg/yqlib/operation.go`:
```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:**
- 10-20: Logical operators (OR, AND, UNION)
- 30: Pipe operator
- 40: Assignment and comparison operators
- 42: Arithmetic operators (ADD, SUBTRACT, MULTIPLY, DIVIDE)
- 50-52: Most other operators
- 55: High precedence (e.g., GET_VARIABLE)
**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)
### Step 3: Register the Operator in lexer_participle.go
Edit `pkg/yqlib/lexer_participle.go` to add the operator to the lexer rules:
- Use `simpleOp()` for simple keyword patterns
- Use object syntax for regex patterns or complex syntax
- Support optional characters with `_?` and aliases with `|`
See existing operators in `lexer_participle.go` for pattern examples.
### Step 4: Create Tests (Mandatory)
Create `pkg/yqlib/operator_<type>_test.go` using the `expressionScenario` pattern:
- Define test scenarios with `description`, `document`, `expression`, and `expected` fields
- `expected` is a slice of strings showing output format: `"D<doc>, P[<path>], (<tag>)::<value>\n"`
- Set `skipDoc: true` for edge cases you don't want in generated documentation
- Include `subdescription` for longer test names
- Set `expectedError` if testing error cases
- Create main test function that iterates over scenarios
Test coverage must include:
- Basic data types and nested structures
- Edge cases (empty inputs, special characters, type errors)
- Multiple outputs if applicable
- Format-specific features
See `operator_reverse_test.go` for a simple example and `operator_keys_test.go` for complex cases.
### Step 5: Create Documentation Header
Create `pkg/yqlib/doc/operators/headers/<type>.md`:
- Use the exact operator name as the title
- Include a concise 1-2 sentence summary
- Add additional context or examples if the operator is complex
See existing headers in `doc/operators/headers/` for examples.
## Working with Context and CandidateNode
### Context Management
- `context.ChildContext(results)` - Create child context with results
- `context.GetVariable("varName")` - Get variables stored in context
- `context.SetVariable("varName", value)` - Set variables in context
### CandidateNode Operations
- `candidate.CreateReplacement(ScalarNode, "!!str", stringValue)` - Create a replacement node
- `candidate.CreateReplacementWithComments(SequenceNode, "!!seq", candidate.Style)` - With style preserved
- `candidate.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 type
- `candidate.AsList()` - Convert to a list representation
## Key Points
**DO:**
- Implement the operator handler with the correct signature
- Register in `operation.go` with appropriate precedence
- Add the lexer pattern in `lexer_participle.go`
- Write comprehensive tests covering normal and edge cases
- Create a documentation header in `doc/operators/headers/`
- Use `Context.ChildContext()` for proper context threading
- Handle all node types gracefully
- Return meaningful error messages
**DON'T:**
- Modify `candidate_node.go` (operators shouldn't need this)
- Modify core navigation or evaluation logic
- Bypass the handler function pattern
- Add format-specific or operator-specific fields to `CandidateNode`
- Skip tests or documentation
## Examples
Refer to existing operator implementations for patterns:
- **No-argument operator**: `operator_reverse.go` - Processes arrays/sequences
- **Single-argument operator**: `operator_map.go` - Takes an expression argument
- **Complex multi-output**: `operator_keys.go` - Produces multiple results
- **With preferences**: `operator_to_number.go` - Configuration options
- **Error handling**: `operator_error.go` - Control flow with errors
- **String operations**: `operator_strings.go` - Multiple related operators
## Testing Patterns
Refer to existing test files for specific patterns:
- Basic expression tests in `operator_reverse_test.go`
- Multi-output tests in `operator_keys_test.go`
- Error handling tests in `operator_error_test.go`
- Tests with `skipDoc` flag to exclude from generated documentation
## Common Patterns
Refer to existing operator implementations for these patterns:
- Simple transformation: see `operator_reverse.go`
- Type checking: see `operator_error.go`
- Working with arguments: see `operator_map.go`
- Post-traversal operators: see `operator_with.go`
+1
View File
@@ -205,6 +205,7 @@ func configureEncoder() (yqlib.Encoder, error) {
yqlib.ConfiguredYamlPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredJSONPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredHclPreferences.ColorsEnabled = colorsEnabled
yqlib.ConfiguredYamlPreferences.PrintDocSeparators = !noDocSeparators
+8
View File
@@ -0,0 +1,8 @@
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
+8
View File
@@ -0,0 +1,8 @@
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
+9
View File
@@ -10,6 +10,7 @@ require (
github.com/go-ini/ini v1.67.0
github.com/goccy/go-json v0.10.5
github.com/goccy/go-yaml v1.19.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/jinzhu/copier v0.4.0
github.com/magiconair/properties v1.8.10
github.com/pelletier/go-toml/v2 v2.2.4
@@ -17,6 +18,7 @@ require (
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/yuin/gopher-lua v1.1.1
github.com/zclconf/go-cty v1.16.3
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/net v0.47.0
golang.org/x/text v0.31.0
@@ -24,10 +26,17 @@ require (
)
require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/tools v0.38.0 // indirect
)
go 1.24.0
+24 -1
View File
@@ -1,14 +1,19 @@
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw=
@@ -17,10 +22,16 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -33,6 +44,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
@@ -50,16 +63,26 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
+501
View File
@@ -0,0 +1,501 @@
//go:build !yq_nohcl
package yqlib
import (
"fmt"
"io"
"math/big"
"sort"
"strconv"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
type hclDecoder struct {
file *hcl.File
fileBytes []byte
readAnything bool
documentIndex uint
}
func NewHclDecoder() Decoder {
return &hclDecoder{}
}
// sortedAttributes returns attributes in declaration order by source position
func sortedAttributes(attrs hclsyntax.Attributes) []*attributeWithName {
var sorted []*attributeWithName
for name, attr := range attrs {
sorted = append(sorted, &attributeWithName{Name: name, Attr: attr})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Attr.Range().Start.Byte < sorted[j].Attr.Range().Start.Byte
})
return sorted
}
type attributeWithName struct {
Name string
Attr *hclsyntax.Attribute
}
// extractLineComment extracts any inline comment after the given position
func extractLineComment(src []byte, endPos int) string {
// Look for # comment after the token
for i := endPos; i < len(src); i++ {
if src[i] == '#' {
// Found comment, extract until end of line
start := i
for i < len(src) && src[i] != '\n' {
i++
}
return strings.TrimSpace(string(src[start:i]))
}
if src[i] == '\n' {
// Hit newline before comment
break
}
// Skip whitespace and other characters
}
return ""
}
// extractLeadingComments extracts comments from the very beginning of the file.
// It returns the comment text and the byte position of the last character in that leading block.
func extractLeadingComments(src []byte) (string, int) {
var comments []string
i := 0
// Skip leading whitespace
for i < len(src) && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i++
}
lastPos := -1
// Extract comment lines from the start
for i < len(src) && src[i] == '#' {
lineStart := i
// Find end of line
for i < len(src) && src[i] != '\n' {
i++
}
lastPos = i - 1
comments = append(comments, strings.TrimSpace(string(src[lineStart:i])))
// Skip newline
if i < len(src) && src[i] == '\n' {
i++
}
// Skip whitespace between comment lines
for i < len(src) && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i++
}
}
if len(comments) > 0 {
return strings.Join(comments, "\n"), lastPos
}
return "", -1
}
// extractHeadComment extracts comments before a given start position
func extractHeadComment(src []byte, startPos int) string {
var comments []string
// Start just before the token and skip trailing whitespace
i := startPos - 1
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i--
}
for i >= 0 {
// Find line boundaries
lineEnd := i
for i >= 0 && src[i] != '\n' {
i--
}
lineStart := i + 1
line := strings.TrimRight(string(src[lineStart:lineEnd+1]), " \t\r")
trimmed := strings.TrimSpace(line)
if trimmed == "" {
break
}
if !strings.HasPrefix(trimmed, "#") {
break
}
comments = append([]string{trimmed}, comments...)
// Move to previous line (skip any whitespace/newlines)
i = lineStart - 1
for i >= 0 && (src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r') {
i--
}
}
if len(comments) > 0 {
return strings.Join(comments, "\n")
}
return ""
}
func (dec *hclDecoder) Init(reader io.Reader) error {
data, err := io.ReadAll(reader)
if err != nil {
return err
}
file, diags := hclsyntax.ParseConfig(data, "input.hcl", hcl.Pos{Line: 1, Column: 1})
if diags != nil && diags.HasErrors() {
return fmt.Errorf("hcl parse error: %w", diags)
}
dec.file = file
dec.fileBytes = data
dec.readAnything = false
dec.documentIndex = 0
return nil
}
func (dec *hclDecoder) Decode() (*CandidateNode, error) {
if dec.readAnything {
return nil, io.EOF
}
dec.readAnything = true
if dec.file == nil {
return nil, fmt.Errorf("no hcl file parsed")
}
root := &CandidateNode{Kind: MappingNode}
// Extract file-level head comments (comments at the very beginning of the file)
leadingComment, _ := extractLeadingComments(dec.fileBytes)
leadingUsed := false
if leadingComment != "" {
root.HeadComment = leadingComment
}
// process attributes in declaration order
body := dec.file.Body.(*hclsyntax.Body)
for _, attrWithName := range sortedAttributes(body.Attributes) {
keyNode := createStringScalarNode(attrWithName.Name)
valNode := convertHclExprToNode(attrWithName.Attr.Expr, dec.fileBytes)
// Attach comments if any
attrRange := attrWithName.Attr.Range()
headComment := extractHeadComment(dec.fileBytes, attrRange.Start.Byte)
if !leadingUsed && leadingComment != "" {
// Avoid double-applying the leading file comment to the first attribute
switch headComment {
case leadingComment:
headComment = ""
case "":
headComment = leadingComment
}
leadingUsed = true
}
if headComment != "" {
keyNode.HeadComment = headComment
}
if lineComment := extractLineComment(dec.fileBytes, attrRange.End.Byte); lineComment != "" {
valNode.LineComment = lineComment
}
root.AddKeyValueChild(keyNode, valNode)
}
// process blocks
for _, block := range body.Blocks {
addBlockToMapping(root, block, dec.fileBytes)
}
dec.documentIndex++
root.document = dec.documentIndex - 1
return root, nil
}
func hclBodyToNode(body *hclsyntax.Body, src []byte) *CandidateNode {
node := &CandidateNode{Kind: MappingNode}
for _, attrWithName := range sortedAttributes(body.Attributes) {
key := createStringScalarNode(attrWithName.Name)
val := convertHclExprToNode(attrWithName.Attr.Expr, src)
// Attach comments if any
attrRange := attrWithName.Attr.Range()
if headComment := extractHeadComment(src, attrRange.Start.Byte); headComment != "" {
key.HeadComment = headComment
}
if lineComment := extractLineComment(src, attrRange.End.Byte); lineComment != "" {
val.LineComment = lineComment
}
node.AddKeyValueChild(key, val)
}
for _, block := range body.Blocks {
addBlockToMapping(node, block, src)
}
return node
}
// addBlockToMapping nests block type and labels into the parent mapping, merging children.
func addBlockToMapping(parent *CandidateNode, block *hclsyntax.Block, src []byte) {
bodyNode := hclBodyToNode(block.Body, src)
current := parent
// ensure block type mapping exists
var typeNode *CandidateNode
for i := 0; i < len(current.Content); i += 2 {
if current.Content[i].Value == block.Type {
typeNode = current.Content[i+1]
break
}
}
if typeNode == nil {
_, typeNode = current.AddKeyValueChild(createStringScalarNode(block.Type), &CandidateNode{Kind: MappingNode})
}
current = typeNode
// walk labels, creating/merging mappings
for _, label := range block.Labels {
var next *CandidateNode
for i := 0; i < len(current.Content); i += 2 {
if current.Content[i].Value == label {
next = current.Content[i+1]
break
}
}
if next == nil {
_, next = current.AddKeyValueChild(createStringScalarNode(label), &CandidateNode{Kind: MappingNode})
}
current = next
}
// merge body attributes/blocks into the final mapping
for i := 0; i < len(bodyNode.Content); i += 2 {
current.AddKeyValueChild(bodyNode.Content[i], bodyNode.Content[i+1])
}
}
func convertHclExprToNode(expr hclsyntax.Expression, src []byte) *CandidateNode {
// handle literal values directly
switch e := expr.(type) {
case *hclsyntax.LiteralValueExpr:
v := e.Val
if v.IsNull() {
return createScalarNode(nil, "")
}
switch {
case v.Type().Equals(cty.String):
// prefer to extract exact source (to avoid extra quoting) when available
// Prefer the actual cty string value
s := v.AsString()
node := createScalarNode(s, s)
// Don't set style for regular quoted strings - let YAML handle naturally
return node
case v.Type().Equals(cty.Bool):
b := v.True()
return createScalarNode(b, strconv.FormatBool(b))
case v.Type() == cty.Number:
// prefer integers when the numeric value is integral
bf := v.AsBigFloat()
if bf == nil {
// fallback to string
return createStringScalarNode(v.GoString())
}
// check if bf represents an exact integer
if intVal, acc := bf.Int(nil); acc == big.Exact {
s := intVal.String()
return createScalarNode(intVal.Int64(), s)
}
s := bf.Text('g', -1)
return createScalarNode(0.0, s)
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
seq := &CandidateNode{Kind: SequenceNode}
it := v.ElementIterator()
for it.Next() {
_, val := it.Element()
// convert cty.Value to a node by wrapping in literal expr via string representation
child := convertCtyValueToNode(val)
seq.AddChild(child)
}
return seq
case v.Type().IsMapType() || v.Type().IsObjectType():
m := &CandidateNode{Kind: MappingNode}
it := v.ElementIterator()
for it.Next() {
key, val := it.Element()
keyStr := key.AsString()
keyNode := createStringScalarNode(keyStr)
valNode := convertCtyValueToNode(val)
m.AddKeyValueChild(keyNode, valNode)
}
return m
default:
// fallback to string
s := v.GoString()
return createStringScalarNode(s)
}
case *hclsyntax.TupleConsExpr:
// parse tuple/list into YAML sequence
seq := &CandidateNode{Kind: SequenceNode}
for _, exprVal := range e.Exprs {
child := convertHclExprToNode(exprVal, src)
seq.AddChild(child)
}
return seq
case *hclsyntax.ObjectConsExpr:
// parse object into YAML mapping
m := &CandidateNode{Kind: MappingNode}
m.Style = FlowStyle // Mark as inline object (flow style) for encoder
for _, item := range e.Items {
// evaluate key expression to get the key string
keyVal, keyDiags := item.KeyExpr.Value(nil)
if keyDiags != nil && keyDiags.HasErrors() {
// fallback: try to extract key from source
r := item.KeyExpr.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
keyNode := createStringScalarNode(strings.TrimSpace(string(src[start:end])))
valNode := convertHclExprToNode(item.ValueExpr, src)
m.AddKeyValueChild(keyNode, valNode)
}
continue
}
keyStr := keyVal.AsString()
keyNode := createStringScalarNode(keyStr)
valNode := convertHclExprToNode(item.ValueExpr, src)
m.AddKeyValueChild(keyNode, valNode)
}
return m
case *hclsyntax.TemplateExpr:
// Reconstruct template string, preserving ${} syntax for interpolations
var parts []string
for _, p := range e.Parts {
switch lp := p.(type) {
case *hclsyntax.LiteralValueExpr:
if lp.Val.Type().Equals(cty.String) {
parts = append(parts, lp.Val.AsString())
} else {
parts = append(parts, lp.Val.GoString())
}
default:
// Non-literal expression - reconstruct with ${} wrapper
r := p.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
exprText := string(src[start:end])
parts = append(parts, "${"+exprText+"}")
} else {
parts = append(parts, fmt.Sprintf("${%v}", p))
}
}
}
combined := strings.Join(parts, "")
node := createScalarNode(combined, combined)
// Set DoubleQuotedStyle for all templates (which includes all quoted strings in HCL)
// This ensures HCL roundtrips preserve quotes, and YAML properly quotes strings with ${}
node.Style = DoubleQuotedStyle
return node
case *hclsyntax.ScopeTraversalExpr:
// Simple identifier/traversal (e.g. unquoted string literal in HCL)
r := e.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
text := strings.TrimSpace(string(src[start:end]))
return createStringScalarNode(text)
}
// Fallback to root name if source unavailable
if len(e.Traversal) > 0 {
if root, ok := e.Traversal[0].(hcl.TraverseRoot); ok {
return createStringScalarNode(root.Name)
}
}
return createStringScalarNode("")
case *hclsyntax.FunctionCallExpr:
// Preserve function calls as raw expressions for roundtrip
r := e.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
text := strings.TrimSpace(string(src[start:end]))
node := createStringScalarNode(text)
node.Style = 0
return node
}
node := createStringScalarNode(e.Name)
node.Style = 0
return node
default:
// try to evaluate the expression (handles unary, binary ops, etc.)
val, diags := expr.Value(nil)
if diags == nil || !diags.HasErrors() {
// successfully evaluated, convert cty.Value to node
return convertCtyValueToNode(val)
}
// fallback: extract source text for the expression
r := expr.Range()
start := r.Start.Byte
end := r.End.Byte
if start >= 0 && end >= start && end <= len(src) {
text := string(src[start:end])
// Mark as unquoted expression so encoder emits without quoting
node := createStringScalarNode(text)
node.Style = 0
return node
}
return createStringScalarNode(fmt.Sprintf("%v", expr))
}
}
func convertCtyValueToNode(v cty.Value) *CandidateNode {
if v.IsNull() {
return createScalarNode(nil, "")
}
switch {
case v.Type().Equals(cty.String):
return createScalarNode("", v.AsString())
case v.Type().Equals(cty.Bool):
b := v.True()
return createScalarNode(b, strconv.FormatBool(b))
case v.Type() == cty.Number:
bf := v.AsBigFloat()
if bf == nil {
return createStringScalarNode(v.GoString())
}
if intVal, acc := bf.Int(nil); acc == big.Exact {
s := intVal.String()
return createScalarNode(intVal.Int64(), s)
}
s := bf.Text('g', -1)
return createScalarNode(0.0, s)
case v.Type().IsTupleType() || v.Type().IsListType() || v.Type().IsSetType():
seq := &CandidateNode{Kind: SequenceNode}
it := v.ElementIterator()
for it.Next() {
_, val := it.Element()
seq.AddChild(convertCtyValueToNode(val))
}
return seq
case v.Type().IsMapType() || v.Type().IsObjectType():
m := &CandidateNode{Kind: MappingNode}
it := v.ElementIterator()
for it.Next() {
key, val := it.Element()
keyNode := createStringScalarNode(key.AsString())
valNode := convertCtyValueToNode(val)
m.AddKeyValueChild(keyNode, valNode)
}
return m
default:
return createStringScalarNode(v.GoString())
}
}
+177
View File
@@ -0,0 +1,177 @@
# HCL
Encode and decode to and from [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl).
HCL is commonly used in HashiCorp tools like Terraform for configuration files. The yq HCL encoder and decoder support:
- Blocks and attributes
- String interpolation and expressions (preserved without quotes)
- Comments (leading, head, and line comments)
- Nested structures (maps and lists)
- Syntax colorization when enabled
## Parse HCL
Given a sample.hcl file of:
```hcl
io_mode = "async"
```
then
```bash
yq -oy sample.hcl
```
will output
```yaml
io_mode: "async"
```
## Roundtrip: Sample Doc
Given a sample.hcl file of:
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
then
```bash
yq sample.hcl
```
will output
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
## Roundtrip: With an update
Given a sample.hcl file of:
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
then
```bash
yq '.service.cat.process.main.command += "meow"' sample.hcl
```
will output
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server", "meow"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
## Parse HCL: Sample Doc
Given a sample.hcl file of:
```hcl
service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
```
then
```bash
yq -oy sample.hcl
```
will output
```yaml
service:
cat:
process:
main:
command:
- "/usr/local/bin/awesome-app"
- "server"
management:
command:
- "/usr/local/bin/awesome-app"
- "management"
```
## Parse HCL: with comments
Given a sample.hcl file of:
```hcl
# Configuration
port = 8080 # server port
```
then
```bash
yq -oy sample.hcl
```
will output
```yaml
# Configuration
port: 8080 # server port
```
## Roundtrip: with comments
Given a sample.hcl file of:
```hcl
# Configuration
port = 8080
```
then
```bash
yq sample.hcl
```
will output
```hcl
# Configuration
port = 8080
```
## Roundtrip: With templates, functions and arithmetic
Given a sample.hcl file of:
```hcl
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
```
then
```bash
yq sample.hcl
```
will output
```hcl
# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
```
+11
View File
@@ -0,0 +1,11 @@
# HCL
Encode and decode to and from [HashiCorp Configuration Language (HCL)](https://github.com/hashicorp/hcl).
HCL is commonly used in HashiCorp tools like Terraform for configuration files. The yq HCL encoder and decoder support:
- Blocks and attributes
- String interpolation and expressions (preserved without quotes)
- Comments (leading, head, and line comments)
- Nested structures (maps and lists)
- Syntax colorization when enabled
+12 -12
View File
@@ -53,7 +53,7 @@ Given a sample.xml file of:
```
then
```bash
yq -oy '.' sample.xml
yq -oy sample.xml
```
will output
```yaml
@@ -100,7 +100,7 @@ Given a sample.xml file of:
```
then
```bash
yq -oy '.' sample.xml
yq -oy sample.xml
```
will output
```yaml
@@ -157,7 +157,7 @@ Given a sample.xml file of:
```
then
```bash
yq -oy '.' sample.xml
yq -oy sample.xml
```
will output
```yaml
@@ -177,7 +177,7 @@ Given a sample.xml file of:
```
then
```bash
yq -oy '.' sample.xml
yq -oy sample.xml
```
will output
```yaml
@@ -196,7 +196,7 @@ Given a sample.xml file of:
```
then
```bash
yq -oy '.' sample.xml
yq -oy sample.xml
```
will output
```yaml
@@ -225,7 +225,7 @@ Given a sample.xml file of:
```
then
```bash
yq '.' sample.xml
yq sample.xml
```
will output
```xml
@@ -256,7 +256,7 @@ Given a sample.xml file of:
```
then
```bash
yq --xml-skip-directives '.' sample.xml
yq --xml-skip-directives sample.xml
```
will output
```xml
@@ -292,7 +292,7 @@ for x --></x>
```
then
```bash
yq -oy '.' sample.xml
yq -oy sample.xml
```
will output
```yaml
@@ -327,7 +327,7 @@ Given a sample.xml file of:
```
then
```bash
yq --xml-keep-namespace=false '.' sample.xml
yq --xml-keep-namespace=false sample.xml
```
will output
```xml
@@ -361,7 +361,7 @@ Given a sample.xml file of:
```
then
```bash
yq --xml-raw-token=false '.' sample.xml
yq --xml-raw-token=false sample.xml
```
will output
```xml
@@ -542,7 +542,7 @@ for x --></x>
```
then
```bash
yq '.' sample.xml
yq sample.xml
```
will output
```xml
@@ -575,7 +575,7 @@ Given a sample.xml file of:
```
then
```bash
yq '.' sample.xml
yq sample.xml
```
will output
```xml
+643
View File
@@ -0,0 +1,643 @@
//go:build !yq_nohcl
package yqlib
import (
"fmt"
"io"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
hclwrite "github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
)
type hclEncoder struct {
prefs HclPreferences
}
// NewHclEncoder creates a new HCL encoder
func NewHclEncoder(prefs HclPreferences) Encoder {
return &hclEncoder{prefs: prefs}
}
func (he *hclEncoder) CanHandleAliases() bool {
return false
}
func (he *hclEncoder) PrintDocumentSeparator(_ io.Writer) error {
return nil
}
func (he *hclEncoder) PrintLeadingContent(_ io.Writer, _ string) error {
return nil
}
func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debugf("I need to encode %v", NodeToString(node))
f := hclwrite.NewEmptyFile()
body := f.Body()
// Collect comments as we encode
commentMap := make(map[string]string)
he.collectComments(node, "", commentMap)
if err := he.encodeNode(body, node); err != nil {
return fmt.Errorf("failed to encode HCL: %w", err)
}
// Get the formatted output and remove extra spacing before '='
output := f.Bytes()
compactOutput := he.compactSpacing(output)
// Inject comments back into the output
finalOutput := he.injectComments(compactOutput, commentMap)
if he.prefs.ColorsEnabled {
colorized := he.colorizeHcl(finalOutput)
_, err := writer.Write(colorized)
return err
}
_, err := writer.Write(finalOutput)
return err
}
// compactSpacing removes extra whitespace before '=' in attribute assignments
func (he *hclEncoder) compactSpacing(input []byte) []byte {
// Use regex to replace multiple spaces before = with single space
re := regexp.MustCompile(`(\S)\s{2,}=`)
return re.ReplaceAll(input, []byte("$1 ="))
}
// collectComments recursively collects comments from nodes for later injection
func (he *hclEncoder) collectComments(node *CandidateNode, prefix string, commentMap map[string]string) {
if node == nil {
return
}
// For mapping nodes, collect comments from keys and values
if node.Kind == MappingNode {
// Collect root-level head comment if at root (prefix is empty)
if prefix == "" && node.HeadComment != "" {
commentMap[".head"] = node.HeadComment
}
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
key := keyNode.Value
// Create a path for this key
path := key
if prefix != "" {
path = prefix + "." + key
}
// Store comments from the key (head comments appear before the attribute)
if keyNode.HeadComment != "" {
commentMap[path+".head"] = keyNode.HeadComment
}
// Store comments from the value (line comments appear after the value)
if valueNode.LineComment != "" {
commentMap[path+".line"] = valueNode.LineComment
}
if valueNode.FootComment != "" {
commentMap[path+".foot"] = valueNode.FootComment
}
// Recurse into nested mappings
if valueNode.Kind == MappingNode {
he.collectComments(valueNode, path, commentMap)
}
}
}
}
// injectComments adds collected comments back into the HCL output
func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string) []byte {
// Convert output to string for easier manipulation
result := string(output)
// Root-level head comment (stored as ".head")
for path, comment := range commentMap {
if path == ".head" {
trimmed := strings.TrimSpace(comment)
if trimmed != "" && !strings.HasPrefix(result, trimmed) {
result = trimmed + "\n" + result
}
}
}
// Attribute head comments: insert above matching assignment
for path, comment := range commentMap {
parts := strings.Split(path, ".")
if len(parts) != 2 {
continue
}
key := parts[0]
commentType := parts[1]
if commentType != "head" || key == "" {
continue
}
trimmed := strings.TrimSpace(comment)
if trimmed == "" {
continue
}
re := regexp.MustCompile(`(?m)^(\s*)` + regexp.QuoteMeta(key) + `\s*=`)
if re.MatchString(result) {
result = re.ReplaceAllString(result, "$1"+trimmed+"\n$0")
}
}
return []byte(result)
}
// colorizeHcl applies syntax highlighting to HCL output using fatih/color
func (he *hclEncoder) colorizeHcl(input []byte) []byte {
hcl := string(input)
result := strings.Builder{}
// Create color functions for different token types
commentColor := color.New(color.FgHiBlack).SprintFunc()
stringColor := color.New(color.FgGreen).SprintFunc()
numberColor := color.New(color.FgHiMagenta).SprintFunc()
keyColor := color.New(color.FgCyan).SprintFunc()
boolColor := color.New(color.FgHiMagenta).SprintFunc()
// Simple tokenization for HCL coloring
i := 0
for i < len(hcl) {
ch := hcl[i]
// Comments - from # to end of line
if ch == '#' {
end := i
for end < len(hcl) && hcl[end] != '\n' {
end++
}
result.WriteString(commentColor(hcl[i:end]))
i = end
continue
}
// Strings - quoted text
if ch == '"' || ch == '\'' {
quote := ch
end := i + 1
for end < len(hcl) && hcl[end] != quote {
if hcl[end] == '\\' {
end++ // skip escaped char
}
end++
}
if end < len(hcl) {
end++ // include closing quote
}
result.WriteString(stringColor(hcl[i:end]))
i = end
continue
}
// Numbers - sequences of digits, possibly with decimal point or minus
if (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < len(hcl) && hcl[i+1] >= '0' && hcl[i+1] <= '9') {
end := i
if ch == '-' {
end++
}
for end < len(hcl) && ((hcl[end] >= '0' && hcl[end] <= '9') || hcl[end] == '.') {
end++
}
result.WriteString(numberColor(hcl[i:end]))
i = end
continue
}
// Identifiers/keys - alphanumeric + underscore
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' {
end := i
for end < len(hcl) && ((hcl[end] >= 'a' && hcl[end] <= 'z') ||
(hcl[end] >= 'A' && hcl[end] <= 'Z') ||
(hcl[end] >= '0' && hcl[end] <= '9') ||
hcl[end] == '_' || hcl[end] == '-') {
end++
}
ident := hcl[i:end]
// Check if this is a keyword/reserved word
switch ident {
case "true", "false", "null":
result.WriteString(boolColor(ident))
default:
// Check if followed by = (it's a key)
j := end
for j < len(hcl) && (hcl[j] == ' ' || hcl[j] == '\t') {
j++
}
if j < len(hcl) && hcl[j] == '=' {
result.WriteString(keyColor(ident))
} else if j < len(hcl) && hcl[j] == '{' {
// Block type
result.WriteString(keyColor(ident))
} else {
result.WriteString(ident) // plain text for other identifiers
}
}
i = end
continue
}
// Everything else (whitespace, operators, brackets) - no color
result.WriteByte(ch)
i++
}
return []byte(result.String())
}
// Helper runes for unquoted identifiers
func isHCLIdentifierStart(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_'
}
func isHCLIdentifierPart(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-'
}
// isValidHCLIdentifier checks if a string is a valid HCL identifier (unquoted)
func isValidHCLIdentifier(s string) bool {
if s == "" {
return false
}
// HCL identifiers must start with a letter or underscore
// and contain only letters, digits, underscores, and hyphens
for i, r := range s {
if i == 0 {
if !isHCLIdentifierStart(r) {
return false
}
continue
}
if !isHCLIdentifierPart(r) {
return false
}
}
return true
}
// tokensForRawHCLExpr produces a minimal token stream for a simple HCL expression so we can
// write it without introducing quotes (e.g. function calls like upper(message)).
func tokensForRawHCLExpr(expr string) (hclwrite.Tokens, error) {
var tokens hclwrite.Tokens
for i := 0; i < len(expr); {
ch := expr[i]
switch {
case ch == ' ' || ch == '\t':
i++
continue
case isHCLIdentifierStart(rune(ch)):
start := i
i++
for i < len(expr) && isHCLIdentifierPart(rune(expr[i])) {
i++
}
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenIdent, Bytes: []byte(expr[start:i])})
continue
case ch >= '0' && ch <= '9':
start := i
i++
for i < len(expr) && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] == '.') {
i++
}
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenNumberLit, Bytes: []byte(expr[start:i])})
continue
case ch == '(':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenOParen, Bytes: []byte{'('}})
case ch == ')':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCParen, Bytes: []byte{')'}})
case ch == ',':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte{','}})
case ch == '.':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenDot, Bytes: []byte{'.'}})
case ch == '+':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenPlus, Bytes: []byte{'+'}})
case ch == '-':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenMinus, Bytes: []byte{'-'}})
case ch == '*':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenStar, Bytes: []byte{'*'}})
case ch == '/':
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenSlash, Bytes: []byte{'/'}})
default:
return nil, fmt.Errorf("unsupported character %q in raw HCL expression", ch)
}
i++
}
return tokens, nil
}
// encodeAttribute encodes a value as an HCL attribute
func (he *hclEncoder) encodeAttribute(body *hclwrite.Body, key string, valueNode *CandidateNode) error {
if valueNode.Kind == ScalarNode && valueNode.Tag == "!!str" {
// Handle unquoted expressions (as-is, without quotes)
if valueNode.Style == 0 {
tokens, err := tokensForRawHCLExpr(valueNode.Value)
if err != nil {
return err
}
body.SetAttributeRaw(key, tokens)
return nil
}
if valueNode.Style&LiteralStyle != 0 {
tokens, err := tokensForRawHCLExpr(valueNode.Value)
if err != nil {
return err
}
body.SetAttributeRaw(key, tokens)
return nil
}
// Check if template with interpolation
if valueNode.Style&DoubleQuotedStyle != 0 && strings.Contains(valueNode.Value, "${") {
return he.encodeTemplateAttribute(body, key, valueNode.Value)
}
// Check if unquoted identifier
if isValidHCLIdentifier(valueNode.Value) && valueNode.Style == 0 {
traversal := hcl.Traversal{
hcl.TraverseRoot{Name: valueNode.Value},
}
body.SetAttributeTraversal(key, traversal)
return nil
}
}
// Default: use cty.Value for quoted strings and all other types
ctyValue, err := nodeToCtyValue(valueNode)
if err != nil {
return err
}
body.SetAttributeValue(key, ctyValue)
return nil
}
// encodeTemplateAttribute encodes a template string with ${} interpolations
func (he *hclEncoder) encodeTemplateAttribute(body *hclwrite.Body, key string, templateStr string) error {
tokens := hclwrite.Tokens{
{Type: hclsyntax.TokenOQuote, Bytes: []byte{'"'}},
}
for i := 0; i < len(templateStr); i++ {
if i < len(templateStr)-1 && templateStr[i] == '$' && templateStr[i+1] == '{' {
// Start of template interpolation
tokens = append(tokens, &hclwrite.Token{
Type: hclsyntax.TokenTemplateInterp,
Bytes: []byte("${"),
})
i++ // skip the '{'
// Find the matching '}'
start := i + 1
depth := 1
for i++; i < len(templateStr) && depth > 0; i++ {
switch templateStr[i] {
case '{':
depth++
case '}':
depth--
}
}
i-- // back up to the '}'
interpExpr := templateStr[start:i]
tokens = append(tokens, &hclwrite.Token{
Type: hclsyntax.TokenIdent,
Bytes: []byte(interpExpr),
})
tokens = append(tokens, &hclwrite.Token{
Type: hclsyntax.TokenTemplateSeqEnd,
Bytes: []byte("}"),
})
} else {
// Regular character
tokens = append(tokens, &hclwrite.Token{
Type: hclsyntax.TokenQuotedLit,
Bytes: []byte{templateStr[i]},
})
}
}
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCQuote, Bytes: []byte{'"'}})
body.SetAttributeRaw(key, tokens)
return nil
}
// encodeBlockIfMapping attempts to encode a value as a block. Returns true if it was encoded as a block.
func (he *hclEncoder) encodeBlockIfMapping(body *hclwrite.Body, key string, valueNode *CandidateNode) bool {
if valueNode.Kind != MappingNode || valueNode.Style == FlowStyle {
return false
}
// Try to extract block labels from a single-entry mapping chain
if labels, bodyNode, ok := extractBlockLabels(valueNode); ok {
if len(labels) > 1 && mappingChildrenAllMappings(bodyNode) {
primaryLabels := labels[:len(labels)-1]
nestedType := labels[len(labels)-1]
block := body.AppendNewBlock(key, primaryLabels)
if handled, err := he.encodeMappingChildrenAsBlocks(block.Body(), nestedType, bodyNode); err == nil && handled {
return true
}
if err := he.encodeNodeAttributes(block.Body(), bodyNode); err == nil {
return true
}
}
block := body.AppendNewBlock(key, labels)
if err := he.encodeNodeAttributes(block.Body(), bodyNode); err == nil {
return true
}
}
// If all child values are mappings, treat each child key as a labeled instance of this block type
if handled, _ := he.encodeMappingChildrenAsBlocks(body, key, valueNode); handled {
return true
}
// No labels detected, render as unlabeled block
block := body.AppendNewBlock(key, nil)
if err := he.encodeNodeAttributes(block.Body(), valueNode); err == nil {
return true
}
return false
}
// encodeNode encodes a CandidateNode directly to HCL, preserving style information
func (he *hclEncoder) encodeNode(body *hclwrite.Body, node *CandidateNode) error {
if node.Kind != MappingNode {
return fmt.Errorf("HCL encoder expects a mapping at the root level")
}
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
key := keyNode.Value
// Render as block or attribute depending on value type
if he.encodeBlockIfMapping(body, key, valueNode) {
continue
}
// Render as attribute: key = value
if err := he.encodeAttribute(body, key, valueNode); err != nil {
return err
}
}
return nil
}
// mappingChildrenAllMappings reports whether all values in a mapping node are non-flow mappings.
func mappingChildrenAllMappings(node *CandidateNode) bool {
if node == nil || node.Kind != MappingNode || node.Style == FlowStyle {
return false
}
if len(node.Content) == 0 {
return false
}
for i := 0; i < len(node.Content); i += 2 {
childVal := node.Content[i+1]
if childVal.Kind != MappingNode || childVal.Style == FlowStyle {
return false
}
}
return true
}
// encodeMappingChildrenAsBlocks emits a block for each mapping child, treating the child key as a label.
// Returns handled=true when it emitted blocks.
func (he *hclEncoder) encodeMappingChildrenAsBlocks(body *hclwrite.Body, blockType string, valueNode *CandidateNode) (bool, error) {
if !mappingChildrenAllMappings(valueNode) {
return false, nil
}
for i := 0; i < len(valueNode.Content); i += 2 {
childKey := valueNode.Content[i].Value
childVal := valueNode.Content[i+1]
labels := []string{childKey}
if extraLabels, bodyNode, ok := extractBlockLabels(childVal); ok {
labels = append(labels, extraLabels...)
childVal = bodyNode
}
block := body.AppendNewBlock(blockType, labels)
if err := he.encodeNodeAttributes(block.Body(), childVal); err != nil {
return true, err
}
}
return true, nil
}
// encodeNodeAttributes encodes the attributes of a mapping node (used for blocks)
func (he *hclEncoder) encodeNodeAttributes(body *hclwrite.Body, node *CandidateNode) error {
if node.Kind != MappingNode {
return fmt.Errorf("expected mapping node for block body")
}
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
key := keyNode.Value
// Render as block or attribute depending on value type
if he.encodeBlockIfMapping(body, key, valueNode) {
continue
}
// Render attribute for non-block value
if err := he.encodeAttribute(body, key, valueNode); err != nil {
return err
}
}
return nil
}
// extractBlockLabels detects a chain of single-entry mappings that encode block labels.
// It returns the collected labels and the final mapping to be used as the block body.
// Pattern: {label1: {label2: { ... {bodyMap} }}}
func extractBlockLabels(node *CandidateNode) ([]string, *CandidateNode, bool) {
var labels []string
current := node
for current != nil && current.Kind == MappingNode && len(current.Content) == 2 {
keyNode := current.Content[0]
valNode := current.Content[1]
if valNode.Kind != MappingNode {
break
}
labels = append(labels, keyNode.Value)
// If the child is itself a single mapping entry with a mapping value, keep descending.
if len(valNode.Content) == 2 && valNode.Content[1].Kind == MappingNode {
current = valNode
continue
}
// Otherwise, we have reached the body mapping.
return labels, valNode, true
}
return nil, nil, false
}
// nodeToCtyValue converts a CandidateNode directly to cty.Value, preserving order
func nodeToCtyValue(node *CandidateNode) (cty.Value, error) {
switch node.Kind {
case ScalarNode:
// Parse scalar value based on its tag
switch node.Tag {
case "!!bool":
return cty.BoolVal(node.Value == "true"), nil
case "!!int":
var i int64
_, err := fmt.Sscanf(node.Value, "%d", &i)
if err != nil {
return cty.NilVal, err
}
return cty.NumberIntVal(i), nil
case "!!float":
var f float64
_, err := fmt.Sscanf(node.Value, "%f", &f)
if err != nil {
return cty.NilVal, err
}
return cty.NumberFloatVal(f), nil
case "!!null":
return cty.NullVal(cty.DynamicPseudoType), nil
default:
// Default to string
return cty.StringVal(node.Value), nil
}
case MappingNode:
// Preserve order by iterating Content directly
m := make(map[string]cty.Value)
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
v, err := nodeToCtyValue(valueNode)
if err != nil {
return cty.NilVal, err
}
m[keyNode.Value] = v
}
return cty.ObjectVal(m), nil
case SequenceNode:
vals := make([]cty.Value, len(node.Content))
for i, item := range node.Content {
v, err := nodeToCtyValue(item)
if err != nil {
return cty.NilVal, err
}
vals[i] = v
}
return cty.TupleVal(vals), nil
case AliasNode:
return cty.NilVal, fmt.Errorf("HCL encoder does not support aliases")
default:
return cty.NilVal, fmt.Errorf("unsupported node kind: %v", node.Kind)
}
}
+6
View File
@@ -67,6 +67,11 @@ var TomlFormat = &Format{"toml", []string{},
func() Decoder { return NewTomlDecoder() },
}
var HclFormat = &Format{"hcl", []string{"h"},
func() Encoder { return NewHclEncoder(ConfiguredHclPreferences) },
func() Decoder { return NewHclDecoder() },
}
var ShellVariablesFormat = &Format{"shell", []string{"s", "sh"},
func() Encoder { return NewShellVariablesEncoder() },
nil,
@@ -93,6 +98,7 @@ var Formats = []*Format{
UriFormat,
ShFormat,
TomlFormat,
HclFormat,
ShellVariablesFormat,
LuaFormat,
INIFormat,
+15
View File
@@ -0,0 +1,15 @@
package yqlib
type HclPreferences struct {
ColorsEnabled bool
}
func NewDefaultHclPreferences() HclPreferences {
return HclPreferences{ColorsEnabled: false}
}
func (p *HclPreferences) Copy() HclPreferences {
return HclPreferences{ColorsEnabled: p.ColorsEnabled}
}
var ConfiguredHclPreferences = NewDefaultHclPreferences()
+429
View File
@@ -0,0 +1,429 @@
//go:build !yq_nohcl
package yqlib
import (
"bufio"
"fmt"
"testing"
"github.com/mikefarah/yq/v4/test"
)
var nestedExample = `service "http" "web_proxy" {
listen_addr = "127.0.0.1:8080"
}`
var nestedExampleYaml = "service:\n http:\n web_proxy:\n listen_addr: \"127.0.0.1:8080\"\n"
var multipleBlockLabelKeys = `service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
`
var multipleBlockLabelKeysExpected = `service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
`
var multipleBlockLabelKeysExpectedUpdate = `service "cat" {
process "main" {
command = ["/usr/local/bin/awesome-app", "server", "meow"]
}
process "management" {
command = ["/usr/local/bin/awesome-app", "management"]
}
}
`
var multipleBlockLabelKeysExpectedYaml = `service:
cat:
process:
main:
command:
- "/usr/local/bin/awesome-app"
- "server"
management:
command:
- "/usr/local/bin/awesome-app"
- "management"
`
var simpleSample = `# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)`
var simpleSampleExpected = `# Arithmetic with literals and application-provided variables
sum = 1 + addend
# String interpolation and templates
message = "Hello, ${name}!"
# Application-provided functions
shouty_message = upper(message)
`
var simpleSampleExpectedYaml = `# Arithmetic with literals and application-provided variables
sum: 1 + addend
# String interpolation and templates
message: "Hello, ${name}!"
# Application-provided functions
shouty_message: upper(message)
`
var hclFormatScenarios = []formatScenario{
{
description: "Parse HCL",
input: `io_mode = "async"`,
expected: "io_mode: \"async\"\n",
scenarioType: "decode",
},
{
description: "Simple decode, no quotes",
skipDoc: true,
input: `io_mode = async`,
expected: "io_mode: async\n",
scenarioType: "decode",
},
{
description: "Simple roundtrip, no quotes",
skipDoc: true,
input: `io_mode = async`,
expected: "io_mode = async\n",
scenarioType: "roundtrip",
},
{
description: "Nested decode",
skipDoc: true,
input: nestedExample,
expected: nestedExampleYaml,
scenarioType: "decode",
},
{
description: "Template decode",
skipDoc: true,
input: `message = "Hello, ${name}!"`,
expected: "message: \"Hello, ${name}!\"\n",
scenarioType: "decode",
},
{
description: "Roundtrip: with template",
skipDoc: true,
input: `message = "Hello, ${name}!"`,
expected: "message = \"Hello, ${name}!\"\n",
scenarioType: "roundtrip",
},
{
description: "Roundtrip: with function",
skipDoc: true,
input: `shouty_message = upper(message)`,
expected: "shouty_message = upper(message)\n",
scenarioType: "roundtrip",
},
{
description: "Roundtrip: with arithmetic",
skipDoc: true,
input: `sum = 1 + addend`,
expected: "sum = 1 + addend\n",
scenarioType: "roundtrip",
},
{
description: "Arithmetic decode",
skipDoc: true,
input: `sum = 1 + addend`,
expected: "sum: 1 + addend\n",
scenarioType: "decode",
},
{
description: "number attribute",
skipDoc: true,
input: `port = 8080`,
expected: "port: 8080\n",
scenarioType: "decode",
},
{
description: "float attribute",
skipDoc: true,
input: `pi = 3.14`,
expected: "pi: 3.14\n",
scenarioType: "decode",
},
{
description: "boolean attribute",
skipDoc: true,
input: `enabled = true`,
expected: "enabled: true\n",
scenarioType: "decode",
},
{
description: "object/map attribute",
skipDoc: true,
input: `obj = { a = 1, b = "two" }`,
expected: "obj: {a: 1, b: \"two\"}\n",
scenarioType: "decode",
},
{
description: "nested block",
skipDoc: true,
input: `server { port = 8080 }`,
expected: "server:\n port: 8080\n",
scenarioType: "decode",
},
{
description: "multiple attributes",
skipDoc: true,
input: "name = \"app\"\nversion = 1\nenabled = true",
expected: "name: \"app\"\nversion: 1\nenabled: true\n",
scenarioType: "decode",
},
{
description: "binary expression",
skipDoc: true,
input: `count = 0 - 42`,
expected: "count: -42\n",
scenarioType: "decode",
},
{
description: "negative number",
skipDoc: true,
input: `count = -42`,
expected: "count: -42\n",
scenarioType: "decode",
},
{
description: "scientific notation",
skipDoc: true,
input: `value = 1e-3`,
expected: "value: 0.001\n",
scenarioType: "decode",
},
{
description: "nested object",
skipDoc: true,
input: `config = { db = { host = "localhost", port = 5432 } }`,
expected: "config: {db: {host: \"localhost\", port: 5432}}\n",
scenarioType: "decode",
},
{
description: "mixed list",
skipDoc: true,
input: `values = [1, "two", true]`,
expected: "values:\n - 1\n - \"two\"\n - true\n",
scenarioType: "decode",
},
{
description: "Roundtrip: Sample Doc",
input: multipleBlockLabelKeys,
expected: multipleBlockLabelKeysExpected,
scenarioType: "roundtrip",
},
{
description: "Roundtrip: With an update",
input: multipleBlockLabelKeys,
expression: `.service.cat.process.main.command += "meow"`,
expected: multipleBlockLabelKeysExpectedUpdate,
scenarioType: "roundtrip",
},
{
description: "Parse HCL: Sample Doc",
input: multipleBlockLabelKeys,
expected: multipleBlockLabelKeysExpectedYaml,
scenarioType: "decode",
},
{
description: "block with labels",
skipDoc: true,
input: `resource "aws_instance" "example" { ami = "ami-12345" }`,
expected: "resource:\n aws_instance:\n example:\n ami: \"ami-12345\"\n",
scenarioType: "decode",
},
{
description: "block with labels roundtrip",
skipDoc: true,
input: `resource "aws_instance" "example" { ami = "ami-12345" }`,
expected: "resource \"aws_instance\" \"example\" {\n ami = \"ami-12345\"\n}\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip simple attribute",
skipDoc: true,
input: `io_mode = "async"`,
expected: `io_mode = "async"` + "\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip number attribute",
skipDoc: true,
input: `port = 8080`,
expected: "port = 8080\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip float attribute",
skipDoc: true,
input: `pi = 3.14`,
expected: "pi = 3.14\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip boolean attribute",
skipDoc: true,
input: `enabled = true`,
expected: "enabled = true\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip list of strings",
skipDoc: true,
input: `tags = ["a", "b"]`,
expected: "tags = [\"a\", \"b\"]\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip object/map attribute",
skipDoc: true,
input: `obj = { a = 1, b = "two" }`,
expected: "obj = {\n a = 1\n b = \"two\"\n}\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip nested block",
skipDoc: true,
input: `server { port = 8080 }`,
expected: "server {\n port = 8080\n}\n",
scenarioType: "roundtrip",
},
{
description: "roundtrip multiple attributes",
skipDoc: true,
input: "name = \"app\"\nversion = 1\nenabled = true",
expected: "name = \"app\"\nversion = 1\nenabled = true\n",
scenarioType: "roundtrip",
},
{
description: "Parse HCL: with comments",
input: "# Configuration\nport = 8080 # server port",
expected: "# Configuration\nport: 8080 # server port\n",
scenarioType: "decode",
},
{
description: "Roundtrip: with comments",
input: "# Configuration\nport = 8080",
expected: "# Configuration\nport = 8080\n",
scenarioType: "roundtrip",
},
{
description: "Roundtrip: With templates, functions and arithmetic",
input: simpleSample,
expected: simpleSampleExpected,
scenarioType: "roundtrip",
},
{
description: "roundtrip example",
skipDoc: true,
input: simpleSample,
expected: simpleSampleExpectedYaml,
scenarioType: "decode",
},
{
description: "Parse HCL: List of strings",
skipDoc: true,
input: `tags = ["a", "b"]`,
expected: "tags:\n - \"a\"\n - \"b\"\n",
scenarioType: "decode",
},
}
func testHclScenario(t *testing.T, s formatScenario) {
switch s.scenarioType {
case "decode":
result := mustProcessFormatScenario(s, NewHclDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))
test.AssertResultWithContext(t, s.expected, result, s.description)
case "roundtrip":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewHclDecoder(), NewHclEncoder(ConfiguredHclPreferences)), s.description)
}
}
func documentHclScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
s := i.(formatScenario)
if s.skipDoc {
return
}
switch s.scenarioType {
case "", "decode":
documentHclDecodeScenario(w, s)
case "roundtrip":
documentHclRoundTripScenario(w, s)
default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
}
}
func documentHclDecodeScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
if s.subdescription != "" {
writeOrPanic(w, s.subdescription)
writeOrPanic(w, "\n\n")
}
writeOrPanic(w, "Given a sample.hcl file of:\n")
writeOrPanic(w, fmt.Sprintf("```hcl\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
expression := s.expression
if s.expression != "" {
expression = fmt.Sprintf(" '%v'", s.expression)
}
writeOrPanic(w, fmt.Sprintf("```bash\nyq -oy%v sample.hcl\n```\n", expression))
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewHclDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
}
func documentHclRoundTripScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
if s.subdescription != "" {
writeOrPanic(w, s.subdescription)
writeOrPanic(w, "\n\n")
}
writeOrPanic(w, "Given a sample.hcl file of:\n")
writeOrPanic(w, fmt.Sprintf("```hcl\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
expression := s.expression
if s.expression != "" {
expression = fmt.Sprintf(" '%v'", s.expression)
}
writeOrPanic(w, fmt.Sprintf("```bash\nyq%v sample.hcl\n```\n", expression))
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```hcl\n%v```\n\n", mustProcessFormatScenario(s, NewHclDecoder(), NewHclEncoder(ConfiguredHclPreferences))))
}
func TestHclFormatScenarios(t *testing.T) {
for _, tt := range hclFormatScenarios {
testHclScenario(t, tt)
}
genericScenarios := make([]interface{}, len(hclFormatScenarios))
for i, s := range hclFormatScenarios {
genericScenarios[i] = s
}
documentScenarios(t, "usage", "hcl", genericScenarios, documentHclScenario)
}
+11
View File
@@ -0,0 +1,11 @@
//go:build yq_nohcl
package yqlib
func NewHclDecoder() Decoder {
return nil
}
func NewHclEncoder(_ HclPreferences) Encoder {
return nil
}
+7 -7
View File
@@ -713,10 +713,10 @@ func documentXMLDecodeScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, "then\n")
expression := s.expression
if expression == "" {
expression = "."
if s.expression != "" {
expression = fmt.Sprintf(" '%v'", s.expression)
}
writeOrPanic(w, fmt.Sprintf("```bash\nyq -oy '%v' sample.xml\n```\n", expression))
writeOrPanic(w, fmt.Sprintf("```bash\nyq -oy%v sample.xml\n```\n", expression))
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewYamlEncoder(ConfiguredYamlPreferences))))
@@ -734,7 +734,7 @@ func documentXMLDecodeKeepNsScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
writeOrPanic(w, "```bash\nyq --xml-keep-namespace=false '.' sample.xml\n```\n")
writeOrPanic(w, "```bash\nyq --xml-keep-namespace=false sample.xml\n```\n")
writeOrPanic(w, "will output\n")
prefs := NewDefaultXmlPreferences()
prefs.KeepNamespace = false
@@ -758,7 +758,7 @@ func documentXMLDecodeKeepNsRawTokenScenario(w *bufio.Writer, s formatScenario)
writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
writeOrPanic(w, "```bash\nyq --xml-raw-token=false '.' sample.xml\n```\n")
writeOrPanic(w, "```bash\nyq --xml-raw-token=false sample.xml\n```\n")
writeOrPanic(w, "will output\n")
prefs := NewDefaultXmlPreferences()
@@ -803,7 +803,7 @@ func documentXMLRoundTripScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
writeOrPanic(w, "```bash\nyq '.' sample.xml\n```\n")
writeOrPanic(w, "```bash\nyq sample.xml\n```\n")
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewXMLEncoder(ConfiguredXMLPreferences))))
@@ -821,7 +821,7 @@ func documentXMLSkipDirectivesScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
writeOrPanic(w, "```bash\nyq --xml-skip-directives '.' sample.xml\n```\n")
writeOrPanic(w, "```bash\nyq --xml-skip-directives sample.xml\n```\n")
writeOrPanic(w, "will output\n")
prefs := NewDefaultXmlPreferences()
prefs.SkipDirectives = true
+12 -1
View File
@@ -38,6 +38,7 @@ cleanup
cmlu
colorise
colors
coloring
compinit
coolioo
coverprofile
@@ -189,8 +190,11 @@ risentveber
rmescandon
Rosey
roundtrip
roundtrips
Roundtrip
roundtripping
Interp
interp
runningvms
sadface
selfupdate
@@ -265,4 +269,11 @@ noprops
nosh
noshell
tinygo
nonexistent
nonexistent
hclsyntax
hclwrite
nohcl
zclconf
cty
go-cty
unlabeled
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/bash
go build -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson" -ldflags "-s -w" .
go build -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson yq_nohcl" -ldflags "-s -w" .
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
# Currently, the `yq_nojson` feature must be enabled when using TinyGo.
tinygo build -no-debug -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson yq_nocsv yq_nobase64 yq_nouri yq_noprops yq_nosh yq_noshell" .
tinygo build -no-debug -tags "yq_nolua yq_noini yq_notoml yq_noxml yq_nojson yq_nocsv yq_nobase64 yq_nouri yq_noprops yq_nosh yq_noshell yq_nohcl" .