mirror of
https://github.com/mikefarah/yq.git
synced 2026-08-25 00:44:43 +08:00
Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45be35c063 | ||
|
|
39fbf01fa8 | ||
|
|
306dc931a5 | ||
|
|
f00852bc6c | ||
|
|
c716d157f2 | ||
|
|
e49e588ab5 | ||
|
|
389486829d | ||
|
|
d32e71f25b | ||
|
|
796317b885 | ||
|
|
258b84a05e | ||
|
|
e056b91a00 | ||
|
|
85b0985a60 | ||
|
|
874cbc4d3c | ||
|
|
f6c780e793 | ||
|
|
8c25f33df4 | ||
|
|
2869919cb4 | ||
|
|
458d02f3ab | ||
|
|
877a47cb19 | ||
|
|
3050ca5303 | ||
|
|
49b6477c49 | ||
|
|
78bc9baffd | ||
|
|
1f2b0fe76b | ||
|
|
1228bcfa75 | ||
|
|
7f72595a12 | ||
|
|
ff2c1c930c | ||
|
|
36d410b348 | ||
|
|
6dfe002058 | ||
|
|
ed4f468c97 | ||
|
|
8b2ba41c6c | ||
|
|
0ecdce24e8 | ||
|
|
01ac615e67 | ||
|
|
6629924dea | ||
|
|
386935470d | ||
|
|
d5dd338707 | ||
|
|
201542b522 | ||
|
|
f3538850f2 | ||
|
|
df92decbe0 | ||
|
|
23060cb8af | ||
|
|
4532346e13 | ||
|
|
ff40a023cc | ||
|
|
7d49d408ef | ||
|
|
02b28073bf | ||
|
|
6957399dc0 | ||
|
|
6251e95af8 | ||
|
|
b2155ebb00 | ||
|
|
911e3942e3 | ||
|
|
35d338cc24 | ||
|
|
6d7aa384c4 | ||
|
|
e89001156a | ||
|
|
c34edcf983 | ||
|
|
a46a5766fa | ||
|
|
af2f86828c | ||
|
|
76c817009f | ||
|
|
f03c9dc599 | ||
|
|
023c85e1e2 | ||
|
|
a3d9e0172f | ||
|
|
ab3be228dc | ||
|
|
93fed3fd7a | ||
|
|
f42534ea0f | ||
|
|
b968963ed4 | ||
|
|
92309b17a4 | ||
|
|
d5757fc82b | ||
|
|
db2a4550e5 | ||
|
|
3018396ed2 | ||
|
|
84b095bbc4 | ||
|
|
f35e57d901 | ||
|
|
70ac3d6c7a | ||
|
|
904215ef4d | ||
|
|
41cc4fb4ac | ||
|
|
9c95a9f379 | ||
|
|
a4720c089a | ||
|
|
3431aebb2c | ||
|
|
ae87394f4a | ||
|
|
5e75db824b | ||
|
|
08ecd39a1e | ||
|
|
b7aa711d94 | ||
|
|
a5b8ef6cb1 | ||
|
|
a47e882c8f | ||
|
|
128bf80eed | ||
|
|
8c06478ade | ||
|
|
fa6dc5c9fb | ||
|
|
78c096fa8f | ||
|
|
ce9a4af0df | ||
|
|
4734be9a4d | ||
|
|
4d88d51b1b | ||
|
|
31628e7324 | ||
|
|
bfcb3fc6b7 | ||
|
|
c3782799c5 | ||
|
|
162ea5437c |
@@ -38,11 +38,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -67,4 +67,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
IMAGE_NAME: mikefarah/yq
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -95,6 +95,6 @@ jobs:
|
||||
-t "${IMAGE_NAME}:4-githubaction" \
|
||||
-t "${IMAGE_NAME}:latest-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
|
||||
-t "ghrc.io/${IMAGE_NAME}:4-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:4-githubaction" \
|
||||
-t "ghcr.io/${IMAGE_NAME}:latest-githubaction" \
|
||||
.
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
publishGitRelease:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1.20'
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
environment: snap
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: snapcore/action-build@v1
|
||||
id: build
|
||||
- uses: snapcore/action-publish@v1
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Get test
|
||||
id: get_value
|
||||
uses: mikefarah/yq@master
|
||||
|
||||
@@ -25,6 +25,7 @@ _testmain.go
|
||||
cover.out
|
||||
coverage.out
|
||||
coverage.html
|
||||
coverage_sorted.txt
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
@@ -23,6 +23,7 @@ builds:
|
||||
- linux_amd64
|
||||
- linux_arm
|
||||
- linux_arm64
|
||||
- linux_loong64
|
||||
- linux_mips
|
||||
- linux_mips64
|
||||
- linux_mips64le
|
||||
|
||||
+200
-28
@@ -1,42 +1,214 @@
|
||||
.# Development
|
||||
# Before you begin
|
||||
Not all new PRs will be merged in
|
||||
|
||||
1. Install (Golang)[https://golang.org/]
|
||||
1. Run `scripts/devtools.sh` to install the required devtools
|
||||
2. Run `make [local] vendor` to install the vendor dependencies
|
||||
2. Run `make [local] test` to ensure you can run the existing tests
|
||||
3. Write unit tests - (see existing examples). Changes will not be accepted without corresponding unit tests.
|
||||
4. Make the code changes.
|
||||
5. `make [local] test` to lint code and run tests
|
||||
6. Profit! ok no profit, but raise a PR and get kudos :)
|
||||
It's recommended to check with the owner first (e.g. raise an issue) to discuss a new feature before developing, to ensure your hard efforts don't go to waste.
|
||||
|
||||
PRs to fix bugs and issues are almost always welcome :pray: please ensure you write tests as well.
|
||||
|
||||
The following types of PRs will _not_ be accepted:
|
||||
- **Significant refactors** take a lot of time to understand and can have all sorts of unintended side effects. If you think there's a better way to do things (that requires significant changes) raise an issue for discussion first :)
|
||||
- **Release pipeline PRs** are a security risk - it's too easy for a serious vulnerability to sneak in (either intended or not). If there is a new cool way of releasing things, raise an issue for discussion first - it will need to be gone over with a fine tooth comb.
|
||||
- **Version bumps** are handled by dependabot, the bot will auto-raise PRs and they will be regularly merged in.
|
||||
- **New release platforms** At this stage, yq is not going to maintain any other release platforms other than GitHub and Docker - that said, I'm more than happy to put in other community maintained methods in the README for visibility :heart:
|
||||
|
||||
|
||||
# Development
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. Install [Golang](https://golang.org/) (version 1.24.0 or later)
|
||||
2. Run `scripts/devtools.sh` to install required development tools:
|
||||
- golangci-lint for code linting
|
||||
- gosec for security analysis
|
||||
3. Run `make [local] vendor` to install vendor dependencies
|
||||
4. Run `make [local] test` to ensure you can run the existing tests
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Write unit tests first** - Changes will not be accepted without corresponding unit tests (see Testing section below)
|
||||
2. **Make your code changes**
|
||||
3. **Run tests and linting**: `make [local] test` (this runs formatting, linting, security checks, and tests)
|
||||
4. **Create your PR** and get kudos! :)
|
||||
|
||||
## Make Commands
|
||||
|
||||
- Use `make [local] <command>` for local development (runs in Docker container)
|
||||
- Use `make <command>` for CI/CD environments
|
||||
- Common commands:
|
||||
- `make [local] vendor` - Install dependencies
|
||||
- `make [local] test` - Run all checks and tests
|
||||
- `make [local] build` - Build the yq binary
|
||||
- `make [local] format` - Format code
|
||||
- `make [local] check` - Run linting and security checks
|
||||
|
||||
# Code Quality
|
||||
|
||||
## Linting and Formatting
|
||||
|
||||
The project uses strict linting rules defined in `.golangci.yml`. All code must pass:
|
||||
|
||||
- **Code formatting**: gofmt, goimports, gci
|
||||
- **Linting**: revive, errorlint, gosec, misspell, and others
|
||||
- **Security checks**: gosec security analysis
|
||||
- **Spelling checks**: misspell detection
|
||||
|
||||
Run `make [local] check` to verify your code meets all quality standards.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- Follow standard Go conventions
|
||||
- Use meaningful variable names
|
||||
- Add comments for public functions and complex logic
|
||||
- Keep functions focused and reasonably sized
|
||||
- Use the project's existing patterns and conventions
|
||||
|
||||
# Testing
|
||||
|
||||
## Test Structure
|
||||
|
||||
Tests in yq use the `expressionScenario` pattern. Each test scenario includes:
|
||||
- `expression`: The yq expression to test
|
||||
- `document`: Input YAML/JSON (optional)
|
||||
- `expected`: Expected output
|
||||
- `skipDoc`: Whether to skip documentation generation
|
||||
|
||||
## Writing Tests
|
||||
|
||||
1. **Find the appropriate test file** (e.g., `operator_add_test.go` for addition operations)
|
||||
2. **Add your test scenario** to the `*OperatorScenarios` slice
|
||||
3. **Run the specific test**: `go test -run TestAddOperatorScenarios` (replace with appropriate test name)
|
||||
4. **Verify documentation generation** (see Documentation section)
|
||||
|
||||
## Test Examples
|
||||
|
||||
```go
|
||||
var addOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
expression: `"foo" + "bar"`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::foobar\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
document: "apples: 3",
|
||||
expression: `.apples + 3`,
|
||||
expected: []string{
|
||||
"D0, P[apples], (!!int)::6\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
- **All tests**: `make [local] test`
|
||||
- **Specific test**: `go test -run TestName`
|
||||
- **With coverage**: `make [local] cover`
|
||||
|
||||
# Documentation
|
||||
|
||||
The documentation is a bit of a mixed bag (sorry in advance, I do plan on simplifying it...) - with some parts automatically generated and stiched together and some statically defined.
|
||||
## Documentation Generation
|
||||
|
||||
Documentation is written in markdown, and is published in the 'gitbook' branch.
|
||||
The project uses a documentation system that combines static headers with dynamically generated content from tests.
|
||||
|
||||
The various operator documentation (e.g. 'strings') are generated from the 'master' branch, and have a statically defined header (e.g. `pkg/yqlib/doc/operators/headers/add.md`) and the bulk of the docs are generated from the unit tests e.g. `pkg/yqlib/operator_add_test.go`.
|
||||
### How It Works
|
||||
|
||||
The pipeline will run the tests and automatically concatenate the files together, and put them under
|
||||
`pkg/qylib/doc/add.md`. These files are checked in the master branch (and are copied to the gitbook branch as part of the release process).
|
||||
1. **Static headers** are defined in `pkg/yqlib/doc/operators/headers/*.md`
|
||||
2. **Dynamic content** is generated from test scenarios in `*_test.go` files
|
||||
3. **Generated docs** are created in `pkg/yqlib/doc/*.md` by concatenating headers with test-generated content
|
||||
4. **Documentation is synced** to the gitbook branch for the website
|
||||
|
||||
## How to contribute
|
||||
### Updating Operator Documentation
|
||||
|
||||
The first step is to find if what you want is automatically generated or not - start by looking in the master branch.
|
||||
#### For Test-Generated Documentation
|
||||
|
||||
Note that PRs with small changes (e.g. minor typos) may not be merged (see https://joel.net/how-one-guy-ruined-hacktoberfest2020-drama).
|
||||
Most operator documentation is generated from tests. To update:
|
||||
|
||||
### Updating dynamic documentation from master
|
||||
- Search for the documentation you want to update. If you find matches in a `*_test.go` file - update that, as that will automatically update the matching `*.md` file
|
||||
- Assuming you are updating a `*_test.go` file, once updated, run the test to regenerated the docs. E.g. for the 'Add' test generated docs, from the pkg/yqlib folder run:
|
||||
`go test -run TestAddOperatorScenarios` which will run that test defined in the `operator_add_test.go` file.
|
||||
- Ensure the tests still pass, and check the generated documentation have your update.
|
||||
- Note: If the documentation is only in a `headers/*.md` file, then just update that directly
|
||||
- Raise a PR to merge the changes into master!
|
||||
1. **Find the test file** (e.g., `operator_add_test.go`)
|
||||
2. **Update test scenarios** - each `expressionScenario` with `skipDoc: false` becomes documentation
|
||||
3. **Run the test** to regenerate docs:
|
||||
```bash
|
||||
cd pkg/yqlib
|
||||
go test -run TestAddOperatorScenarios
|
||||
```
|
||||
4. **Verify the generated documentation** in `pkg/yqlib/doc/add.md`
|
||||
5. **Create a PR** with your changes
|
||||
|
||||
### Updating static documentation from the gitbook branch
|
||||
If you haven't found what you want to update in the master branch, then check the gitbook branch directly as there are a few pages in there that are not in master.
|
||||
#### For Header-Only Documentation
|
||||
|
||||
- Update the `*.md` files
|
||||
- Raise a PR to merge the changes into gitbook.
|
||||
If documentation exists only in `headers/*.md` files:
|
||||
1. **Update the header file directly** (e.g., `pkg/yqlib/doc/operators/headers/add.md`)
|
||||
2. **Create a PR** with your changes
|
||||
|
||||
### Updating Static Documentation
|
||||
|
||||
For documentation not in the master branch:
|
||||
|
||||
1. **Check the gitbook branch** for additional pages
|
||||
2. **Update the `*.md` files** directly
|
||||
3. **Create a PR** to the gitbook branch
|
||||
|
||||
### Documentation Best Practices
|
||||
|
||||
- **Write clear, concise examples** in test scenarios
|
||||
- **Use meaningful variable names** in examples
|
||||
- **Include edge cases** and error conditions
|
||||
- **Test your documentation changes** by running the specific test
|
||||
- **Verify generated output** matches expectations
|
||||
|
||||
Note: PRs with small changes (e.g. minor typos) may not be merged (see https://joel.net/how-one-guy-ruined-hacktoberfest2020-drama).
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Common Setup Issues
|
||||
|
||||
### Docker/Podman Issues
|
||||
- **Problem**: `make` commands fail with Docker errors
|
||||
- **Solution**: Ensure Docker or Podman is running and accessible
|
||||
- **Alternative**: Use `make local <command>` to run in containers
|
||||
|
||||
### Go Version Issues
|
||||
- **Problem**: Build fails with Go version errors
|
||||
- **Solution**: Ensure you have Go 1.24.0 or later installed
|
||||
- **Check**: Run `go version` to verify
|
||||
|
||||
### Vendor Dependencies
|
||||
- **Problem**: `make vendor` fails or dependencies are outdated
|
||||
- **Solution**:
|
||||
```bash
|
||||
go mod tidy
|
||||
make [local] vendor
|
||||
```
|
||||
|
||||
### Linting Failures
|
||||
- **Problem**: `make check` fails with linting errors
|
||||
- **Solution**:
|
||||
```bash
|
||||
make [local] format # Auto-fix formatting
|
||||
# Manually fix remaining linting issues
|
||||
make [local] check # Verify fixes
|
||||
```
|
||||
|
||||
### Test Failures
|
||||
- **Problem**: Tests fail locally but pass in CI
|
||||
- **Solution**:
|
||||
```bash
|
||||
make [local] test # Run in Docker container
|
||||
```
|
||||
|
||||
### Documentation Generation Issues
|
||||
- **Problem**: Generated docs don't update after test changes
|
||||
- **Solution**:
|
||||
```bash
|
||||
cd pkg/yqlib
|
||||
go test -run TestSpecificOperatorScenarios
|
||||
# Check if generated file updated in pkg/yqlib/doc/
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Check existing issues**: Search GitHub issues for similar problems
|
||||
- **Create an issue**: If you can't find a solution, create a detailed issue
|
||||
- **Ask questions**: Use GitHub Discussions for general questions
|
||||
- **Join the community**: Check the project's community channels
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.5 AS builder
|
||||
FROM golang:1.25.4 AS builder
|
||||
|
||||
WORKDIR /go/src/mikefarah/yq
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.5
|
||||
FROM golang:1.25.4
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y npm && \
|
||||
|
||||
@@ -3,44 +3,46 @@
|
||||
    
|
||||
|
||||
|
||||
a lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml files as well as json, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
|
||||
A lightweight and portable command-line YAML, JSON, INI and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) (a popular JSON processor) like syntax but works with yaml files as well as json, xml, ini, properties, csv and tsv. It doesn't yet support everything `jq` does - but it does support the most common operations and functions, and more is being added continuously.
|
||||
|
||||
yq is written in go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as Docker and Podman, all listed below.
|
||||
yq is written in Go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as Docker and Podman, all listed below.
|
||||
|
||||
## Quick Usage Guide
|
||||
|
||||
Read a value:
|
||||
### Basic Operations
|
||||
|
||||
**Read a value:**
|
||||
```bash
|
||||
yq '.a.b[0].c' file.yaml
|
||||
```
|
||||
|
||||
Pipe from STDIN:
|
||||
**Pipe from STDIN:**
|
||||
```bash
|
||||
yq '.a.b[0].c' < file.yaml
|
||||
```
|
||||
|
||||
Update a yaml file, in place
|
||||
**Update a yaml file in place:**
|
||||
```bash
|
||||
yq -i '.a.b[0].c = "cool"' file.yaml
|
||||
```
|
||||
|
||||
Update using environment variables
|
||||
**Update using environment variables:**
|
||||
```bash
|
||||
NAME=mike yq -i '.a.b[0].c = strenv(NAME)' file.yaml
|
||||
```
|
||||
|
||||
Merge multiple files
|
||||
### Advanced Operations
|
||||
|
||||
**Merge multiple files:**
|
||||
```bash
|
||||
# merge two files
|
||||
yq -n 'load("file1.yaml") * load("file2.yaml")'
|
||||
|
||||
# merge using globs:
|
||||
# note the use of `ea` to evaluate all the files at once
|
||||
# instead of in sequence
|
||||
# merge using globs (note: `ea` evaluates all files at once instead of in sequence)
|
||||
yq ea '. as $item ireduce ({}; . * $item )' path/to/*.yml
|
||||
```
|
||||
|
||||
Multiple updates to a yaml file
|
||||
**Multiple updates to a yaml file:**
|
||||
```bash
|
||||
yq -i '
|
||||
.a.b[0].c = "cool" |
|
||||
@@ -49,14 +51,22 @@ yq -i '
|
||||
' file.yaml
|
||||
```
|
||||
|
||||
Find and update an item in an array:
|
||||
**Find and update an item in an array:**
|
||||
```bash
|
||||
yq '(.[] | select(.name == "foo") | .address) = "12 cat st"'
|
||||
# Note: requires input file - add your file at the end
|
||||
yq -i '(.[] | select(.name == "foo") | .address) = "12 cat st"' data.yaml
|
||||
```
|
||||
|
||||
Convert JSON to YAML
|
||||
**Convert between formats:**
|
||||
```bash
|
||||
# Convert JSON to YAML (pretty print)
|
||||
yq -Poy sample.json
|
||||
|
||||
# Convert YAML to JSON
|
||||
yq -o json file.yaml
|
||||
|
||||
# Convert XML to YAML
|
||||
yq -o yaml file.xml
|
||||
```
|
||||
|
||||
See [recipes](https://mikefarah.gitbook.io/yq/recipes) for more examples and the [documentation](https://mikefarah.gitbook.io/yq/) for more information.
|
||||
@@ -68,31 +78,31 @@ Take a look at the discussions for [common questions](https://github.com/mikefar
|
||||
### [Download the latest binary](https://github.com/mikefarah/yq/releases/latest)
|
||||
|
||||
### wget
|
||||
Use wget to download, gzipped pre-compiled binaries:
|
||||
Use wget to download pre-compiled binaries. Choose your platform and architecture:
|
||||
|
||||
|
||||
For instance, VERSION=v4.2.0 and BINARY=yq_linux_amd64
|
||||
|
||||
#### Compressed via tar.gz
|
||||
**For Linux (example):**
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\
|
||||
tar xz && mv ${BINARY} /usr/local/bin/yq
|
||||
```
|
||||
# Set your platform variables (adjust as needed)
|
||||
VERSION=v4.2.0
|
||||
PLATFORM=linux_amd64
|
||||
|
||||
#### Plain binary
|
||||
# Download compressed binary
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_${PLATFORM}.tar.gz -O - |\
|
||||
tar xz && sudo mv yq_${PLATFORM} /usr/local/bin/yq
|
||||
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/local/bin/yq &&\
|
||||
# Or download plain binary
|
||||
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/yq_${PLATFORM} -O /usr/local/bin/yq &&\
|
||||
chmod +x /usr/local/bin/yq
|
||||
```
|
||||
|
||||
#### Latest version
|
||||
|
||||
**Latest version (Linux AMD64):**
|
||||
```bash
|
||||
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq &&\
|
||||
chmod +x /usr/local/bin/yq
|
||||
```
|
||||
|
||||
**Available platforms:** `linux_amd64`, `linux_arm64`, `linux_arm`, `linux_386`, `darwin_amd64`, `darwin_arm64`, `windows_amd64`, `windows_386`, etc.
|
||||
|
||||
### MacOS / Linux via Homebrew:
|
||||
Using [Homebrew](https://brew.sh/)
|
||||
```
|
||||
@@ -123,28 +133,31 @@ rm /etc/myfile.tmp
|
||||
```
|
||||
|
||||
### Run with Docker or Podman
|
||||
#### Oneshot use:
|
||||
|
||||
#### One-time use:
|
||||
```bash
|
||||
docker run --rm -v "${PWD}":/workdir mikefarah/yq [command] [flags] [expression ]FILE...
|
||||
# Docker - process files in current directory
|
||||
docker run --rm -v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
|
||||
|
||||
# Podman - same usage as Docker
|
||||
podman run --rm -v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
|
||||
```
|
||||
|
||||
Note that you can run `yq` in docker without network access and other privileges if you desire,
|
||||
namely `--security-opt=no-new-privileges --cap-drop all --network none`.
|
||||
|
||||
**Security note:** You can run `yq` in Docker with restricted privileges:
|
||||
```bash
|
||||
podman run --rm -v "${PWD}":/workdir mikefarah/yq [command] [flags] [expression ]FILE...
|
||||
docker run --rm --security-opt=no-new-privileges --cap-drop all --network none \
|
||||
-v "${PWD}":/workdir mikefarah/yq '.a.b[0].c' file.yaml
|
||||
```
|
||||
|
||||
#### Pipe in via STDIN:
|
||||
#### Pipe data via STDIN:
|
||||
|
||||
You'll need to pass the `-i\--interactive` flag to docker:
|
||||
You'll need to pass the `-i --interactive` flag to Docker/Podman:
|
||||
|
||||
```bash
|
||||
# Process piped data
|
||||
docker run -i --rm mikefarah/yq '.this.thing' < myfile.yml
|
||||
```
|
||||
|
||||
```bash
|
||||
# Same with Podman
|
||||
podman run -i --rm mikefarah/yq '.this.thing' < myfile.yml
|
||||
```
|
||||
|
||||
@@ -294,7 +307,7 @@ Using [winget](https://learn.microsoft.com/en-us/windows/package-manager/)
|
||||
winget install --id MikeFarah.yq
|
||||
```
|
||||
|
||||
### Mac:
|
||||
### MacPorts:
|
||||
Using [MacPorts](https://www.macports.org/)
|
||||
```
|
||||
sudo port selfupdate
|
||||
@@ -324,6 +337,14 @@ Flox can be used to install yq on Linux, MacOS, and Windows through WSL.
|
||||
flox install yq
|
||||
```
|
||||
|
||||
|
||||
### MacOS / Linux via gah:
|
||||
Using [gah](https://github.com/marverix/gah)
|
||||
|
||||
```
|
||||
gah install yq
|
||||
```
|
||||
|
||||
## Features
|
||||
- [Detailed documentation with many examples](https://mikefarah.gitbook.io/yq/)
|
||||
- Written in portable go, so you can download a lovely dependency free binary
|
||||
@@ -332,7 +353,7 @@ flox install yq
|
||||
- Supports yaml [front matter](https://mikefarah.gitbook.io/yq/usage/front-matter) blocks (e.g. jekyll/assemble)
|
||||
- Colorized yaml output
|
||||
- [Date/Time manipulation and formatting with TZ](https://mikefarah.gitbook.io/yq/operators/datetime)
|
||||
- [Deeply data structures](https://mikefarah.gitbook.io/yq/operators/traverse-read)
|
||||
- [Deep data structures](https://mikefarah.gitbook.io/yq/operators/traverse-read)
|
||||
- [Sort keys](https://mikefarah.gitbook.io/yq/operators/sort-keys)
|
||||
- Manipulate yaml [comments](https://mikefarah.gitbook.io/yq/operators/comment-operators), [styling](https://mikefarah.gitbook.io/yq/operators/style), [tags](https://mikefarah.gitbook.io/yq/operators/tag) and [anchors and aliases](https://mikefarah.gitbook.io/yq/operators/anchor-and-alias-operators).
|
||||
- [Update in place](https://mikefarah.gitbook.io/yq/v/v4.x/commands/evaluate#flags)
|
||||
@@ -415,6 +436,27 @@ Flags:
|
||||
|
||||
Use "yq [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**PowerShell quoting issues:**
|
||||
```powershell
|
||||
# Use single quotes for expressions
|
||||
yq '.a.b[0].c' file.yaml
|
||||
|
||||
# Or escape double quotes
|
||||
yq ".a.b[0].c = \"value\"" file.yaml
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
- **Check existing issues**: [GitHub Issues](https://github.com/mikefarah/yq/issues)
|
||||
- **Ask questions**: [GitHub Discussions](https://github.com/mikefarah/yq/discussions)
|
||||
- **Documentation**: [Complete documentation](https://mikefarah.gitbook.io/yq/)
|
||||
- **Examples**: [Recipes and examples](https://mikefarah.gitbook.io/yq/recipes)
|
||||
|
||||
## Known Issues / Missing Features
|
||||
- `yq` attempts to preserve comment positions and whitespace as much as possible, but it does not handle all scenarios (see https://github.com/go-yaml/yaml/tree/v3 for details)
|
||||
- Powershell has its own...[opinions on quoting yq](https://mikefarah.gitbook.io/yq/usage/tips-and-tricks#quotes-in-windows-powershell)
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateEvaluateAllCommand(t *testing.T) {
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
if cmd == nil {
|
||||
t.Fatal("createEvaluateAllCommand returned nil")
|
||||
}
|
||||
|
||||
// Test basic command properties
|
||||
if cmd.Use != "eval-all [expression] [yaml_file1]..." {
|
||||
t.Errorf("Expected Use to be 'eval-all [expression] [yaml_file1]...', got %q", cmd.Use)
|
||||
}
|
||||
|
||||
if cmd.Short == "" {
|
||||
t.Error("Expected Short description to be non-empty")
|
||||
}
|
||||
|
||||
if cmd.Long == "" {
|
||||
t.Error("Expected Long description to be non-empty")
|
||||
}
|
||||
|
||||
// Test aliases
|
||||
expectedAliases := []string{"ea"}
|
||||
if len(cmd.Aliases) != len(expectedAliases) {
|
||||
t.Errorf("Expected %d aliases, got %d", len(expectedAliases), len(cmd.Aliases))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAliases {
|
||||
if i >= len(cmd.Aliases) || cmd.Aliases[i] != expected {
|
||||
t.Errorf("Expected alias %d to be %q, got %q", i, expected, cmd.Aliases[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_NoArgs(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with no arguments and no null input
|
||||
nullInput = false
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateAll(cmd, []string{})
|
||||
|
||||
// Should not error, but should print usage
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with no args should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have printed usage information
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected usage information to be printed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_NullInput(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with null input
|
||||
nullInput = true
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateAll(cmd, []string{})
|
||||
|
||||
// Should not error when using null input
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with null input should not error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithSingleFile(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with a single file
|
||||
err = evaluateAll(cmd, []string{yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with single file should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have some output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with single file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithMultipleFiles(t *testing.T) {
|
||||
// Create temporary YAML files
|
||||
tempDir := t.TempDir()
|
||||
|
||||
yamlFile1 := filepath.Join(tempDir, "test1.yaml")
|
||||
yamlContent1 := []byte("name: test1\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile1, yamlContent1, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file 1: %v", err)
|
||||
}
|
||||
|
||||
yamlFile2 := filepath.Join(tempDir, "test2.yaml")
|
||||
yamlContent2 := []byte("name: test2\nage: 30\n")
|
||||
err = os.WriteFile(yamlFile2, yamlContent2, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file 2: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with multiple files
|
||||
err = evaluateAll(cmd, []string{yamlFile1, yamlFile2})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with multiple files should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with multiple files")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithExpression(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with expression
|
||||
err = evaluateAll(cmd, []string{".name", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with expression should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with expression")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WriteInPlace(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable write in place
|
||||
originalWriteInplace := writeInplace
|
||||
writeInplace = true
|
||||
defer func() { writeInplace = originalWriteInplace }()
|
||||
|
||||
// Test with write in place
|
||||
err = evaluateAll(cmd, []string{".name = \"updated\"", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with write in place should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was updated
|
||||
updatedContent, err := os.ReadFile(yamlFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated file: %v", err)
|
||||
}
|
||||
|
||||
// Should contain the updated content
|
||||
if !strings.Contains(string(updatedContent), "updated") {
|
||||
t.Errorf("Expected file to contain 'updated', got: %s", string(updatedContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_ExitStatus(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable exit status
|
||||
originalExitStatus := exitStatus
|
||||
exitStatus = true
|
||||
defer func() { exitStatus = originalExitStatus }()
|
||||
|
||||
// Test with expression that should find no matches
|
||||
err = evaluateAll(cmd, []string{".nonexistent", yamlFile})
|
||||
|
||||
// Should error when no matches found and exit status is enabled
|
||||
if err == nil {
|
||||
t.Error("Expected error when no matches found and exit status is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_WithMultipleDocuments(t *testing.T) {
|
||||
// Create a temporary YAML file with multiple documents
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("---\nname: doc1\nage: 25\n---\nname: doc2\nage: 30\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with multiple documents
|
||||
err = evaluateAll(cmd, []string{".", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with multiple documents should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with multiple documents")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateAll_NulSepOutput(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateAllCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable nul separator output
|
||||
originalNulSepOutput := nulSepOutput
|
||||
nulSepOutput = true
|
||||
defer func() { nulSepOutput = originalNulSepOutput }()
|
||||
|
||||
// Test with nul separator output
|
||||
err = evaluateAll(cmd, []string{".name", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateAll with nul separator output should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateAll with nul separator output")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateEvaluateSequenceCommand(t *testing.T) {
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
if cmd == nil {
|
||||
t.Fatal("createEvaluateSequenceCommand returned nil")
|
||||
}
|
||||
|
||||
// Test basic command properties
|
||||
if cmd.Use != "eval [expression] [yaml_file1]..." {
|
||||
t.Errorf("Expected Use to be 'eval [expression] [yaml_file1]...', got %q", cmd.Use)
|
||||
}
|
||||
|
||||
if cmd.Short == "" {
|
||||
t.Error("Expected Short description to be non-empty")
|
||||
}
|
||||
|
||||
if cmd.Long == "" {
|
||||
t.Error("Expected Long description to be non-empty")
|
||||
}
|
||||
|
||||
// Test aliases
|
||||
expectedAliases := []string{"e"}
|
||||
if len(cmd.Aliases) != len(expectedAliases) {
|
||||
t.Errorf("Expected %d aliases, got %d", len(expectedAliases), len(cmd.Aliases))
|
||||
}
|
||||
|
||||
for i, expected := range expectedAliases {
|
||||
if i >= len(cmd.Aliases) || cmd.Aliases[i] != expected {
|
||||
t.Errorf("Expected alias %d to be %q, got %q", i, expected, cmd.Aliases[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessExpression(t *testing.T) {
|
||||
// Reset global variables
|
||||
originalPrettyPrint := prettyPrint
|
||||
defer func() { prettyPrint = originalPrettyPrint }()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prettyPrint bool
|
||||
expression string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty expression without pretty print",
|
||||
prettyPrint: false,
|
||||
expression: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty expression with pretty print",
|
||||
prettyPrint: true,
|
||||
expression: "",
|
||||
expected: `(... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
|
||||
},
|
||||
{
|
||||
name: "simple expression without pretty print",
|
||||
prettyPrint: false,
|
||||
expression: ".a.b",
|
||||
expected: ".a.b",
|
||||
},
|
||||
{
|
||||
name: "simple expression with pretty print",
|
||||
prettyPrint: true,
|
||||
expression: ".a.b",
|
||||
expected: `.a.b | (... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
|
||||
},
|
||||
{
|
||||
name: "complex expression with pretty print",
|
||||
prettyPrint: true,
|
||||
expression: ".items[] | select(.active == true)",
|
||||
expected: `.items[] | select(.active == true) | (... | (select(tag != "!!str"), select(tag == "!!str") | select(test("(?i)^(y|yes|n|no|on|off)$") | not)) ) style=""`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
prettyPrint = tt.prettyPrint
|
||||
result := processExpression(tt.expression)
|
||||
if result != tt.expected {
|
||||
t.Errorf("processExpression(%q) = %q, want %q", tt.expression, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_NoArgs(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with no arguments and no null input
|
||||
nullInput = false
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateSequence(cmd, []string{})
|
||||
|
||||
// Should not error, but should print usage
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with no args should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have printed usage information
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected usage information to be printed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_NullInput(t *testing.T) {
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with null input
|
||||
nullInput = true
|
||||
defer func() { nullInput = false }()
|
||||
|
||||
err := evaluateSequence(cmd, []string{})
|
||||
|
||||
// Should not error when using null input
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with null input should not error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_WithFile(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with a file
|
||||
err = evaluateSequence(cmd, []string{yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with file should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have some output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateSequence with file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_WithExpressionAndFile(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Test with expression and file
|
||||
err = evaluateSequence(cmd, []string{".name", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with expression and file should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Should have output
|
||||
if output.Len() == 0 {
|
||||
t.Error("Expected output from evaluateSequence with expression and file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_WriteInPlace(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable write in place
|
||||
originalWriteInplace := writeInplace
|
||||
writeInplace = true
|
||||
defer func() { writeInplace = originalWriteInplace }()
|
||||
|
||||
// Test with write in place
|
||||
err = evaluateSequence(cmd, []string{".name = \"updated\"", yamlFile})
|
||||
|
||||
// Should not error
|
||||
if err != nil {
|
||||
t.Errorf("evaluateSequence with write in place should not error, got: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was updated
|
||||
updatedContent, err := os.ReadFile(yamlFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated file: %v", err)
|
||||
}
|
||||
|
||||
// Should contain the updated content
|
||||
if !strings.Contains(string(updatedContent), "updated") {
|
||||
t.Errorf("Expected file to contain 'updated', got: %s", string(updatedContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateSequence_ExitStatus(t *testing.T) {
|
||||
// Create a temporary YAML file
|
||||
tempDir := t.TempDir()
|
||||
yamlFile := filepath.Join(tempDir, "test.yaml")
|
||||
yamlContent := []byte("name: test\nage: 25\n")
|
||||
err := os.WriteFile(yamlFile, yamlContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test YAML file: %v", err)
|
||||
}
|
||||
|
||||
// Create a temporary command
|
||||
cmd := createEvaluateSequenceCommand()
|
||||
|
||||
// Set up command to capture output
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
|
||||
// Enable exit status
|
||||
originalExitStatus := exitStatus
|
||||
exitStatus = true
|
||||
defer func() { exitStatus = originalExitStatus }()
|
||||
|
||||
// Test with expression that should find no matches
|
||||
err = evaluateSequence(cmd, []string{".nonexistent", yamlFile})
|
||||
|
||||
// Should error when no matches found and exit status is enabled
|
||||
if err == nil {
|
||||
t.Error("Expected error when no matches found and exit status is enabled")
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,11 @@ yq -P -oy sample.json
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values")
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "properties-array-brackets", yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "use [x] in array paths (e.g. for SpringBoot)")
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "shell-key-separator", yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "separator for shell variable key paths")
|
||||
if err = rootCmd.RegisterFlagCompletionFunc("shell-key-separator", cobra.NoFileCompletions); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolVar(&yqlib.StringInterpolationEnabled, "string-interpolation", yqlib.StringInterpolationEnabled, "Toggles strings interpolation of \\(exp)")
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.")
|
||||
@@ -213,6 +218,9 @@ yq -P -oy sample.json
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableEnvOps, "security-disable-env-ops", "", false, "Disable env related operations.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredSecurityPreferences.DisableFileOps, "security-disable-file-ops", "", false, "Disable file related operations (e.g. load)")
|
||||
|
||||
rootCmd.AddCommand(
|
||||
createEvaluateSequenceCommand(),
|
||||
createEvaluateAllCommand(),
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewRuneVar(t *testing.T) {
|
||||
var r rune
|
||||
runeVar := newRuneVar(&r)
|
||||
|
||||
if runeVar == nil {
|
||||
t.Fatal("newRuneVar returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
runeVal rune
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple character",
|
||||
runeVal: 'a',
|
||||
expected: "a",
|
||||
},
|
||||
{
|
||||
name: "special character",
|
||||
runeVal: '\n',
|
||||
expected: "\n",
|
||||
},
|
||||
{
|
||||
name: "unicode character",
|
||||
runeVal: 'ñ',
|
||||
expected: "ñ",
|
||||
},
|
||||
{
|
||||
name: "zero rune",
|
||||
runeVal: 0,
|
||||
expected: string(rune(0)),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
runeVal := runeValue(tt.runeVal)
|
||||
result := runeVal.String()
|
||||
if result != tt.expected {
|
||||
t.Errorf("runeValue.String() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_Set(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected rune
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "simple character",
|
||||
input: "a",
|
||||
expected: 'a',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "newline escape",
|
||||
input: "\\n",
|
||||
expected: '\n',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "tab escape",
|
||||
input: "\\t",
|
||||
expected: '\t',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "carriage return escape",
|
||||
input: "\\r",
|
||||
expected: '\r',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "form feed escape",
|
||||
input: "\\f",
|
||||
expected: '\f',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "vertical tab escape",
|
||||
input: "\\v",
|
||||
expected: '\v',
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: 0,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "multiple characters",
|
||||
input: "ab",
|
||||
expected: 0,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "special character",
|
||||
input: "ñ",
|
||||
expected: 'ñ',
|
||||
expectError: true, // This will fail because the Set function checks len(val) != 1
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var r rune
|
||||
runeVal := newRuneVar(&r)
|
||||
|
||||
err := runeVal.Set(tt.input)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for input %q, but got none", tt.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for input %q: %v", tt.input, err)
|
||||
}
|
||||
if r != tt.expected {
|
||||
t.Errorf("Expected rune %q (%d), got %q (%d)",
|
||||
string(tt.expected), tt.expected, string(r), r)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_Set_ErrorMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "empty string error",
|
||||
input: "",
|
||||
expectedError: "[] is not a valid character. Must be length 1 was 0",
|
||||
},
|
||||
{
|
||||
name: "multiple characters error",
|
||||
input: "abc",
|
||||
expectedError: "[abc] is not a valid character. Must be length 1 was 3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var r rune
|
||||
runeVal := newRuneVar(&r)
|
||||
|
||||
err := runeVal.Set(tt.input)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for input %q, but got none", tt.input)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), tt.expectedError) {
|
||||
t.Errorf("Expected error message to contain %q, got %q",
|
||||
tt.expectedError, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuneValue_Type(t *testing.T) {
|
||||
var r rune
|
||||
runeVal := newRuneVar(&r)
|
||||
|
||||
result := runeVal.Type()
|
||||
expected := "char"
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("runeValue.Type() = %q, want %q", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
rootCmd := New()
|
||||
|
||||
if rootCmd == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
}
|
||||
|
||||
// Test basic command properties
|
||||
if rootCmd.Use != "yq" {
|
||||
t.Errorf("Expected Use to be 'yq', got %q", rootCmd.Use)
|
||||
}
|
||||
|
||||
if rootCmd.Short == "" {
|
||||
t.Error("Expected Short description to be non-empty")
|
||||
}
|
||||
|
||||
if rootCmd.Long == "" {
|
||||
t.Error("Expected Long description to be non-empty")
|
||||
}
|
||||
|
||||
// Test that the command has the expected subcommands
|
||||
expectedCommands := []string{"eval", "eval-all", "completion"}
|
||||
actualCommands := make([]string, 0, len(rootCmd.Commands()))
|
||||
|
||||
for _, cmd := range rootCmd.Commands() {
|
||||
actualCommands = append(actualCommands, cmd.Name())
|
||||
}
|
||||
|
||||
for _, expected := range expectedCommands {
|
||||
found := false
|
||||
for _, actual := range actualCommands {
|
||||
if actual == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected command %q not found in actual commands: %v",
|
||||
expected, actualCommands)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_FlagCompletions(t *testing.T) {
|
||||
rootCmd := New()
|
||||
|
||||
// Test that flag completion functions are registered
|
||||
// This is a basic smoke test - we can't easily test the actual completion logic
|
||||
// without more complex setup
|
||||
flags := []string{
|
||||
"output-format",
|
||||
"input-format",
|
||||
"xml-attribute-prefix",
|
||||
"xml-content-name",
|
||||
"xml-proc-inst-prefix",
|
||||
"xml-directive-name",
|
||||
"lua-prefix",
|
||||
"lua-suffix",
|
||||
"properties-separator",
|
||||
"indent",
|
||||
"front-matter",
|
||||
"expression",
|
||||
"split-exp",
|
||||
}
|
||||
|
||||
for _, flagName := range flags {
|
||||
flag := rootCmd.PersistentFlags().Lookup(flagName)
|
||||
if flag == nil {
|
||||
t.Errorf("Expected flag %q to exist", flagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ var (
|
||||
GitDescribe string
|
||||
|
||||
// Version is main version number that is being run at the moment.
|
||||
Version = "v4.46.1"
|
||||
Version = "v4.49.1"
|
||||
|
||||
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
|
||||
// then it means that it is a final release. Otherwise, this is a pre-release
|
||||
|
||||
+3
-8
@@ -1,8 +1,3 @@
|
||||
# 001
|
||||
---
|
||||
abc: # 001
|
||||
- 1 # one
|
||||
- 2 # two
|
||||
|
||||
---
|
||||
def # 002
|
||||
[[fruits]]
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious
|
||||
+4
-24
@@ -1,26 +1,6 @@
|
||||
[[fruits]]
|
||||
|
||||
[animals]
|
||||
|
||||
# This is a TOML document
|
||||
|
||||
title = "TOML Example"
|
||||
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
dob = 1979-05-27T07:32:00-08:00
|
||||
|
||||
[database]
|
||||
enabled = true
|
||||
ports = [ 8000, 8001, 8002 ]
|
||||
data = [ ["delta", "phi"], [3.14] ]
|
||||
temp_targets = { cpu = 79.5, case = 72.0 }
|
||||
|
||||
[servers]
|
||||
|
||||
[servers.alpha]
|
||||
ip = "10.0.0.1"
|
||||
role = "frontend"
|
||||
|
||||
[servers.beta]
|
||||
ip = "10.0.0.2"
|
||||
role = "backend"
|
||||
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious"
|
||||
@@ -3,7 +3,7 @@ module github.com/mikefarah/yq/v4
|
||||
require (
|
||||
github.com/a8m/envsubst v1.4.3
|
||||
github.com/alecthomas/participle/v2 v2.1.4
|
||||
github.com/alecthomas/repr v0.4.0
|
||||
github.com/alecthomas/repr v0.5.2
|
||||
github.com/dimchansky/utfbom v1.1.1
|
||||
github.com/elliotchance/orderedmap v1.8.0
|
||||
github.com/fatih/color v1.18.0
|
||||
@@ -14,12 +14,12 @@ require (
|
||||
github.com/magiconair/properties v1.8.10
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/text v0.27.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/text v0.31.0
|
||||
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
|
||||
)
|
||||
|
||||
@@ -27,9 +27,9 @@ require (
|
||||
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
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
)
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
@@ -4,8 +4,8 @@ github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8v
|
||||
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.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
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/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=
|
||||
@@ -40,25 +40,25 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
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=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
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/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
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,269 @@
|
||||
//go:build !yq_nobase64
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
)
|
||||
|
||||
const base64EncodedSimple = "YSBzcGVjaWFsIHN0cmluZw=="
|
||||
const base64DecodedSimpleExtraSpaces = "\n " + base64EncodedSimple + " \n"
|
||||
const base64DecodedSimple = "a special string"
|
||||
|
||||
const base64EncodedUTF8 = "V29ya3Mgd2l0aCBVVEYtMTYg8J+Yig=="
|
||||
const base64DecodedUTF8 = "Works with UTF-16 😊"
|
||||
|
||||
const base64EncodedYaml = "YTogYXBwbGUK"
|
||||
const base64DecodedYaml = "a: apple\n"
|
||||
|
||||
const base64EncodedEmpty = ""
|
||||
const base64DecodedEmpty = ""
|
||||
|
||||
const base64MissingPadding = "Y2F0cw"
|
||||
const base64DecodedMissingPadding = "cats"
|
||||
|
||||
const base64EncodedCats = "Y2F0cw=="
|
||||
const base64DecodedCats = "cats"
|
||||
|
||||
var base64Scenarios = []formatScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "empty decode",
|
||||
input: base64EncodedEmpty,
|
||||
expected: base64DecodedEmpty + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "simple decode",
|
||||
input: base64EncodedSimple,
|
||||
expected: base64DecodedSimple + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Decode base64: simple",
|
||||
subdescription: "Decoded data is assumed to be a string.",
|
||||
input: base64EncodedSimple,
|
||||
expected: base64DecodedSimple + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Decode base64: UTF-8",
|
||||
subdescription: "Base64 decoding supports UTF-8 encoded strings.",
|
||||
input: base64EncodedUTF8,
|
||||
expected: base64DecodedUTF8 + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "decode missing padding",
|
||||
input: base64MissingPadding,
|
||||
expected: base64DecodedMissingPadding + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
|
||||
description: "Decode with extra spaces",
|
||||
subdescription: "Extra leading/trailing whitespace is stripped",
|
||||
input: base64DecodedSimpleExtraSpaces,
|
||||
expected: base64DecodedSimple + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "decode with padding",
|
||||
input: base64EncodedCats,
|
||||
expected: base64DecodedCats + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "decode yaml document",
|
||||
input: base64EncodedYaml,
|
||||
expected: base64DecodedYaml + "\n",
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Encode base64: string",
|
||||
input: "\"" + base64DecodedSimple + "\"",
|
||||
expected: base64EncodedSimple,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
description: "Encode base64: string from document",
|
||||
subdescription: "Extract a string field and encode it to base64.",
|
||||
input: "coolData: \"" + base64DecodedSimple + "\"",
|
||||
expression: ".coolData",
|
||||
expected: base64EncodedSimple,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "encode empty string",
|
||||
input: "\"\"",
|
||||
expected: "",
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "encode UTF-8 string",
|
||||
input: "\"" + base64DecodedUTF8 + "\"",
|
||||
expected: base64EncodedUTF8,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "encode cats",
|
||||
input: "\"" + base64DecodedCats + "\"",
|
||||
expected: base64EncodedCats,
|
||||
scenarioType: "encode",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: simple",
|
||||
skipDoc: true,
|
||||
input: base64EncodedSimple,
|
||||
expected: base64EncodedSimple,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: UTF-8",
|
||||
skipDoc: true,
|
||||
input: base64EncodedUTF8,
|
||||
expected: base64EncodedUTF8,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: missing padding",
|
||||
skipDoc: true,
|
||||
input: base64MissingPadding,
|
||||
expected: base64EncodedCats,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Roundtrip: empty",
|
||||
skipDoc: true,
|
||||
input: base64EncodedEmpty,
|
||||
expected: base64EncodedEmpty,
|
||||
scenarioType: "roundtrip",
|
||||
},
|
||||
{
|
||||
description: "Encode error: non-string",
|
||||
skipDoc: true,
|
||||
input: "123",
|
||||
expectedError: "cannot encode !!int as base64, can only operate on strings",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
{
|
||||
description: "Encode error: array",
|
||||
skipDoc: true,
|
||||
input: "[1, 2, 3]",
|
||||
expectedError: "cannot encode !!seq as base64, can only operate on strings",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
{
|
||||
description: "Encode error: map",
|
||||
skipDoc: true,
|
||||
input: "{b: c}",
|
||||
expectedError: "cannot encode !!map as base64, can only operate on strings",
|
||||
scenarioType: "encode-error",
|
||||
},
|
||||
}
|
||||
|
||||
func testBase64Scenario(t *testing.T, s formatScenario) {
|
||||
switch s.scenarioType {
|
||||
case "", "decode":
|
||||
yamlPrefs := ConfiguredYamlPreferences.Copy()
|
||||
yamlPrefs.Indent = 4
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(yamlPrefs)), s.description)
|
||||
case "encode":
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder()), s.description)
|
||||
case "roundtrip":
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewBase64Decoder(), NewBase64Encoder()), s.description)
|
||||
case "encode-error":
|
||||
result, err := processFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())
|
||||
if err == nil {
|
||||
t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result)
|
||||
} else {
|
||||
test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description)
|
||||
}
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
}
|
||||
}
|
||||
|
||||
func documentBase64Scenario(_ *testing.T, w *bufio.Writer, i interface{}) {
|
||||
s := i.(formatScenario)
|
||||
|
||||
if s.skipDoc {
|
||||
return
|
||||
}
|
||||
switch s.scenarioType {
|
||||
case "", "decode":
|
||||
documentBase64DecodeScenario(w, s)
|
||||
case "encode":
|
||||
documentBase64EncodeScenario(w, s)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
|
||||
}
|
||||
}
|
||||
|
||||
func documentBase64DecodeScenario(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.txt file of:\n")
|
||||
writeOrPanic(w, fmt.Sprintf("```\n%v\n```\n", s.input))
|
||||
|
||||
writeOrPanic(w, "then\n")
|
||||
expression := s.expression
|
||||
if expression == "" {
|
||||
expression = "."
|
||||
}
|
||||
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=base64 -oy '%v' sample.txt\n```\n", expression))
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewBase64Decoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
|
||||
}
|
||||
|
||||
func documentBase64EncodeScenario(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.yml file of:\n")
|
||||
writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input))
|
||||
|
||||
writeOrPanic(w, "then\n")
|
||||
expression := s.expression
|
||||
if expression == "" {
|
||||
expression = "."
|
||||
}
|
||||
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=base64 '%v' sample.yml\n```\n", expression))
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewBase64Encoder())))
|
||||
}
|
||||
|
||||
func TestBase64Scenarios(t *testing.T) {
|
||||
for _, tt := range base64Scenarios {
|
||||
testBase64Scenario(t, tt)
|
||||
}
|
||||
genericScenarios := make([]interface{}, len(base64Scenarios))
|
||||
for i, s := range base64Scenarios {
|
||||
genericScenarios[i] = s
|
||||
}
|
||||
documentScenarios(t, "usage", "base64", genericScenarios, documentBase64Scenario)
|
||||
}
|
||||
@@ -152,8 +152,6 @@ func TestCandidateNodeAddKeyValueChild(t *testing.T) {
|
||||
key := CandidateNode{Value: "cool", IsMapKey: true}
|
||||
node := CandidateNode{}
|
||||
|
||||
// if we use a key in a new node as a value, it should no longer be marked as a key
|
||||
|
||||
_, keyIsValueNow := node.AddKeyValueChild(&CandidateNode{Value: "newKey"}, &key)
|
||||
|
||||
test.AssertResult(t, keyIsValueNow.IsMapKey, false)
|
||||
@@ -204,3 +202,193 @@ func TestConvertToNodeInfo(t *testing.T) {
|
||||
test.AssertResult(t, 2, childInfo.Line)
|
||||
test.AssertResult(t, 3, childInfo.Column)
|
||||
}
|
||||
|
||||
func TestCandidateNodeGetPath(t *testing.T) {
|
||||
// Test root node with no parent
|
||||
root := CandidateNode{Value: "root"}
|
||||
path := root.GetPath()
|
||||
test.AssertResult(t, 0, len(path))
|
||||
|
||||
// Test node with key
|
||||
key := createStringScalarNode("myKey")
|
||||
node := CandidateNode{Key: key, Value: "myValue"}
|
||||
path = node.GetPath()
|
||||
test.AssertResult(t, 1, len(path))
|
||||
test.AssertResult(t, "myKey", path[0])
|
||||
|
||||
// Test nested path
|
||||
parent := CandidateNode{}
|
||||
parentKey := createStringScalarNode("parent")
|
||||
parent.Key = parentKey
|
||||
node.Parent = &parent
|
||||
path = node.GetPath()
|
||||
test.AssertResult(t, 2, len(path))
|
||||
test.AssertResult(t, "parent", path[0])
|
||||
test.AssertResult(t, "myKey", path[1])
|
||||
}
|
||||
|
||||
func TestCandidateNodeGetNicePath(t *testing.T) {
|
||||
// Test simple key
|
||||
key := createStringScalarNode("simple")
|
||||
node := CandidateNode{Key: key}
|
||||
nicePath := node.GetNicePath()
|
||||
test.AssertResult(t, "simple", nicePath)
|
||||
|
||||
// Test array index
|
||||
arrayKey := createScalarNode(0, "0")
|
||||
arrayNode := CandidateNode{Key: arrayKey}
|
||||
nicePath = arrayNode.GetNicePath()
|
||||
test.AssertResult(t, "[0]", nicePath)
|
||||
|
||||
dotKey := createStringScalarNode("key.with.dots")
|
||||
dotNode := CandidateNode{Key: dotKey}
|
||||
nicePath = dotNode.GetNicePath()
|
||||
test.AssertResult(t, "key.with.dots", nicePath)
|
||||
|
||||
// Test nested path
|
||||
parentKey := createStringScalarNode("parent")
|
||||
parent := CandidateNode{Key: parentKey}
|
||||
childKey := createStringScalarNode("child")
|
||||
child := CandidateNode{Key: childKey, Parent: &parent}
|
||||
nicePath = child.GetNicePath()
|
||||
test.AssertResult(t, "parent.child", nicePath)
|
||||
}
|
||||
|
||||
func TestCandidateNodeFilterMapContentByKey(t *testing.T) {
|
||||
// Create a map with multiple key-value pairs
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
key3 := createStringScalarNode("key3")
|
||||
value3 := createStringScalarNode("value3")
|
||||
|
||||
mapNode := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key2, value2, key3, value3},
|
||||
}
|
||||
|
||||
// Filter by key predicate that matches key1 and key3
|
||||
filtered := mapNode.FilterMapContentByKey(func(key *CandidateNode) bool {
|
||||
return key.Value == "key1" || key.Value == "key3"
|
||||
})
|
||||
|
||||
// Should return key1, value1, key3, value3
|
||||
test.AssertResult(t, 4, len(filtered))
|
||||
test.AssertResult(t, "key1", filtered[0].Value)
|
||||
test.AssertResult(t, "value1", filtered[1].Value)
|
||||
test.AssertResult(t, "key3", filtered[2].Value)
|
||||
test.AssertResult(t, "value3", filtered[3].Value)
|
||||
}
|
||||
|
||||
func TestCandidateNodeVisitValues(t *testing.T) {
|
||||
// Test mapping node
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
|
||||
mapNode := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key2, value2},
|
||||
}
|
||||
|
||||
var visited []string
|
||||
err := mapNode.VisitValues(func(node *CandidateNode) error {
|
||||
visited = append(visited, node.Value)
|
||||
return nil
|
||||
})
|
||||
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, 2, len(visited))
|
||||
test.AssertResult(t, "value1", visited[0])
|
||||
test.AssertResult(t, "value2", visited[1])
|
||||
|
||||
// Test sequence node
|
||||
item1 := createStringScalarNode("item1")
|
||||
item2 := createStringScalarNode("item2")
|
||||
|
||||
seqNode := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Content: []*CandidateNode{item1, item2},
|
||||
}
|
||||
|
||||
visited = []string{}
|
||||
err = seqNode.VisitValues(func(node *CandidateNode) error {
|
||||
visited = append(visited, node.Value)
|
||||
return nil
|
||||
})
|
||||
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, 2, len(visited))
|
||||
test.AssertResult(t, "item1", visited[0])
|
||||
test.AssertResult(t, "item2", visited[1])
|
||||
|
||||
// Test scalar node (should not visit anything)
|
||||
scalarNode := &CandidateNode{
|
||||
Kind: ScalarNode,
|
||||
Value: "scalar",
|
||||
}
|
||||
|
||||
visited = []string{}
|
||||
err = scalarNode.VisitValues(func(node *CandidateNode) error {
|
||||
visited = append(visited, node.Value)
|
||||
return nil
|
||||
})
|
||||
|
||||
test.AssertResult(t, nil, err)
|
||||
test.AssertResult(t, 0, len(visited))
|
||||
}
|
||||
|
||||
func TestCandidateNodeCanVisitValues(t *testing.T) {
|
||||
mapNode := &CandidateNode{Kind: MappingNode}
|
||||
seqNode := &CandidateNode{Kind: SequenceNode}
|
||||
scalarNode := &CandidateNode{Kind: ScalarNode}
|
||||
|
||||
test.AssertResult(t, true, mapNode.CanVisitValues())
|
||||
test.AssertResult(t, true, seqNode.CanVisitValues())
|
||||
test.AssertResult(t, false, scalarNode.CanVisitValues())
|
||||
}
|
||||
|
||||
func TestCandidateNodeAddChild(t *testing.T) {
|
||||
parent := &CandidateNode{Kind: SequenceNode}
|
||||
child := createStringScalarNode("child")
|
||||
|
||||
parent.AddChild(child)
|
||||
|
||||
test.AssertResult(t, 1, len(parent.Content))
|
||||
test.AssertResult(t, false, parent.Content[0].IsMapKey)
|
||||
test.AssertResult(t, "0", parent.Content[0].Key.Value)
|
||||
// Check that parent is set correctly
|
||||
if parent.Content[0].Parent != parent {
|
||||
t.Errorf("Expected parent to be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateNodeAddChildren(t *testing.T) {
|
||||
// Test sequence node
|
||||
parent := &CandidateNode{Kind: SequenceNode}
|
||||
child1 := createStringScalarNode("child1")
|
||||
child2 := createStringScalarNode("child2")
|
||||
|
||||
parent.AddChildren([]*CandidateNode{child1, child2})
|
||||
|
||||
test.AssertResult(t, 2, len(parent.Content))
|
||||
test.AssertResult(t, "child1", parent.Content[0].Value)
|
||||
test.AssertResult(t, "child2", parent.Content[1].Value)
|
||||
|
||||
// Test mapping node
|
||||
mapParent := &CandidateNode{Kind: MappingNode}
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
|
||||
mapParent.AddChildren([]*CandidateNode{key1, value1, key2, value2})
|
||||
|
||||
test.AssertResult(t, 4, len(mapParent.Content))
|
||||
test.AssertResult(t, true, mapParent.Content[0].IsMapKey) // key1
|
||||
test.AssertResult(t, false, mapParent.Content[1].IsMapKey) // value1
|
||||
test.AssertResult(t, true, mapParent.Content[2].IsMapKey) // key2
|
||||
test.AssertResult(t, false, mapParent.Content[3].IsMapKey) // value2
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package yqlib
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func MapYamlStyle(original yaml.Style) Style {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//go:build linux
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestChangeOwner(t *testing.T) {
|
||||
// Create a temporary file for testing
|
||||
tempDir := t.TempDir()
|
||||
testFile := filepath.Join(tempDir, "testfile.txt")
|
||||
|
||||
// Create a test file
|
||||
err := os.WriteFile(testFile, []byte("test content"), 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat test file: %v", err)
|
||||
}
|
||||
|
||||
// Create another temporary file to change ownership of
|
||||
tempFile, err := os.CreateTemp(tempDir, "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Test changeOwner function
|
||||
err = changeOwner(info, tempFile)
|
||||
if err != nil {
|
||||
t.Errorf("changeOwner failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify that the function doesn't panic with valid input
|
||||
tempFile2, err := os.CreateTemp(tempDir, "chown_test2_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create second temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile2.Name())
|
||||
tempFile2.Close()
|
||||
|
||||
// Test with the second file
|
||||
err = changeOwner(info, tempFile2)
|
||||
if err != nil {
|
||||
t.Errorf("changeOwner failed on second file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeOwnerWithInvalidFileInfo(t *testing.T) {
|
||||
// Create a mock file info that doesn't have syscall.Stat_t
|
||||
mockInfo := &mockFileInfo{
|
||||
name: "mock",
|
||||
size: 0,
|
||||
mode: 0600,
|
||||
}
|
||||
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Test changeOwner with mock file info (should not panic)
|
||||
err = changeOwner(mockInfo, tempFile)
|
||||
if err != nil {
|
||||
t.Errorf("changeOwner failed with mock file info: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeOwnerWithNonExistentFile(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat temp file: %v", err)
|
||||
}
|
||||
|
||||
// Remove the file
|
||||
os.Remove(tempFile.Name())
|
||||
|
||||
err = changeOwner(info, tempFile)
|
||||
// The function should not panic even if the file doesn't exist
|
||||
if err != nil {
|
||||
t.Logf("Expected error when changing owner of non-existent file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// mockFileInfo implements fs.FileInfo but doesn't have syscall.Stat_t
|
||||
type mockFileInfo struct {
|
||||
name string
|
||||
size int64
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
func (m *mockFileInfo) Name() string { return m.name }
|
||||
func (m *mockFileInfo) Size() int64 { return m.size }
|
||||
func (m *mockFileInfo) Mode() os.FileMode { return m.mode }
|
||||
func (m *mockFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (m *mockFileInfo) IsDir() bool { return false }
|
||||
func (m *mockFileInfo) Sys() interface{} { return nil } // This will cause the type assertion to fail
|
||||
|
||||
func TestChangeOwnerWithSyscallStatT(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tempFile, err := os.CreateTemp(t.TempDir(), "chown_test_*.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
// Get file info
|
||||
info, err := os.Stat(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat temp file: %v", err)
|
||||
}
|
||||
|
||||
err = changeOwner(info, tempFile)
|
||||
if err != nil {
|
||||
t.Logf("changeOwner returned error (this might be expected in some environments): %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
attr color.Attribute
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "reset color",
|
||||
attr: color.Reset,
|
||||
expected: "\x1b[0m",
|
||||
},
|
||||
{
|
||||
name: "red color",
|
||||
attr: color.FgRed,
|
||||
expected: "\x1b[31m",
|
||||
},
|
||||
{
|
||||
name: "green color",
|
||||
attr: color.FgGreen,
|
||||
expected: "\x1b[32m",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := format(tt.attr)
|
||||
if result != tt.expected {
|
||||
t.Errorf("format(%d) = %q, want %q", tt.attr, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorizeAndPrint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
yamlBytes []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple yaml",
|
||||
yamlBytes: []byte("name: test\nage: 25\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml with strings",
|
||||
yamlBytes: []byte("name: \"hello world\"\nactive: true\ncount: 42\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml with anchors and aliases",
|
||||
yamlBytes: []byte("default: &default\n name: test\nuser: *default\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "yaml with comments",
|
||||
yamlBytes: []byte("# This is a comment\nname: test\n"),
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty yaml",
|
||||
yamlBytes: []byte(""),
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := colorizeAndPrint(tt.yamlBytes, &buf)
|
||||
|
||||
if tt.expectErr && err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
if !tt.expectErr && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Check that output contains escape sequences (color codes)
|
||||
if !tt.expectErr && len(tt.yamlBytes) > 0 {
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "\x1b[") {
|
||||
t.Error("Expected output to contain color escape sequences")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorizeAndPrintWithDifferentYamlTypes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
yaml string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "boolean values",
|
||||
yaml: "active: true\ninactive: false\n",
|
||||
},
|
||||
{
|
||||
name: "numeric values",
|
||||
yaml: "integer: 42\nfloat: 3.14\nnegative: -10\n",
|
||||
},
|
||||
{
|
||||
name: "map keys",
|
||||
yaml: "user:\n name: john\n age: 30\n",
|
||||
},
|
||||
{
|
||||
name: "string values",
|
||||
yaml: "message: \"hello world\"\ndescription: 'single quotes'\n",
|
||||
},
|
||||
{
|
||||
name: "mixed types",
|
||||
yaml: "config:\n debug: true\n port: 8080\n host: \"localhost\"\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := colorizeAndPrint([]byte(tc.yaml), &buf)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify output contains color codes
|
||||
if !tc.expectErr {
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "\x1b[") {
|
||||
t.Error("Expected output to contain color escape sequences")
|
||||
}
|
||||
// Should end with newline
|
||||
if !strings.HasSuffix(output, "\n") {
|
||||
t.Error("Expected output to end with newline")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -312,7 +312,6 @@ func TestDeeplyAssign_ErrorHandling(t *testing.T) {
|
||||
Value: "value",
|
||||
}
|
||||
|
||||
// Try to assign to a path on a scalar (should fail)
|
||||
path := []interface{}{"key"}
|
||||
err := navigator.DeeplyAssign(context, path, assignNode)
|
||||
|
||||
@@ -321,7 +320,6 @@ func TestDeeplyAssign_ErrorHandling(t *testing.T) {
|
||||
t.Logf("Actual error: %v", err)
|
||||
}
|
||||
|
||||
// This should fail because we can't assign to a scalar
|
||||
test.AssertResult(t, nil, err)
|
||||
}
|
||||
|
||||
|
||||
+19
-23
@@ -9,28 +9,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type base64Padder struct {
|
||||
count int
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (c *base64Padder) pad(buf []byte) (int, error) {
|
||||
pad := strings.Repeat("=", (4 - c.count%4))
|
||||
n, err := strings.NewReader(pad).Read(buf)
|
||||
c.count += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *base64Padder) Read(buf []byte) (int, error) {
|
||||
n, err := c.Reader.Read(buf)
|
||||
c.count += n
|
||||
|
||||
if err == io.EOF && c.count%4 != 0 {
|
||||
return c.pad(buf)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
type base64Decoder struct {
|
||||
reader io.Reader
|
||||
finished bool
|
||||
@@ -43,7 +21,25 @@ func NewBase64Decoder() Decoder {
|
||||
}
|
||||
|
||||
func (dec *base64Decoder) Init(reader io.Reader) error {
|
||||
dec.reader = &base64Padder{Reader: reader}
|
||||
// Read all data from the reader and strip leading/trailing whitespace
|
||||
// This is necessary because base64 decoding needs to see the complete input
|
||||
// to handle padding correctly, and we need to strip whitespace before decoding.
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(reader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Strip leading and trailing whitespace
|
||||
stripped := strings.TrimSpace(buf.String())
|
||||
|
||||
// Add padding if needed (base64 strings should be a multiple of 4 characters)
|
||||
padLen := len(stripped) % 4
|
||||
if padLen > 0 {
|
||||
stripped += strings.Repeat("=", 4-padLen)
|
||||
}
|
||||
|
||||
// Create a new reader from the stripped and padded data
|
||||
dec.reader = strings.NewReader(stripped)
|
||||
dec.readAnything = false
|
||||
dec.finished = false
|
||||
return nil
|
||||
|
||||
+63
-24
@@ -267,6 +267,14 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
|
||||
fullPath := dec.getFullPath(currentNode.Child())
|
||||
log.Debug("fullpath: %v", fullPath)
|
||||
|
||||
c := Context{}
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
|
||||
fullPath, err := getPathToUse(fullPath, dec, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
tableNodeValue := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
@@ -275,25 +283,22 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
|
||||
|
||||
var tableValue *toml.Node
|
||||
runAgainstCurrentExp := false
|
||||
var err error
|
||||
hasValue := dec.parser.NextExpression()
|
||||
// check to see if there is any table data
|
||||
if hasValue {
|
||||
tableValue = dec.parser.Expression()
|
||||
// next expression is not table data, so we are done
|
||||
if tableValue.Kind != toml.KeyValue {
|
||||
log.Debug("got an empty table, returning")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
log.Debug("got an empty table")
|
||||
runAgainstCurrentExp = true
|
||||
} else {
|
||||
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c := Context{}
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
err = dec.d.DeeplyAssign(c, fullPath, tableNodeValue)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -324,35 +329,69 @@ func (dec *tomlDecoder) arrayAppend(context Context, path []interface{}, rhsNode
|
||||
}
|
||||
|
||||
func (dec *tomlDecoder) processArrayTable(currentNode *toml.Node) (bool, error) {
|
||||
log.Debug("Entering processArrayTable")
|
||||
log.Debug("Enter processArrayTable")
|
||||
fullPath := dec.getFullPath(currentNode.Child())
|
||||
log.Debug("Fullpath: %v", fullPath)
|
||||
|
||||
c := Context{}
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
|
||||
fullPath, err := getPathToUse(fullPath, dec, c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// need to use the array append exp to add another entry to
|
||||
// this array: fullpath += [ thing ]
|
||||
|
||||
hasValue := dec.parser.NextExpression()
|
||||
if !hasValue {
|
||||
return false, fmt.Errorf("error retrieving table %v value: %w", fullPath, dec.parser.Error())
|
||||
}
|
||||
|
||||
tableNodeValue := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Tag: "!!map",
|
||||
}
|
||||
|
||||
tableValue := dec.parser.Expression()
|
||||
runAgainstCurrentExp, err := dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
||||
log.Debugf("table node err: %w", err)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
runAgainstCurrentExp := false
|
||||
// if the next value is a ArrayTable or Table, then its not part of this declaration (not a key value pair)
|
||||
// so lets leave that expression for the next round of parsing
|
||||
if hasValue && (dec.parser.Expression().Kind == toml.ArrayTable || dec.parser.Expression().Kind == toml.Table) {
|
||||
runAgainstCurrentExp = true
|
||||
} else if hasValue {
|
||||
// otherwise, if there is a value, it must be some key value pairs of the
|
||||
// first object in the array!
|
||||
tableValue := dec.parser.Expression()
|
||||
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(tableNodeValue, tableValue)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
c := Context{}
|
||||
|
||||
c = c.SingleChildContext(dec.rootMap)
|
||||
|
||||
// += function
|
||||
err = dec.arrayAppend(c, fullPath, tableNodeValue)
|
||||
|
||||
return runAgainstCurrentExp, err
|
||||
}
|
||||
|
||||
// if fullPath points to an array of maps rather than a map
|
||||
// then it should set this element into the _last_ element of that array.
|
||||
// Because TOML. So we'll inject the last index into the path.
|
||||
|
||||
func getPathToUse(fullPath []interface{}, dec *tomlDecoder, c Context) ([]interface{}, error) {
|
||||
pathToCheck := fullPath
|
||||
if len(fullPath) >= 1 {
|
||||
pathToCheck = fullPath[:len(fullPath)-1]
|
||||
}
|
||||
readOp := createTraversalTree(pathToCheck, traversePreferences{DontAutoCreate: true}, false)
|
||||
|
||||
resultContext, err := dec.d.GetMatchingNodes(c, readOp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resultContext.MatchingNodes.Len() >= 1 {
|
||||
match := resultContext.MatchingNodes.Front().Value.(*CandidateNode)
|
||||
// path refers to an array, we need to add this to the last element in the array
|
||||
if match.Kind == SequenceNode {
|
||||
fullPath = append(pathToCheck, len(match.Content)-1, fullPath[len(fullPath)-1])
|
||||
log.Debugf("Adding to end of %v array, using path: %v", pathToCheck, fullPath)
|
||||
}
|
||||
}
|
||||
return fullPath, err
|
||||
}
|
||||
|
||||
@@ -270,10 +270,16 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
|
||||
log.Debug("start element %v", se.Name.Local)
|
||||
elem.state = "started"
|
||||
// Build new a new current element and link it to its parent
|
||||
var label = se.Name.Local
|
||||
if dec.prefs.KeepNamespace {
|
||||
if se.Name.Space != "" {
|
||||
label = se.Name.Space + ":" + se.Name.Local
|
||||
}
|
||||
}
|
||||
elem = &element{
|
||||
parent: elem,
|
||||
n: &xmlNode{},
|
||||
label: se.Name.Local,
|
||||
label: label,
|
||||
}
|
||||
|
||||
// Extract attributes as children
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
yaml "go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type yamlDecoder struct {
|
||||
@@ -129,6 +129,8 @@ func (dec *yamlDecoder) Decode() (*CandidateNode, error) {
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else if len(yamlNode.Content) == 0 {
|
||||
return nil, errors.New("yaml node has no content")
|
||||
}
|
||||
|
||||
candidateNode := CandidateNode{document: dec.documentIndex}
|
||||
|
||||
@@ -5,6 +5,18 @@ Use the `alias` and `anchor` operators to read and write yaml aliases and anchor
|
||||
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
|
||||
|
||||
|
||||
## NOTE --yaml-fix-merge-anchor-to-spec flag
|
||||
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
|
||||
|
||||
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed).
|
||||
|
||||
This flag also enables advanced merging, like inline maps, as well as fixes to ensure when exploding a particular path, neighbours are not affect ed.
|
||||
|
||||
Long story short, you should be setting this flag to true.
|
||||
|
||||
See examples of the flag differences below, where LEGACY is with the flag off; and FIXED is with the flag on.
|
||||
|
||||
|
||||
## Merge one map
|
||||
see https://yaml.org/type/merge.html
|
||||
|
||||
@@ -179,7 +191,7 @@ Given a sample.yml file of:
|
||||
```yaml
|
||||
f:
|
||||
a: &a cat
|
||||
*a: b
|
||||
*a : b
|
||||
```
|
||||
then
|
||||
```bash
|
||||
@@ -208,7 +220,7 @@ thingTwo:
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.thingOne |= explode(.) * {"value": false}' sample.yml
|
||||
yq '.thingOne |= (explode(.) | sort_keys(.)) * {"value": false}' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
@@ -222,8 +234,57 @@ thingTwo:
|
||||
!!merge <<: *item_value
|
||||
```
|
||||
|
||||
## Merge multiple maps
|
||||
see https://yaml.org/type/merge.html
|
||||
## LEGACY: Explode with merge anchors
|
||||
Caution: this is for when --yaml-fix-merge-anchor-to-spec=false; it's not to YAML spec because the merge anchors incorrectly override the object values (foobarList.b is set to bar_b when it should still be foobarList_b). Flag will default to true in late 2025
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'explode(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
foo:
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar:
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: bar_b
|
||||
thing: foo_thing
|
||||
c: foobarList_c
|
||||
a: foo_a
|
||||
foobar:
|
||||
c: foo_c
|
||||
a: foo_a
|
||||
thing: foobar_thing
|
||||
```
|
||||
|
||||
## LEGACY: Merge multiple maps
|
||||
see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -252,8 +313,8 @@ x: 1
|
||||
y: 2
|
||||
```
|
||||
|
||||
## Override
|
||||
see https://yaml.org/type/merge.html
|
||||
## LEGACY: Override
|
||||
see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -284,57 +345,9 @@ x: 1
|
||||
y: 2
|
||||
```
|
||||
|
||||
## LEGACY: Explode with merge anchors
|
||||
Caution: this is for when --yaml-fix-merge-anchor-to-spec=false; it's not to YAML spec because the merge anchors incorrectly override the object values. Flag will default to true in late 2025
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'explode(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
foo:
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar:
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: bar_b
|
||||
thing: foo_thing
|
||||
c: foobarList_c
|
||||
a: foo_a
|
||||
foobar:
|
||||
c: foo_c
|
||||
a: foo_a
|
||||
thing: foobar_thing
|
||||
```
|
||||
|
||||
## FIXED: Explode with merge anchors
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Flag will default to true in late 2025
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
|
||||
Observe that foobarList.b property is still foobarList_b.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -382,3 +395,89 @@ foobar:
|
||||
thing: foobar_thing
|
||||
```
|
||||
|
||||
## FIXED: Merge multiple maps
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
|
||||
Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
x: 0
|
||||
y: 2
|
||||
- &BIG
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- *CENTER
|
||||
- *BIG
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.[4] | explode(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
x: 1
|
||||
y: 2
|
||||
r: 10
|
||||
```
|
||||
|
||||
## FIXED: Override
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
|
||||
Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- &CENTER
|
||||
x: 1
|
||||
y: 2
|
||||
- &LEFT
|
||||
x: 0
|
||||
y: 2
|
||||
- &BIG
|
||||
r: 10
|
||||
- &SMALL
|
||||
r: 1
|
||||
- !!merge <<:
|
||||
- *BIG
|
||||
- *LEFT
|
||||
- *SMALL
|
||||
x: 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.[4] | explode(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
r: 10
|
||||
y: 2
|
||||
x: 1
|
||||
```
|
||||
|
||||
## Exploding inline merge anchor
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
|
||||
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b: &b 42
|
||||
!!merge <<:
|
||||
c: *b
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'explode(.) | sort_keys(.)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
b: 42
|
||||
c: 42
|
||||
```
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ as follows:
|
||||
yq '(.. | select(tag == "!!str")) |= envsubst' file.yaml
|
||||
```
|
||||
|
||||
## Disabling env operators
|
||||
If required, you can use the `--security-disable-env-ops` to disable env operations.
|
||||
|
||||
|
||||
## Read string environment variable
|
||||
Running
|
||||
@@ -254,3 +257,39 @@ will output
|
||||
Error: variable ${notThere} not set
|
||||
```
|
||||
|
||||
## env() operation fails when security is enabled
|
||||
Use `--security-disable-env-ops` to disable env operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'env("MYENV")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: env operations have been disabled
|
||||
```
|
||||
|
||||
## strenv() operation fails when security is enabled
|
||||
Use `--security-disable-env-ops` to disable env operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'strenv("MYENV")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: env operations have been disabled
|
||||
```
|
||||
|
||||
## envsubst() operation fails when security is enabled
|
||||
Use `--security-disable-env-ops` to disable env operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input '"value: ${MYENV}" | envsubst'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: env operations have been disabled
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
# First
|
||||
|
||||
Returns the first matching element in an array, or first matching value in a map.
|
||||
|
||||
Can be given an expression to match with, otherwise will just return the first.
|
||||
|
||||
## First matching element from array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element from array with multiple matches
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
b: firstCat
|
||||
- a: apple
|
||||
- a: cat
|
||||
b: secondCat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
b: firstCat
|
||||
```
|
||||
|
||||
## First matching element from array with numeric condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: 10
|
||||
- a: 100
|
||||
- a: 1
|
||||
- a: 101
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a > 50)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 100
|
||||
```
|
||||
|
||||
## First matching element from array with boolean condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: false
|
||||
- a: true
|
||||
b: firstTrue
|
||||
- a: false
|
||||
- a: true
|
||||
b: secondTrue
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == true)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: true
|
||||
b: firstTrue
|
||||
```
|
||||
|
||||
## First matching element from array with null values
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: null
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a != null)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element from array with complex condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: dog
|
||||
b: 7
|
||||
- a: cat
|
||||
b: 3
|
||||
- a: apple
|
||||
b: 5
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.b > 4 and .b < 6)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: apple
|
||||
b: 5
|
||||
```
|
||||
|
||||
## First matching element from map
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
x:
|
||||
a: banana
|
||||
y:
|
||||
a: cat
|
||||
z:
|
||||
a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element from map with numeric condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
x:
|
||||
a: 10
|
||||
y:
|
||||
a: 100
|
||||
z:
|
||||
a: 101
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a > 50)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 100
|
||||
```
|
||||
|
||||
## First matching element from nested structure
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
items:
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.items | first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element with no matches
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "dog")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element from empty array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
[]
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element from scalar node
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
hello
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. == "hello")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element from null node
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
null
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. == "hello")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
```
|
||||
|
||||
## First matching element with string condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: banana
|
||||
- a: cat
|
||||
- a: apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a | test("^c"))' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat
|
||||
```
|
||||
|
||||
## First matching element with length condition
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: hi
|
||||
- a: hello
|
||||
- a: world
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(.a | length > 4)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: hello
|
||||
```
|
||||
|
||||
## First matching element from array of strings
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- banana
|
||||
- cat
|
||||
- apple
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. == "cat")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
cat
|
||||
```
|
||||
|
||||
## First matching element from array of numbers
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- 10
|
||||
- 100
|
||||
- 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first(. > 50)' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
100
|
||||
```
|
||||
|
||||
## First element with no filter from array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- 10
|
||||
- 100
|
||||
- 1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
10
|
||||
```
|
||||
|
||||
## First element with no filter from array of maps
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
- a: 10
|
||||
- a: 100
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'first' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 10
|
||||
```
|
||||
|
||||
@@ -4,3 +4,15 @@ Use the `alias` and `anchor` operators to read and write yaml aliases and anchor
|
||||
|
||||
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
|
||||
|
||||
|
||||
## NOTE --yaml-fix-merge-anchor-to-spec flag
|
||||
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
|
||||
|
||||
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed).
|
||||
|
||||
This flag also enables advanced merging, like inline maps, as well as fixes to ensure when exploding a particular path, neighbours are not affect ed.
|
||||
|
||||
Long story short, you should be setting this flag to true.
|
||||
|
||||
See examples of the flag differences below, where LEGACY is with the flag off; and FIXED is with the flag on.
|
||||
|
||||
|
||||
@@ -29,3 +29,6 @@ as follows:
|
||||
yq '(.. | select(tag == "!!str")) |= envsubst' file.yaml
|
||||
```
|
||||
|
||||
## Disabling env operators
|
||||
If required, you can use the `--security-disable-env-ops` to disable env operations.
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# First
|
||||
|
||||
Returns the first matching element in an array, or first matching value in a map.
|
||||
|
||||
Can be given an expression to match with, otherwise will just return the first.
|
||||
@@ -46,3 +46,7 @@ this.is = a properties file
|
||||
```
|
||||
bXkgc2VjcmV0IGNoaWxsaSByZWNpcGUgaXMuLi4u
|
||||
```
|
||||
|
||||
## Disabling file operators
|
||||
If required, you can use the `--security-disable-file-ops` to disable file operations.
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
# Traverse (Read)
|
||||
|
||||
This is the simplest (and perhaps most used) operator. It is used to navigate deeply into yaml structures.
|
||||
|
||||
|
||||
## NOTE --yaml-fix-merge-anchor-to-spec flag
|
||||
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
|
||||
|
||||
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed)
|
||||
|
||||
See examples of the flag differences below, where LEGACY is the flag off; and FIXED is with the flag on.
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ this.is = a properties file
|
||||
bXkgc2VjcmV0IGNoaWxsaSByZWNpcGUgaXMuLi4u
|
||||
```
|
||||
|
||||
## Disabling file operators
|
||||
If required, you can use the `--security-disable-file-ops` to disable file operations.
|
||||
|
||||
|
||||
## Simple example
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -194,3 +198,63 @@ cool: things
|
||||
more_stuff: my secret chilli recipe is....
|
||||
```
|
||||
|
||||
## load() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load("../../examples/thing.yml")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_str() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_str("../../examples/thing.yml")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_xml() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_xml("../../examples/small.xml")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_props() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_props("../../examples/small.properties")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
## load_base64() operation fails when security is enabled
|
||||
Use `--security-disable-file-ops` to disable file operations for security.
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'load_base64("../../examples/base64.txt")'
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: file operations have been disabled
|
||||
```
|
||||
|
||||
|
||||
@@ -56,6 +56,29 @@ will output
|
||||
sam
|
||||
```
|
||||
|
||||
## Get parents
|
||||
Match all parents
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a.b.c | parents' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- c: cat
|
||||
- b:
|
||||
c: cat
|
||||
- a:
|
||||
b:
|
||||
c: cat
|
||||
```
|
||||
|
||||
## N-th parent
|
||||
You can optionally supply the number of levels to go up for the parent, the default being 1.
|
||||
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
This is the simplest (and perhaps most used) operator. It is used to navigate deeply into yaml structures.
|
||||
|
||||
|
||||
## NOTE --yaml-fix-merge-anchor-to-spec flag
|
||||
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
|
||||
|
||||
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed)
|
||||
|
||||
See examples of the flag differences below, where LEGACY is the flag off; and FIXED is with the flag on.
|
||||
|
||||
|
||||
## Simple map navigation
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -303,37 +312,6 @@ will output
|
||||
foo_a
|
||||
```
|
||||
|
||||
## Traversing merge anchors with override
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.foobar.c' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
foo_c
|
||||
```
|
||||
|
||||
## Traversing merge anchors with local override
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
@@ -365,7 +343,93 @@ will output
|
||||
foobar_thing
|
||||
```
|
||||
|
||||
## Splatting merge anchors
|
||||
## Select multiple indices
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a[0, 2]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a
|
||||
c
|
||||
```
|
||||
|
||||
## LEGACY: Traversing merge anchors with override
|
||||
This is legacy behaviour, see --yaml-fix-merge-anchor-to-spec
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.foobar.c' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
foo_c
|
||||
```
|
||||
|
||||
## LEGACY: Traversing merge anchor lists
|
||||
Note that the later merge anchors override previous, but this is legacy behaviour, see --yaml-fix-merge-anchor-to-spec
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.foobarList.thing' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
bar_thing
|
||||
```
|
||||
|
||||
## LEGACY: Splatting merge anchors
|
||||
With legacy override behaviour, see --yaml-fix-merge-anchor-to-spec
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
@@ -398,40 +462,9 @@ foo_a
|
||||
foobar_thing
|
||||
```
|
||||
|
||||
## Traversing merge anchor lists
|
||||
Note that the later merge anchors override previous
|
||||
## LEGACY: Splatting merge anchor lists
|
||||
With legacy override behaviour, see --yaml-fix-merge-anchor-to-spec
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.foobarList.thing' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
bar_thing
|
||||
```
|
||||
|
||||
## Splatting merge anchor lists
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
@@ -465,21 +498,140 @@ bar_thing
|
||||
foobarList_c
|
||||
```
|
||||
|
||||
## Select multiple indices
|
||||
## FIXED: Traversing merge anchors with override
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a[0, 2]' sample.yml
|
||||
yq '.foobar.c' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a
|
||||
c
|
||||
foobar_c
|
||||
```
|
||||
|
||||
## FIXED: Traversing merge anchor lists
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.foobarList.thing' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
foo_thing
|
||||
```
|
||||
|
||||
## FIXED: Splatting merge anchors
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.foobar[]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
foo_a
|
||||
foobar_thing
|
||||
foobar_c
|
||||
```
|
||||
|
||||
## FIXED: Splatting merge anchor lists
|
||||
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
foo: &foo
|
||||
a: foo_a
|
||||
thing: foo_thing
|
||||
c: foo_c
|
||||
bar: &bar
|
||||
b: bar_b
|
||||
thing: bar_thing
|
||||
c: bar_c
|
||||
foobarList:
|
||||
b: foobarList_b
|
||||
!!merge <<:
|
||||
- *foo
|
||||
- *bar
|
||||
c: foobarList_c
|
||||
foobar:
|
||||
c: foobar_c
|
||||
!!merge <<: *foo
|
||||
thing: foobar_thing
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.foobarList[]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
foobarList_b
|
||||
foo_thing
|
||||
foobarList_c
|
||||
foo_a
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Base64
|
||||
|
||||
Encode and decode to and from Base64.
|
||||
|
||||
Base64 assumes [RFC4648](https://rfc-editor.org/rfc/rfc4648.html) encoding. Encoding and decoding both assume that the content is a UTF-8 string and not binary content.
|
||||
|
||||
|
||||
See below for examples
|
||||
|
||||
|
||||
## Decode base64: simple
|
||||
Decoded data is assumed to be a string.
|
||||
|
||||
Given a sample.txt file of:
|
||||
```
|
||||
YSBzcGVjaWFsIHN0cmluZw==
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=base64 -oy '.' sample.txt
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a special string
|
||||
```
|
||||
|
||||
## Decode base64: UTF-8
|
||||
Base64 decoding supports UTF-8 encoded strings.
|
||||
|
||||
Given a sample.txt file of:
|
||||
```
|
||||
V29ya3Mgd2l0aCBVVEYtMTYg8J+Yig==
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=base64 -oy '.' sample.txt
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
Works with UTF-16 😊
|
||||
```
|
||||
|
||||
## Decode with extra spaces
|
||||
Extra leading/trailing whitespace is stripped
|
||||
|
||||
Given a sample.txt file of:
|
||||
```
|
||||
|
||||
YSBzcGVjaWFsIHN0cmluZw==
|
||||
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -p=base64 -oy '.' sample.txt
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a special string
|
||||
```
|
||||
|
||||
## Encode base64: string
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
"a special string"
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=base64 '.' sample.yml
|
||||
```
|
||||
will output
|
||||
```
|
||||
YSBzcGVjaWFsIHN0cmluZw==```
|
||||
|
||||
## Encode base64: string from document
|
||||
Extract a string field and encode it to base64.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
coolData: "a special string"
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=base64 '.coolData' sample.yml
|
||||
```
|
||||
will output
|
||||
```
|
||||
YSBzcGVjaWFsIHN0cmluZw==```
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Base64
|
||||
|
||||
Encode and decode to and from Base64.
|
||||
|
||||
Base64 assumes [RFC4648](https://rfc-editor.org/rfc/rfc4648.html) encoding. Encoding and decoding both assume that the content is a UTF-8 string and not binary content.
|
||||
|
||||
|
||||
See below for examples
|
||||
|
||||
@@ -84,3 +84,23 @@ will output
|
||||
name='Miles O'"'"'Brien'
|
||||
```
|
||||
|
||||
## Encode shell variables: custom separator
|
||||
Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
my_app:
|
||||
db_config:
|
||||
host: localhost
|
||||
port: 5432
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -o=shell --shell-key-separator="__" sample.yml
|
||||
```
|
||||
will output
|
||||
```sh
|
||||
my_app__db_config__host=localhost
|
||||
my_app__db_config__port=5432
|
||||
```
|
||||
|
||||
|
||||
@@ -104,6 +104,27 @@ owner:
|
||||
suburb: nice
|
||||
```
|
||||
|
||||
## Parse: Array of Array Table
|
||||
Given a sample.toml file of:
|
||||
```toml
|
||||
|
||||
[[fruits]]
|
||||
name = "apple"
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious"
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq -oy '.' sample.toml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
fruits:
|
||||
- name: apple
|
||||
varieties:
|
||||
- name: red delicious
|
||||
```
|
||||
|
||||
## Parse: Empty Table
|
||||
Given a sample.toml file of:
|
||||
```toml
|
||||
|
||||
@@ -319,7 +319,10 @@ Defaults to true
|
||||
Given a sample.xml file of:
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
<item foo="bar">baz</item>
|
||||
<xsi:item>foobar</xsi:item>
|
||||
</map>
|
||||
|
||||
```
|
||||
then
|
||||
@@ -329,13 +332,19 @@ yq --xml-keep-namespace=false '.' sample.xml
|
||||
will output
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xsi="some-instance" schemaLocation="some-url"></map>
|
||||
<map xmlns="some-namespace" xsi="some-instance" schemaLocation="some-url">
|
||||
<item foo="bar">baz</item>
|
||||
<item>foobar</item>
|
||||
</map>
|
||||
```
|
||||
|
||||
instead of
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
<item foo="bar">baz</item>
|
||||
<xsi:item>foobar</xsi:item>
|
||||
</map>
|
||||
```
|
||||
|
||||
## Parse xml: keep raw attribute namespace
|
||||
@@ -344,7 +353,10 @@ Defaults to true
|
||||
Given a sample.xml file of:
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
<item foo="bar">baz</item>
|
||||
<xsi:item>foobar</xsi:item>
|
||||
</map>
|
||||
|
||||
```
|
||||
then
|
||||
@@ -354,13 +366,19 @@ yq --xml-raw-token=false '.' sample.xml
|
||||
will output
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" some-instance:schemaLocation="some-url"></map>
|
||||
<some-namespace:map xmlns="some-namespace" xmlns:xsi="some-instance" some-instance:schemaLocation="some-url">
|
||||
<some-namespace:item foo="bar">baz</some-namespace:item>
|
||||
<some-instance:item>foobar</some-instance:item>
|
||||
</some-namespace:map>
|
||||
```
|
||||
|
||||
instead of
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
<item foo="bar">baz</item>
|
||||
<xsi:item>foobar</xsi:item>
|
||||
</map>
|
||||
```
|
||||
|
||||
## Encode xml: simple
|
||||
|
||||
@@ -12,10 +12,13 @@ import (
|
||||
)
|
||||
|
||||
type shellVariablesEncoder struct {
|
||||
prefs ShellVariablesPreferences
|
||||
}
|
||||
|
||||
func NewShellVariablesEncoder() Encoder {
|
||||
return &shellVariablesEncoder{}
|
||||
return &shellVariablesEncoder{
|
||||
prefs: ConfiguredShellVariablesPreferences,
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *shellVariablesEncoder) CanHandleAliases() bool {
|
||||
@@ -58,7 +61,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
|
||||
return err
|
||||
case SequenceNode:
|
||||
for index, child := range node.Content {
|
||||
err := pe.doEncode(w, child, appendPath(path, index))
|
||||
err := pe.doEncode(w, child, pe.appendPath(path, index))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -68,7 +71,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
|
||||
for index := 0; index < len(node.Content); index = index + 2 {
|
||||
key := node.Content[index]
|
||||
value := node.Content[index+1]
|
||||
err := pe.doEncode(w, value, appendPath(path, key.Value))
|
||||
err := pe.doEncode(w, value, pe.appendPath(path, key.Value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -81,7 +84,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
|
||||
}
|
||||
}
|
||||
|
||||
func appendPath(cookedPath string, rawKey interface{}) string {
|
||||
func (pe *shellVariablesEncoder) appendPath(cookedPath string, rawKey interface{}) string {
|
||||
|
||||
// Shell variable names must match
|
||||
// [a-zA-Z_]+[a-zA-Z0-9_]*
|
||||
@@ -126,7 +129,7 @@ func appendPath(cookedPath string, rawKey interface{}) string {
|
||||
}
|
||||
return key
|
||||
}
|
||||
return cookedPath + "_" + key
|
||||
return cookedPath + pe.prefs.KeySeparator + key
|
||||
}
|
||||
|
||||
func quoteValue(value string) string {
|
||||
|
||||
@@ -91,3 +91,47 @@ func TestShellVariablesEncoderEmptyMap(t *testing.T) {
|
||||
func TestShellVariablesEncoderScalarNode(t *testing.T) {
|
||||
assertEncodesTo(t, "some string", "value='some string'")
|
||||
}
|
||||
|
||||
func assertEncodesToWithSeparator(t *testing.T, yaml string, shellvars string, separator string) {
|
||||
var output bytes.Buffer
|
||||
writer := bufio.NewWriter(&output)
|
||||
|
||||
// Save the original separator
|
||||
originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator
|
||||
defer func() {
|
||||
ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator
|
||||
}()
|
||||
|
||||
// Set the custom separator
|
||||
ConfiguredShellVariablesPreferences.KeySeparator = separator
|
||||
|
||||
var encoder = NewShellVariablesEncoder()
|
||||
inputs, err := readDocuments(strings.NewReader(yaml), "test.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
node := inputs.Front().Value.(*CandidateNode)
|
||||
err = encoder.Encode(writer, node)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
writer.Flush()
|
||||
|
||||
test.AssertResult(t, shellvars, strings.TrimSuffix(output.String(), "\n"))
|
||||
}
|
||||
|
||||
func TestShellVariablesEncoderCustomSeparator(t *testing.T) {
|
||||
assertEncodesToWithSeparator(t, "a:\n b: Lewis\n c: Carroll", "a__b=Lewis\na__c=Carroll", "__")
|
||||
}
|
||||
|
||||
func TestShellVariablesEncoderCustomSeparatorNested(t *testing.T) {
|
||||
assertEncodesToWithSeparator(t, "my_app:\n db_config:\n host: localhost", "my_app__db_config__host=localhost", "__")
|
||||
}
|
||||
|
||||
func TestShellVariablesEncoderCustomSeparatorArray(t *testing.T) {
|
||||
assertEncodesToWithSeparator(t, "a: [{n: Alice}, {n: Bob}]", "a__0__n=Alice\na__1__n=Bob", "__")
|
||||
}
|
||||
|
||||
func TestShellVariablesEncoderCustomSeparatorSingleChar(t *testing.T) {
|
||||
assertEncodesToWithSeparator(t, "a:\n b: value", "aXb=value", "X")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type yamlEncoder struct {
|
||||
|
||||
@@ -54,6 +54,12 @@ func (p *expressionParserImpl) createExpressionTree(postFixPath []*Operation) (*
|
||||
switch numArgs {
|
||||
case 1:
|
||||
if len(stack) < 1 {
|
||||
// Allow certain unary ops to accept zero args by interpreting missing RHS as nil
|
||||
// TODO - make this more general on OperationType
|
||||
if Operation.OperationType == firstOpType {
|
||||
// no RHS provided; proceed without popping
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("'%v' expects 1 arg but received none", strings.TrimSpace(Operation.StringValue))
|
||||
}
|
||||
remaining, rhs := stack[:len(stack)-1], stack[len(stack)-1]
|
||||
|
||||
@@ -84,3 +84,42 @@ func TestParserExtraArgs(t *testing.T) {
|
||||
_, err := getExpressionParser().ParseExpression("sortKeys(.) explode(.)")
|
||||
test.AssertResultComplex(t, "bad expression, please check expression syntax", err.Error())
|
||||
}
|
||||
|
||||
func TestParserEmptyExpression(t *testing.T) {
|
||||
_, err := getExpressionParser().ParseExpression("")
|
||||
test.AssertResultComplex(t, nil, err)
|
||||
}
|
||||
|
||||
func TestParserSingleOperation(t *testing.T) {
|
||||
result, err := getExpressionParser().ParseExpression(".")
|
||||
test.AssertResultComplex(t, nil, err)
|
||||
if result == nil {
|
||||
t.Fatal("Expected non-nil result for single operation")
|
||||
}
|
||||
if result.Operation == nil {
|
||||
t.Fatal("Expected operation to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserFirstOpWithZeroArgs(t *testing.T) {
|
||||
// Test the special case where firstOpType can accept zero args
|
||||
result, err := getExpressionParser().ParseExpression("first")
|
||||
test.AssertResultComplex(t, nil, err)
|
||||
if result == nil {
|
||||
t.Fatal("Expected non-nil result for first operation with zero args")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParserInvalidExpressionTree(t *testing.T) {
|
||||
// This tests the createExpressionTree function with malformed postfix
|
||||
parser := getExpressionParser().(*expressionParserImpl)
|
||||
|
||||
// Create invalid postfix operations that would leave more than one item on stack
|
||||
invalidOps := []*Operation{
|
||||
{OperationType: &operationType{NumArgs: 0}},
|
||||
{OperationType: &operationType{NumArgs: 0}},
|
||||
}
|
||||
|
||||
_, err := parser.createExpressionTree(invalidOps)
|
||||
test.AssertResultComplex(t, "bad expression, please check expression syntax", err.Error())
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ var participleYqRules = []*participleYqRule{
|
||||
|
||||
simpleOp("sort_?by", sortByOpType),
|
||||
simpleOp("sort", sortOpType),
|
||||
simpleOp("first", firstOpType),
|
||||
|
||||
simpleOp("reverse", reverseOpType),
|
||||
|
||||
@@ -130,6 +131,7 @@ var participleYqRules = []*participleYqRule{
|
||||
simpleOp("contains", containsOpType),
|
||||
simpleOp("split", splitStringOpType),
|
||||
|
||||
simpleOp("parents", getParentsOpType),
|
||||
{"ParentWithLevel", `parent\([0-9]+\)`, parentWithLevel(), 0},
|
||||
{"ParentWithDefaultLevel", `parent`, parentWithDefaultLevel(), 0},
|
||||
|
||||
@@ -377,9 +379,7 @@ func stringValue() yqAction {
|
||||
log.Debug("rawTokenvalue: %v", rawToken.Value)
|
||||
value := unwrap(rawToken.Value)
|
||||
log.Debug("unwrapped: %v", value)
|
||||
value = strings.ReplaceAll(value, "\\\"", "\"")
|
||||
value = strings.ReplaceAll(value, "\\n", "\n")
|
||||
log.Debug("replaced: %v", value)
|
||||
value = processEscapeCharacters(value)
|
||||
return &token{TokenType: operationToken, Operation: &Operation{
|
||||
OperationType: stringInterpolationOpType,
|
||||
StringValue: value,
|
||||
|
||||
@@ -704,6 +704,90 @@ var participleLexerScenarios = []participleLexerScenario{
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: `"string with a\r"`,
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: stringInterpolationOpType,
|
||||
Value: "string with a\r",
|
||||
StringValue: "string with a\r",
|
||||
Preferences: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: `"string with a\t"`,
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: stringInterpolationOpType,
|
||||
Value: "string with a\t",
|
||||
StringValue: "string with a\t",
|
||||
Preferences: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: `"string with a\f"`,
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: stringInterpolationOpType,
|
||||
Value: "string with a\f",
|
||||
StringValue: "string with a\f",
|
||||
Preferences: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: `"string with a\v"`,
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: stringInterpolationOpType,
|
||||
Value: "string with a\v",
|
||||
StringValue: "string with a\v",
|
||||
Preferences: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: `"string with a\b"`,
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: stringInterpolationOpType,
|
||||
Value: "string with a\b",
|
||||
StringValue: "string with a\b",
|
||||
Preferences: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expression: `"string with a\a"`,
|
||||
tokens: []*token{
|
||||
{
|
||||
TokenType: operationToken,
|
||||
Operation: &Operation{
|
||||
OperationType: stringInterpolationOpType,
|
||||
Value: "string with a\a",
|
||||
StringValue: "string with a\a",
|
||||
Preferences: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestParticipleLexer(t *testing.T) {
|
||||
|
||||
@@ -186,6 +186,22 @@ func parseInt(numberString string) (int, error) {
|
||||
return int(parsed), err
|
||||
}
|
||||
|
||||
func processEscapeCharacters(original string) string {
|
||||
value := original
|
||||
value = strings.ReplaceAll(value, "\\\"", "\"")
|
||||
value = strings.ReplaceAll(value, "\\n", "\n")
|
||||
value = strings.ReplaceAll(value, "\\t", "\t")
|
||||
value = strings.ReplaceAll(value, "\\r", "\r")
|
||||
value = strings.ReplaceAll(value, "\\f", "\f")
|
||||
value = strings.ReplaceAll(value, "\\v", "\v")
|
||||
value = strings.ReplaceAll(value, "\\b", "\b")
|
||||
value = strings.ReplaceAll(value, "\\a", "\a")
|
||||
if value != original {
|
||||
log.Debug("processEscapeCharacters from [%v] to [%v]", original, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func headAndLineComment(node *CandidateNode) string {
|
||||
return headComment(node) + lineComment(node)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mikefarah/yq/v4/test"
|
||||
@@ -160,3 +161,250 @@ func TestParseInt64(t *testing.T) {
|
||||
test.AssertResultComplexWithContext(t, tt.expectedFormatString, fmt.Sprintf(format, actualNumber), fmt.Sprintf("Formatting of: %v", tt.numberString))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetContentValueByKey(t *testing.T) {
|
||||
// Create content with key-value pairs
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
|
||||
content := []*CandidateNode{key1, value1, key2, value2}
|
||||
|
||||
// Test finding existing key
|
||||
result := getContentValueByKey(content, "key1")
|
||||
test.AssertResult(t, value1, result)
|
||||
|
||||
// Test finding another existing key
|
||||
result = getContentValueByKey(content, "key2")
|
||||
test.AssertResult(t, value2, result)
|
||||
|
||||
// Test finding non-existing key
|
||||
result = getContentValueByKey(content, "nonexistent")
|
||||
test.AssertResult(t, (*CandidateNode)(nil), result)
|
||||
|
||||
// Test with empty content
|
||||
result = getContentValueByKey([]*CandidateNode{}, "key1")
|
||||
test.AssertResult(t, (*CandidateNode)(nil), result)
|
||||
}
|
||||
|
||||
func TestRecurseNodeArrayEqual(t *testing.T) {
|
||||
// Create two arrays with same content
|
||||
array1 := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Content: []*CandidateNode{
|
||||
createStringScalarNode("item1"),
|
||||
createStringScalarNode("item2"),
|
||||
},
|
||||
}
|
||||
|
||||
array2 := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Content: []*CandidateNode{
|
||||
createStringScalarNode("item1"),
|
||||
createStringScalarNode("item2"),
|
||||
},
|
||||
}
|
||||
|
||||
array3 := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Content: []*CandidateNode{
|
||||
createStringScalarNode("item1"),
|
||||
createStringScalarNode("different"),
|
||||
},
|
||||
}
|
||||
|
||||
array4 := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Content: []*CandidateNode{
|
||||
createStringScalarNode("item1"),
|
||||
},
|
||||
}
|
||||
|
||||
test.AssertResult(t, true, recurseNodeArrayEqual(array1, array2))
|
||||
test.AssertResult(t, false, recurseNodeArrayEqual(array1, array3))
|
||||
test.AssertResult(t, false, recurseNodeArrayEqual(array1, array4))
|
||||
}
|
||||
|
||||
func TestFindInArray(t *testing.T) {
|
||||
item1 := createStringScalarNode("item1")
|
||||
item2 := createStringScalarNode("item2")
|
||||
item3 := createStringScalarNode("item3")
|
||||
|
||||
array := &CandidateNode{
|
||||
Kind: SequenceNode,
|
||||
Content: []*CandidateNode{item1, item2, item3},
|
||||
}
|
||||
|
||||
// Test finding existing items
|
||||
test.AssertResult(t, 0, findInArray(array, item1))
|
||||
test.AssertResult(t, 1, findInArray(array, item2))
|
||||
test.AssertResult(t, 2, findInArray(array, item3))
|
||||
|
||||
// Test finding non-existing item
|
||||
nonExistent := createStringScalarNode("nonexistent")
|
||||
test.AssertResult(t, -1, findInArray(array, nonExistent))
|
||||
}
|
||||
|
||||
func TestFindKeyInMap(t *testing.T) {
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
|
||||
mapNode := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key2, value2},
|
||||
}
|
||||
|
||||
// Test finding existing keys
|
||||
test.AssertResult(t, 0, findKeyInMap(mapNode, key1))
|
||||
test.AssertResult(t, 2, findKeyInMap(mapNode, key2))
|
||||
|
||||
// Test finding non-existing key
|
||||
nonExistent := createStringScalarNode("nonexistent")
|
||||
test.AssertResult(t, -1, findKeyInMap(mapNode, nonExistent))
|
||||
}
|
||||
|
||||
func TestRecurseNodeObjectEqual(t *testing.T) {
|
||||
// Create two objects with same content
|
||||
key1 := createStringScalarNode("key1")
|
||||
value1 := createStringScalarNode("value1")
|
||||
key2 := createStringScalarNode("key2")
|
||||
value2 := createStringScalarNode("value2")
|
||||
|
||||
obj1 := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key2, value2},
|
||||
}
|
||||
|
||||
obj2 := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key2, value2},
|
||||
}
|
||||
|
||||
// Create object with different values
|
||||
value3 := createStringScalarNode("value3")
|
||||
obj3 := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value3, key2, value2},
|
||||
}
|
||||
|
||||
// Create object with different keys
|
||||
key3 := createStringScalarNode("key3")
|
||||
obj4 := &CandidateNode{
|
||||
Kind: MappingNode,
|
||||
Content: []*CandidateNode{key1, value1, key3, value2},
|
||||
}
|
||||
|
||||
test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2))
|
||||
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3))
|
||||
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4))
|
||||
}
|
||||
|
||||
func TestParseInt(t *testing.T) {
|
||||
type parseIntScenario struct {
|
||||
numberString string
|
||||
expectedParsedNumber int
|
||||
expectedError string
|
||||
}
|
||||
|
||||
scenarios := []parseIntScenario{
|
||||
{
|
||||
numberString: "34",
|
||||
expectedParsedNumber: 34,
|
||||
},
|
||||
{
|
||||
numberString: "10_000",
|
||||
expectedParsedNumber: 10000,
|
||||
},
|
||||
{
|
||||
numberString: "0x10",
|
||||
expectedParsedNumber: 16,
|
||||
},
|
||||
{
|
||||
numberString: "0o10",
|
||||
expectedParsedNumber: 8,
|
||||
},
|
||||
{
|
||||
numberString: "invalid",
|
||||
expectedError: "strconv.ParseInt",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range scenarios {
|
||||
actualNumber, err := parseInt(tt.numberString)
|
||||
if tt.expectedError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for '%s' but got none", tt.numberString)
|
||||
} else if !strings.Contains(err.Error(), tt.expectedError) {
|
||||
t.Errorf("Expected error containing '%s' for '%s', got '%s'", tt.expectedError, tt.numberString, err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for '%s': %v", tt.numberString, err)
|
||||
}
|
||||
test.AssertResultComplexWithContext(t, tt.expectedParsedNumber, actualNumber, tt.numberString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadAndLineComment(t *testing.T) {
|
||||
node := &CandidateNode{
|
||||
HeadComment: "# head comment",
|
||||
LineComment: "# line comment",
|
||||
}
|
||||
|
||||
result := headAndLineComment(node)
|
||||
test.AssertResult(t, " head comment line comment", result)
|
||||
}
|
||||
|
||||
func TestHeadComment(t *testing.T) {
|
||||
node := &CandidateNode{
|
||||
HeadComment: "# head comment",
|
||||
}
|
||||
|
||||
result := headComment(node)
|
||||
test.AssertResult(t, " head comment", result)
|
||||
|
||||
// Test without #
|
||||
node.HeadComment = "no hash comment"
|
||||
result = headComment(node)
|
||||
test.AssertResult(t, "no hash comment", result)
|
||||
}
|
||||
|
||||
func TestLineComment(t *testing.T) {
|
||||
node := &CandidateNode{
|
||||
LineComment: "# line comment",
|
||||
}
|
||||
|
||||
result := lineComment(node)
|
||||
test.AssertResult(t, " line comment", result)
|
||||
|
||||
// Test without #
|
||||
node.LineComment = "no hash comment"
|
||||
result = lineComment(node)
|
||||
test.AssertResult(t, "no hash comment", result)
|
||||
}
|
||||
|
||||
func TestFootComment(t *testing.T) {
|
||||
node := &CandidateNode{
|
||||
FootComment: "# foot comment",
|
||||
}
|
||||
|
||||
result := footComment(node)
|
||||
test.AssertResult(t, " foot comment", result)
|
||||
|
||||
// Test without #
|
||||
node.FootComment = "no hash comment"
|
||||
result = footComment(node)
|
||||
test.AssertResult(t, "no hash comment", result)
|
||||
}
|
||||
|
||||
func TestKindString(t *testing.T) {
|
||||
test.AssertResult(t, "ScalarNode", KindString(ScalarNode))
|
||||
test.AssertResult(t, "SequenceNode", KindString(SequenceNode))
|
||||
test.AssertResult(t, "MappingNode", KindString(MappingNode))
|
||||
test.AssertResult(t, "AliasNode", KindString(AliasNode))
|
||||
test.AssertResult(t, "unknown!", KindString(Kind(999))) // Invalid kind
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ var getKindOpType = &operationType{Type: "GET_KIND", NumArgs: 0, Precedence: 50,
|
||||
var getKeyOpType = &operationType{Type: "GET_KEY", NumArgs: 0, Precedence: 50, Handler: getKeyOperator}
|
||||
var isKeyOpType = &operationType{Type: "IS_KEY", NumArgs: 0, Precedence: 50, Handler: isKeyOperator}
|
||||
var getParentOpType = &operationType{Type: "GET_PARENT", NumArgs: 0, Precedence: 50, Handler: getParentOperator}
|
||||
var getParentsOpType = &operationType{Type: "GET_PARENTS", NumArgs: 0, Precedence: 50, Handler: getParentsOperator}
|
||||
|
||||
var getCommentOpType = &operationType{Type: "GET_COMMENT", NumArgs: 0, Precedence: 50, Handler: getCommentsOperator}
|
||||
var getAnchorOpType = &operationType{Type: "GET_ANCHOR", NumArgs: 0, Precedence: 50, Handler: getAnchorOperator}
|
||||
@@ -142,6 +143,7 @@ var delPathsOpType = &operationType{Type: "DEL_PATHS", NumArgs: 1, Precedence: 5
|
||||
|
||||
var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 52, Handler: explodeOperator, CheckForPostTraverse: true}
|
||||
var sortByOpType = &operationType{Type: "SORT_BY", NumArgs: 1, Precedence: 52, Handler: sortByOperator, CheckForPostTraverse: true}
|
||||
var firstOpType = &operationType{Type: "FIRST", NumArgs: 1, Precedence: 52, Handler: firstOperator, CheckForPostTraverse: true}
|
||||
var reverseOpType = &operationType{Type: "REVERSE", NumArgs: 0, Precedence: 52, Handler: reverseOperator, CheckForPostTraverse: true}
|
||||
var sortOpType = &operationType{Type: "SORT", NumArgs: 0, Precedence: 52, Handler: sortOperator, CheckForPostTraverse: true}
|
||||
var shuffleOpType = &operationType{Type: "SHUFFLE", NumArgs: 0, Precedence: 52, Handler: shuffleOperator, CheckForPostTraverse: true}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var showMergeAnchorToSpecWarning = true
|
||||
|
||||
func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
|
||||
log.Debugf("AssignAlias operator!")
|
||||
@@ -144,11 +146,21 @@ func fixedReconstructAliasedMap(node *CandidateNode) error {
|
||||
for index := 0; index < len(node.Content); index = index + 2 {
|
||||
keyNode := node.Content[index]
|
||||
valueNode := node.Content[index+1]
|
||||
if keyNode.Value != "<<" {
|
||||
if keyNode.Tag != "!!merge" {
|
||||
// always add in plain nodes
|
||||
// explode both the key and value nodes
|
||||
if err := explodeNode(keyNode, Context{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := explodeNode(valueNode, Context{}); err != nil {
|
||||
return err
|
||||
}
|
||||
newContent = append(newContent, keyNode, valueNode)
|
||||
} else {
|
||||
sequence := valueNode
|
||||
if sequence.Kind == AliasNode {
|
||||
sequence = sequence.Alias
|
||||
}
|
||||
if sequence.Kind != SequenceNode {
|
||||
sequence = &CandidateNode{Content: []*CandidateNode{sequence}}
|
||||
}
|
||||
@@ -159,13 +171,21 @@ func fixedReconstructAliasedMap(node *CandidateNode) error {
|
||||
mergeNodeSeq = mergeNodeSeq.Alias
|
||||
}
|
||||
if mergeNodeSeq.Kind != MappingNode {
|
||||
return fmt.Errorf("merge anchor only supports maps, got !!seq instead")
|
||||
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", mergeNodeSeq.Tag)
|
||||
}
|
||||
itemsToAdd := mergeNodeSeq.FilterMapContentByKey(func(keyNode *CandidateNode) bool {
|
||||
return getContentValueByKey(node.Content, keyNode.Value) == nil &&
|
||||
getContentValueByKey(newContent, keyNode.Value) == nil
|
||||
})
|
||||
newContent = append(newContent, itemsToAdd...)
|
||||
|
||||
for _, item := range itemsToAdd {
|
||||
// copy to ensure exploding doesn't modify the original node
|
||||
itemCopy := item.Copy()
|
||||
if err := explodeNode(itemCopy, Context{}); err != nil {
|
||||
return err
|
||||
}
|
||||
newContent = append(newContent, itemCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +273,10 @@ func explodeNode(node *CandidateNode, context Context) error {
|
||||
if ConfiguredYamlPreferences.FixMergeAnchorToSpec {
|
||||
return fixedReconstructAliasedMap(node)
|
||||
}
|
||||
log.Warning("--yaml-fix-merge-anchor-to-spec is false; causing merge anchors to override the existing values which isn't to the yaml spec. This flag will default to true in late 2025.")
|
||||
if showMergeAnchorToSpecWarning {
|
||||
log.Warning("--yaml-fix-merge-anchor-to-spec is false; causing merge anchors to override the existing values which isn't to the yaml spec. This flag will default to true in late 2025.")
|
||||
showMergeAnchorToSpecWarning = false
|
||||
}
|
||||
// this is a slow op, which is why we want to check before running it.
|
||||
return reconstructAliasedMap(node, context)
|
||||
}
|
||||
@@ -283,7 +306,7 @@ func applyAlias(node *CandidateNode, alias *CandidateNode, aliasIndex int, newCo
|
||||
}
|
||||
log.Debug("alias: %v", NodeToString(alias))
|
||||
if alias.Kind != MappingNode {
|
||||
return fmt.Errorf("merge anchor only supports maps, got %v instead", alias.Tag)
|
||||
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", alias.Tag)
|
||||
}
|
||||
for index := 0; index < len(alias.Content); index = index + 2 {
|
||||
keyNode := alias.Content[index]
|
||||
|
||||
@@ -102,31 +102,16 @@ var explodeWhenKeysExistExpected = `D0, P[], (!!map)::objects:
|
||||
|
||||
var fixedAnchorOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "merge anchor after existing keys",
|
||||
document: explodeWhenKeysExistDocument,
|
||||
expression: "explode(.)",
|
||||
expected: []string{explodeWhenKeysExistExpected},
|
||||
},
|
||||
{
|
||||
skipDoc: true, // skip doc for now, only difference is order of keys
|
||||
description: "Merge multiple maps",
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::x: 1\ny: 2\nr: 10\n"},
|
||||
},
|
||||
{
|
||||
skipDoc: true, // skip doc for now, only difference is order of keys
|
||||
description: "Override",
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\ny: 2\nx: 1\n"},
|
||||
skipDoc: true,
|
||||
description: "merge anchor after existing keys",
|
||||
subdescription: "Does not override existing keys - note the name field in the second element is still ellipse.",
|
||||
document: explodeWhenKeysExistDocument,
|
||||
expression: "explode(.)",
|
||||
expected: []string{explodeWhenKeysExistExpected},
|
||||
},
|
||||
{
|
||||
description: "FIXED: Explode with merge anchors",
|
||||
subdescription: "Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Flag will default to true in late 2025 ",
|
||||
subdescription: "Observe that foobarList.b property is still foobarList_b.",
|
||||
document: mergeDocSample,
|
||||
expression: `explode(.)`,
|
||||
expected: []string{explodeMergeAnchorsFixedExpected},
|
||||
@@ -151,6 +136,95 @@ var fixedAnchorOperatorScenarios = []expressionScenario{
|
||||
"D0, P[foobar], (!!map)::{c: foobar_c, a: foo_a, thing: foobar_thing}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "FIXED: Merge multiple maps",
|
||||
subdescription: "Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.",
|
||||
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::x: 1\ny: 2\nr: 10\n"},
|
||||
},
|
||||
{
|
||||
description: "FIXED: Override",
|
||||
subdescription: "Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.",
|
||||
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\ny: 2\nx: 1\n"},
|
||||
},
|
||||
{
|
||||
description: "Exploding inline merge anchor",
|
||||
// subdescription: "`<<` map must be exploded, otherwise `c: *b` will become invalid",
|
||||
document: `{a: {b: &b 42}, <<: {c: *b}}`,
|
||||
expression: `explode(.) | sort_keys(.)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{a: {b: 42}, c: 42}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Exploding inline merge anchor in sequence",
|
||||
subdescription: "`<<` map must be exploded, otherwise `c: *b` will become invalid",
|
||||
document: `{a: {b: &b 42}, <<: [{c: *b}]}`,
|
||||
expression: `explode(.) | sort_keys(.)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{a: {b: 42}, c: 42}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Exploding merge anchor should not explode neighbors",
|
||||
subdescription: "b must not be exploded, as `r: *a` will become invalid",
|
||||
document: `{b: &b {a: &a 42}, r: *a, c: {<<: *b}}`,
|
||||
expression: `explode(.c)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{b: &b {a: &a 42}, r: *a, c: {a: 42}}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Exploding sequence merge anchor should not explode neighbors",
|
||||
subdescription: "b must not be exploded, as `r: *a` will become invalid",
|
||||
document: `{b: &b {a: &a 42}, r: *a, c: {<<: [*b]}}`,
|
||||
expression: `explode(.c)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{b: &b {a: &a 42}, r: *a, c: {a: 42}}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Merge anchor with inline map",
|
||||
document: `{<<: {a: 42}}`,
|
||||
expression: `explode(.)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{a: 42}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Merge anchor with sequence with inline map",
|
||||
document: `{<<: [{a: 42}]}`,
|
||||
expression: `explode(.)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::{a: 42}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Merge anchor with aliased sequence with inline map",
|
||||
document: `{s: &s [{a: 42}], m: {<<: *s}}`,
|
||||
expression: `.m | explode(.)`,
|
||||
expected: []string{
|
||||
"D0, P[m], (!!map)::{a: 42}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "deleting after explode",
|
||||
document: "x: 37\na: &a\n b: 42\n<<: *a",
|
||||
expression: `explode(.) | del(.x)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::a:\n b: 42\nb: 42\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var badAnchorOperatorScenarios = []expressionScenario{
|
||||
@@ -161,23 +235,9 @@ var badAnchorOperatorScenarios = []expressionScenario{
|
||||
expression: "explode(.)",
|
||||
expected: []string{explodeWhenKeysExistLegacy},
|
||||
},
|
||||
{
|
||||
description: "Merge multiple maps", // functionally correct, but key order gets mangled
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
|
||||
},
|
||||
{
|
||||
description: "Override", // functionally correct, but key order gets mangled
|
||||
subdescription: "see https://yaml.org/type/merge.html",
|
||||
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
|
||||
},
|
||||
{
|
||||
description: "LEGACY: Explode with merge anchors", // incorrect overrides
|
||||
subdescription: "Caution: this is for when --yaml-fix-merge-anchor-to-spec=false; it's not to YAML spec because the merge anchors incorrectly override the object values. Flag will default to true in late 2025",
|
||||
subdescription: "Caution: this is for when --yaml-fix-merge-anchor-to-spec=false; it's not to YAML spec because the merge anchors incorrectly override the object values (foobarList.b is set to bar_b when it should still be foobarList_b). Flag will default to true in late 2025",
|
||||
document: mergeDocSample,
|
||||
expression: `explode(.)`,
|
||||
expected: []string{explodeMergeAnchorsExpected},
|
||||
@@ -202,6 +262,21 @@ var badAnchorOperatorScenarios = []expressionScenario{
|
||||
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "LEGACY: Merge multiple maps",
|
||||
subdescription: "see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.",
|
||||
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
|
||||
},
|
||||
{
|
||||
description: "LEGACY: Override",
|
||||
subdescription: "see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.",
|
||||
|
||||
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
|
||||
},
|
||||
}
|
||||
|
||||
var anchorOperatorScenarios = []expressionScenario{
|
||||
@@ -216,7 +291,7 @@ var anchorOperatorScenarios = []expressionScenario{
|
||||
skipDoc: true,
|
||||
description: "merge anchor not map",
|
||||
document: "a: &a\n - 0\nc:\n <<: [*a]\n",
|
||||
expectedError: "merge anchor only supports maps, got !!seq instead",
|
||||
expectedError: "can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing !!seq",
|
||||
expression: "explode(.)",
|
||||
},
|
||||
{
|
||||
@@ -226,6 +301,7 @@ var anchorOperatorScenarios = []expressionScenario{
|
||||
expression: ".[4] | explode(.)",
|
||||
expected: []string{expectedSpecResult},
|
||||
},
|
||||
|
||||
{
|
||||
description: "Get anchor",
|
||||
document: `a: &billyBob cat`,
|
||||
@@ -398,9 +474,29 @@ var anchorOperatorScenarios = []expressionScenario{
|
||||
description: "Dereference and update a field",
|
||||
subdescription: "Use explode with multiply to dereference an object",
|
||||
document: simpleArrayRef,
|
||||
expression: `.thingOne |= explode(.) * {"value": false}`,
|
||||
expression: `.thingOne |= (explode(.) | sort_keys(.)) * {"value": false}`,
|
||||
expected: []string{expectedUpdatedArrayRef},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Duplicate keys",
|
||||
subdescription: "outside merge anchor",
|
||||
document: `{a: 1, a: 2}`,
|
||||
expression: `explode(.)`,
|
||||
expected: []string{
|
||||
// {a: 2} would also be fine
|
||||
"D0, P[], (!!map)::{a: 1, a: 2}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "!!str << should not be treated as merge anchor",
|
||||
document: `{!!str <<: {a: 37}}`,
|
||||
expression: `explode(.).a`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!null)::null\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestAnchorAliasOperatorScenarios(t *testing.T) {
|
||||
@@ -414,7 +510,10 @@ func TestAnchorAliasOperatorAlignedToSpecScenarios(t *testing.T) {
|
||||
ConfiguredYamlPreferences.FixMergeAnchorToSpec = true
|
||||
for _, tt := range append(fixedAnchorOperatorScenarios, anchorOperatorScenarios...) {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
|
||||
for i, tt := range fixedAnchorOperatorScenarios {
|
||||
fixedAnchorOperatorScenarios[i].subdescription = "Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).\n" + tt.subdescription
|
||||
}
|
||||
appendOperatorDocumentScenario(t, "anchor-and-alias-operators", fixedAnchorOperatorScenarios)
|
||||
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
|
||||
|
||||
@@ -17,6 +17,9 @@ type envOpPreferences struct {
|
||||
}
|
||||
|
||||
func envOperator(_ *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
if ConfiguredSecurityPreferences.DisableEnvOps {
|
||||
return Context{}, fmt.Errorf("env operations have been disabled")
|
||||
}
|
||||
envName := expressionNode.Operation.CandidateNode.Value
|
||||
log.Debug("EnvOperator, env name:", envName)
|
||||
|
||||
@@ -29,7 +32,7 @@ func envOperator(_ *dataTreeNavigator, context Context, expressionNode *Expressi
|
||||
node = &CandidateNode{
|
||||
Kind: ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: rawValue,
|
||||
Value: processEscapeCharacters(rawValue),
|
||||
}
|
||||
} else if rawValue == "" {
|
||||
return Context{}, fmt.Errorf("value for env variable '%v' not provided in env()", envName)
|
||||
@@ -54,6 +57,9 @@ func envOperator(_ *dataTreeNavigator, context Context, expressionNode *Expressi
|
||||
}
|
||||
|
||||
func envsubstOperator(_ *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
if ConfiguredSecurityPreferences.DisableEnvOps {
|
||||
return Context{}, fmt.Errorf("env operations have been disabled")
|
||||
}
|
||||
var results = list.New()
|
||||
preferences := envOpPreferences{}
|
||||
if expressionNode.Operation.Preferences != nil {
|
||||
|
||||
@@ -63,6 +63,78 @@ var envOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], ()::a: \"12\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with newline escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\n"},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: |\n string with a\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with tab escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\t"},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: \"string with a\\t\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with carriage return escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\r"},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: \"string with a\\r\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with form feed escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\f"},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: \"string with a\\f\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with vertical tab escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\v"},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: \"string with a\\v\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with backspace escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\b"},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: \"string with a\\b\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with alert/bell escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\a"},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: \"string with a\\a\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "strenv with double quote escape",
|
||||
skipDoc: true,
|
||||
environmentVariables: map[string]string{"myenv": "string with a\\\""},
|
||||
expression: `.a = strenv(myenv)`,
|
||||
expected: []string{
|
||||
"D0, P[], ()::a: string with a\"\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Dynamically update a path from an environment variable",
|
||||
subdescription: "The env variable can be any valid yq expression.",
|
||||
@@ -178,3 +250,40 @@ func TestEnvOperatorScenarios(t *testing.T) {
|
||||
}
|
||||
documentOperatorScenarios(t, "env-variable-operators", envOperatorScenarios)
|
||||
}
|
||||
|
||||
var envOperatorSecurityDisabledScenarios = []expressionScenario{
|
||||
{
|
||||
description: "env() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-env-ops` to disable env operations for security.",
|
||||
expression: `env("MYENV")`,
|
||||
expectedError: "env operations have been disabled",
|
||||
},
|
||||
{
|
||||
description: "strenv() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-env-ops` to disable env operations for security.",
|
||||
expression: `strenv("MYENV")`,
|
||||
expectedError: "env operations have been disabled",
|
||||
},
|
||||
{
|
||||
description: "envsubst() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-env-ops` to disable env operations for security.",
|
||||
expression: `"value: ${MYENV}" | envsubst`,
|
||||
expectedError: "env operations have been disabled",
|
||||
},
|
||||
}
|
||||
|
||||
func TestEnvOperatorSecurityDisabledScenarios(t *testing.T) {
|
||||
// Save original security preferences
|
||||
originalDisableEnvOps := ConfiguredSecurityPreferences.DisableEnvOps
|
||||
defer func() {
|
||||
ConfiguredSecurityPreferences.DisableEnvOps = originalDisableEnvOps
|
||||
}()
|
||||
|
||||
// Test that env() fails when DisableEnvOps is true
|
||||
ConfiguredSecurityPreferences.DisableEnvOps = true
|
||||
|
||||
for _, tt := range envOperatorSecurityDisabledScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
appendOperatorDocumentScenario(t, "env-variable-operators", envOperatorSecurityDisabledScenarios)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package yqlib
|
||||
|
||||
import "container/list"
|
||||
|
||||
func firstOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
results := list.New()
|
||||
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
candidate := el.Value.(*CandidateNode)
|
||||
|
||||
// If no RHS expression is provided, simply return the first entry in candidate.Content
|
||||
if expressionNode == nil || expressionNode.RHS == nil {
|
||||
if len(candidate.Content) > 0 {
|
||||
results.PushBack(candidate.Content[0])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
splatted, err := splat(context.SingleChildContext(candidate), traversePreferences{})
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
for splatEl := splatted.MatchingNodes.Front(); splatEl != nil; splatEl = splatEl.Next() {
|
||||
splatCandidate := splatEl.Value.(*CandidateNode)
|
||||
// Create a new context for this splatted candidate
|
||||
splatContext := context.SingleChildContext(splatCandidate)
|
||||
// Evaluate the RHS expression against this splatted candidate
|
||||
rhs, err := d.GetMatchingNodes(splatContext, expressionNode.RHS)
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
includeResult := false
|
||||
|
||||
for resultEl := rhs.MatchingNodes.Front(); resultEl != nil; resultEl = resultEl.Next() {
|
||||
result := resultEl.Value.(*CandidateNode)
|
||||
includeResult = isTruthyNode(result)
|
||||
if includeResult {
|
||||
break
|
||||
}
|
||||
}
|
||||
if includeResult {
|
||||
results.PushBack(splatCandidate)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return context.ChildContext(results), nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package yqlib
|
||||
|
||||
import "testing"
|
||||
|
||||
var firstOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "First matching element from array",
|
||||
document: "[{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with multiple matches",
|
||||
document: "[{a: banana},{a: cat, b: firstCat},{a: apple},{a: cat, b: secondCat}]",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat, b: firstCat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with numeric condition",
|
||||
document: "[{a: 10},{a: 100},{a: 1},{a: 101}]",
|
||||
expression: `first(.a > 50)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: 100}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with boolean condition",
|
||||
document: "[{a: false},{a: true, b: firstTrue},{a: false}, {a: true, b: secondTrue}]",
|
||||
expression: `first(.a == true)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: true, b: firstTrue}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with null values",
|
||||
document: "[{a: null},{a: cat},{a: apple}]",
|
||||
expression: `first(.a != null)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array with complex condition",
|
||||
document: "[{a: dog, b: 7},{a: cat, b: 3},{a: apple, b: 5}]",
|
||||
expression: `first(.b > 4 and .b < 6)`,
|
||||
expected: []string{
|
||||
"D0, P[2], (!!map)::{a: apple, b: 5}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from map",
|
||||
document: "x: {a: banana}\ny: {a: cat}\nz: {a: apple}",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[y], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from map with numeric condition",
|
||||
document: "x: {a: 10}\ny: {a: 100}\nz: {a: 101}",
|
||||
expression: `first(.a > 50)`,
|
||||
expected: []string{
|
||||
"D0, P[y], (!!map)::{a: 100}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from nested structure",
|
||||
document: "items: [{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `.items | first(.a == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[items 1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element with no matches",
|
||||
document: "[{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `first(.a == "dog")`,
|
||||
expected: []string{
|
||||
// No output expected when no matches
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from empty array",
|
||||
document: "[]",
|
||||
expression: `first(.a == "cat")`,
|
||||
expected: []string{
|
||||
// No output expected when array is empty
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from scalar node",
|
||||
document: "hello",
|
||||
expression: `first(. == "hello")`,
|
||||
expected: []string{
|
||||
// No output expected when node is scalar (no content to splat)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from null node",
|
||||
document: "null",
|
||||
expression: `first(. == "hello")`,
|
||||
expected: []string{
|
||||
// No output expected when node is null (no content to splat)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element with string condition",
|
||||
document: "[{a: banana},{a: cat},{a: apple}]",
|
||||
expression: `first(.a | test("^c"))`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element with length condition",
|
||||
document: "[{a: hi},{a: hello},{a: world}]",
|
||||
expression: `first(.a | length > 4)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!map)::{a: hello}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array of strings",
|
||||
document: "[banana, cat, apple]",
|
||||
expression: `first(. == "cat")`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!str)::cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First matching element from array of numbers",
|
||||
document: "[10, 100, 1]",
|
||||
expression: `first(. > 50)`,
|
||||
expected: []string{
|
||||
"D0, P[1], (!!int)::100\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First element with no filter from array",
|
||||
document: "[10, 100, 1]",
|
||||
expression: `first`,
|
||||
expected: []string{
|
||||
"D0, P[0], (!!int)::10\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "First element with no filter from array of maps",
|
||||
document: "[{a: 10},{a: 100}]",
|
||||
expression: `first`,
|
||||
expected: []string{
|
||||
"D0, P[0], (!!map)::{a: 10}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "No filter on empty array returns nothing",
|
||||
skipDoc: true,
|
||||
document: "[]",
|
||||
expression: `first`,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "No filter on scalar returns nothing",
|
||||
skipDoc: true,
|
||||
document: "hello",
|
||||
expression: `first`,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "No filter on null returns nothing",
|
||||
skipDoc: true,
|
||||
document: "null",
|
||||
expression: `first`,
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
func TestFirstOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range firstOperatorScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "first", firstOperatorScenarios)
|
||||
}
|
||||
@@ -63,6 +63,9 @@ func loadWithDecoder(filename string, decoder Decoder) (*CandidateNode, error) {
|
||||
|
||||
func loadStringOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
log.Debugf("loadString")
|
||||
if ConfiguredSecurityPreferences.DisableFileOps {
|
||||
return Context{}, fmt.Errorf("file operations have been disabled")
|
||||
}
|
||||
|
||||
var results = list.New()
|
||||
|
||||
@@ -94,6 +97,9 @@ func loadStringOperator(d *dataTreeNavigator, context Context, expressionNode *E
|
||||
|
||||
func loadOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
log.Debugf("loadOperator")
|
||||
if ConfiguredSecurityPreferences.DisableFileOps {
|
||||
return Context{}, fmt.Errorf("file operations have been disabled")
|
||||
}
|
||||
|
||||
loadPrefs := expressionNode.Operation.Preferences.(loadPrefs)
|
||||
|
||||
|
||||
@@ -131,3 +131,52 @@ func TestLoadScenarios(t *testing.T) {
|
||||
}
|
||||
documentOperatorScenarios(t, "load", loadScenarios)
|
||||
}
|
||||
|
||||
var loadOperatorSecurityDisabledScenarios = []expressionScenario{
|
||||
{
|
||||
description: "load() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-file-ops` to disable file operations for security.",
|
||||
expression: `load("../../examples/thing.yml")`,
|
||||
expectedError: "file operations have been disabled",
|
||||
},
|
||||
{
|
||||
description: "load_str() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-file-ops` to disable file operations for security.",
|
||||
expression: `load_str("../../examples/thing.yml")`,
|
||||
expectedError: "file operations have been disabled",
|
||||
},
|
||||
{
|
||||
description: "load_xml() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-file-ops` to disable file operations for security.",
|
||||
expression: `load_xml("../../examples/small.xml")`,
|
||||
expectedError: "file operations have been disabled",
|
||||
},
|
||||
{
|
||||
description: "load_props() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-file-ops` to disable file operations for security.",
|
||||
expression: `load_props("../../examples/small.properties")`,
|
||||
expectedError: "file operations have been disabled",
|
||||
},
|
||||
{
|
||||
description: "load_base64() operation fails when security is enabled",
|
||||
subdescription: "Use `--security-disable-file-ops` to disable file operations for security.",
|
||||
expression: `load_base64("../../examples/base64.txt")`,
|
||||
expectedError: "file operations have been disabled",
|
||||
},
|
||||
}
|
||||
|
||||
func TestLoadOperatorSecurityDisabledScenarios(t *testing.T) {
|
||||
// Save original security preferences
|
||||
originalDisableFileOps := ConfiguredSecurityPreferences.DisableFileOps
|
||||
defer func() {
|
||||
ConfiguredSecurityPreferences.DisableFileOps = originalDisableFileOps
|
||||
}()
|
||||
|
||||
// Test that load operations fail when DisableFileOps is true
|
||||
ConfiguredSecurityPreferences.DisableFileOps = true
|
||||
|
||||
for _, tt := range loadOperatorSecurityDisabledScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
appendOperatorDocumentScenario(t, "load", loadOperatorSecurityDisabledScenarios)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,26 @@ type parentOpPreferences struct {
|
||||
Level int
|
||||
}
|
||||
|
||||
func getParentsOperator(_ *dataTreeNavigator, context Context, _ *ExpressionNode) (Context, error) {
|
||||
log.Debugf("getParentsOperator")
|
||||
|
||||
var results = list.New()
|
||||
|
||||
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
|
||||
candidate := el.Value.(*CandidateNode)
|
||||
parentsList := &CandidateNode{Kind: SequenceNode, Tag: "!!seq"}
|
||||
parent := candidate.Parent
|
||||
for parent != nil {
|
||||
parentsList.AddChild(parent)
|
||||
parent = parent.Parent
|
||||
}
|
||||
results.PushBack(parentsList)
|
||||
}
|
||||
|
||||
return context.ChildContext(results), nil
|
||||
|
||||
}
|
||||
|
||||
func getParentOperator(_ *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
log.Debugf("getParentOperator")
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ var parentOperatorScenarios = []expressionScenario{
|
||||
"D0, P[b name], (!!str)::sam\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Get parents",
|
||||
subdescription: "Match all parents",
|
||||
document: "{a: {b: {c: cat} } }",
|
||||
expression: `.a.b.c | parents`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!seq)::- {c: cat}\n- {b: {c: cat}}\n- {a: {b: {c: cat}}}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "N-th parent",
|
||||
subdescription: "You can optionally supply the number of levels to go up for the parent, the default being 1.",
|
||||
|
||||
@@ -3,6 +3,7 @@ package yqlib
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/elliotchance/orderedmap"
|
||||
)
|
||||
@@ -265,26 +266,58 @@ func doTraverseMap(newMatches *orderedmap.OrderedMap, node *CandidateNode, wante
|
||||
// if we don't find a match directly on this node first.
|
||||
|
||||
var contents = node.Content
|
||||
|
||||
if !prefs.DontFollowAlias {
|
||||
if ConfiguredYamlPreferences.FixMergeAnchorToSpec {
|
||||
// First evaluate merge keys to make explicit keys take precedence, following spec
|
||||
// We also iterate in reverse to make earlier merge keys take precedence,
|
||||
// although normally there's just one '<<'
|
||||
for index := len(node.Content) - 2; index >= 0; index -= 2 {
|
||||
keyNode := node.Content[index]
|
||||
valueNode := node.Content[index+1]
|
||||
if keyNode.Tag == "!!merge" {
|
||||
log.Debug("Merge anchor")
|
||||
err := traverseMergeAnchor(newMatches, valueNode, wantedKey, prefs, splat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for index := 0; index+1 < len(contents); index = index + 2 {
|
||||
key := contents[index]
|
||||
value := contents[index+1]
|
||||
|
||||
//skip the 'merge' tag, find a direct match first
|
||||
if key.Tag == "!!merge" && !prefs.DontFollowAlias && wantedKey != "<<" {
|
||||
log.Debug("Merge anchor")
|
||||
err := traverseMergeAnchor(newMatches, value, wantedKey, prefs, splat)
|
||||
if err != nil {
|
||||
return err
|
||||
if key.Tag == "!!merge" && !prefs.DontFollowAlias && wantedKey != key.Value {
|
||||
if !ConfiguredYamlPreferences.FixMergeAnchorToSpec {
|
||||
log.Debug("Merge anchor")
|
||||
if showMergeAnchorToSpecWarning {
|
||||
log.Warning("--yaml-fix-merge-anchor-to-spec is false; causing merge anchors to override the existing values which isn't to the yaml spec. This flag will default to true in late 2025. See https://mikefarah.gitbook.io/yq/operators/traverse-read for more details.")
|
||||
showMergeAnchorToSpecWarning = false
|
||||
}
|
||||
err := traverseMergeAnchor(newMatches, value, wantedKey, prefs, splat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if splat || keyMatches(key, wantedKey) {
|
||||
log.Debug("MATCHED")
|
||||
if prefs.IncludeMapKeys {
|
||||
log.Debug("including key")
|
||||
newMatches.Set(key.GetKey(), key)
|
||||
keyName := key.GetKey()
|
||||
if !newMatches.Set(keyName, key) {
|
||||
log.Debug("overwriting existing key")
|
||||
}
|
||||
}
|
||||
if !prefs.DontIncludeMapValues {
|
||||
log.Debug("including value")
|
||||
newMatches.Set(value.GetKey(), value)
|
||||
valueName := value.GetKey()
|
||||
if !newMatches.Set(valueName, value) {
|
||||
log.Debug("overwriting existing value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,22 +325,39 @@ func doTraverseMap(newMatches *orderedmap.OrderedMap, node *CandidateNode, wante
|
||||
return nil
|
||||
}
|
||||
|
||||
func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, value *CandidateNode, wantedKey string, prefs traversePreferences, splat bool) error {
|
||||
switch value.Kind {
|
||||
case AliasNode:
|
||||
if value.Alias.Kind != MappingNode {
|
||||
return fmt.Errorf("can only use merge anchors with maps (!!map), but got %v", value.Alias.Tag)
|
||||
}
|
||||
return doTraverseMap(newMatches, value.Alias, wantedKey, prefs, splat)
|
||||
func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, merge *CandidateNode, wantedKey string, prefs traversePreferences, splat bool) error {
|
||||
if merge.Kind == AliasNode {
|
||||
merge = merge.Alias
|
||||
}
|
||||
switch merge.Kind {
|
||||
case MappingNode:
|
||||
return doTraverseMap(newMatches, merge, wantedKey, prefs, splat)
|
||||
case SequenceNode:
|
||||
for _, childValue := range value.Content {
|
||||
err := traverseMergeAnchor(newMatches, childValue, wantedKey, prefs, splat)
|
||||
content := slices.All(merge.Content)
|
||||
if ConfiguredYamlPreferences.FixMergeAnchorToSpec {
|
||||
// Reverse to make earlier values take precedence, following spec
|
||||
content = slices.Backward(merge.Content)
|
||||
}
|
||||
for _, childValue := range content {
|
||||
if childValue.Kind == AliasNode {
|
||||
childValue = childValue.Alias
|
||||
}
|
||||
if childValue.Kind != MappingNode {
|
||||
log.Debugf(
|
||||
"can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v",
|
||||
childValue.Tag)
|
||||
return nil
|
||||
}
|
||||
err := doTraverseMap(newMatches, childValue, wantedKey, prefs, splat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
log.Debugf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got %v", merge.Tag)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func traverseArray(candidate *CandidateNode, operation *Operation, prefs traversePreferences) (*list.List, error) {
|
||||
|
||||
@@ -25,14 +25,111 @@ foobar:
|
||||
thing: foobar_thing
|
||||
`
|
||||
|
||||
// cannot use merge anchors with arrays
|
||||
var badAliasSample = `
|
||||
_common: &common-docker-file
|
||||
- FROM ubuntu:18.04
|
||||
var fixedTraversePathOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "FIXED: Traversing merge anchors with override",
|
||||
subdescription: "Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour.",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobar.c`,
|
||||
expected: []string{
|
||||
"D0, P[foobar c], (!!str)::foobar_c\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "FIXED: Traversing merge anchor lists",
|
||||
subdescription: "Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList.thing`,
|
||||
expected: []string{
|
||||
"D0, P[foo thing], (!!str)::foo_thing\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "FIXED: Splatting merge anchors",
|
||||
subdescription: "Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobar[]`,
|
||||
expected: []string{
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
"D0, P[foobar thing], (!!str)::foobar_thing\n",
|
||||
"D0, P[foobar c], (!!str)::foobar_c\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "FIXED: Splatting merge anchor lists",
|
||||
subdescription: "Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList[]`,
|
||||
expected: []string{
|
||||
"D0, P[foobarList b], (!!str)::foobarList_b\n",
|
||||
"D0, P[foo thing], (!!str)::foo_thing\n",
|
||||
"D0, P[foobarList c], (!!str)::foobarList_c\n",
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList.b`,
|
||||
expected: []string{
|
||||
"D0, P[foobarList b], (!!str)::foobarList_b\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
steps:
|
||||
<<: *common-docker-file
|
||||
`
|
||||
var badTraversePathOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
description: "LEGACY: Traversing merge anchors with override",
|
||||
subdescription: "This is legacy behaviour, see --yaml-fix-merge-anchor-to-spec",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobar.c`,
|
||||
expected: []string{
|
||||
"D0, P[foo c], (!!str)::foo_c\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "LEGACY: Traversing merge anchor lists",
|
||||
subdescription: "Note that the later merge anchors override previous, " +
|
||||
"but this is legacy behaviour, see --yaml-fix-merge-anchor-to-spec",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList.thing`,
|
||||
expected: []string{
|
||||
"D0, P[bar thing], (!!str)::bar_thing\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "LEGACY: Splatting merge anchors",
|
||||
subdescription: "With legacy override behaviour, see --yaml-fix-merge-anchor-to-spec",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobar[]`,
|
||||
expected: []string{
|
||||
"D0, P[foo c], (!!str)::foo_c\n",
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
"D0, P[foobar thing], (!!str)::foobar_thing\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "LEGACY: Splatting merge anchor lists",
|
||||
subdescription: "With legacy override behaviour, see --yaml-fix-merge-anchor-to-spec",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList[]`,
|
||||
expected: []string{
|
||||
"D0, P[bar b], (!!str)::bar_b\n",
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
"D0, P[bar thing], (!!str)::bar_thing\n",
|
||||
"D0, P[foobarList c], (!!str)::foobarList_c\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
subdescription: "This is legacy behaviour, see --yaml-fix-merge-anchor-to-spec",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList.b`,
|
||||
expected: []string{
|
||||
"D0, P[bar b], (!!str)::bar_b\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var traversePathOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
@@ -365,6 +462,33 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[0], (!!null)::null\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Merge anchor with inline map",
|
||||
document: `{<<: {a: 42}}`,
|
||||
expression: `.a`,
|
||||
expected: []string{
|
||||
"D0, P[<< a], (!!int)::42\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Merge anchor with sequence with inline map",
|
||||
document: `{<<: [{a: 42}]}`,
|
||||
expression: `.a`,
|
||||
expected: []string{
|
||||
"D0, P[<< 0 a], (!!int)::42\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Merge anchor with aliased sequence with inline map",
|
||||
document: `{s: &s [{a: 42}], m: {<<: *s}}`,
|
||||
expression: `.m.a`,
|
||||
expected: []string{
|
||||
"D0, P[s 0 a], (!!int)::42\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: mergeDocSample,
|
||||
@@ -381,14 +505,6 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Traversing merge anchors with override",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobar.c`,
|
||||
expected: []string{
|
||||
"D0, P[foo c], (!!str)::foo_c\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Traversing merge anchors with local override",
|
||||
document: mergeDocSample,
|
||||
@@ -397,16 +513,6 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[foobar thing], (!!str)::foobar_thing\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Splatting merge anchors",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobar[]`,
|
||||
expected: []string{
|
||||
"D0, P[foo c], (!!str)::foo_c\n",
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
"D0, P[foobar thing], (!!str)::foobar_thing\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: mergeDocSample,
|
||||
@@ -423,15 +529,6 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Traversing merge anchor lists",
|
||||
subdescription: "Note that the later merge anchors override previous",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList.thing`,
|
||||
expected: []string{
|
||||
"D0, P[bar thing], (!!str)::bar_thing\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: mergeDocSample,
|
||||
@@ -440,25 +537,6 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
"D0, P[foobarList c], (!!str)::foobarList_c\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList.b`,
|
||||
expected: []string{
|
||||
"D0, P[bar b], (!!str)::bar_b\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Splatting merge anchor lists",
|
||||
document: mergeDocSample,
|
||||
expression: `.foobarList[]`,
|
||||
expected: []string{
|
||||
"D0, P[bar b], (!!str)::bar_b\n",
|
||||
"D0, P[foo a], (!!str)::foo_a\n",
|
||||
"D0, P[bar thing], (!!str)::bar_thing\n",
|
||||
"D0, P[foobarList c], (!!str)::foobarList_c\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[a,b,c]`,
|
||||
@@ -550,17 +628,57 @@ var traversePathOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: badAliasSample,
|
||||
expression: ".steps[]",
|
||||
expectedError: "can only use merge anchors with maps (!!map), but got !!seq",
|
||||
skipForGoccy: true, // throws an error on parsing, that's fine
|
||||
skipDoc: true,
|
||||
description: "Duplicate keys",
|
||||
subdescription: "outside merge anchor",
|
||||
document: `{a: 1, a: 2}`,
|
||||
expression: `.a`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!int)::2\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Traversing map with invalid merge anchor should not fail",
|
||||
subdescription: "Otherwise code cannot do anything with it",
|
||||
document: `{a: 42, <<: 37}`,
|
||||
expression: `.a`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!int)::42\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Directly accessing invalid merge anchor should not fail",
|
||||
document: `{<<: 37}`,
|
||||
expression: `.<<`,
|
||||
expected: []string{
|
||||
"D0, P[<<], (!!int)::37\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "!!str << should not be treated as merge anchor",
|
||||
document: `{!!str <<: {a: 37}}`,
|
||||
expression: `.a`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!null)::null\n",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestTraversePathOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range traversePathOperatorScenarios {
|
||||
for _, tt := range append(traversePathOperatorScenarios, badTraversePathOperatorScenarios...) {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "traverse-read", traversePathOperatorScenarios)
|
||||
documentOperatorScenarios(t, "traverse-read", append(traversePathOperatorScenarios, badTraversePathOperatorScenarios...))
|
||||
}
|
||||
|
||||
func TestTraversePathOperatorAlignedToSpecScenarios(t *testing.T) {
|
||||
ConfiguredYamlPreferences.FixMergeAnchorToSpec = true
|
||||
for _, tt := range append(fixedTraversePathOperatorScenarios, traversePathOperatorScenarios...) {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
appendOperatorDocumentScenario(t, "traverse-read", fixedTraversePathOperatorScenarios)
|
||||
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func documentScenarios(t *testing.T, folder string, title string, scenarios []in
|
||||
|
||||
func appendOperatorDocumentScenario(t *testing.T, title string, scenarios []expressionScenario) {
|
||||
filename := fmt.Sprintf("doc/%v/%v.md", "operators", title)
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND, fs.ModeAppend)
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR|os.O_APPEND, fs.ModeAppend)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
@@ -385,13 +385,13 @@ func documentOutput(t *testing.T, w *bufio.Writer, s expressionScenario, formatt
|
||||
|
||||
inputs, err = readDocument(formattedDoc, "sample.yml", 0)
|
||||
if err != nil {
|
||||
t.Error(err, s.document, s.expression)
|
||||
t.Error(err, formattedDoc, "exp: "+s.expression)
|
||||
return
|
||||
}
|
||||
if s.document2 != "" {
|
||||
moreInputs, err := readDocument(formattedDoc2, "another.yml", 1)
|
||||
if err != nil {
|
||||
t.Error(err, s.document, s.expression)
|
||||
t.Error(err, formattedDoc2, "exp: "+s.expression)
|
||||
return
|
||||
}
|
||||
inputs.PushBackList(moreInputs)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"container/list"
|
||||
"io"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
type nodeInfoPrinter struct {
|
||||
|
||||
@@ -414,3 +414,100 @@ func TestPrinterRootUnwrap(t *testing.T) {
|
||||
`
|
||||
test.AssertResult(t, expected, output.String())
|
||||
}
|
||||
|
||||
func TestRemoveLastEOL(t *testing.T) {
|
||||
// Test with \r\n
|
||||
buffer := bytes.NewBufferString("test\r\n")
|
||||
removeLastEOL(buffer)
|
||||
test.AssertResult(t, "test", buffer.String())
|
||||
|
||||
// Test with \n only
|
||||
buffer = bytes.NewBufferString("test\n")
|
||||
removeLastEOL(buffer)
|
||||
test.AssertResult(t, "test", buffer.String())
|
||||
|
||||
// Test with \r only
|
||||
buffer = bytes.NewBufferString("test\r")
|
||||
removeLastEOL(buffer)
|
||||
test.AssertResult(t, "test", buffer.String())
|
||||
|
||||
// Test with no EOL
|
||||
buffer = bytes.NewBufferString("test")
|
||||
removeLastEOL(buffer)
|
||||
test.AssertResult(t, "test", buffer.String())
|
||||
|
||||
// Test with empty buffer
|
||||
buffer = bytes.NewBufferString("")
|
||||
removeLastEOL(buffer)
|
||||
test.AssertResult(t, "", buffer.String())
|
||||
|
||||
// Test with multiple \r\n
|
||||
buffer = bytes.NewBufferString("line1\r\nline2\r\n")
|
||||
removeLastEOL(buffer)
|
||||
test.AssertResult(t, "line1\r\nline2", buffer.String())
|
||||
}
|
||||
|
||||
func TestPrinterPrintedAnything(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, true, 2, true)
|
||||
|
||||
test.AssertResult(t, false, printer.PrintedAnything())
|
||||
|
||||
// Print a scalar value
|
||||
node := createStringScalarNode("test")
|
||||
nodeList := nodeToList(node)
|
||||
err := printer.PrintResults(nodeList)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should now be true
|
||||
test.AssertResult(t, true, printer.PrintedAnything())
|
||||
}
|
||||
|
||||
func TestPrinterNulSeparatorWithNullChar(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, true, 2, false)
|
||||
printer.SetNulSepOutput(true)
|
||||
|
||||
// Create a node with null character
|
||||
node := createStringScalarNode("test\x00value")
|
||||
nodeList := nodeToList(node)
|
||||
|
||||
err := printer.PrintResults(nodeList)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for null character in NUL separated output")
|
||||
}
|
||||
|
||||
expectedError := "can't serialize value because it contains NUL char and you are using NUL separated output"
|
||||
if err.Error() != expectedError {
|
||||
t.Fatalf("Expected error '%s', got '%s'", expectedError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrinterSetNulSepOutput(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, true, 2, false)
|
||||
|
||||
// Test setting NUL separator output
|
||||
printer.SetNulSepOutput(true)
|
||||
test.AssertResult(t, true, true) // Placeholder assertion
|
||||
|
||||
printer.SetNulSepOutput(false)
|
||||
// Should also not cause errors
|
||||
test.AssertResult(t, false, false) // Placeholder assertion
|
||||
}
|
||||
|
||||
func TestPrinterSetAppendix(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
var writer = bufio.NewWriter(&output)
|
||||
printer := NewSimpleYamlPrinter(writer, true, 2, true)
|
||||
|
||||
// Test setting appendix
|
||||
appendix := strings.NewReader("appendix content")
|
||||
printer.SetAppendix(appendix)
|
||||
test.AssertResult(t, true, true) // Placeholder assertion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package yqlib
|
||||
|
||||
type SecurityPreferences struct {
|
||||
DisableEnvOps bool
|
||||
DisableFileOps bool
|
||||
}
|
||||
|
||||
var ConfiguredSecurityPreferences = SecurityPreferences{
|
||||
DisableEnvOps: false,
|
||||
DisableFileOps: false,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package yqlib
|
||||
|
||||
type ShellVariablesPreferences struct {
|
||||
KeySeparator string
|
||||
}
|
||||
|
||||
func NewDefaultShellVariablesPreferences() ShellVariablesPreferences {
|
||||
return ShellVariablesPreferences{
|
||||
KeySeparator: "_",
|
||||
}
|
||||
}
|
||||
|
||||
var ConfiguredShellVariablesPreferences = NewDefaultShellVariablesPreferences()
|
||||
@@ -54,12 +54,33 @@ var shellVariablesScenarios = []formatScenario{
|
||||
input: "name: Miles O'Brien",
|
||||
expected: `name='Miles O'"'"'Brien'` + "\n",
|
||||
},
|
||||
{
|
||||
description: "Encode shell variables: custom separator",
|
||||
subdescription: "Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores.",
|
||||
input: "" +
|
||||
"my_app:" + "\n" +
|
||||
" db_config:" + "\n" +
|
||||
" host: localhost" + "\n" +
|
||||
" port: 5432",
|
||||
expected: "" +
|
||||
"my_app__db_config__host=localhost" + "\n" +
|
||||
"my_app__db_config__port=5432" + "\n",
|
||||
scenarioType: "shell-separator",
|
||||
},
|
||||
}
|
||||
|
||||
func TestShellVariableScenarios(t *testing.T) {
|
||||
for _, s := range shellVariablesScenarios {
|
||||
//fmt.Printf("\t<%s> <%s>\n", s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()))
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description)
|
||||
if s.scenarioType == "shell-separator" {
|
||||
// Save and restore the original separator
|
||||
originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator
|
||||
ConfiguredShellVariablesPreferences.KeySeparator = "__"
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description)
|
||||
ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator
|
||||
} else {
|
||||
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description)
|
||||
}
|
||||
}
|
||||
genericScenarios := make([]interface{}, len(shellVariablesScenarios))
|
||||
for i, s := range shellVariablesScenarios {
|
||||
@@ -87,12 +108,22 @@ func documentShellVariableScenario(_ *testing.T, w *bufio.Writer, i interface{})
|
||||
|
||||
expression := s.expression
|
||||
|
||||
if expression != "" {
|
||||
if s.scenarioType == "shell-separator" {
|
||||
writeOrPanic(w, "```bash\nyq -o=shell --shell-key-separator=\"__\" sample.yml\n```\n")
|
||||
} else if expression != "" {
|
||||
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=shell '%v' sample.yml\n```\n", expression))
|
||||
} else {
|
||||
writeOrPanic(w, "```bash\nyq -o=shell sample.yml\n```\n")
|
||||
}
|
||||
writeOrPanic(w, "will output\n")
|
||||
|
||||
writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder())))
|
||||
if s.scenarioType == "shell-separator" {
|
||||
// Save and restore the original separator
|
||||
originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator
|
||||
ConfiguredShellVariablesPreferences.KeySeparator = "__"
|
||||
writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder())))
|
||||
ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator
|
||||
} else {
|
||||
writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ street = "first street"
|
||||
[owner]
|
||||
name = "Tom Preston-Werner"
|
||||
`
|
||||
|
||||
var expectedTableArrayBeforeOwners = `owner:
|
||||
addresses:
|
||||
- street: first street
|
||||
@@ -36,6 +37,78 @@ owner:
|
||||
age: 36
|
||||
`
|
||||
|
||||
var doubleArrayTable = `
|
||||
[[fruits]]
|
||||
name = "apple"
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious"`
|
||||
|
||||
var doubleArrayTableExpected = `fruits:
|
||||
- name: apple
|
||||
varieties:
|
||||
- name: red delicious
|
||||
`
|
||||
|
||||
var doubleArrayTableMultipleEntries = `
|
||||
[[fruits]]
|
||||
name = "banana"
|
||||
[[fruits]]
|
||||
name = "apple"
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious"`
|
||||
|
||||
var doubleArrayTableMultipleEntriesExpected = `fruits:
|
||||
- name: banana
|
||||
- name: apple
|
||||
varieties:
|
||||
- name: red delicious
|
||||
`
|
||||
|
||||
var doubleArrayTableNothingAbove = `
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious"`
|
||||
|
||||
var doubleArrayTableNothingAboveExpected = `fruits:
|
||||
varieties:
|
||||
- name: red delicious
|
||||
`
|
||||
|
||||
var doubleArrayTableEmptyAbove = `
|
||||
[[fruits]]
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious"`
|
||||
|
||||
var doubleArrayTableEmptyAboveExpected = `fruits:
|
||||
- varieties:
|
||||
- name: red delicious
|
||||
`
|
||||
|
||||
var emptyArrayTableThenTable = `
|
||||
[[fruits]]
|
||||
[animals]
|
||||
[[fruits.varieties]] # nested array of tables
|
||||
name = "red delicious"`
|
||||
|
||||
var emptyArrayTableThenTableExpected = `fruits:
|
||||
- varieties:
|
||||
- name: red delicious
|
||||
animals: {}
|
||||
`
|
||||
|
||||
var arrayTableThenArray = `
|
||||
[[rootA.kidB]]
|
||||
cat = "meow"
|
||||
|
||||
[rootA.kidB.kidC]
|
||||
dog = "bark"`
|
||||
|
||||
var arrayTableThenArrayExpected = `rootA:
|
||||
kidB:
|
||||
- cat: meow
|
||||
kidC:
|
||||
dog: bark
|
||||
`
|
||||
|
||||
var sampleArrayTable = `
|
||||
[owner.contact]
|
||||
name = "Tom Preston-Werner"
|
||||
@@ -67,6 +140,29 @@ var emptyTable = `
|
||||
|
||||
var emptyTableExpected = "dependencies: {}\n"
|
||||
|
||||
var multipleEmptyTables = `
|
||||
[firstEmptyTable]
|
||||
[firstTableWithContent]
|
||||
key = "value"
|
||||
[secondEmptyTable]
|
||||
[thirdEmptyTable]
|
||||
[secondTableWithContent]
|
||||
key = "value"
|
||||
[fourthEmptyTable]
|
||||
[fifthEmptyTable]
|
||||
`
|
||||
|
||||
var expectedMultipleEmptyTables = `firstEmptyTable: {}
|
||||
firstTableWithContent:
|
||||
key: value
|
||||
secondEmptyTable: {}
|
||||
thirdEmptyTable: {}
|
||||
secondTableWithContent:
|
||||
key: value
|
||||
fourthEmptyTable: {}
|
||||
fifthEmptyTable: {}
|
||||
`
|
||||
|
||||
var sampleWithHeader = `
|
||||
[servers]
|
||||
|
||||
@@ -225,6 +321,47 @@ var tomlScenarios = []formatScenario{
|
||||
expected: sampleArrayTableExpected,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Parse: Array of Array Table",
|
||||
input: doubleArrayTable,
|
||||
expected: doubleArrayTableExpected,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Parse: Array of Array Table; nothing above",
|
||||
input: doubleArrayTableNothingAbove,
|
||||
expected: doubleArrayTableNothingAboveExpected,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Parse: Array of Array Table; empty above",
|
||||
input: doubleArrayTableEmptyAbove,
|
||||
expected: doubleArrayTableEmptyAboveExpected,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Parse: Array of Array Table; multiple entries",
|
||||
input: doubleArrayTableMultipleEntries,
|
||||
expected: doubleArrayTableMultipleEntriesExpected,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Parse: Array of Array Table; then table; then array table",
|
||||
input: emptyArrayTableThenTable,
|
||||
expected: emptyArrayTableThenTableExpected,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Parse: Array of Array Table; then table",
|
||||
input: arrayTableThenArray,
|
||||
expected: arrayTableThenArrayExpected,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Parse: Empty Table",
|
||||
input: emptyTable,
|
||||
@@ -238,6 +375,13 @@ var tomlScenarios = []formatScenario{
|
||||
expected: expectedSampleWithHeader,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
{
|
||||
description: "Parse: multiple empty tables",
|
||||
skipDoc: true,
|
||||
input: multipleEmptyTables,
|
||||
expected: expectedMultipleEmptyTables,
|
||||
scenarioType: "decode",
|
||||
},
|
||||
}
|
||||
|
||||
func testTomlScenario(t *testing.T, s formatScenario) {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteInPlaceHandlerImpl_CreateTempFile(t *testing.T) {
|
||||
// Create a temporary directory and file for testing
|
||||
tempDir := t.TempDir()
|
||||
inputFile := filepath.Join(tempDir, "input.yaml")
|
||||
|
||||
// Create input file with some content
|
||||
content := []byte("test: value\n")
|
||||
err := os.WriteFile(inputFile, content, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create input file: %v", err)
|
||||
}
|
||||
|
||||
handler := NewWriteInPlaceHandler(inputFile)
|
||||
tempFile, err := handler.CreateTempFile()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTempFile failed: %v", err)
|
||||
}
|
||||
|
||||
if tempFile == nil {
|
||||
t.Fatal("CreateTempFile returned nil file")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
tempFile.Close()
|
||||
os.Remove(tempFile.Name())
|
||||
}
|
||||
|
||||
func TestWriteInPlaceHandlerImpl_CreateTempFile_NonExistentInput(t *testing.T) {
|
||||
// Test with non-existent input file
|
||||
handler := NewWriteInPlaceHandler("/non/existent/file.yaml")
|
||||
tempFile, err := handler.CreateTempFile()
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for non-existent input file, got nil")
|
||||
}
|
||||
|
||||
if tempFile != nil {
|
||||
t.Error("Expected nil temp file for non-existent input file")
|
||||
tempFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInPlaceHandlerImpl_FinishWriteInPlace_Success(t *testing.T) {
|
||||
// Create a temporary directory and file for testing
|
||||
tempDir := t.TempDir()
|
||||
inputFile := filepath.Join(tempDir, "input.yaml")
|
||||
|
||||
// Create input file with some content
|
||||
content := []byte("test: value\n")
|
||||
err := os.WriteFile(inputFile, content, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create input file: %v", err)
|
||||
}
|
||||
|
||||
handler := NewWriteInPlaceHandler(inputFile)
|
||||
tempFile, err := handler.CreateTempFile()
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTempFile failed: %v", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Write some content to temp file
|
||||
tempContent := []byte("updated: content\n")
|
||||
_, err = tempFile.Write(tempContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to temp file: %v", err)
|
||||
}
|
||||
tempFile.Close()
|
||||
|
||||
// Test successful finish
|
||||
err = handler.FinishWriteInPlace(true)
|
||||
if err != nil {
|
||||
t.Fatalf("FinishWriteInPlace failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the original file was updated
|
||||
updatedContent, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read updated file: %v", err)
|
||||
}
|
||||
|
||||
if string(updatedContent) != string(tempContent) {
|
||||
t.Errorf("File content not updated correctly. Expected %q, got %q",
|
||||
string(tempContent), string(updatedContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInPlaceHandlerImpl_FinishWriteInPlace_Failure(t *testing.T) {
|
||||
// Create a temporary directory and file for testing
|
||||
tempDir := t.TempDir()
|
||||
inputFile := filepath.Join(tempDir, "input.yaml")
|
||||
|
||||
// Create input file with some content
|
||||
content := []byte("test: value\n")
|
||||
err := os.WriteFile(inputFile, content, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create input file: %v", err)
|
||||
}
|
||||
|
||||
handler := NewWriteInPlaceHandler(inputFile)
|
||||
tempFile, err := handler.CreateTempFile()
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTempFile failed: %v", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Write some content to temp file
|
||||
tempContent := []byte("updated: content\n")
|
||||
_, err = tempFile.Write(tempContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to temp file: %v", err)
|
||||
}
|
||||
tempFile.Close()
|
||||
|
||||
// Test failure finish (should not update the original file)
|
||||
err = handler.FinishWriteInPlace(false)
|
||||
if err != nil {
|
||||
t.Fatalf("FinishWriteInPlace failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the original file was NOT updated
|
||||
originalContent, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read original file: %v", err)
|
||||
}
|
||||
|
||||
if string(originalContent) != string(content) {
|
||||
t.Errorf("File content should not have been updated. Expected %q, got %q",
|
||||
string(content), string(originalContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInPlaceHandlerImpl_CreateTempFile_Permissions(t *testing.T) {
|
||||
// Create a temporary directory and file for testing
|
||||
tempDir := t.TempDir()
|
||||
inputFile := filepath.Join(tempDir, "input.yaml")
|
||||
|
||||
// Create input file with specific permissions
|
||||
content := []byte("test: value\n")
|
||||
err := os.WriteFile(inputFile, content, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create input file: %v", err)
|
||||
}
|
||||
|
||||
handler := NewWriteInPlaceHandler(inputFile)
|
||||
tempFile, err := handler.CreateTempFile()
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTempFile failed: %v", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Check that temp file has same permissions as input file
|
||||
tempFileInfo, err := os.Stat(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat temp file: %v", err)
|
||||
}
|
||||
|
||||
inputFileInfo, err := os.Stat(inputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat input file: %v", err)
|
||||
}
|
||||
|
||||
if tempFileInfo.Mode() != inputFileInfo.Mode() {
|
||||
t.Errorf("Temp file permissions don't match input file. Expected %v, got %v",
|
||||
inputFileInfo.Mode(), tempFileInfo.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteInPlaceHandlerImpl_Integration(t *testing.T) {
|
||||
// Create a temporary directory and file for testing
|
||||
tempDir := t.TempDir()
|
||||
inputFile := filepath.Join(tempDir, "integration_test.yaml")
|
||||
|
||||
// Create input file with some content
|
||||
originalContent := []byte("original: content\n")
|
||||
err := os.WriteFile(inputFile, originalContent, 0600)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create input file: %v", err)
|
||||
}
|
||||
|
||||
handler := NewWriteInPlaceHandler(inputFile)
|
||||
|
||||
// Create temp file
|
||||
tempFile, err := handler.CreateTempFile()
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTempFile failed: %v", err)
|
||||
}
|
||||
|
||||
// Write new content to temp file
|
||||
newContent := []byte("new: content\n")
|
||||
_, err = tempFile.Write(newContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write to temp file: %v", err)
|
||||
}
|
||||
tempFile.Close()
|
||||
|
||||
// Finish with success
|
||||
err = handler.FinishWriteInPlace(true)
|
||||
if err != nil {
|
||||
t.Fatalf("FinishWriteInPlace failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the file was updated
|
||||
finalContent, err := os.ReadFile(inputFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read final file: %v", err)
|
||||
}
|
||||
|
||||
if string(finalContent) != string(newContent) {
|
||||
t.Errorf("File not updated correctly. Expected %q, got %q",
|
||||
string(newContent), string(finalContent))
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -188,7 +188,10 @@ above_cat
|
||||
`
|
||||
|
||||
const inputXMLWithNamespacedAttr = `<?xml version="1.0"?>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url"></map>
|
||||
<map xmlns="some-namespace" xmlns:xsi="some-instance" xsi:schemaLocation="some-url">
|
||||
<item foo="bar">baz</item>
|
||||
<xsi:item>foobar</xsi:item>
|
||||
</map>
|
||||
`
|
||||
|
||||
const expectedYAMLWithNamespacedAttr = `+p_xml: version="1.0"
|
||||
@@ -196,6 +199,10 @@ map:
|
||||
+@xmlns: some-namespace
|
||||
+@xmlns:xsi: some-instance
|
||||
+@xsi:schemaLocation: some-url
|
||||
item:
|
||||
+content: baz
|
||||
+@foo: bar
|
||||
xsi:item: foobar
|
||||
`
|
||||
|
||||
const expectedYAMLWithRawNamespacedAttr = `+p_xml: version="1.0"
|
||||
@@ -203,13 +210,21 @@ map:
|
||||
+@xmlns: some-namespace
|
||||
+@xmlns:xsi: some-instance
|
||||
+@xsi:schemaLocation: some-url
|
||||
item:
|
||||
+content: baz
|
||||
+@foo: bar
|
||||
xsi:item: foobar
|
||||
`
|
||||
|
||||
const expectedYAMLWithoutRawNamespacedAttr = `+p_xml: version="1.0"
|
||||
map:
|
||||
some-namespace:map:
|
||||
+@xmlns: some-namespace
|
||||
+@xmlns:xsi: some-instance
|
||||
+@some-instance:schemaLocation: some-url
|
||||
some-namespace:item:
|
||||
+content: baz
|
||||
+@foo: bar
|
||||
some-instance:item: foobar
|
||||
`
|
||||
|
||||
const xmlWithCustomDtd = `
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
4.49.1:
|
||||
- Added `--security` flags to disable env and file ops #2515
|
||||
- Fixing TOML ArrayTable parsing issues #1758
|
||||
- Fixing parsing of escaped characters #2506
|
||||
|
||||
4.48.2:
|
||||
- Strip whitespace when decoding base64 #2507
|
||||
- Upgraded to go-yaml v4! (thanks @ccoVeille, @ingydotnet)
|
||||
- Add linux/loong64 to release target (thanks @znley)
|
||||
- Added --shell-key-separator flag for customizable shell output format #2497 (thanks @rsleedbx)
|
||||
- Bumped dependencies
|
||||
|
||||
4.48.1:
|
||||
- Added 'parents' operator, to return a list of all the hierarchical parents of a node
|
||||
- Added 'first(exp)' operator, to return the first entry matching an expression in an array
|
||||
- Fixed xml namespace prefixes #1730 (thanks @baodrate)
|
||||
- Fixed out of range panic in yaml decoder #2460 (thanks @n471d)
|
||||
- Bumped dependencies
|
||||
|
||||
4.47.2:
|
||||
- Conversion from TOML to JSON no longer omits empty tables #2459 (thanks @louislouislouislouis)
|
||||
- Bumped dependencies
|
||||
|
||||
4.47.1:
|
||||
- Fixed merge anchor behaviour (`<<`); #2404, #2110, #2386, #2178 Huge thanks to @stevenwdv!
|
||||
Note that you will need to set --yaml-fix-merge-anchor-to-spec to see the fixes
|
||||
- Fixed panic for syntax error when creating a map #2423
|
||||
- Bumped dependencies
|
||||
|
||||
4.46.1:
|
||||
- Added INI support
|
||||
- Fixed 'add' operator when piped in with no data #2378, #2383, #2384
|
||||
|
||||
@@ -2,5 +2,71 @@
|
||||
|
||||
set -e
|
||||
|
||||
echo "Running tests and generating coverage..."
|
||||
go test -coverprofile=coverage.out -v $(go list ./... | grep -v -E 'examples' | grep -v -E 'test')
|
||||
|
||||
echo "Generating HTML coverage report..."
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
echo ""
|
||||
echo "Generating sorted coverage table..."
|
||||
|
||||
# Create a simple approach using grep and sed to extract file coverage
|
||||
# First, get the total coverage
|
||||
total_coverage=$(go tool cover -func=coverage.out | grep "^total:" | sed 's/.*([^)]*)[[:space:]]*\([0-9.]*\)%.*/\1/')
|
||||
|
||||
# Extract file-level coverage by finding the last occurrence of each file
|
||||
go tool cover -func=coverage.out | grep -E "\.go:[0-9]+:" | \
|
||||
sed 's/^\([^:]*\.go\):.*[[:space:]]\([0-9.]*\)%.*/\2 \1/' | \
|
||||
sort -k2 | \
|
||||
awk '{file_coverage[$2] = $1} END {for (file in file_coverage) printf "%.2f %s\n", file_coverage[file], file}' | \
|
||||
sort -nr > coverage_sorted.txt
|
||||
|
||||
# Add total coverage to the file
|
||||
if [[ -n "$total_coverage" && "$total_coverage" != "0" ]]; then
|
||||
echo "TOTAL: $total_coverage" >> coverage_sorted.txt
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Coverage Summary (sorted by percentage - lowest coverage first):"
|
||||
echo "================================================================="
|
||||
printf "%-60s %10s %12s\n" "FILE" "COVERAGE" "STATUS"
|
||||
echo "================================================================="
|
||||
|
||||
# Display results with status indicators
|
||||
tail -n +1 coverage_sorted.txt | while read percent file; do
|
||||
if [[ "$file" == "TOTAL:" ]]; then
|
||||
echo ""
|
||||
printf "%-60s %8s%% %12s\n" "OVERALL PROJECT COVERAGE" "$percent" "📊 TOTAL"
|
||||
echo "================================================================="
|
||||
continue
|
||||
fi
|
||||
|
||||
filename=$(basename "$file")
|
||||
status=""
|
||||
if (( $(echo "$percent < 50" | bc -l 2>/dev/null || echo "0") )); then
|
||||
status="🔴 CRITICAL"
|
||||
elif (( $(echo "$percent < 70" | bc -l 2>/dev/null || echo "0") )); then
|
||||
status="🟡 LOW"
|
||||
elif (( $(echo "$percent < 90" | bc -l 2>/dev/null || echo "0") )); then
|
||||
status="🟢 GOOD"
|
||||
else
|
||||
status="✅ EXCELLENT"
|
||||
fi
|
||||
|
||||
printf "%-60s %8s%% %12s\n" "$filename" "$percent" "$status"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Top 10 files needing attention (lowest coverage):"
|
||||
echo "================================================="
|
||||
grep -v "TOTAL:" coverage_sorted.txt | tail -10 | while read percent file; do
|
||||
filename=$(basename "$file")
|
||||
printf "%-60s %8.1f%%\n" "$filename" "$percent"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Coverage reports generated:"
|
||||
echo "- HTML report: coverage.html (detailed line-by-line coverage)"
|
||||
echo "- Sorted table: coverage_sorted.txt"
|
||||
echo "- Use 'go tool cover -func=coverage.out' for function-level details"
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
name: yq
|
||||
version: 'v4.46.1'
|
||||
version: 'v4.49.1'
|
||||
summary: A lightweight and portable command-line data file processor
|
||||
description: |
|
||||
`yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml, json, xml, csv, properties and TOML files.
|
||||
@@ -27,6 +27,6 @@ parts:
|
||||
build-environment:
|
||||
- CGO_ENABLED: 0
|
||||
source: https://github.com/mikefarah/yq.git
|
||||
source-tag: v4.46.1
|
||||
source-tag: v4.49.1
|
||||
build-snaps:
|
||||
- go/latest/stable
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
command "github.com/mikefarah/yq/v4/cmd"
|
||||
)
|
||||
|
||||
func TestMainFunction(t *testing.T) {
|
||||
// This is a basic smoke test for the main function
|
||||
// We can't easily test the main function directly since it calls os.Exit
|
||||
// But we can test the logic that would be executed
|
||||
|
||||
cmd := command.New()
|
||||
if cmd == nil {
|
||||
t.Fatal("command.New() returned nil")
|
||||
}
|
||||
|
||||
if cmd.Use != "yq" {
|
||||
t.Errorf("Expected command Use to be 'yq', got %q", cmd.Use)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainFunctionLogic(t *testing.T) {
|
||||
// Test the logic that would be executed in main()
|
||||
cmd := command.New()
|
||||
|
||||
args := []string{}
|
||||
_, _, err := cmd.Find(args)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error with empty args, but got: %v", err)
|
||||
}
|
||||
|
||||
args = []string{"invalid-command"}
|
||||
_, _, err = cmd.Find(args)
|
||||
if err == nil {
|
||||
t.Error("Expected error when invalid command found, but got nil")
|
||||
}
|
||||
|
||||
args = []string{"eval"}
|
||||
_, _, err = cmd.Find(args)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error with valid command 'eval', got: %v", err)
|
||||
}
|
||||
|
||||
args = []string{"__complete"}
|
||||
_, _, err = cmd.Find(args)
|
||||
if err == nil {
|
||||
t.Error("Expected error when no command found for '__complete', but got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainFunctionWithArgs(t *testing.T) {
|
||||
// Test the argument processing logic
|
||||
cmd := command.New()
|
||||
|
||||
args := []string{}
|
||||
_, _, err := cmd.Find(args)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error with empty args, but got: %v", err)
|
||||
}
|
||||
|
||||
// When Find fails and args[0] is not "__complete", main would set args to ["eval"] + original args
|
||||
// This is the logic: newArgs := []string{"eval"}
|
||||
// cmd.SetArgs(append(newArgs, os.Args[1:]...))
|
||||
|
||||
args = []string{"invalid"}
|
||||
_, _, err = cmd.Find(args)
|
||||
if err == nil {
|
||||
t.Error("Expected error with invalid command")
|
||||
}
|
||||
|
||||
args = []string{"__complete"}
|
||||
_, _, err = cmd.Find(args)
|
||||
if err == nil {
|
||||
t.Error("Expected error with __complete command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainFunctionExecution(t *testing.T) {
|
||||
// Test that the command can be executed without crashing
|
||||
cmd := command.New()
|
||||
|
||||
cmd.SetArgs([]string{"--version"})
|
||||
|
||||
// We can't easily test os.Exit(1) behaviour, but we can test that
|
||||
// the command structure is correct and can be configured
|
||||
if cmd == nil {
|
||||
t.Fatal("Command should not be nil")
|
||||
}
|
||||
|
||||
if cmd.Use != "yq" {
|
||||
t.Errorf("Expected command Use to be 'yq', got %q", cmd.Use)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainFunctionErrorHandling(t *testing.T) {
|
||||
// Test the error handling logic that would be in main()
|
||||
cmd := command.New()
|
||||
|
||||
args := []string{"nonexistent-command"}
|
||||
_, _, err := cmd.Find(args)
|
||||
if err == nil {
|
||||
t.Error("Expected error with nonexistent command")
|
||||
}
|
||||
|
||||
// The main function logic would be:
|
||||
// if err != nil && args[0] != "__complete" {
|
||||
// newArgs := []string{"eval"}
|
||||
// cmd.SetArgs(append(newArgs, os.Args[1:]...))
|
||||
// }
|
||||
|
||||
// Test that this logic would work
|
||||
if args[0] != "__complete" {
|
||||
// This is what main() would do
|
||||
newArgs := []string{"eval"}
|
||||
cmd.SetArgs(append(newArgs, args...))
|
||||
|
||||
// We can't easily verify the args were set correctly since cmd.Args is a function
|
||||
// But we can test that SetArgs doesn't crash and the command is still valid
|
||||
if cmd == nil {
|
||||
t.Error("Command should not be nil after SetArgs")
|
||||
}
|
||||
|
||||
_, _, err := cmd.Find([]string{"eval"})
|
||||
if err != nil {
|
||||
t.Errorf("Should be able to find eval command after SetArgs: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainFunctionWithCompletionCommand(t *testing.T) {
|
||||
// Test that __complete command doesn't trigger default command logic
|
||||
cmd := command.New()
|
||||
|
||||
args := []string{"__complete"}
|
||||
_, _, err := cmd.Find(args)
|
||||
if err == nil {
|
||||
t.Error("Expected error with __complete command")
|
||||
}
|
||||
|
||||
// The main function logic would be:
|
||||
// if err != nil && args[0] != "__complete" {
|
||||
// // This should NOT execute for __complete
|
||||
// }
|
||||
|
||||
// Verify that __complete doesn't trigger the default command logic
|
||||
if args[0] == "__complete" {
|
||||
// This means the default command logic should NOT execute
|
||||
t.Log("__complete command correctly identified, default command logic should not execute")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainFunctionIntegration(t *testing.T) {
|
||||
// Integration test to verify the main function logic works end-to-end
|
||||
|
||||
cmd := command.New()
|
||||
cmd.SetArgs([]string{"eval", "--help"})
|
||||
|
||||
// This should not crash (we can't test the actual execution due to os.Exit)
|
||||
if cmd == nil {
|
||||
t.Fatal("Command should not be nil")
|
||||
}
|
||||
|
||||
cmd2 := command.New()
|
||||
cmd2.SetArgs([]string{"invalid-command"})
|
||||
|
||||
// Simulate the main function logic
|
||||
args := []string{"invalid-command"}
|
||||
_, _, err := cmd2.Find(args)
|
||||
if err != nil {
|
||||
// This is what main() would do
|
||||
newArgs := []string{"eval"}
|
||||
cmd2.SetArgs(append(newArgs, args...))
|
||||
}
|
||||
|
||||
// We can't directly access cmd.Args since it's a function, but we can test
|
||||
// that SetArgs worked by ensuring the command is still functional
|
||||
if cmd2 == nil {
|
||||
t.Error("Command should not be nil after SetArgs")
|
||||
}
|
||||
|
||||
_, _, err = cmd2.Find([]string{"eval"})
|
||||
if err != nil {
|
||||
t.Errorf("Should be able to find eval command after SetArgs: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user