mirror of
https://github.com/mikefarah/yq.git
synced 2026-08-24 08:22:13 +08:00
Compare commits
25
Commits
draft-4.53.1
...
hcl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60eb7d22a | ||
|
|
86f3f77b8c | ||
|
|
bc92673d6f | ||
|
|
87374edce5 | ||
|
|
5c166e3038 | ||
|
|
491ab8e70f | ||
|
|
bf48626ad8 | ||
|
|
42f2436e1f | ||
|
|
6c4d1bd066 | ||
|
|
a3150396b5 | ||
|
|
c7c5a478c6 | ||
|
|
30974679ae | ||
|
|
d4a11521c3 | ||
|
|
beaebf7790 | ||
|
|
360de4f7af | ||
|
|
7923d04bdd | ||
|
|
bd3a647650 | ||
|
|
fa42080148 | ||
|
|
82100a82a4 | ||
|
|
3d768bab73 | ||
|
|
7b1c52a588 | ||
|
|
48e63a0386 | ||
|
|
0cff8c234a | ||
|
|
2be7e904be | ||
|
|
d405ab6c10 |
@@ -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`
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build yq_nohcl
|
||||
|
||||
package yqlib
|
||||
|
||||
func NewHclDecoder() Decoder {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewHclEncoder(_ HclPreferences) Encoder {
|
||||
return nil
|
||||
}
|
||||
@@ -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
@@ -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,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,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" .
|
||||
Reference in New Issue
Block a user