Compare commits

...

55 Commits

Author SHA1 Message Date
dependabot[bot]
9c73286368
Bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 03:30:34 +00:00
dependabot[bot]
2072808def Bump golang from 1.25.4 to 1.25.5
Bumps golang from 1.25.4 to 1.25.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:29:15 +11:00
dependabot[bot]
7d47b36b69 Bump github.com/spf13/cobra from 1.10.1 to 1.10.2
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:29:08 +11:00
dependabot[bot]
53f10ae360 Bump github.com/goccy/go-yaml from 1.18.0 to 1.19.0
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/goccy/go-yaml/releases)
- [Changelog](https://github.com/goccy/go-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/goccy/go-yaml/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/goccy/go-yaml
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:29:01 +11:00
dependabot[bot]
22510ab8d5 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 14:28:39 +11:00
Alexander
588d0bb3dd Bumped to core24 and removed riscv64 2025-11-26 09:31:58 +11:00
Mike Farah
7ccaf8e700 Bumping version 2025-11-25 10:45:39 +11:00
Mike Farah
a1a27b8536 Updating release notes 2025-11-25 10:45:35 +11:00
Mike Farah
1b91fc63ea Removing escape char processing from strenv #2517 2025-11-25 10:44:03 +11:00
Mike Farah
9e0c5fd3c9 Fixing escape charaters again 😢 #2517 2025-11-25 10:17:43 +11:00
Alexander
5d0481c0d2 Running build step on launchpad remote builder with supported architectures 2025-11-25 08:55:36 +11:00
Alexander
f91176a204 Fixed architecture builders 2025-11-25 08:55:36 +11:00
Mike Farah
8e86bdb876 Attempting to fix snap again 2025-11-22 18:33:44 +11:00
Mike Farah
fc164ca9c3 Updating README with latest yq help 2025-11-22 15:04:39 +11:00
Mike Farah
810e9d921e Syncing how-it-works from gitbook branch 2025-11-22 15:02:03 +11:00
Mike Farah
45be35c063 Bumping version 2025-11-22 14:52:48 +11:00
Mike Farah
39fbf01fa8 Fixing TOML ArrayTable parsing issues #1758 2025-11-22 14:49:49 +11:00
Mike Farah
306dc931a5 Fixing TOML ArrayTable parsing issues #1758 2025-11-22 14:35:07 +11:00
Mike Farah
f00852bc6c Added flags to disable env and file ops #2515 2025-11-22 09:40:03 +11:00
Mike Farah
c716d157f2 Fixing parsing of escaped characters in strenv #2506 2025-11-16 09:22:21 +11:00
Mike Farah
e49e588ab5 Fixing parsing of escaped characters #2506 2025-11-16 09:12:13 +11:00
Mike Farah
389486829d Updating release notes 2025-11-15 14:44:18 +11:00
Mike Farah
d32e71f25b Updating release notes 2025-11-15 14:37:33 +11:00
Mike Farah
796317b885 Bumping version 2025-11-15 14:32:46 +11:00
Mike Farah
258b84a05e Strip whitespace when decoding base64 #2507 2025-11-15 14:11:55 +11:00
dependabot[bot]
e056b91a00 Bump golang.org/x/net from 0.46.0 to 0.47.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.46.0 to 0.47.0.
- [Commits](https://github.com/golang/net/compare/v0.46.0...v0.47.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 14:11:03 +11:00
znley
85b0985a60 build: add linux/loong64 to release target
Signed-off-by: znley <shanjiantao@loongson.cn>
2025-11-15 11:36:53 +11:00
dependabot[bot]
874cbc4d3c Bump golang.org/x/text from 0.30.0 to 0.31.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 11:36:33 +11:00
Mike Farah
f6c780e793 Reverting test 2025-11-09 16:21:50 +11:00
Mike Farah
8c25f33df4 Merge in master 2025-11-09 16:19:04 +11:00
Mike Farah
2869919cb4 Merge branch 'master' into go-yaml-v4 2025-11-09 16:18:49 +11:00
Mike Farah
458d02f3ab Bumping to go-yaml 4-rc3 2025-11-09 16:15:36 +11:00
Mike Farah
877a47cb19 Improving first op docs 2025-11-09 16:12:17 +11:00
Mike Farah
3050ca5303 Adding first operator 2025-11-09 16:12:17 +11:00
Navid
49b6477c49 Fix out of range panic in yaml decoder 2025-11-09 16:12:17 +11:00
Mike Farah
78bc9baffd Added parents operator 2025-11-09 16:12:17 +11:00
Robert Lee
1f2b0fe76b Add --shell-key-separator flag for customizable shell output format
- Add ShellVariablesPreferences struct with KeySeparator field (default: '_')
- Update shellVariablesEncoder to use configurable separator
- Add --shell-key-separator CLI flag
- Add comprehensive tests for custom separator functionality
- Update documentation with example usage for custom separator

This feature allows users to specify a custom separator (e.g. '__') when
outputting shell variables, which helps disambiguate nested keys from
keys that contain underscores in their names.

Example:
  yq -o=shell --shell-key-separator='__' file.yaml

Fixes ambiguity when original YAML keys contain underscores.
2025-11-07 20:02:58 +11:00
dependabot[bot]
1228bcfa75 Bump golang from 1.25.2 to 1.25.4
Bumps golang from 1.25.2 to 1.25.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-07 19:44:03 +11:00
Mike Farah
7f72595a12 Cursor generated unit tests 2025-10-12 15:38:40 +11:00
Mike Farah
ff2c1c930c Adding more tests 2025-10-12 14:50:41 +11:00
Mike Farah
36d410b348 Updating readme 2025-10-12 14:45:10 +11:00
Mike Farah
6dfe002058 Updating contrib doc 2025-10-12 14:37:05 +11:00
Mike Farah
ed4f468c97 Release notes 2025-10-12 14:33:02 +11:00
Mike Farah
8b2ba41c6c Improving first op test 2025-10-12 14:32:28 +11:00
Mike Farah
0ecdce24e8 Bumping version 2025-10-12 14:21:13 +11:00
Mike Farah
01ac615e67 Updating contrib 2025-10-12 14:08:32 +11:00
dependabot[bot]
6629924dea Bump github.com/alecthomas/repr from 0.5.1 to 0.5.2
Bumps [github.com/alecthomas/repr](https://github.com/alecthomas/repr) from 0.5.1 to 0.5.2.
- [Commits](https://github.com/alecthomas/repr/compare/v0.5.1...v0.5.2)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/repr
  dependency-version: 0.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 13:47:59 +11:00
dependabot[bot]
386935470d Bump golang from 1.25.0 to 1.25.2
Bumps golang from 1.25.0 to 1.25.2.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 13:46:01 +11:00
dependabot[bot]
d5dd338707 Bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 13:42:48 +11:00
dependabot[bot]
201542b522 Bump golang.org/x/net from 0.43.0 to 0.46.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.43.0 to 0.46.0.
- [Commits](https://github.com/golang/net/compare/v0.43.0...v0.46.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 13:42:37 +11:00
Bao Trinh
f3538850f2 fix: keep xml namespace prefixes for tags 2025-10-12 13:42:24 +11:00
Bao Trinh
df92decbe0 chore: add xml namespace prefix test cases 2025-10-12 13:42:24 +11:00
Mike Farah
23060cb8af Improving first op docs 2025-09-19 14:59:19 +10:00
Mike Farah
02b28073bf Fixing error reporting 2025-09-09 20:16:49 +10:00
Mike Farah
6957399dc0 Updating go-yaml from v3 to v4 2025-09-09 20:05:58 +10:00
70 changed files with 3982 additions and 332 deletions

View File

@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# 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

View File

@ -14,7 +14,7 @@ jobs:
IMAGE_NAME: mikefarah/yq
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

View File

@ -11,13 +11,13 @@ jobs:
steps:
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: '^1.20'
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get dependencies
run: |

View File

@ -9,8 +9,8 @@ jobs:
publishGitRelease:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '^1.20'
check-latest: true

View File

@ -12,12 +12,16 @@ jobs:
environment: snap
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: snapcore/action-build@v1
id: build
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
snapcraft-args: "remote-build --launchpad-accept-public-upload"
- uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }}
with:
snap: ${{ steps.build.outputs.snap }}
release: stable
release: stable

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get test
id: get_value
uses: mikefarah/yq@master

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ _testmain.go
cover.out
coverage.out
coverage.html
coverage_sorted.txt
*.exe
*.test
*.prof

View File

@ -23,6 +23,7 @@ builds:
- linux_amd64
- linux_arm
- linux_arm64
- linux_loong64
- linux_mips
- linux_mips64
- linux_mips64le

View File

@ -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

View File

@ -1,4 +1,4 @@
FROM golang:1.25.0 AS builder
FROM golang:1.25.5 AS builder
WORKDIR /go/src/mikefarah/yq

View File

@ -1,4 +1,4 @@
FROM golang:1.25.0
FROM golang:1.25.5
RUN apt-get update && \
apt-get install -y npm && \

209
README.md
View File

@ -3,44 +3,46 @@
![Build](https://github.com/mikefarah/yq/workflows/Build/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/mikefarah/yq.svg) ![Github Releases (by Release)](https://img.shields.io/github/downloads/mikefarah/yq/total.svg) ![Go Report](https://goreportcard.com/badge/github.com/mikefarah/yq) ![CodeQL](https://github.com/mikefarah/yq/workflows/CodeQL/badge.svg)
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
```
@ -340,7 +353,7 @@ gah 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)
@ -367,10 +380,18 @@ Usage:
Examples:
# yq defaults to 'eval' command if no command is specified. See "yq eval --help" for more examples.
yq '.stuff' < myfile.yml # outputs the data at the "stuff" node from "myfile.yml"
# yq tries to auto-detect the file format based off the extension, and defaults to YAML if it's unknown (or piping through STDIN)
# Use the '-p/--input-format' flag to specify a format type.
cat file.xml | yq -p xml
yq -i '.stuff = "foo"' myfile.yml # update myfile.yml in place
# read the "stuff" node from "myfile.yml"
yq '.stuff' < myfile.yml
# update myfile.yml in place
yq -i '.stuff = "foo"' myfile.yml
# print contents of sample.json as idiomatic YAML
yq -P -oy sample.json
Available Commands:
@ -380,49 +401,75 @@ Available Commands:
help Help about any command
Flags:
-C, --colors force print with colors
--csv-auto-parse parse CSV YAML/JSON values (default true)
--csv-separator char CSV Separator character (default ,)
-e, --exit-status set exit status if there are no matches or null or false is returned
--expression string forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file.
--from-file string Load expression from specified file.
-f, --front-matter string (extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact
--header-preprocess Slurp any header comments and separators before processing expression. (default true)
-h, --help help for yq
-I, --indent int sets indent level for output (default 2)
-i, --inplace update the file in place of first file given.
-p, --input-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|lua|l|ini|i] parse format for input. (default "auto")
--lua-globals output keys as top-level global variables
--lua-prefix string prefix (default "return ")
--lua-suffix string suffix (default ";\n")
--lua-unquoted output unquoted string keys (e.g. {foo="bar"})
-M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---)
-0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.
-o, --output-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|shell|s|lua|l|ini|i] output format type. (default "auto")
-P, --prettyPrint pretty print, shorthand for '... style = ""'
--properties-array-brackets use [x] in array paths (e.g. for SpringBoot)
--properties-separator string separator to use between keys and values (default " = ")
-s, --split-exp string print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.
--split-exp-file string Use a file to specify the split-exp expression.
--string-interpolation Toggles strings interpolation of \(exp) (default true)
--tsv-auto-parse parse TSV YAML/JSON values (default true)
-r, --unwrapScalar unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml (default true)
-v, --verbose verbose mode
-V, --version Print version information and quit
--xml-attribute-prefix string prefix for xml attributes (default "+@")
--xml-content-name string name for xml content (if no attribute name is present). (default "+content")
--xml-directive-name string name for xml directives (e.g. <!DOCTYPE thing cat>) (default "+directive")
--xml-keep-namespace enables keeping namespace after parsing attributes (default true)
--xml-proc-inst-prefix string prefix for xml processing instructions (e.g. <?xml version="1"?>) (default "+p_")
--xml-raw-token enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details. (default true)
--xml-skip-directives skip over directives (e.g. <!DOCTYPE thing cat>)
--xml-skip-proc-inst skip over process instructions (e.g. <?xml version="1"?>)
--xml-strict-mode enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.
-C, --colors force print with colors
--csv-auto-parse parse CSV YAML/JSON values (default true)
--csv-separator char CSV Separator character (default ,)
--debug-node-info debug node info
-e, --exit-status set exit status if there are no matches or null or false is returned
--expression string forcibly set the expression argument. Useful when yq argument detection thinks your expression is a file.
--from-file string Load expression from specified file.
-f, --front-matter string (extract|process) first input as yaml front-matter. Extract will pull out the yaml content, process will run the expression against the yaml content, leaving the remaining data intact
--header-preprocess Slurp any header comments and separators before processing expression. (default true)
-h, --help help for yq
-I, --indent int sets indent level for output (default 2)
-i, --inplace update the file in place of first file given.
-p, --input-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|lua|l|ini|i] parse format for input. (default "auto")
--lua-globals output keys as top-level global variables
--lua-prefix string prefix (default "return ")
--lua-suffix string suffix (default ";\n")
--lua-unquoted output unquoted string keys (e.g. {foo="bar"})
-M, --no-colors force print with no colors
-N, --no-doc Don't print document separators (---)
-0, --nul-output Use NUL char to separate values. If unwrap scalar is also set, fail if unwrapped scalar contains NUL char.
-n, --null-input Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.
-o, --output-format string [auto|a|yaml|y|json|j|props|p|csv|c|tsv|t|xml|x|base64|uri|toml|shell|s|lua|l|ini|i] output format type. (default "auto")
-P, --prettyPrint pretty print, shorthand for '... style = ""'
--properties-array-brackets use [x] in array paths (e.g. for SpringBoot)
--properties-separator string separator to use between keys and values (default " = ")
--security-disable-env-ops Disable env related operations.
--security-disable-file-ops Disable file related operations (e.g. load)
--shell-key-separator string separator for shell variable key paths (default "_")
-s, --split-exp string print each result (or doc) into a file named (exp). [exp] argument must return a string. You can use $index in the expression as the result counter. The necessary directories will be created.
--split-exp-file string Use a file to specify the split-exp expression.
--string-interpolation Toggles strings interpolation of \(exp) (default true)
--tsv-auto-parse parse TSV YAML/JSON values (default true)
-r, --unwrapScalar unwrap scalar, print the value with no quotes, colors or comments. Defaults to true for yaml (default true)
-v, --verbose verbose mode
-V, --version Print version information and quit
--xml-attribute-prefix string prefix for xml attributes (default "+@")
--xml-content-name string name for xml content (if no attribute name is present). (default "+content")
--xml-directive-name string name for xml directives (e.g. <!DOCTYPE thing cat>) (default "+directive")
--xml-keep-namespace enables keeping namespace after parsing attributes (default true)
--xml-proc-inst-prefix string prefix for xml processing instructions (e.g. <?xml version="1"?>) (default "+p_")
--xml-raw-token enables using RawToken method instead Token. Commonly disables namespace translations. See https://pkg.go.dev/encoding/xml#Decoder.RawToken for details. (default true)
--xml-skip-directives skip over directives (e.g. <!DOCTYPE thing cat>)
--xml-skip-proc-inst skip over process instructions (e.g. <?xml version="1"?>)
--xml-strict-mode enables strict parsing of XML. See https://pkg.go.dev/encoding/xml for more details.
--yaml-fix-merge-anchor-to-spec Fix merge anchor to match YAML spec. Will default to true in late 2025
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)

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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(),

264
cmd/root_test.go Normal file
View File

@ -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)
}
}
}

View File

@ -11,7 +11,7 @@ var (
GitDescribe string
// Version is main version number that is being run at the moment.
Version = "v4.47.2"
Version = "v4.49.2"
// 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

View File

@ -1,8 +1 @@
# 001
---
abc: # 001
- 1 # one
- 2 # two
---
def # 002
a: apple

View File

@ -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"

16
go.mod
View File

@ -3,23 +3,23 @@ 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.5.1
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
github.com/go-ini/ini v1.67.0
github.com/goccy/go-json v0.10.5
github.com/goccy/go-yaml v1.18.0
github.com/goccy/go-yaml v1.19.0
github.com/jinzhu/copier v0.4.0
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.10.1
github.com/spf13/cobra v1.10.2
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.43.0
golang.org/x/text v0.28.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.35.0 // indirect
golang.org/x/sys v0.38.0 // indirect
)
go 1.24
go 1.24.0
toolchain go1.24.1

31
go.sum
View File

@ -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.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/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=
@ -19,8 +19,8 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -40,8 +40,8 @@ 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.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
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=
@ -50,19 +50,18 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
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=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,8 +1,63 @@
# How it works
# Expression Syntax: A Visual Guide
In `yq`, expressions are made up of operators and pipes. A context of nodes is passed through the expression, and each operation takes the context as input and returns a new context as output. That output is piped in as input for the next operation in the expression.
In `yq` expressions are made up of operators and pipes. A context of nodes is passed through the expression and each operation takes the context as input and returns a new context as output. That output is piped in as input for the next operation in the expression. To begin with, the context is set to the first yaml document of the first yaml file (if processing in sequence using eval).
Let's break down the process step by step using a diagram. We'll start with a single YAML document, apply an expression, and observe how the context changes at each step.
Lets look at a couple of examples.
Given a document like:
```yaml
root:
items:
- name: apple
type: fruit
- name: carrot
type: vegetable
- name: banana
type: fruit
```
You can use dot notation to access nested structures. For example, to access the `name` of the first item, you would use the expression `.root.items[0].name`, which would return `apple`.
But lets see how we could find all the fruit under `items`
## Step 1: Initial Context
The context starts at the root of the YAML document. In this case, the entire document is the initial context.
```
root
└── items
├── name: apple
│ type: fruit
├── name: carrot
│ type: vegetable
└── name: banana
type: fruit
```
## Step 2: Splatting the Array
Using the expression `.root.items[]`, we "splat" the items array. This means each element of the array becomes its own node in the context:
```
Node 1: { name: apple, type: fruit }
Node 2: { name: carrot, type: vegetable }
Node 3: { name: banana, type: fruit }
```
## Step 3: Filtering the Nodes
Next, we apply a filter to select only the nodes where type is fruit. The expression `.root.items[] | select(.type == "fruit")` filters the nodes:
```
Filtered Node 1: { name: apple, type: fruit }
Filtered Node 2: { name: banana, type: fruit }
```
## Step 4: Extracting a Field
Finally, we extract the name field from the filtered nodes using `.root.items[] | select(.type == "fruit") | .name` This results in:
```
apple
banana
```
## Simple assignment example
@ -44,7 +99,6 @@ a: dog
b: dog
```
## Complex assignment, operator precedence rules
Just like math expressions - `yq` expressions have an order of precedence. The pipe `|` operator has a low order of precedence, so operators with higher precedence will get evaluated first.
@ -73,7 +127,7 @@ name: sally
fruit: mango
```
To properly update this yaml, you will need to use brackets (think BODMAS from maths) and wrap the entire LHS:
**Important**: To properly update this YAML, you must wrap the entire LHS in parentheses. Think of it like using brackets in math to ensure the correct order of operations.
`(.[] | select(.name == "sally") | .fruit) = "mango"`
@ -126,4 +180,4 @@ The assignment operator then copies across the value from the RHS to the value o
```yaml
a: 2
b: thing
```
```

269
pkg/yqlib/base64_test.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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")
}
}
})
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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,7 +283,6 @@ 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 {
@ -292,8 +299,6 @@ func (dec *tomlDecoder) processTable(currentNode *toml.Node) (bool, error) {
}
}
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
}

View File

@ -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

View File

@ -8,7 +8,7 @@ import (
"regexp"
"strings"
yaml "go.yaml.in/yaml/v3"
yaml "go.yaml.in/yaml/v4"
)
type yamlDecoder struct {

View File

@ -191,7 +191,7 @@ Given a sample.yml file of:
```yaml
f:
a: &a cat
*a: b
*a : b
```
then
```bash

View File

@ -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
```

View File

@ -1,3 +1,8 @@
# 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:
@ -20,8 +25,10 @@ Given a sample.yml file of:
```yaml
- a: banana
- a: cat
b: firstCat
- a: apple
- a: cat
b: secondCat
```
then
```bash
@ -30,6 +37,7 @@ yq 'first(.a == "cat")' sample.yml
will output
```yaml
a: cat
b: firstCat
```
## First matching element from array with numeric condition
@ -38,6 +46,7 @@ Given a sample.yml file of:
- a: 10
- a: 100
- a: 1
- a: 101
```
then
```bash
@ -53,7 +62,10 @@ Given a sample.yml file of:
```yaml
- a: false
- a: true
b: firstTrue
- a: false
- a: true
b: secondTrue
```
then
```bash
@ -62,6 +74,7 @@ yq 'first(.a == true)' sample.yml
will output
```yaml
a: true
b: firstTrue
```
## First matching element from array with null values
@ -84,19 +97,19 @@ a: cat
Given a sample.yml file of:
```yaml
- a: dog
b: 5
b: 7
- a: cat
b: 3
- a: apple
b: 7
b: 5
```
then
```bash
yq 'first(.b > 4)' sample.yml
yq 'first(.b > 4 and .b < 6)' sample.yml
```
will output
```yaml
a: dog
a: apple
b: 5
```
@ -127,7 +140,7 @@ x:
y:
a: 100
z:
a: 1
a: 101
```
then
```bash
@ -273,7 +286,7 @@ will output
100
```
## First element with no RHS from array
## First element with no filter from array
Given a sample.yml file of:
```yaml
- 10
@ -289,7 +302,7 @@ will output
10
```
## First element with no RHS from array of maps
## First element with no filter from array of maps
Given a sample.yml file of:
```yaml
- a: 10
@ -304,42 +317,3 @@ will output
a: 10
```
## No RHS on empty array returns nothing
Given a sample.yml file of:
```yaml
[]
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
```
## No RHS on scalar returns nothing
Given a sample.yml file of:
```yaml
hello
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
```
## No RHS on null returns nothing
Given a sample.yml file of:
```yaml
null
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
```

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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
```

View File

@ -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==```

View File

@ -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

View File

@ -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
```

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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")
}

View File

@ -9,7 +9,7 @@ import (
"strings"
"github.com/fatih/color"
"go.yaml.in/yaml/v3"
"go.yaml.in/yaml/v4"
)
type yamlEncoder struct {

View File

@ -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())
}

View File

@ -379,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,

View File

@ -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) {

View File

@ -186,6 +186,76 @@ func parseInt(numberString string) (int, error) {
return int(parsed), err
}
func processEscapeCharacters(original string) string {
if original == "" {
return original
}
var result strings.Builder
runes := []rune(original)
for i := 0; i < len(runes); i++ {
if runes[i] == '\\' && i < len(runes)-1 {
next := runes[i+1]
switch next {
case '\\':
// Check if followed by opening bracket - if so, preserve both backslashes
// this is required for string interpolation to work correctly.
if i+2 < len(runes) && runes[i+2] == '(' {
// Preserve \\ when followed by (
result.WriteRune('\\')
result.WriteRune('\\')
i++ // Skip the next backslash (we'll process the ( normally on next iteration)
continue
}
// Escaped backslash: \\ -> \
result.WriteRune('\\')
i++ // Skip the next backslash
continue
case '"':
result.WriteRune('"')
i++ // Skip the quote
continue
case 'n':
result.WriteRune('\n')
i++ // Skip the 'n'
continue
case 't':
result.WriteRune('\t')
i++ // Skip the 't'
continue
case 'r':
result.WriteRune('\r')
i++ // Skip the 'r'
continue
case 'f':
result.WriteRune('\f')
i++ // Skip the 'f'
continue
case 'v':
result.WriteRune('\v')
i++ // Skip the 'v'
continue
case 'b':
result.WriteRune('\b')
i++ // Skip the 'b'
continue
case 'a':
result.WriteRune('\a')
i++ // Skip the 'a'
continue
}
}
result.WriteRune(runes[i])
}
value := result.String()
if value != original {
log.Debug("processEscapeCharacters from [%v] to [%v]", original, value)
}
return value
}
func headAndLineComment(node *CandidateNode) string {
return headComment(node) + lineComment(node)
}

View File

@ -2,6 +2,7 @@ package yqlib
import (
"fmt"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
@ -160,3 +161,369 @@ 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
}
type processEscapeCharactersScenario struct {
input string
expected string
}
var processEscapeCharactersScenarios = []processEscapeCharactersScenario{
{
input: "",
expected: "",
},
{
input: "hello",
expected: "hello",
},
{
input: "\\\"",
expected: "\"",
},
{
input: "hello\\\"world",
expected: "hello\"world",
},
{
input: "\\n",
expected: "\n",
},
{
input: "line1\\nline2",
expected: "line1\nline2",
},
{
input: "\\t",
expected: "\t",
},
{
input: "hello\\tworld",
expected: "hello\tworld",
},
{
input: "\\r",
expected: "\r",
},
{
input: "hello\\rworld",
expected: "hello\rworld",
},
{
input: "\\f",
expected: "\f",
},
{
input: "hello\\fworld",
expected: "hello\fworld",
},
{
input: "\\v",
expected: "\v",
},
{
input: "hello\\vworld",
expected: "hello\vworld",
},
{
input: "\\b",
expected: "\b",
},
{
input: "hello\\bworld",
expected: "hello\bworld",
},
{
input: "\\a",
expected: "\a",
},
{
input: "hello\\aworld",
expected: "hello\aworld",
},
{
input: "\\\"\\n\\t\\r\\f\\v\\b\\a",
expected: "\"\n\t\r\f\v\b\a",
},
{
input: "multiple\\nlines\\twith\\ttabs",
expected: "multiple\nlines\twith\ttabs",
},
{
input: "quote\\\"here",
expected: "quote\"here",
},
{
input: "\\\\",
expected: "\\", // Backslash is processed: "\\\\" becomes "\\"
},
{
input: "\\\"test\\\"",
expected: "\"test\"",
},
{
input: "a\\\\b",
expected: "a\\b", // Tests roundtrip: "a\\\\b" should become "a\\b"
},
{
input: "Hi \\\\(.value)",
expected: "Hi \\\\(.value)",
},
{
input: `a\\b`,
expected: "a\\b",
},
}
func TestProcessEscapeCharacters(t *testing.T) {
for _, tt := range processEscapeCharactersScenarios {
actual := processEscapeCharacters(tt.input)
test.AssertResultComplexWithContext(t, tt.expected, actual, fmt.Sprintf("Input: %q", tt.input))
}
}

View File

@ -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)
@ -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 {

View File

@ -178,3 +178,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)
}

View File

@ -13,15 +13,15 @@ var firstOperatorScenarios = []expressionScenario{
},
{
description: "First matching element from array with multiple matches",
document: "[{a: banana},{a: cat},{a: apple},{a: cat}]",
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}\n",
"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}]",
document: "[{a: 10},{a: 100},{a: 1},{a: 101}]",
expression: `first(.a > 50)`,
expected: []string{
"D0, P[1], (!!map)::{a: 100}\n",
@ -29,10 +29,10 @@ var firstOperatorScenarios = []expressionScenario{
},
{
description: "First matching element from array with boolean condition",
document: "[{a: false},{a: true},{a: false}]",
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}\n",
"D0, P[1], (!!map)::{a: true, b: firstTrue}\n",
},
},
{
@ -45,10 +45,10 @@ var firstOperatorScenarios = []expressionScenario{
},
{
description: "First matching element from array with complex condition",
document: "[{a: dog, b: 5},{a: cat, b: 3},{a: apple, b: 7}]",
expression: `first(.b > 4)`,
document: "[{a: dog, b: 7},{a: cat, b: 3},{a: apple, b: 5}]",
expression: `first(.b > 4 and .b < 6)`,
expected: []string{
"D0, P[0], (!!map)::{a: dog, b: 5}\n",
"D0, P[2], (!!map)::{a: apple, b: 5}\n",
},
},
{
@ -61,7 +61,7 @@ var firstOperatorScenarios = []expressionScenario{
},
{
description: "First matching element from map with numeric condition",
document: "x: {a: 10}\ny: {a: 100}\nz: {a: 1}",
document: "x: {a: 10}\ny: {a: 100}\nz: {a: 101}",
expression: `first(.a > 50)`,
expected: []string{
"D0, P[y], (!!map)::{a: 100}\n",
@ -139,9 +139,8 @@ var firstOperatorScenarios = []expressionScenario{
"D0, P[1], (!!int)::100\n",
},
},
// New tests for no RHS (return first child)
{
description: "First element with no RHS from array",
description: "First element with no filter from array",
document: "[10, 100, 1]",
expression: `first`,
expected: []string{
@ -149,7 +148,7 @@ var firstOperatorScenarios = []expressionScenario{
},
},
{
description: "First element with no RHS from array of maps",
description: "First element with no filter from array of maps",
document: "[{a: 10},{a: 100}]",
expression: `first`,
expected: []string{
@ -157,19 +156,22 @@ var firstOperatorScenarios = []expressionScenario{
},
},
{
description: "No RHS on empty array returns nothing",
description: "No filter on empty array returns nothing",
skipDoc: true,
document: "[]",
expression: `first`,
expected: []string{},
},
{
description: "No RHS on scalar returns nothing",
description: "No filter on scalar returns nothing",
skipDoc: true,
document: "hello",
expression: `first`,
expected: []string{},
},
{
description: "No RHS on null returns nothing",
description: "No filter on null returns nothing",
skipDoc: true,
document: "null",
expression: `first`,
expected: []string{},

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -5,7 +5,7 @@ import (
"container/list"
"io"
"go.yaml.in/yaml/v3"
"go.yaml.in/yaml/v4"
)
type nodeInfoPrinter struct {

View File

@ -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
}

View File

@ -0,0 +1,11 @@
package yqlib
type SecurityPreferences struct {
DisableEnvOps bool
DisableFileOps bool
}
var ConfiguredSecurityPreferences = SecurityPreferences{
DisableEnvOps: false,
DisableFileOps: false,
}

View File

@ -0,0 +1,13 @@
package yqlib
type ShellVariablesPreferences struct {
KeySeparator string
}
func NewDefaultShellVariablesPreferences() ShellVariablesPreferences {
return ShellVariablesPreferences{
KeySeparator: "_",
}
}
var ConfiguredShellVariablesPreferences = NewDefaultShellVariablesPreferences()

View File

@ -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())))
}
}

View File

@ -37,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"
@ -249,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,

View File

@ -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))
}
}

View File

@ -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 = `

View File

@ -1,3 +1,27 @@
4.49.2:
- Fixing escape character bugs :sweat: #2517
- Fixing snap release pipeline #2518 Thanks @aalexjo
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

View File

@ -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"

View File

@ -1,22 +1,28 @@
name: yq
version: 'v4.47.2'
version: 'v4.49.2'
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.
base: core22
base: core24
grade: stable # devel|stable. must be 'stable' to release into candidate/stable channels
confinement: strict
architectures:
- build-on: [amd64]
build-for: [all]
# architectures:
# - build-on: s390x
# - build-on: ppc64el
# - build-on: arm64
# - build-on: armhf
# - build-on: amd64
# - build-on: i386
# - build-on: riscv64
platforms:
amd64:
build-on: [amd64]
build-for: [amd64]
arm64:
build-on: [arm64]
build-for: [arm64]
armhf:
build-on: [armhf]
build-for: [armhf]
s390x:
build-on: [s390x]
build-for: [s390x]
ppc64el:
build-on: [ppc64el]
build-for: [ppc64el]
apps:
yq:
command: bin/yq
@ -27,6 +33,6 @@ parts:
build-environment:
- CGO_ENABLED: 0
source: https://github.com/mikefarah/yq.git
source-tag: v4.47.2
source-tag: v4.49.2
build-snaps:
- go/latest/stable

187
yq_test.go Normal file
View File

@ -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)
}
}