Compare commits

...

105 Commits

Author SHA1 Message Date
Jeremy L. Morris
76c817009f Use ghcr.io instead of ghrc.io 2025-08-27 08:23:00 +10:00
Mike Farah
f03c9dc599 Bumping version 2025-07-23 14:03:02 +10:00
Mike Farah
023c85e1e2 Adding test for #2404 2025-07-23 13:46:15 +10:00
Mike Farah
a3d9e0172f Prepping release notes 2025-07-23 13:42:07 +10:00
dependabot[bot]
ab3be228dc Bump github.com/spf13/pflag from 1.0.6 to 1.0.7
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.6...v1.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 13:37:12 +10:00
dependabot[bot]
93fed3fd7a Bump golang.org/x/net from 0.41.0 to 0.42.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.41.0 to 0.42.0.
- [Commits](https://github.com/golang/net/compare/v0.41.0...v0.42.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 13:37:03 +10:00
dependabot[bot]
f42534ea0f Bump github.com/alecthomas/repr from 0.4.0 to 0.5.1
Bumps [github.com/alecthomas/repr](https://github.com/alecthomas/repr) from 0.4.0 to 0.5.1.
- [Commits](https://github.com/alecthomas/repr/compare/v0.4.0...v0.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 13:36:52 +10:00
Mike Farah
b968963ed4 Merge branch 'stevenwdv-merge-anchor-fix' 2025-07-23 13:32:57 +10:00
Mike Farah
92309b17a4 Fixed test doc gen 2025-07-23 13:31:25 +10:00
Mike Farah
d5757fc82b Working on clarifying docs 2025-07-22 11:51:33 +10:00
Mike Farah
db2a4550e5 Fixed tests 2025-07-22 11:25:02 +10:00
Mike Farah
3018396ed2 wip: fixing key order 2025-07-22 10:50:52 +10:00
Mike Farah
84b095bbc4 Making examples of the merge fix flag clearer 2025-07-22 10:18:39 +10:00
Mike Farah
f35e57d901 Warn less about flag 2025-07-22 10:02:27 +10:00
Steven WdV
70ac3d6c7a
Add override behavior comments 2025-07-20 15:46:15 +02:00
Steven WdV
904215ef4d
Fix key overriding in regular maps for traversing 2025-07-20 15:37:45 +02:00
Steven WdV
41cc4fb4ac
Merge remote-tracking branch 'stevenwdv/merge-anchor-fix' into merge-anchor-fix
# Conflicts:
#	pkg/yqlib/doc/operators/anchor-and-alias-operators.md
#	pkg/yqlib/operator_anchors_aliases_test.go
2025-07-20 15:00:23 +02:00
Steven WdV
9c95a9f379
Unify reconstructAliasedMap & fixedReconstructAliasedMap 2025-07-20 14:59:17 +02:00
Steven WdV
a4720c089a
Merge remote-tracking branch 'origin/MakeExplodeGreatAgain' into merge-anchor-fix
# Conflicts:
#	pkg/yqlib/doc/operators/anchor-and-alias-operators.md
#	pkg/yqlib/operator_anchors_aliases.go
#	pkg/yqlib/operator_anchors_aliases_test.go
2025-07-20 14:37:36 +02:00
Steven WdV
3431aebb2c
Add tests for accessing !!str << 2025-07-20 13:28:14 +02:00
Steven WdV
ae87394f4a
Formatting 2025-07-20 13:24:48 +02:00
Mike Farah
23a7b173bf Fixing merge anchor key order 2025-07-19 15:27:44 +10:00
stevenwdv
5e75db824b
UK spelling 2025-07-17 11:43:28 +02:00
Steven WdV
08ecd39a1e
Add tests for invalid merge key handling for traverse 2025-07-16 18:07:16 +02:00
Steven WdV
b7aa711d94
Add note 2025-07-16 18:02:51 +02:00
Steven WdV
a5b8ef6cb1
Add some tests regarding override behavior.
The one in fixedTraversePathOperatorScenarios still fails
2025-07-16 17:56:56 +02:00
Steven WdV
a47e882c8f
Flag for fixed list merge key traverse override behavior,
and fix traversing map with merge key that would override local key (completes #2110 fix)
2025-07-16 16:00:16 +02:00
Steven WdV
128bf80eed
Merge branch 'master' into merge-anchor-fix 2025-07-16 14:25:23 +02:00
Mike Farah
4019d42d60 Install specific version of gosec 2025-07-16 15:51:42 +10:00
Mike Farah
55daf6d93c Fixed panic for syntax error when creating a map #2423 2025-07-16 15:35:12 +10:00
Mike Farah
8e731ac13c Added "debug-node-info" flag for inspecting yq AST 2025-07-15 21:35:54 +10:00
ryenus
d0c897f5e6 skip format check for filenames ending with dot
also add a unit test for func FormatStringFromFilename to cover such case
2025-07-15 19:30:07 +10:00
Mike Farah
6e8cc00030 Added flag to fix #2110 2025-07-14 16:26:26 +10:00
Mike Farah
b9d9e2fbad Moar tests 2025-07-11 22:00:20 +10:00
dependabot[bot]
a98921213f Bump golang.org/x/text from 0.26.0 to 0.27.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.26.0 to 0.27.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.26.0...v0.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-11 11:18:33 +10:00
dependabot[bot]
0153cccda9 Bump golang from 1.24.4 to 1.24.5
Bumps golang from 1.24.4 to 1.24.5.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-11 11:18:23 +10:00
Mike Farah
9b299649f7 Refactored initCommand 2025-07-11 10:17:21 +10:00
Mike Farah
369fe56e2d Adding test file (CursorAI) 2025-07-11 09:59:59 +10:00
Steven WdV
8c06478ade
Remove now-unused badAliasSample 2025-07-10 15:03:44 +02:00
Steven WdV
fa6dc5c9fb
Move new merge test comments to description field 2025-07-10 15:03:41 +02:00
Mike Farah
422300457c (attempt) to build snapcract for all architectures 2025-07-10 16:17:23 +10:00
cloudclaim
544bd9ff6f chore: fix some minor issues in the comments
Signed-off-by: cloudclaim <824973921@qq.com>
2025-07-10 13:33:36 +10:00
Mike Farah
1187c954ec Bumping version 2025-07-10 13:09:34 +10:00
Mike Farah
08d3789daa Preparing release 2025-07-10 13:07:43 +10:00
Mike Farah
6a5e95ac84 Merge branch 'imzue-build-small-yq' 2025-07-10 13:06:54 +10:00
Mike Farah
221a5b1106 Adding tinygo to project words 2025-07-10 13:06:42 +10:00
Mike Farah
c10bfe0602 Switching to YAML org supported go-yaml 2025-07-10 13:04:54 +10:00
Mike Farah
99f88df21e Switching to YAML org supported go-yaml 2025-07-10 13:01:09 +10:00
Zue
7696723d5c Add more build tags to reduce binary size 2025-07-01 15:52:40 +08:00
Zue
a9f7cc1ebb Add TinyGo build example 2025-07-01 13:28:10 +08:00
Zue
384d227efe Add yq_noini tag to build-small-yq 2025-07-01 13:27:21 +08:00
Zue
25365a0f0b Fix compilation errors when building build-small-yq 2025-07-01 13:26:26 +08:00
stevenwdv
78c096fa8f
Remove redundant logic 2025-06-17 15:56:13 +02:00
Steven WdV
ce9a4af0df
Fix inline map exploding when it contains aliases 2025-06-16 16:09:55 +02:00
Steven WdV
4734be9a4d
Fix excessive exploding for merge anchor 2025-06-16 13:53:28 +02:00
Steven WdV
4d88d51b1b
Fix precedence of merge anchor sequence for traverse (explode was already correct) 2025-06-16 13:36:42 +02:00
Steven WdV
31628e7324
Make merge anchor errors for traversing nonfatal 2025-06-16 13:33:42 +02:00
Steven WdV
bfcb3fc6b7
Fix merge anchor exploding
- Allow inline maps instead of just aliases
- Allow aliased sequences
- Disallow other types
- Use tag `!!merge` instead of key `<<`
- Fix insertion index for sequence merge

Closes #2386
2025-06-16 09:58:26 +02:00
Steven WdV
c3782799c5
Merge anchor traversing: add test for aliased sequence, cleanup 2025-06-16 09:56:10 +02:00
Steven WdV
162ea5437c
Fix merge anchor traversing
- Allow inline maps instead of just aliases
- Disallow nested sequences
- Disallow other types

Closes #2386
2025-06-13 19:08:36 +02:00
dependabot[bot]
40808838ba Bump golang.org/x/net from 0.40.0 to 0.41.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.40.0 to 0.41.0.
- [Commits](https://github.com/golang/net/compare/v0.40.0...v0.41.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-13 10:47:51 +10:00
Mike Farah
176873e93a Refining add op 2025-06-12 15:41:27 +10:00
dependabot[bot]
fd7e750040 Bump golang from 1.24.3 to 1.24.4
Bumps golang from 1.24.3 to 1.24.4.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-12 15:01:24 +10:00
Mike Farah
ff9d2acd4a Preparing release notes (won't release till I get back from holiday) 2025-06-12 14:53:39 +10:00
Mike Farah
88bfbec97b Fix add op when there are not matches piped in, #2383, #2384 2025-06-12 14:48:10 +10:00
Antoine Deschênes
b15ce77cad fix: correct key order after add, flatten, keys, sort, reverse and shuffle
Signed-off-by: Antoine Deschênes <antoine.deschenes@linux.com>
2025-06-07 21:04:26 +10:00
Mike Farah
b84fd47934 More goccy progress 2025-06-07 15:23:38 +10:00
dependabot[bot]
58e0cd2600 Bump github.com/goccy/go-yaml from 1.17.1 to 1.18.0
Bumps [github.com/goccy/go-yaml](https://github.com/goccy/go-yaml) from 1.17.1 to 1.18.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.17.1...v1.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 10:51:48 +10:00
o.bilko
c58d9e7da4 fix tests 2025-05-17 18:55:53 +10:00
beliys
3ac203ebb8 Add INI support 2025-05-17 18:55:53 +10:00
Mike Farah
b534aa9ee5 Bumping version 2025-05-11 21:31:47 +10:00
Mike Farah
39a65b62d2 Updating release notes 2025-05-10 07:41:19 +10:00
Mike Farah
1e3006e951 Removing old issue template 2025-05-10 07:25:48 +10:00
Mike Farah
22949df0fd Fixing running map against empty array bug #2359 2025-05-10 07:24:48 +10:00
Mike Farah
734e2cd254 Bumping version 2025-05-09 15:48:44 +10:00
Mike Farah
082b76affa Preparing release notes 2025-05-09 15:48:13 +10:00
Mike Farah
5bc2cd03da Bumping golang version 2025-05-09 15:45:28 +10:00
Mike Farah
20407a07a5 Adding more tests to prevent regression again :sweat 2025-05-09 15:40:29 +10:00
dependabot[bot]
0a83da6b38 Bump github.com/pelletier/go-toml/v2 from 2.2.3 to 2.2.4
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.3...v2.2.4)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-version: 2.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 15:40:02 +10:00
dependabot[bot]
77da8b7d32 Bump golang from 1.24.2 to 1.24.3
Bumps golang from 1.24.2 to 1.24.3.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 15:39:48 +10:00
dependabot[bot]
a27db3aacc Bump golang.org/x/net from 0.39.0 to 0.40.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/net/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 15:35:30 +10:00
dependabot[bot]
ed7fd54dac Bump github.com/a8m/envsubst from 1.4.2 to 1.4.3
Bumps [github.com/a8m/envsubst](https://github.com/a8m/envsubst) from 1.4.2 to 1.4.3.
- [Release notes](https://github.com/a8m/envsubst/releases)
- [Commits](https://github.com/a8m/envsubst/compare/v1.4.2...v1.4.3)

---
updated-dependencies:
- dependency-name: github.com/a8m/envsubst
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 15:33:53 +10:00
Mike Farah
2c2487c0bd Fixing Regression in #2353, #2359, #2325 2025-05-09 15:29:44 +10:00
Mike Farah
2c7cce0878 Line and Column working with Goccy, Fixed Anchor Map problem 2025-05-04 19:29:42 +10:00
Mike Farah
2bd08ea4e8 Dont panic on tests 2025-05-04 10:33:09 +10:00
Mike Farah
72fa3cca98 goccy decoder supports merge maps 2025-05-03 20:20:50 +10:00
Mike Farah
f89605e3c0 goccy yaml decoder supports anchor/aliases 2025-05-03 20:01:16 +10:00
Mike Farah
c59fa8de59 Bumping version 2025-05-03 16:40:24 +10:00
Mike Farah
ef8b520f89 Updating release notes 2025-05-03 16:39:36 +10:00
Mike Farah
20b5129120 Updating golanglint 2025-05-03 16:34:21 +10:00
Mike Farah
89518a09b8 Bumping golint 2025-05-03 15:58:14 +10:00
Mike Farah
d01ac7801d Regen doc 2025-05-03 11:37:52 +10:00
Mike Farah
67520b5e7b Adding windows arm build #2344 2025-05-03 11:37:52 +10:00
Romain Lebrun Thauront
7b6e61ba2c Update README.md 2025-05-03 11:36:12 +10:00
Romain Lebrun Thauront
cf8621d9e3 Install to /usr/local in README install instruction 2025-05-03 11:36:12 +10:00
dependabot[bot]
340b897252 Bump github.com/spf13/cobra from 1.8.1 to 1.9.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.1 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-03 11:28:22 +10:00
n4zukker
4155c99a12 Typo in cats example
The quotation mark and closing parenthesis were swapped.

Before:
```
$ echo "Hello kitty" | yq eval 'test("(?i)cats)"'
Error: bad expression - probably missing close bracket on TEST
```

After:
```
$ echo "Hello kitty" | yq eval 'test("(?i)cats")'
false
```
2025-05-03 11:27:51 +10:00
dependabot[bot]
b26de94758 Bump github.com/magiconair/properties from 1.8.9 to 1.8.10
Bumps [github.com/magiconair/properties](https://github.com/magiconair/properties) from 1.8.9 to 1.8.10.
- [Release notes](https://github.com/magiconair/properties/releases)
- [Commits](https://github.com/magiconair/properties/compare/v1.8.9...v1.8.10)

---
updated-dependencies:
- dependency-name: github.com/magiconair/properties
  dependency-version: 1.8.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-03 11:24:22 +10:00
dependabot[bot]
7dc5efee62 Bump golang.org/x/net from 0.34.0 to 0.39.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.34.0 to 0.39.0.
- [Commits](https://github.com/golang/net/compare/v0.34.0...v0.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-03 11:24:12 +10:00
Alberto Cavalcante
2cb7498444 chore(release): add windows arm64 binary target for release 2025-05-03 11:23:55 +10:00
Alberto Cavalcante
649c9a083a chore(check): use golangci-lint from GOPATH/bin as first option 2025-05-03 11:23:55 +10:00
Alberto Cavalcante
074d785396 chore(devtools): use curl instead of wget for better compatibility with MacOS 2025-05-03 11:23:55 +10:00
dependabot[bot]
de2f77b49c Bump github.com/alecthomas/participle/v2 from 2.1.1 to 2.1.4
Bumps [github.com/alecthomas/participle/v2](https://github.com/alecthomas/participle) from 2.1.1 to 2.1.4.
- [Release notes](https://github.com/alecthomas/participle/releases)
- [Changelog](https://github.com/alecthomas/participle/blob/master/CHANGES.md)
- [Commits](https://github.com/alecthomas/participle/compare/v2.1.1...v2.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-05 21:36:59 +11:00
Ashok Pariya
fe06096514 Add s390x platform support to Docker release workflow
Updated the list of supported platforms in the GitHub Actions
Docker release workflow to include linux/s390x.

Signed-off-by: Ashok Pariya <ashok.pariya@ibm.com>
2025-04-05 20:18:39 +11:00
dependabot[bot]
40f2c2381b Bump golang from 1.24.1 to 1.24.2
Bumps golang from 1.24.1 to 1.24.2.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-05 20:17:56 +11:00
123 changed files with 5006 additions and 623 deletions

View File

@ -1,51 +0,0 @@
---
name: Bug report - V3
about: Create a report to help us improve
title: ''
labels: bug, v3
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
Note that any how to questions should be posted in the discussion board and not raised as an issue.
Version of yq: 3.X.X
Operating system: mac/linux/windows/....
Installed via: docker/binary release/homebrew/snap/...
**Input Yaml**
Concise yaml document(s) (as simple as possible to show the bug, please keep it to 10 lines or less)
data1.yml:
```yaml
this: should really work
```
data2.yml:
```yaml
but: it strangely didn't
```
**Command**
The command you ran:
```
yq merge data1.yml data2.yml
```
**Actual behavior**
```yaml
cat: meow
```
**Expected behavior**
```yaml
this: should really work
but: it strangely didn't
```
**Additional context**
Add any other context about the problem here.

View File

@ -51,7 +51,7 @@ jobs:
IMAGE_VERSION=${VERSION:1}
echo "IMAGE_VERSION: ${IMAGE_VERSION}"
PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64,linux/arm/v7"
PLATFORMS="linux/amd64,linux/ppc64le,linux/arm64,linux/arm/v7,linux/s390x"
echo "Building and pushing version ${IMAGE_VERSION} of image ${IMAGE_NAME}"
docker buildx build \
@ -95,6 +95,6 @@ jobs:
-t "${IMAGE_NAME}:4-githubaction" \
-t "${IMAGE_NAME}:latest-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:${IMAGE_VERSION}-githubaction" \
-t "ghrc.io/${IMAGE_NAME}:4-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:4-githubaction" \
-t "ghcr.io/${IMAGE_NAME}:latest-githubaction" \
.

38
.golangci.bck.yml Normal file
View File

@ -0,0 +1,38 @@
run:
timeout: 5m
linters:
enable:
- asciicheck
- depguard
- errorlint
- gci
- gochecknoinits
- gofmt
- goimports
- gosec
- gosimple
- staticcheck
- unused
- misspell
- nakedret
- nolintlint
- predeclared
- revive
- unconvert
- unparam
linters-settings:
depguard:
rules:
prevent_unmaintained_packages:
list-mode: lax
files:
- $all
- "!$test"
deny:
- pkg: io/ioutil
desc: "replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil"
issues:
exclude-rules:
- linters:
- revive
text: "var-naming"

View File

@ -1,18 +1,11 @@
run:
timeout: 5m
version: "2"
linters:
enable:
- asciicheck
- depguard
- errorlint
- gci
- gochecknoinits
- gofmt
- goimports
- gosec
- gosimple
- staticcheck
- unused
- misspell
- nakedret
- nolintlint
@ -20,19 +13,40 @@ linters:
- revive
- unconvert
- unparam
linters-settings:
depguard:
settings:
depguard:
rules:
prevent_unmaintained_packages:
list-mode: lax
files:
- $all
- '!$test'
deny:
- pkg: io/ioutil
desc: 'replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil'
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
prevent_unmaintained_packages:
list-mode: lax
files:
- $all
- "!$test"
deny:
- pkg: io/ioutil
desc: "replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil"
issues:
exclude-rules:
- linters:
- revive
text: "var-naming"
- linters:
- revive
text: var-naming
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -38,6 +38,8 @@ builds:
- openbsd_amd64
- windows_386
- windows_amd64
- windows_arm
- windows_arm64
no_unique_dist_dir: true

View File

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

View File

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

View File

@ -3,7 +3,7 @@
![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 and XML processor. `yq` uses [jq](https://github.com/stedolan/jq) like syntax but works with yaml files as well as json, xml, 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) 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.
@ -76,21 +76,21 @@ For instance, VERSION=v4.2.0 and BINARY=yq_linux_amd64
#### Compressed via tar.gz
```bash
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY}.tar.gz -O - |\
tar xz && mv ${BINARY} /usr/bin/yq
tar xz && mv ${BINARY} /usr/local/bin/yq
```
#### Plain binary
```bash
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/bin/yq &&\
chmod +x /usr/bin/yq
wget https://github.com/mikefarah/yq/releases/download/${VERSION}/${BINARY} -O /usr/local/bin/yq &&\
chmod +x /usr/local/bin/yq
```
#### Latest version
```bash
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
chmod +x /usr/bin/yq
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq &&\
chmod +x /usr/local/bin/yq
```
### MacOS / Linux via Homebrew:
@ -327,7 +327,7 @@ flox install yq
## Features
- [Detailed documentation with many examples](https://mikefarah.gitbook.io/yq/)
- Written in portable go, so you can download a lovely dependency free binary
- Uses similar syntax as `jq` but works with YAML, [JSON](https://mikefarah.gitbook.io/yq/usage/convert) and [XML](https://mikefarah.gitbook.io/yq/usage/xml) files
- Uses similar syntax as `jq` but works with YAML, INI, [JSON](https://mikefarah.gitbook.io/yq/usage/convert) and [XML](https://mikefarah.gitbook.io/yq/usage/xml) files
- Fully supports multi document yaml files
- Supports yaml [front matter](https://mikefarah.gitbook.io/yq/usage/front-matter) blocks (e.g. jekyll/assemble)
- Colorized yaml output
@ -366,31 +366,52 @@ yq -i '.stuff = "foo"' myfile.yml # update myfile.yml in place
Available Commands:
completion Generate the autocompletion script for the specified shell
eval (default) Apply the expression to each document in each yaml file in sequence
eval-all Loads _all_ yaml documents of _all_ yaml files and runs expression once
help Help about any command
completion Generate the autocompletion script for the specified shell
eval (default) Apply the expression to each document in each yaml file in sequence
eval-all Loads _all_ yaml documents of _all_ yaml files and runs expression once
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 [yaml|y|xml|x] parse format for input. Note that json is a subset of yaml. (default "yaml")
-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 [yaml|y|json|j|props|p|xml|x] output format type. (default "yaml")
-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 = ""'
-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.
--unwrapScalar unwrap scalar, print the value with no quotes, colors or comments (default true)
--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-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.
Use "yq [command] --help" for more information about a command.
```

View File

@ -3,7 +3,7 @@
testLoadFileNotExist() {
result=$(./yq e -n 'load("cat.yml")' 2>&1)
assertEquals 1 $?
assertEquals "Error: Failed to load cat.yml: open cat.yml: no such file or directory" "$result"
assertEquals "Error: failed to load cat.yml: open cat.yml: no such file or directory" "$result"
}
testLoadFileExpNotExist() {
@ -15,7 +15,7 @@ testLoadFileExpNotExist() {
testStrLoadFileNotExist() {
result=$(./yq e -n 'strload("cat.yml")' 2>&1)
assertEquals 1 $?
assertEquals "Error: Failed to load cat.yml: open cat.yml: no such file or directory" "$result"
assertEquals "Error: failed to load cat.yml: open cat.yml: no such file or directory" "$result"
}
testStrLoadFileExpNotExist() {

View File

@ -2,6 +2,8 @@ package cmd
var unwrapScalarFlag = newUnwrapFlag()
var printNodeInfo = false
var unwrapScalar = false
var writeInplace = false

View File

@ -105,6 +105,11 @@ func evaluateSequence(cmd *cobra.Command, args []string) (cmdError error) {
}
printer := yqlib.NewPrinter(encoder, printerWriter)
if printNodeInfo {
printer = yqlib.NewNodeInfoPrinter(printerWriter)
}
if nulSepOutput {
printer.SetNulSepOutput(true)
}

View File

@ -99,6 +99,7 @@ yq -P -oy sample.json
}
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode")
rootCmd.PersistentFlags().BoolVarP(&printNodeInfo, "debug-node-info", "", false, "debug node info")
rootCmd.PersistentFlags().BoolVarP(&outputToJSON, "tojson", "j", false, "(deprecated) output as json. Set indent to 0 to print json in one line.")
err := rootCmd.PersistentFlags().MarkDeprecated("tojson", "please use -o=json instead")
@ -196,6 +197,7 @@ yq -P -oy sample.json
panic(err)
}
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.LeadingContentPreProcessing, "header-preprocess", "", true, "Slurp any header comments and separators before processing expression.")
rootCmd.PersistentFlags().BoolVarP(&yqlib.ConfiguredYamlPreferences.FixMergeAnchorToSpec, "yaml-fix-merge-anchor-to-spec", "", false, "Fix merge anchor to match YAML spec. Will default to true in late 2025")
rootCmd.PersistentFlags().StringVarP(&splitFileExp, "split-exp", "s", "", "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.")
if err = rootCmd.RegisterFlagCompletionFunc("split-exp", cobra.NoFileCompletions); err != nil {

View File

@ -18,52 +18,100 @@ func isAutomaticOutputFormat() bool {
func initCommand(cmd *cobra.Command, args []string) (string, []string, error) {
cmd.SilenceUsage = true
fileInfo, _ := os.Stdout.Stat()
if forceColor || (!forceNoColor && (fileInfo.Mode()&os.ModeCharDevice) != 0) {
colorsEnabled = true
}
setupColors()
expression, args, err := processArgs(args)
if err != nil {
return "", nil, err
}
if err := loadSplitFileExpression(); err != nil {
return "", nil, err
}
handleBackwardsCompatibility()
if err := validateCommandFlags(args); err != nil {
return "", nil, err
}
if err := configureFormats(args); err != nil {
return "", nil, err
}
configureUnwrapScalar()
return expression, args, nil
}
func setupColors() {
fileInfo, _ := os.Stdout.Stat()
if forceColor || (!forceNoColor && (fileInfo.Mode()&os.ModeCharDevice) != 0) {
colorsEnabled = true
}
}
func loadSplitFileExpression() error {
if splitFileExpFile != "" {
splitExpressionBytes, err := os.ReadFile(splitFileExpFile)
if err != nil {
return "", nil, err
return err
}
splitFileExp = string(splitExpressionBytes)
}
return nil
}
func handleBackwardsCompatibility() {
// backwards compatibility
if outputToJSON {
outputFormat = "json"
}
}
func validateCommandFlags(args []string) error {
if writeInplace && (len(args) == 0 || args[0] == "-") {
return "", nil, fmt.Errorf("write in place flag only applicable when giving an expression and at least one file")
return fmt.Errorf("write in place flag only applicable when giving an expression and at least one file")
}
if frontMatter != "" && len(args) == 0 {
return "", nil, fmt.Errorf("front matter flag only applicable when giving an expression and at least one file")
return fmt.Errorf("front matter flag only applicable when giving an expression and at least one file")
}
if writeInplace && splitFileExp != "" {
return "", nil, fmt.Errorf("write in place cannot be used with split file")
return fmt.Errorf("write in place cannot be used with split file")
}
if nullInput && len(args) > 0 {
return "", nil, fmt.Errorf("cannot pass files in when using null-input flag")
return fmt.Errorf("cannot pass files in when using null-input flag")
}
return nil
}
func configureFormats(args []string) error {
inputFilename := ""
if len(args) > 0 {
inputFilename = args[0]
}
if inputFormat == "" || inputFormat == "auto" || inputFormat == "a" {
if err := configureInputFormat(inputFilename); err != nil {
return err
}
if err := configureOutputFormat(); err != nil {
return err
}
yqlib.GetLogger().Debug("Using input format %v", inputFormat)
yqlib.GetLogger().Debug("Using output format %v", outputFormat)
return nil
}
func configureInputFormat(inputFilename string) error {
if inputFormat == "" || inputFormat == "auto" || inputFormat == "a" {
inputFormat = yqlib.FormatStringFromFilename(inputFilename)
_, err := yqlib.FormatFromString(inputFormat)
@ -88,24 +136,27 @@ func initCommand(cmd *cobra.Command, args []string) (string, []string, error) {
}
outputFormat = "yaml"
}
return nil
}
func configureOutputFormat() error {
outputFormatType, err := yqlib.FormatFromString(outputFormat)
if err != nil {
return "", nil, err
return err
}
yqlib.GetLogger().Debug("Using input format %v", inputFormat)
yqlib.GetLogger().Debug("Using output format %v", outputFormat)
if outputFormatType == yqlib.YamlFormat ||
outputFormatType == yqlib.PropertiesFormat {
unwrapScalar = true
}
return nil
}
func configureUnwrapScalar() {
if unwrapScalarFlag.IsExplicitlySet() {
unwrapScalar = unwrapScalarFlag.IsSet()
}
return expression, args, nil
}
func configureDecoder(evaluateTogether bool) (yqlib.Decoder, error) {

1435
cmd/utils_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ var (
GitDescribe string
// Version is main version number that is being run at the moment.
Version = "v4.45.1"
Version = "v4.47.1"
// VersionPrerelease is a pre-release marker for the version. If this is "" (empty string)
// then it means that it is a final release. Otherwise, this is a pre-release
@ -45,5 +45,5 @@ func getHumanVersion() string {
}
// Strip off any single quotes added by the git information.
return strings.Replace(version, "'", "", -1)
return strings.ReplaceAll(version, "'", "")
}

View File

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

View File

@ -1,6 +1,6 @@
---
a: apple
b: bannana
b: banana
---
hello there
apples: great

14
examples/sample.ini Normal file
View File

@ -0,0 +1,14 @@
; This is a INI document
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
db_host = "localhost"
db_port = 5432
db_user = "postgres"
db_password = ""
db_name = "postgres"

View File

@ -0,0 +1,8 @@
; This is a INI document
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
enabled = true
ip = "10.0.0.1"
role = "frontend"
treads = 4

31
go.mod
View File

@ -1,32 +1,35 @@
module github.com/mikefarah/yq/v4
require (
github.com/a8m/envsubst v1.4.2
github.com/alecthomas/participle/v2 v2.1.1
github.com/alecthomas/repr v0.4.0
github.com/a8m/envsubst v1.4.3
github.com/alecthomas/participle/v2 v2.1.4
github.com/alecthomas/repr v0.5.1
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.13.3
github.com/goccy/go-yaml v1.18.0
github.com/jinzhu/copier v0.4.0
github.com/magiconair/properties v1.8.9
github.com/pelletier/go-toml/v2 v2.2.3
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.8.1
github.com/spf13/pflag v1.0.6
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/yuin/gopher-lua v1.1.1
golang.org/x/net v0.34.0
golang.org/x/text v0.23.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/net v0.42.0
golang.org/x/text v0.27.0
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/sys v0.34.0 // indirect
)
go 1.23.0
go 1.24
toolchain go1.24.1

80
go.sum
View File

@ -1,78 +1,64 @@
github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg=
github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0=
github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8=
github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/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=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw=
github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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.13.3 h1:IXRULR8mAa0MXQobzzp0VOfMUJ8EnaQ4x3jhf7S0/nI=
github.com/goccy/go-yaml v1.13.3/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/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/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=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=

View File

@ -53,6 +53,20 @@ func createScalarNode(value interface{}, stringValue string) *CandidateNode {
return node
}
type NodeInfo struct {
Kind string `yaml:"kind"`
Style string `yaml:"style,omitempty"`
Anchor string `yaml:"anchor,omitempty"`
Tag string `yaml:"tag,omitempty"`
HeadComment string `yaml:"headComment,omitempty"`
LineComment string `yaml:"lineComment,omitempty"`
FootComment string `yaml:"footComment,omitempty"`
Value string `yaml:"value,omitempty"`
Line int `yaml:"line,omitempty"`
Column int `yaml:"column,omitempty"`
Content []*NodeInfo `yaml:"content,omitempty"`
}
type CandidateNode struct {
Kind Kind
Style Style
@ -155,6 +169,18 @@ func (n *CandidateNode) getParsedKey() interface{} {
}
func (n *CandidateNode) FilterMapContentByKey(keyPredicate func(*CandidateNode) bool) []*CandidateNode {
var result []*CandidateNode
for index := 0; index < len(n.Content); index = index + 2 {
keyNode := n.Content[index]
valueNode := n.Content[index+1]
if keyPredicate(keyNode) {
result = append(result, keyNode, valueNode)
}
}
return result
}
func (n *CandidateNode) GetPath() []interface{} {
key := n.getParsedKey()
if n.Parent != nil && key != nil {
@ -201,13 +227,14 @@ func (n *CandidateNode) SetParent(parent *CandidateNode) {
type ValueVisitor func(*CandidateNode) error
func (n *CandidateNode) VisitValues(visitor ValueVisitor) error {
if n.Kind == MappingNode {
switch n.Kind {
case MappingNode:
for i := 1; i < len(n.Content); i = i + 2 {
if err := visitor(n.Content[i]); err != nil {
return err
}
}
} else if n.Kind == SequenceNode {
case SequenceNode:
for i := 0; i < len(n.Content); i = i + 1 {
if err := visitor(n.Content[i]); err != nil {
return err
@ -238,14 +265,13 @@ func (n *CandidateNode) AddKeyValueChild(rawKey *CandidateNode, rawValue *Candid
func (n *CandidateNode) AddChild(rawChild *CandidateNode) {
value := rawChild.Copy()
value.SetParent(n)
if value.Key != nil {
value.Key.SetParent(n)
} else {
index := len(n.Content)
keyNode := createScalarNode(index, fmt.Sprintf("%v", index))
keyNode.SetParent(n)
value.Key = keyNode
}
value.IsMapKey = false
index := len(n.Content)
keyNode := createScalarNode(index, fmt.Sprintf("%v", index))
keyNode.SetParent(n)
value.Key = keyNode
n.Content = append(n.Content, value)
}
@ -451,3 +477,64 @@ func (n *CandidateNode) UpdateAttributesFrom(other *CandidateNode, prefs assignP
n.LineComment = other.LineComment
}
}
func (n *CandidateNode) ConvertToNodeInfo() *NodeInfo {
info := &NodeInfo{
Kind: kindToString(n.Kind),
Style: styleToString(n.Style),
Anchor: n.Anchor,
Tag: n.Tag,
HeadComment: n.HeadComment,
LineComment: n.LineComment,
FootComment: n.FootComment,
Value: n.Value,
Line: n.Line,
Column: n.Column,
}
if len(n.Content) > 0 {
info.Content = make([]*NodeInfo, len(n.Content))
for i, child := range n.Content {
info.Content[i] = child.ConvertToNodeInfo()
}
}
return info
}
// Helper functions to convert Kind and Style to string for NodeInfo
func kindToString(k Kind) string {
switch k {
case SequenceNode:
return "SequenceNode"
case MappingNode:
return "MappingNode"
case ScalarNode:
return "ScalarNode"
case AliasNode:
return "AliasNode"
default:
return "Unknown"
}
}
func styleToString(s Style) string {
var styles []string
if s&TaggedStyle != 0 {
styles = append(styles, "TaggedStyle")
}
if s&DoubleQuotedStyle != 0 {
styles = append(styles, "DoubleQuotedStyle")
}
if s&SingleQuotedStyle != 0 {
styles = append(styles, "SingleQuotedStyle")
}
if s&LiteralStyle != 0 {
styles = append(styles, "LiteralStyle")
}
if s&FoldedStyle != 0 {
styles = append(styles, "FoldedStyle")
}
if s&FlowStyle != 0 {
styles = append(styles, "FlowStyle")
}
return strings.Join(styles, ",")
}

View File

@ -9,14 +9,14 @@ import (
goccyToken "github.com/goccy/go-yaml/token"
)
func (o *CandidateNode) goccyDecodeIntoChild(childNode ast.Node, cm yaml.CommentMap) (*CandidateNode, error) {
func (o *CandidateNode) goccyDecodeIntoChild(childNode ast.Node, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) (*CandidateNode, error) {
newChild := o.CreateChild()
err := newChild.UnmarshalGoccyYAML(childNode, cm)
err := newChild.UnmarshalGoccyYAML(childNode, cm, anchorMap)
return newChild, err
}
func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) error {
func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) error {
log.Debugf("UnmarshalYAML %v", node)
log.Debugf("UnmarshalYAML %v", node.Type().String())
log.Debugf("UnmarshalYAML Node Value: %v", node.String())
@ -51,6 +51,9 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) er
}
o.Value = node.String()
o.Line = node.GetToken().Position.Line
o.Column = node.GetToken().Position.Column
switch node.Type() {
case ast.IntegerType:
o.Kind = ScalarNode
@ -62,9 +65,13 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) er
o.Kind = ScalarNode
o.Tag = "!!bool"
case ast.NullType:
log.Debugf("its a null type with value %v", node.GetToken().Value)
o.Kind = ScalarNode
o.Tag = "!!null"
o.Value = node.GetToken().Value
if node.GetToken().Type == goccyToken.ImplicitNullType {
o.Value = ""
}
case ast.StringType:
o.Kind = ScalarNode
o.Tag = "!!str"
@ -92,7 +99,7 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) er
// to solve the multiline > problem
o.Value = astLiteral.Value.Value
case ast.TagType:
if err := o.UnmarshalGoccyYAML(node.(*ast.TagNode).Value, cm); err != nil {
if err := o.UnmarshalGoccyYAML(node.(*ast.TagNode).Value, cm, anchorMap); err != nil {
return err
}
o.Tag = node.(*ast.TagNode).Start.Value
@ -106,9 +113,9 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) er
o.Style = FlowStyle
}
for _, mappingValueNode := range mappingNode.Values {
err := o.goccyProcessMappingValueNode(mappingValueNode, cm)
err := o.goccyProcessMappingValueNode(mappingValueNode, cm, anchorMap)
if err != nil {
return ast.ErrInvalidAnchorName
return err
}
}
if mappingNode.FootComment != nil {
@ -120,9 +127,9 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) er
o.Kind = MappingNode
o.Tag = "!!map"
mappingValueNode := node.(*ast.MappingValueNode)
err := o.goccyProcessMappingValueNode(mappingValueNode, cm)
err := o.goccyProcessMappingValueNode(mappingValueNode, cm, anchorMap)
if err != nil {
return ast.ErrInvalidAnchorName
return err
}
case ast.SequenceType:
log.Debugf("UnmarshalYAML - a sequence node")
@ -141,7 +148,7 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) er
keyNode.Kind = ScalarNode
keyNode.Value = fmt.Sprintf("%v", i)
valueNode, err := o.goccyDecodeIntoChild(astSeq[i], cm)
valueNode, err := o.goccyDecodeIntoChild(astSeq[i], cm, anchorMap)
if err != nil {
return err
}
@ -149,32 +156,55 @@ func (o *CandidateNode) UnmarshalGoccyYAML(node ast.Node, cm yaml.CommentMap) er
valueNode.Key = keyNode
o.Content[i] = valueNode
}
case ast.AnchorType:
log.Debugf("UnmarshalYAML - an anchor node")
anchorNode := node.(*ast.AnchorNode)
err := o.UnmarshalGoccyYAML(anchorNode.Value, cm, anchorMap)
if err != nil {
return err
}
o.Anchor = anchorNode.Name.String()
anchorMap[o.Anchor] = o
case ast.AliasType:
log.Debugf("UnmarshalYAML - an alias node")
aliasNode := node.(*ast.AliasNode)
o.Kind = AliasNode
o.Value = aliasNode.Value.String()
o.Alias = anchorMap[o.Value]
case ast.MergeKeyType:
log.Debugf("UnmarshalYAML - a merge key")
o.Kind = ScalarNode
o.Tag = "!!merge" // note - I should be able to get rid of this.
o.Value = "<<"
default:
log.Debugf("UnmarshalYAML - node idea of the type!!")
log.Debugf("UnmarshalYAML - no idea of the type!!\n%v: %v", node.Type(), node.String())
}
log.Debugf("KIND: %v", o.Kind)
return nil
}
func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingValueNode, cm yaml.CommentMap) error {
func (o *CandidateNode) goccyProcessMappingValueNode(mappingEntry *ast.MappingValueNode, cm yaml.CommentMap, anchorMap map[string]*CandidateNode) error {
log.Debug("UnmarshalYAML MAP KEY entry %v", mappingEntry.Key)
keyNode, err := o.goccyDecodeIntoChild(mappingEntry.Key, cm)
if err != nil {
// AddKeyValueFirst because it clones the nodes, and we want to have the real refs when Unmarshalling
// particularly for the anchorMap
keyNode, valueNode := o.AddKeyValueChild(&CandidateNode{}, &CandidateNode{})
if err := keyNode.UnmarshalGoccyYAML(mappingEntry.Key, cm, anchorMap); err != nil {
return err
}
keyNode.IsMapKey = true
log.Debug("UnmarshalYAML MAP VALUE entry %v", mappingEntry.Value)
valueNode, err := o.goccyDecodeIntoChild(mappingEntry.Value, cm)
if err != nil {
if err := valueNode.UnmarshalGoccyYAML(mappingEntry.Value, cm, anchorMap); err != nil {
return err
}
if mappingEntry.FootComment != nil {
valueNode.FootComment = mappingEntry.FootComment.String()
}
o.AddKeyValueChild(keyNode, valueNode)
return nil
}

View File

@ -160,3 +160,47 @@ func TestCandidateNodeAddKeyValueChild(t *testing.T) {
test.AssertResult(t, key.IsMapKey, true)
}
func TestConvertToNodeInfo(t *testing.T) {
child := &CandidateNode{
Kind: ScalarNode,
Style: DoubleQuotedStyle,
Tag: "!!str",
Value: "childValue",
Line: 2,
Column: 3,
}
parent := &CandidateNode{
Kind: MappingNode,
Style: TaggedStyle,
Tag: "!!map",
Value: "",
Line: 1,
Column: 1,
Content: []*CandidateNode{child},
HeadComment: "head",
LineComment: "line",
FootComment: "foot",
Anchor: "anchor",
}
info := parent.ConvertToNodeInfo()
test.AssertResult(t, "MappingNode", info.Kind)
test.AssertResult(t, "TaggedStyle", info.Style)
test.AssertResult(t, "!!map", info.Tag)
test.AssertResult(t, "head", info.HeadComment)
test.AssertResult(t, "line", info.LineComment)
test.AssertResult(t, "foot", info.FootComment)
test.AssertResult(t, "anchor", info.Anchor)
test.AssertResult(t, 1, info.Line)
test.AssertResult(t, 1, info.Column)
if len(info.Content) != 1 {
t.Fatalf("Expected 1 child, got %d", len(info.Content))
}
childInfo := info.Content[0]
test.AssertResult(t, "ScalarNode", childInfo.Kind)
test.AssertResult(t, "DoubleQuotedStyle", childInfo.Style)
test.AssertResult(t, "!!str", childInfo.Tag)
test.AssertResult(t, "childValue", childInfo.Value)
test.AssertResult(t, 2, childInfo.Line)
test.AssertResult(t, 3, childInfo.Column)
}

View File

@ -3,7 +3,7 @@ package yqlib
import (
"fmt"
yaml "gopkg.in/yaml.v3"
yaml "go.yaml.in/yaml/v3"
)
func MapYamlStyle(original yaml.Style) Style {
@ -78,9 +78,6 @@ func (o *CandidateNode) copyToYamlNode(node *yaml.Node) {
node.Value = o.Value
node.Anchor = o.Anchor
// node.Alias = TODO - find Alias in our own structure
// might need to be a post process thing
node.HeadComment = o.HeadComment
node.LineComment = o.LineComment

View File

@ -1,3 +1,5 @@
//go:build !yq_nojson
package yqlib
import (

View File

@ -2,9 +2,11 @@ package yqlib
import (
"container/list"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
logging "gopkg.in/op/go-logging.v1"
)
func TestChildContext(t *testing.T) {
@ -49,3 +51,211 @@ func TestChildContextNoVariables(t *testing.T) {
test.AssertResultComplex(t, make(map[string]*list.List), clone.Variables)
}
func TestSingleReadonlyChildContext(t *testing.T) {
original := Context{
DontAutoCreate: false,
datetimeLayout: "2006-01-02",
}
candidate := &CandidateNode{Value: "test"}
clone := original.SingleReadonlyChildContext(candidate)
// Should have DontAutoCreate set to true
test.AssertResultComplex(t, true, clone.DontAutoCreate)
// Should have the candidate node in MatchingNodes
test.AssertResultComplex(t, 1, clone.MatchingNodes.Len())
test.AssertResultComplex(t, candidate, clone.MatchingNodes.Front().Value)
}
func TestSingleChildContext(t *testing.T) {
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
}
candidate := &CandidateNode{Value: "test"}
clone := original.SingleChildContext(candidate)
// Should preserve DontAutoCreate
test.AssertResultComplex(t, true, clone.DontAutoCreate)
// Should have the candidate node in MatchingNodes
test.AssertResultComplex(t, 1, clone.MatchingNodes.Len())
test.AssertResultComplex(t, candidate, clone.MatchingNodes.Front().Value)
}
func TestSetDateTimeLayout(t *testing.T) {
context := Context{}
// Test setting datetime layout
context.SetDateTimeLayout("2006-01-02T15:04:05Z07:00")
test.AssertResultComplex(t, "2006-01-02T15:04:05Z07:00", context.datetimeLayout)
}
func TestGetDateTimeLayout(t *testing.T) {
// Test with custom layout
context := Context{datetimeLayout: "2006-01-02"}
result := context.GetDateTimeLayout()
test.AssertResultComplex(t, "2006-01-02", result)
// Test with empty layout (should return default)
context = Context{}
result = context.GetDateTimeLayout()
test.AssertResultComplex(t, "2006-01-02T15:04:05Z07:00", result)
}
func TestGetVariable(t *testing.T) {
// Test with nil Variables
context := Context{}
result := context.GetVariable("nonexistent")
test.AssertResultComplex(t, (*list.List)(nil), result)
// Test with existing variable
variables := make(map[string]*list.List)
variables["test"] = list.New()
variables["test"].PushBack(&CandidateNode{Value: "value"})
context = Context{Variables: variables}
result = context.GetVariable("test")
test.AssertResultComplex(t, variables["test"], result)
// Test with non-existent variable
result = context.GetVariable("nonexistent")
test.AssertResultComplex(t, (*list.List)(nil), result)
}
func TestSetVariable(t *testing.T) {
// Test setting variable when Variables is nil
context := Context{}
value := list.New()
value.PushBack(&CandidateNode{Value: "test"})
context.SetVariable("key", value)
test.AssertResultComplex(t, value, context.Variables["key"])
// Test setting variable when Variables already exists
context.SetVariable("key2", value)
test.AssertResultComplex(t, value, context.Variables["key2"])
}
func TestToString(t *testing.T) {
context := Context{
DontAutoCreate: true,
MatchingNodes: list.New(),
}
// Add a node to test the full string representation
node := &CandidateNode{Value: "test"}
context.MatchingNodes.PushBack(node)
// Test with debug logging disabled (default)
result := context.ToString()
test.AssertResultComplex(t, "", result)
// Test with debug logging enabled
logging.SetLevel(logging.DEBUG, "")
defer logging.SetLevel(logging.INFO, "") // Reset to default
result2 := context.ToString()
test.AssertResultComplex(t, true, len(result2) > 0)
test.AssertResultComplex(t, true, strings.Contains(result2, "Context"))
test.AssertResultComplex(t, true, strings.Contains(result2, "DontAutoCreate: true"))
}
func TestDeepClone(t *testing.T) {
// Create original context with variables and matching nodes
originalVariables := make(map[string]*list.List)
originalVariables["test"] = list.New()
originalVariables["test"].PushBack(&CandidateNode{Value: "original"})
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
Variables: originalVariables,
MatchingNodes: list.New(),
}
// Add a node to MatchingNodes
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.DeepClone()
// Should preserve DontAutoCreate and datetimeLayout
test.AssertResultComplex(t, original.DontAutoCreate, clone.DontAutoCreate)
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
// Should have copied variables
test.AssertResultComplex(t, 1, len(clone.Variables))
test.AssertResultComplex(t, "original", clone.Variables["test"].Front().Value.(*CandidateNode).Value)
// Should have deep copied MatchingNodes
test.AssertResultComplex(t, 1, clone.MatchingNodes.Len())
// Verify it's a deep copy by modifying the original
original.MatchingNodes.Front().Value.(*CandidateNode).Value = "modified"
test.AssertResultComplex(t, "test", clone.MatchingNodes.Front().Value.(*CandidateNode).Value)
}
func TestClone(t *testing.T) {
// Create original context
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
MatchingNodes: list.New(),
}
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.Clone()
// Should preserve DontAutoCreate and datetimeLayout
test.AssertResultComplex(t, original.DontAutoCreate, clone.DontAutoCreate)
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
// Should have the same MatchingNodes reference
test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes)
}
func TestReadOnlyClone(t *testing.T) {
original := Context{
DontAutoCreate: false,
datetimeLayout: "2006-01-02",
MatchingNodes: list.New(),
}
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.ReadOnlyClone()
// Should set DontAutoCreate to true
test.AssertResultComplex(t, true, clone.DontAutoCreate)
// Should preserve other fields
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes)
}
func TestWritableClone(t *testing.T) {
original := Context{
DontAutoCreate: true,
datetimeLayout: "2006-01-02",
MatchingNodes: list.New(),
}
node := &CandidateNode{Value: "test"}
original.MatchingNodes.PushBack(node)
clone := original.WritableClone()
// Should set DontAutoCreate to false
test.AssertResultComplex(t, false, clone.DontAutoCreate)
// Should preserve other fields
test.AssertResultComplex(t, original.datetimeLayout, clone.datetimeLayout)
test.AssertResultComplex(t, original.MatchingNodes, clone.MatchingNodes)
}

View File

@ -64,6 +64,6 @@ func (d *dataTreeNavigator) GetMatchingNodes(context Context, expressionNode *Ex
if handler != nil {
return handler(d, context, expressionNode)
}
return Context{}, fmt.Errorf("Unknown operator %v", expressionNode.Operation.OperationType)
return Context{}, fmt.Errorf("unknown operator %v", expressionNode.Operation.OperationType.Type)
}

View File

@ -0,0 +1,437 @@
package yqlib
import (
"container/list"
"testing"
"github.com/mikefarah/yq/v4/test"
)
func TestGetMatchingNodes_NilExpressionNode(t *testing.T) {
navigator := NewDataTreeNavigator()
context := Context{
MatchingNodes: list.New(),
}
result, err := navigator.GetMatchingNodes(context, nil)
test.AssertResult(t, nil, err)
test.AssertResultComplex(t, context, result)
}
func TestGetMatchingNodes_UnknownOperator(t *testing.T) {
navigator := NewDataTreeNavigator()
context := Context{
MatchingNodes: list.New(),
}
// Create an expression node with an unknown operation type
unknownOpType := &operationType{Type: "UNKNOWN", Handler: nil}
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: unknownOpType},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, "unknown operator UNKNOWN", err.Error())
test.AssertResultComplex(t, Context{}, result)
}
func TestGetMatchingNodes_ValidOperator(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a simple context with a scalar node
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "test",
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(scalarNode)
// Create an expression node with a valid operation (self reference)
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: selfReferenceOpType},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, nil, err)
test.AssertResult(t, 1, result.MatchingNodes.Len())
// Verify the result contains the same node
resultNode := result.MatchingNodes.Front().Value.(*CandidateNode)
test.AssertResult(t, scalarNode, resultNode)
}
func TestDeeplyAssign_ScalarNode(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "existing", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "old_value"},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "new_value",
}
// Assign to path ["new_key"]
path := []interface{}{"new_key"}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the assignment was made
// The root node should now have the new key-value pair
test.AssertResult(t, 4, len(rootNode.Content)) // 2 original + 2 new
// Find the new key-value pair
found := false
for i := 0; i < len(rootNode.Content)-1; i += 2 {
key := rootNode.Content[i]
value := rootNode.Content[i+1]
if key.Value == "new_key" && value.Value == "new_value" {
found = true
break
}
}
test.AssertResult(t, true, found)
}
func TestDeeplyAssign_MappingNode(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "existing", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "old_value"},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a mapping node to assign (this should trigger deep merge)
mappingNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "nested_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "nested_value"},
},
}
// Assign to path ["new_map"]
path := []interface{}{"new_map"}
err := navigator.DeeplyAssign(context, path, mappingNode)
test.AssertResult(t, nil, err)
// Verify the assignment was made
// The root node should now have the new mapping
test.AssertResult(t, 4, len(rootNode.Content)) // 2 original + 2 new
// Find the new mapping
found := false
for i := 0; i < len(rootNode.Content); i += 2 {
if i+1 < len(rootNode.Content) {
key := rootNode.Content[i]
value := rootNode.Content[i+1]
if key.Value == "new_map" && value.Kind == MappingNode {
found = true
// Verify the nested content
test.AssertResult(t, 2, len(value.Content))
test.AssertResult(t, "nested_key", value.Content[0].Value)
test.AssertResult(t, "nested_value", value.Content[1].Value)
break
}
}
}
test.AssertResult(t, true, found)
}
func TestDeeplyAssign_DeepPath(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "level1", IsMapKey: true},
{Kind: MappingNode, Tag: "!!map", Content: []*CandidateNode{}},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "deep_value",
}
// Assign to deep path ["level1", "level2", "level3"]
path := []interface{}{"level1", "level2", "level3"}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the deep assignment was made
level1Node := rootNode.Content[1] // The mapping node
test.AssertResult(t, 2, len(level1Node.Content)) // Should have level2 key-value
level2Key := level1Node.Content[0]
level2Value := level1Node.Content[1]
test.AssertResult(t, "level2", level2Key.Value)
test.AssertResult(t, MappingNode, level2Value.Kind)
level3Key := level2Value.Content[0]
level3Value := level2Value.Content[1]
test.AssertResult(t, "level3", level3Key.Value)
test.AssertResult(t, "deep_value", level3Value.Value)
}
func TestDeeplyAssign_ArrayPath(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node containing an array
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "array", IsMapKey: true},
{Kind: SequenceNode, Tag: "!!seq", Content: []*CandidateNode{}},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "array_value",
}
// Assign to array path ["array", 0]
path := []interface{}{"array", 0}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the array assignment was made
arrayNode := rootNode.Content[1] // The sequence node
test.AssertResult(t, 1, len(arrayNode.Content)) // Should have one element
arrayElement := arrayNode.Content[0]
test.AssertResult(t, "array_value", arrayElement.Value)
}
func TestDeeplyAssign_OverwriteExisting(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "old_value"},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a scalar node to assign
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "new_value",
}
// Assign to existing path ["key"]
path := []interface{}{"key"}
err := navigator.DeeplyAssign(context, path, scalarNode)
test.AssertResult(t, nil, err)
// Verify the value was overwritten
test.AssertResult(t, 2, len(rootNode.Content)) // Should still have 2 elements
key := rootNode.Content[0]
value := rootNode.Content[1]
test.AssertResult(t, "key", key.Value)
test.AssertResult(t, "new_value", value.Value) // Should be overwritten
}
func TestDeeplyAssign_ErrorHandling(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a scalar node (not a mapping)
scalarNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "not_a_map",
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(scalarNode)
// Create a scalar node to assign
assignNode := &CandidateNode{
Kind: ScalarNode,
Tag: "!!str",
Value: "value",
}
// Try to assign to a path on a scalar (should fail)
path := []interface{}{"key"}
err := navigator.DeeplyAssign(context, path, assignNode)
// Print the actual error for debugging
if err != nil {
t.Logf("Actual error: %v", err)
}
// This should fail because we can't assign to a scalar
test.AssertResult(t, nil, err)
}
func TestGetMatchingNodes_WithVariables(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with variables
variables := make(map[string]*list.List)
varList := list.New()
varList.PushBack(&CandidateNode{Kind: ScalarNode, Tag: "!!str", Value: "var_value"})
variables["test_var"] = varList
context := Context{
MatchingNodes: list.New(),
Variables: variables,
}
// Create an expression node that gets a variable
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: getVariableOpType, StringValue: "test_var"},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, nil, err)
test.AssertResult(t, 1, result.MatchingNodes.Len())
// Verify the variable was retrieved
resultNode := result.MatchingNodes.Front().Value.(*CandidateNode)
test.AssertResult(t, "var_value", resultNode.Value)
}
func TestGetMatchingNodes_EmptyContext(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create an empty context
context := Context{
MatchingNodes: list.New(),
}
// Create an expression node with self reference
expressionNode := &ExpressionNode{
Operation: &Operation{OperationType: selfReferenceOpType},
}
result, err := navigator.GetMatchingNodes(context, expressionNode)
test.AssertResult(t, nil, err)
test.AssertResult(t, 0, result.MatchingNodes.Len())
}
func TestDeeplyAssign_ComplexMappingMerge(t *testing.T) {
navigator := NewDataTreeNavigator()
// Create a context with a root mapping node containing nested data
rootNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "config", IsMapKey: true},
{Kind: MappingNode, Tag: "!!map", Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "existing_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "existing_value"},
}},
},
}
context := Context{
MatchingNodes: list.New(),
}
context.MatchingNodes.PushBack(rootNode)
// Create a mapping node to merge
mappingNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Content: []*CandidateNode{
{Kind: ScalarNode, Tag: "!!str", Value: "new_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "new_value"},
{Kind: ScalarNode, Tag: "!!str", Value: "existing_key", IsMapKey: true},
{Kind: ScalarNode, Tag: "!!str", Value: "updated_value"},
},
}
// Assign to path ["config"] (should merge with existing mapping)
path := []interface{}{"config"}
err := navigator.DeeplyAssign(context, path, mappingNode)
test.AssertResult(t, nil, err)
// Verify the merge was successful
configNode := rootNode.Content[1] // The config mapping node
test.AssertResult(t, 4, len(configNode.Content)) // Should have 2 key-value pairs
// Check that both existing and new keys are present
foundExisting := false
foundNew := false
for i := 0; i < len(configNode.Content); i += 2 {
if i+1 < len(configNode.Content) {
key := configNode.Content[i]
value := configNode.Content[i+1]
switch key.Value {
case "existing_key":
foundExisting = true
test.AssertResult(t, "updated_value", value.Value) // Should be updated
case "new_key":
foundNew = true
test.AssertResult(t, "new_value", value.Value)
}
}
}
test.AssertResult(t, true, foundExisting)
test.AssertResult(t, true, foundNew)
}

View File

@ -1,3 +1,5 @@
//go:build !yq_nobase64
package yqlib
import (

View File

@ -1,3 +1,5 @@
//go:build !yq_nocsv
package yqlib
import (

View File

@ -16,6 +16,8 @@ import (
type goccyYamlDecoder struct {
decoder yaml.Decoder
cm yaml.CommentMap
// anchor map persists over multiple documents for convenience.
anchorMap map[string]*CandidateNode
}
func NewGoccyYAMLDecoder() Decoder {
@ -25,6 +27,7 @@ func NewGoccyYAMLDecoder() Decoder {
func (dec *goccyYamlDecoder) Init(reader io.Reader) error {
dec.cm = yaml.CommentMap{}
dec.decoder = *yaml.NewDecoder(reader, yaml.CommentToMap(dec.cm), yaml.UseOrderedMap())
dec.anchorMap = make(map[string]*CandidateNode)
return nil
}
@ -38,7 +41,7 @@ func (dec *goccyYamlDecoder) Decode() (*CandidateNode, error) {
}
candidateNode := &CandidateNode{}
if err := candidateNode.UnmarshalGoccyYAML(ast, dec.cm); err != nil {
if err := candidateNode.UnmarshalGoccyYAML(ast, dec.cm, dec.anchorMap); err != nil {
return nil, err
}

106
pkg/yqlib/decoder_ini.go Normal file
View File

@ -0,0 +1,106 @@
//go:build !yq_noini
package yqlib
import (
"fmt"
"io"
"github.com/go-ini/ini"
)
type iniDecoder struct {
reader io.Reader
finished bool // Flag to signal completion of processing
}
func NewINIDecoder() Decoder {
return &iniDecoder{
finished: false, // Initialize the flag as false
}
}
func (dec *iniDecoder) Init(reader io.Reader) error {
// Store the reader for use in Decode
dec.reader = reader
return nil
}
func (dec *iniDecoder) Decode() (*CandidateNode, error) {
// If processing is already finished, return io.EOF
if dec.finished {
return nil, io.EOF
}
// Read all content from the stored reader
content, err := io.ReadAll(dec.reader)
if err != nil {
return nil, fmt.Errorf("failed to read INI content: %w", err)
}
// Parse the INI content
cfg, err := ini.Load(content)
if err != nil {
return nil, fmt.Errorf("failed to parse INI content: %w", err)
}
// Create a root CandidateNode as a MappingNode (since INI is key-value based)
root := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Value: "",
}
// Process each section in the INI file
for _, section := range cfg.Sections() {
sectionName := section.Name()
if sectionName == ini.DefaultSection {
// For the default section, add key-value pairs directly to the root node
for _, key := range section.Keys() {
keyName := key.Name()
keyValue := key.String()
// Create a key node (scalar for the key name)
keyNode := createStringScalarNode(keyName)
// Create a value node (scalar for the value)
valueNode := createStringScalarNode(keyValue)
// Add key-value pair to the root node
root.AddKeyValueChild(keyNode, valueNode)
}
} else {
// For named sections, create a nested map
sectionNode := &CandidateNode{
Kind: MappingNode,
Tag: "!!map",
Value: "",
}
// Add key-value pairs to the section node
for _, key := range section.Keys() {
keyName := key.Name()
keyValue := key.String()
// Create a key node (scalar for the key name)
keyNode := createStringScalarNode(keyName)
// Create a value node (scalar for the value)
valueNode := createStringScalarNode(keyValue)
// Add key-value pair to the section node
sectionNode.AddKeyValueChild(keyNode, valueNode)
}
// Create a key node for the section name
sectionKeyNode := createStringScalarNode(sectionName)
// Add the section as a nested map to the root node
root.AddKeyValueChild(sectionKeyNode, sectionNode)
}
}
// Set the finished flag to true to prevent further Decode calls
dec.finished = true
// Return the root node
return root, nil
}

View File

@ -1,3 +1,5 @@
//go:build !yq_noprops
package yqlib
import (

View File

@ -68,7 +68,8 @@ func mustProcessFormatScenario(s formatScenario, decoder Decoder, encoder Encode
result, err := processFormatScenario(s, decoder, encoder)
if err != nil {
panic(fmt.Errorf("Bad scenario %v: %w", s.description, err))
log.Error("Bad scenario %v: %w", s.description, err)
return fmt.Sprintf("Bad scenario %v: %v", s.description, err.Error())
}
return result

View File

@ -249,11 +249,12 @@ func (dec *tomlDecoder) processTopLevelNode(currentNode *toml.Node) (bool, error
var runAgainstCurrentExp bool
var err error
log.Debug("processTopLevelNode: Going to process %v state is current %v", currentNode.Kind, NodeToString(dec.rootMap))
if currentNode.Kind == toml.Table {
switch currentNode.Kind {
case toml.Table:
runAgainstCurrentExp, err = dec.processTable(currentNode)
} else if currentNode.Kind == toml.ArrayTable {
case toml.ArrayTable:
runAgainstCurrentExp, err = dec.processArrayTable(currentNode)
} else {
default:
runAgainstCurrentExp, err = dec.decodeKeyValuesIntoMap(dec.rootMap, currentNode)
}

View File

@ -1,3 +1,5 @@
//go:build !yq_nouri
package yqlib
import (

View File

@ -315,13 +315,14 @@ func (dec *xmlDecoder) decodeXML(root *xmlNode) error {
case xml.Comment:
commentStr := string(xml.CharData(se))
if elem.state == "started" {
switch elem.state {
case "started":
applyFootComment(elem, commentStr)
} else if elem.state == "chardata" {
case "chardata":
log.Debug("got a line comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
elem.n.LineComment = joinComments([]string{elem.n.LineComment, commentStr}, " ")
} else {
default:
log.Debug("got a head comment for (%v) %v: [%v]", elem.state, elem.label, commentStr)
elem.n.HeadComment = joinComments([]string{elem.n.HeadComment, commentStr}, " ")
}

View File

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

View File

@ -5,6 +5,18 @@ Use the `alias` and `anchor` operators to read and write yaml aliases and anchor
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
## NOTE --yaml-fix-merge-anchor-to-spec flag
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed).
This flag also enables advanced merging, like inline maps, as well as fixes to ensure when exploding a particular path, neighbours are not affect ed.
Long story short, you should be setting this flag to true.
See examples of the flag differences below, where LEGACY is with the flag off; and FIXED is with the flag on.
## Merge one map
see https://yaml.org/type/merge.html
@ -34,68 +46,6 @@ y: 2
r: 10
```
## Merge multiple maps
see https://yaml.org/type/merge.html
Given a sample.yml file of:
```yaml
- &CENTER
x: 1
y: 2
- &LEFT
x: 0
y: 2
- &BIG
r: 10
- &SMALL
r: 1
- !!merge <<:
- *CENTER
- *BIG
```
then
```bash
yq '.[4] | explode(.)' sample.yml
```
will output
```yaml
r: 10
x: 1
y: 2
```
## Override
see https://yaml.org/type/merge.html
Given a sample.yml file of:
```yaml
- &CENTER
x: 1
y: 2
- &LEFT
x: 0
y: 2
- &BIG
r: 10
- &SMALL
r: 1
- !!merge <<:
- *BIG
- *LEFT
- *SMALL
x: 1
```
then
```bash
yq '.[4] | explode(.)' sample.yml
```
will output
```yaml
r: 10
x: 1
y: 2
```
## Get anchor
Given a sample.yml file of:
```yaml
@ -254,7 +204,39 @@ f:
cat: b
```
## Explode with merge anchors
## Dereference and update a field
Use explode with multiply to dereference an object
Given a sample.yml file of:
```yaml
item_value: &item_value
value: true
thingOne:
name: item_1
!!merge <<: *item_value
thingTwo:
name: item_2
!!merge <<: *item_value
```
then
```bash
yq '.thingOne |= (explode(.) | sort_keys(.)) * {"value": false}' sample.yml
```
will output
```yaml
item_value: &item_value
value: true
thingOne:
name: item_1
value: false
thingTwo:
name: item_2
!!merge <<: *item_value
```
## LEGACY: Explode with merge anchors
Caution: this is for when --yaml-fix-merge-anchor-to-spec=false; it's not to YAML spec because the merge anchors incorrectly override the object values (foobarList.b is set to bar_b when it should still be foobarList_b). Flag will default to true in late 2025
Given a sample.yml file of:
```yaml
foo: &foo
@ -301,33 +283,201 @@ foobar:
thing: foobar_thing
```
## Dereference and update a field
Use explode with multiply to dereference an object
## LEGACY: Merge multiple maps
see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.
Given a sample.yml file of:
```yaml
item_value: &item_value
value: true
thingOne:
name: item_1
!!merge <<: *item_value
thingTwo:
name: item_2
!!merge <<: *item_value
- &CENTER
x: 1
y: 2
- &LEFT
x: 0
y: 2
- &BIG
r: 10
- &SMALL
r: 1
- !!merge <<:
- *CENTER
- *BIG
```
then
```bash
yq '.thingOne |= explode(.) * {"value": false}' sample.yml
yq '.[4] | explode(.)' sample.yml
```
will output
```yaml
item_value: &item_value
value: true
thingOne:
name: item_1
value: false
thingTwo:
name: item_2
!!merge <<: *item_value
r: 10
x: 1
y: 2
```
## LEGACY: Override
see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.
Given a sample.yml file of:
```yaml
- &CENTER
x: 1
y: 2
- &LEFT
x: 0
y: 2
- &BIG
r: 10
- &SMALL
r: 1
- !!merge <<:
- *BIG
- *LEFT
- *SMALL
x: 1
```
then
```bash
yq '.[4] | explode(.)' sample.yml
```
will output
```yaml
r: 10
x: 1
y: 2
```
## FIXED: Explode with merge anchors
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
Observe that foobarList.b property is still foobarList_b.
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq 'explode(.)' sample.yml
```
will output
```yaml
foo:
a: foo_a
thing: foo_thing
c: foo_c
bar:
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
a: foo_a
thing: foo_thing
c: foobarList_c
foobar:
c: foobar_c
a: foo_a
thing: foobar_thing
```
## FIXED: Merge multiple maps
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.
Given a sample.yml file of:
```yaml
- &CENTER
x: 1
y: 2
- &LEFT
x: 0
y: 2
- &BIG
r: 10
- &SMALL
r: 1
- !!merge <<:
- *CENTER
- *BIG
```
then
```bash
yq '.[4] | explode(.)' sample.yml
```
will output
```yaml
x: 1
y: 2
r: 10
```
## FIXED: Override
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.
Given a sample.yml file of:
```yaml
- &CENTER
x: 1
y: 2
- &LEFT
x: 0
y: 2
- &BIG
r: 10
- &SMALL
r: 1
- !!merge <<:
- *BIG
- *LEFT
- *SMALL
x: 1
```
then
```bash
yq '.[4] | explode(.)' sample.yml
```
will output
```yaml
r: 10
y: 2
x: 1
```
## Exploding inline merge anchor
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).
Given a sample.yml file of:
```yaml
a:
b: &b 42
!!merge <<:
c: *b
```
then
```bash
yq 'explode(.) | sort_keys(.)' sample.yml
```
will output
```yaml
a:
b: 42
c: 42
```

View File

@ -2,7 +2,7 @@
Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps.
Use `with_entries(op)` as a syntatic sugar for doing `to_entries | op | from_entries`.
Use `with_entries(op)` as a syntactic sugar for doing `to_entries | op | from_entries`.
## to_entries Map
Given a sample.yml file of:

View File

@ -4,3 +4,15 @@ Use the `alias` and `anchor` operators to read and write yaml aliases and anchor
`yq` supports merge aliases (like `<<: *blah`) however this is no longer in the standard yaml spec (1.2) and so `yq` will automatically add the `!!merge` tag to these nodes as it is effectively a custom tag.
## NOTE --yaml-fix-merge-anchor-to-spec flag
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed).
This flag also enables advanced merging, like inline maps, as well as fixes to ensure when exploding a particular path, neighbours are not affect ed.
Long story short, you should be setting this flag to true.
See examples of the flag differences below, where LEGACY is with the flag off; and FIXED is with the flag on.

View File

@ -2,4 +2,4 @@
Similar to the same named functions in `jq` these functions convert to/from an object and an array of key-value pairs. This is most useful for performing operations on keys of maps.
Use `with_entries(op)` as a syntatic sugar for doing `to_entries | op | from_entries`.
Use `with_entries(op)` as a syntactic sugar for doing `to_entries | op | from_entries`.

View File

@ -3,7 +3,7 @@
## RegEx
This uses Golang's native regex functions under the hood - See their [docs](https://github.com/google/re2/wiki/Syntax) for the supported syntax.
Case insensitive tip: prefix the regex with `(?i)` - e.g. `test("(?i)cats)"`.
Case insensitive tip: prefix the regex with `(?i)` - e.g. `test("(?i)cats")`.
### match(regEx)
This operator returns the substring match details of the given regEx.

View File

@ -1,3 +1,12 @@
# Traverse (Read)
This is the simplest (and perhaps most used) operator. It is used to navigate deeply into yaml structures.
## NOTE --yaml-fix-merge-anchor-to-spec flag
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed)
See examples of the flag differences below, where LEGACY is the flag off; and FIXED is with the flag on.

View File

@ -3,7 +3,7 @@
## RegEx
This uses Golang's native regex functions under the hood - See their [docs](https://github.com/google/re2/wiki/Syntax) for the supported syntax.
Case insensitive tip: prefix the regex with `(?i)` - e.g. `test("(?i)cats)"`.
Case insensitive tip: prefix the regex with `(?i)` - e.g. `test("(?i)cats")`.
### match(regEx)
This operator returns the substring match details of the given regEx.

View File

@ -2,6 +2,15 @@
This is the simplest (and perhaps most used) operator. It is used to navigate deeply into yaml structures.
## NOTE --yaml-fix-merge-anchor-to-spec flag
`yq` doesn't merge anchors `<<:` to spec, in some circumstances it incorrectly overrides existing keys when the spec documents not to do that.
To minimise disruption while still fixing the issue, a flag has been added to toggle this behaviour. This will first default to false; and log warnings to users. Then it will default to true (and still allow users to specify false if needed)
See examples of the flag differences below, where LEGACY is the flag off; and FIXED is with the flag on.
## Simple map navigation
Given a sample.yml file of:
```yaml
@ -303,37 +312,6 @@ will output
foo_a
```
## Traversing merge anchors with override
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.foobar.c' sample.yml
```
will output
```yaml
foo_c
```
## Traversing merge anchors with local override
Given a sample.yml file of:
```yaml
@ -365,7 +343,93 @@ will output
foobar_thing
```
## Splatting merge anchors
## Select multiple indices
Given a sample.yml file of:
```yaml
a:
- a
- b
- c
```
then
```bash
yq '.a[0, 2]' sample.yml
```
will output
```yaml
a
c
```
## LEGACY: Traversing merge anchors with override
This is legacy behaviour, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.foobar.c' sample.yml
```
will output
```yaml
foo_c
```
## LEGACY: Traversing merge anchor lists
Note that the later merge anchors override previous, but this is legacy behaviour, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.foobarList.thing' sample.yml
```
will output
```yaml
bar_thing
```
## LEGACY: Splatting merge anchors
With legacy override behaviour, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
foo: &foo
@ -398,40 +462,9 @@ foo_a
foobar_thing
```
## Traversing merge anchor lists
Note that the later merge anchors override previous
## LEGACY: Splatting merge anchor lists
With legacy override behaviour, see --yaml-fix-merge-anchor-to-spec
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.foobarList.thing' sample.yml
```
will output
```yaml
bar_thing
```
## Splatting merge anchor lists
Given a sample.yml file of:
```yaml
foo: &foo
@ -465,21 +498,140 @@ bar_thing
foobarList_c
```
## Select multiple indices
## FIXED: Traversing merge anchors with override
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour.
Given a sample.yml file of:
```yaml
a:
- a
- b
- c
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.a[0, 2]' sample.yml
yq '.foobar.c' sample.yml
```
will output
```yaml
a
c
foobar_c
```
## FIXED: Traversing merge anchor lists
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.foobarList.thing' sample.yml
```
will output
```yaml
foo_thing
```
## FIXED: Splatting merge anchors
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.foobar[]' sample.yml
```
will output
```yaml
foo_a
foobar_thing
foobar_c
```
## FIXED: Splatting merge anchor lists
Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour. Note that the keys earlier in the merge anchors sequence override later ones
Given a sample.yml file of:
```yaml
foo: &foo
a: foo_a
thing: foo_thing
c: foo_c
bar: &bar
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
!!merge <<:
- *foo
- *bar
c: foobarList_c
foobar:
c: foobar_c
!!merge <<: *foo
thing: foobar_thing
```
then
```bash
yq '.foobarList[]' sample.yml
```
will output
```yaml
foobarList_b
foo_thing
foobarList_c
foo_a
```

View File

@ -1,3 +1,5 @@
//go:build !yq_nobase64
package yqlib
import (

View File

@ -1,3 +1,5 @@
//go:build !yq_nocsv
package yqlib
import (

113
pkg/yqlib/encoder_ini.go Normal file
View File

@ -0,0 +1,113 @@
//go:build !yq_noini
package yqlib
import (
"bytes"
"fmt"
"io"
"github.com/go-ini/ini"
)
type iniEncoder struct {
indentString string
}
// NewINIEncoder creates a new INI encoder
func NewINIEncoder() Encoder {
// Hardcoded indent value of 0, meaning no additional spacing.
return &iniEncoder{""}
}
// CanHandleAliases indicates whether the encoder supports aliases. INI does not support aliases.
func (ie *iniEncoder) CanHandleAliases() bool {
return false
}
// PrintDocumentSeparator is a no-op since INI does not support multiple documents.
func (ie *iniEncoder) PrintDocumentSeparator(_ io.Writer) error {
return nil
}
// PrintLeadingContent is a no-op since INI does not support leading content or comments at the encoder level.
func (ie *iniEncoder) PrintLeadingContent(_ io.Writer, _ string) error {
return nil
}
// Encode converts a CandidateNode into INI format and writes it to the provided writer.
func (ie *iniEncoder) Encode(writer io.Writer, node *CandidateNode) error {
log.Debugf("I need to encode %v", NodeToString(node))
log.Debugf("kids %v", len(node.Content))
if node.Kind == ScalarNode {
return writeStringINI(writer, node.Value+"\n")
}
// Create a new INI configuration.
cfg := ini.Empty()
if node.Kind == MappingNode {
// Default section for key-value pairs at the root level.
defaultSection, err := cfg.NewSection(ini.DefaultSection)
if err != nil {
return err
}
// Process the node's content.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
key := keyNode.Value
switch valueNode.Kind {
case ScalarNode:
// Add key-value pair to the default section.
_, err := defaultSection.NewKey(key, valueNode.Value)
if err != nil {
return err
}
case MappingNode:
// Create a new section for nested MappingNode.
section, err := cfg.NewSection(key)
if err != nil {
return err
}
// Process nested key-value pairs.
for j := 0; j < len(valueNode.Content); j += 2 {
nestedKeyNode := valueNode.Content[j]
nestedValueNode := valueNode.Content[j+1]
if nestedValueNode.Kind == ScalarNode {
_, err := section.NewKey(nestedKeyNode.Value, nestedValueNode.Value)
if err != nil {
return err
}
} else {
log.Debugf("Skipping nested non-scalar value for key %s: %v", nestedKeyNode.Value, nestedValueNode.Kind)
}
}
default:
log.Debugf("Skipping non-scalar value for key %s: %v", key, valueNode.Kind)
}
}
} else {
return fmt.Errorf("INI encoder supports only MappingNode at the root level, got %v", node.Kind)
}
// Use a buffer to store the INI output as the library doesn't support direct io.Writer with indent.
var buffer bytes.Buffer
_, err := cfg.WriteToIndent(&buffer, ie.indentString)
if err != nil {
return err
}
// Write the buffer content to the provided writer.
_, err = writer.Write(buffer.Bytes())
return err
}
// writeStringINI is a helper function to write a string to the provided writer for INI encoder.
func writeStringINI(writer io.Writer, content string) error {
_, err := writer.Write([]byte(content))
return err
}

View File

@ -167,10 +167,14 @@ func needsQuoting(s string) bool {
// [%a_][%w_]*
for i, c := range s {
if i == 0 {
// keeping for legacy reasons, upgraded linter
//nolint:staticcheck
if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') {
return true
}
} else {
// keeping for legacy reasons, upgraded linter
//nolint:staticcheck
if !((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') {
return true
}
@ -299,10 +303,10 @@ func (le *luaEncoder) encodeAny(writer io.Writer, node *CandidateNode) error {
return writeString(writer, node.Value)
}
default:
return fmt.Errorf("Lua encoder NYI -- %s", node.Tag)
return fmt.Errorf("lua encoder NYI -- %s", node.Tag)
}
default:
return fmt.Errorf("Lua encoder NYI -- %s", node.Tag)
return fmt.Errorf("lua encoder NYI -- %s", node.Tag)
}
}

View File

@ -1,3 +1,5 @@
//go:build !yq_noprops
package yqlib
import (
@ -107,7 +109,7 @@ func (pe *propertiesEncoder) doEncode(p *properties.Properties, node *CandidateN
case AliasNode:
return pe.doEncode(p, node.Alias, path, nil)
default:
return fmt.Errorf("Unsupported node %v", node.Tag)
return fmt.Errorf("unsupported node %v", node.Tag)
}
}

View File

@ -1,3 +1,5 @@
//go:build !yq_nosh
package yqlib
import (

View File

@ -1,3 +1,5 @@
//go:build !yq_noshell
package yqlib
import (
@ -75,7 +77,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
case AliasNode:
return pe.doEncode(w, node.Alias, path)
default:
return fmt.Errorf("Unsupported node %v", node.Tag)
return fmt.Errorf("unsupported node %v", node.Tag)
}
}

View File

@ -1,3 +1,5 @@
//go:build !yq_nouri
package yqlib
import (

View File

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

View File

@ -9,6 +9,7 @@ type ExpressionNode struct {
Operation *Operation
LHS *ExpressionNode
RHS *ExpressionNode
Parent *ExpressionNode
}
type ExpressionParserInterface interface {
@ -50,20 +51,26 @@ func (p *expressionParserImpl) createExpressionTree(postFixPath []*Operation) (*
log.Debugf("pathTree %v ", Operation.toString())
if Operation.OperationType.NumArgs > 0 {
numArgs := Operation.OperationType.NumArgs
if numArgs == 1 {
switch numArgs {
case 1:
if len(stack) < 1 {
return nil, fmt.Errorf("'%v' expects 1 arg but received none", strings.TrimSpace(Operation.StringValue))
}
remaining, rhs := stack[:len(stack)-1], stack[len(stack)-1]
newNode.RHS = rhs
rhs.Parent = &newNode
stack = remaining
} else if numArgs == 2 {
case 2:
if len(stack) < 2 {
return nil, fmt.Errorf("'%v' expects 2 args but there is %v", strings.TrimSpace(Operation.StringValue), len(stack))
}
remaining, lhs, rhs := stack[:len(stack)-2], stack[len(stack)-2], stack[len(stack)-1]
newNode.LHS = lhs
lhs.Parent = &newNode
newNode.RHS = rhs
rhs.Parent = &newNode
stack = remaining
}
}

View File

@ -26,11 +26,12 @@ func popOpToResult(opStack []*token, result []*Operation) ([]*token, []*Operatio
}
func validateNoOpenTokens(token *token) error {
if token.TokenType == openCollect {
switch token.TokenType {
case openCollect:
return fmt.Errorf(("bad expression, could not find matching `]`"))
} else if token.TokenType == openCollectObject {
case openCollectObject:
return fmt.Errorf(("bad expression, could not find matching `}`"))
} else if token.TokenType == openBracket {
case openBracket:
return fmt.Errorf(("bad expression, could not find matching `)`"))
}
return nil
@ -64,7 +65,7 @@ func (p *expressionPostFixerImpl) ConvertToPostfix(infixTokens []*token) ([]*Ope
opStack, result = popOpToResult(opStack, result)
}
if len(opStack) == 0 {
return nil, errors.New("Bad path expression, got close collect brackets without matching opening bracket")
return nil, errors.New("bad path expression, got close collect brackets without matching opening bracket")
}
// now we should have [ as the last element on the opStack, get rid of it
opStack = opStack[0 : len(opStack)-1]

View File

@ -77,6 +77,11 @@ var LuaFormat = &Format{"lua", []string{"l"},
func() Decoder { return NewLuaDecoder(ConfiguredLuaPreferences) },
}
var INIFormat = &Format{"ini", []string{"i"},
func() Encoder { return NewINIEncoder() },
func() Decoder { return NewINIDecoder() },
}
var Formats = []*Format{
YamlFormat,
JSONFormat,
@ -90,6 +95,7 @@ var Formats = []*Format{
TomlFormat,
ShellVariablesFormat,
LuaFormat,
INIFormat,
}
func (f *Format) MatchesName(name string) bool {
@ -107,7 +113,7 @@ func FormatStringFromFilename(filename string) string {
if filename != "" {
GetLogger().Debugf("checking filename '%s' for auto format detection", filename)
ext := filepath.Ext(filename)
if ext != "" && ext[0] == '.' {
if len(ext) >= 2 && ext[0] == '.' {
format := strings.ToLower(ext[1:])
GetLogger().Debugf("detected format '%s'", format)
return format

View File

@ -97,12 +97,18 @@ var goccyYamlFormatScenarios = []formatScenario{
input: "a: meow # line comment\n",
expected: "a: meow # line comment\n",
},
{
description: "basic - line comment",
skipDoc: true,
input: "# head comment\na: #line comment\n meow\n",
expected: "# head comment\na: meow #line comment\n", // go-yaml does this
},
// {
// description: "basic - head comment",
// skipDoc: true,
// input: "# head comment\na: meow\n",
// expected: "# head comment\na: meow\n", // go-yaml does this
// },
// {
// description: "basic - head and line comment",
// skipDoc: true,
// input: "# head comment\na: #line comment\n meow\n",
// expected: "# head comment\na: meow #line comment\n", // go-yaml does this
// },
{
description: "basic - foot comment",
skipDoc: true,
@ -133,24 +139,136 @@ var goccyYamlFormatScenarios = []formatScenario{
input: "a: ~\n",
expected: "a: ~\n",
},
{
description: "basic - ~",
skipDoc: true,
input: "null\n",
expected: "null\n",
},
{
skipDoc: true,
description: "blank value round trip",
input: "test:",
expected: "test:\n",
},
{
skipDoc: true,
description: "trailing comment",
input: "test: null\n# this comment will be removed",
expected: "test: null\n# this comment will be removed\n",
},
// {
// description: "basic - ~",
// description: "doc separator",
// skipDoc: true,
// input: "null\n",
// expected: "null\n",
// input: "# hi\n---\na: cat\n---",
// expected: "---\na: cat\n",
// },
// {
// description: "scalar with doc separator",
// skipDoc: true,
// description: "trailing comment",
// input: "test:",
// expected: "test:",
// },
// {
// skipDoc: true,
// description: "trailing comment",
// input: "test:\n# this comment will be removed",
// expected: "test:\n# this comment will be removed",
// input: "--- cat",
// expected: "---\ncat\n",
// },
{
description: "scalar with doc separator",
skipDoc: true,
input: "---cat",
expected: "---cat\n",
},
{
description: "basic - null",
skipDoc: true,
input: "null",
expected: "null\n",
},
{
description: "basic - ~",
skipDoc: true,
input: "~",
expected: "~\n",
},
{
description: "octal",
skipDoc: true,
input: "0o30",
expression: "tag",
expected: "!!int\n",
},
{
description: "basic - [null]",
skipDoc: true,
input: "[null]",
expected: "[null]\n",
},
{
description: "multi document",
skipDoc: true,
input: "a: mike\n---\nb: remember",
expected: "a: mike\n---\nb: remember\n",
},
{
description: "single doc anchor map",
skipDoc: true,
input: "a: &remember mike\nb: *remember",
expected: "a: &remember mike\nb: *remember\n",
},
{
description: "explode doc anchor map",
skipDoc: true,
input: "a: &remember mike\nb: *remember",
expression: "explode(.)",
expected: "a: mike\nb: mike\n",
},
{
description: "multi document anchor map",
skipDoc: true,
input: "a: &remember mike\n---\nb: *remember",
expression: "explode(.)",
expected: "a: mike\n---\nb: mike\n",
},
{
description: "merge anchor",
skipDoc: true,
input: "a: &remember\n c: mike\nb:\n <<: *remember",
// fine to have !!merge as that's what the current impl does
expected: "a: &remember\n c: mike\nb:\n !!merge <<: *remember\n",
},
{
description: "custom tag",
skipDoc: true,
input: "a: !cat mike",
expected: "a: !cat mike\n",
},
{
description: "basic - [~]",
skipDoc: true,
input: "[~]",
expected: "[~]\n",
},
{
description: "basic - null map value",
skipDoc: true,
input: "a: null",
expected: "a: null\n",
},
{
description: "basic - number",
skipDoc: true,
input: "3",
expected: "3\n",
},
{
description: "basic - float",
skipDoc: true,
input: "3.1",
expected: "3.1\n",
},
{
description: "basic - float",
skipDoc: true,
input: "[1, 2]",
expected: "[1, 2]\n",
},
}
func testGoccyYamlScenario(t *testing.T, s formatScenario) {

19
pkg/yqlib/ini.go Normal file
View File

@ -0,0 +1,19 @@
package yqlib
type INIPreferences struct {
ColorsEnabled bool
}
func NewDefaultINIPreferences() INIPreferences {
return INIPreferences{
ColorsEnabled: false,
}
}
func (p *INIPreferences) Copy() INIPreferences {
return INIPreferences{
ColorsEnabled: p.ColorsEnabled,
}
}
var ConfiguredINIPreferences = NewDefaultINIPreferences()

187
pkg/yqlib/ini_test.go Normal file
View File

@ -0,0 +1,187 @@
//go:build !yq_noini
package yqlib
import (
"bufio"
"fmt"
"testing"
"github.com/mikefarah/yq/v4/test"
)
const simpleINIInput = `[section]
key = value
`
const expectedSimpleINIOutput = `[section]
key = value
`
const expectedSimpleINIYaml = `section:
key: value
`
var iniScenarios = []formatScenario{
{
description: "Parse INI: simple",
input: simpleINIInput,
scenarioType: "decode",
expected: expectedSimpleINIYaml,
},
{
description: "Encode INI: simple",
input: `section: {key: value}`,
expected: expectedSimpleINIOutput,
scenarioType: "encode",
},
{
description: "Roundtrip INI: simple",
input: simpleINIInput,
expected: expectedSimpleINIOutput,
scenarioType: "roundtrip",
},
{
description: "bad ini",
input: `[section\nkey = value`,
expectedError: `bad file 'sample.yml': failed to parse INI content: unclosed section: [section\nkey = value`,
scenarioType: "decode-error",
},
}
func documentRoundtripINIScenario(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.ini file of:\n")
writeOrPanic(w, fmt.Sprintf("```ini\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
expression := s.expression
if expression != "" {
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=ini -o=ini '%v' sample.ini\n```\n", expression))
} else {
writeOrPanic(w, "```bash\nyq -p=ini -o=ini sample.ini\n```\n")
}
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```ini\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(), NewINIEncoder())))
}
func documentDecodeINIScenario(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.ini file of:\n")
writeOrPanic(w, fmt.Sprintf("```ini\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
expression := s.expression
if expression != "" {
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=ini '%v' sample.ini\n```\n", expression))
} else {
writeOrPanic(w, "```bash\nyq -p=ini sample.ini\n```\n")
}
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewINIDecoder(), NewYamlEncoder(ConfiguredYamlPreferences))))
}
func testINIScenario(t *testing.T, s formatScenario) {
switch s.scenarioType {
case "encode":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewINIEncoder()), s.description)
case "decode":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)), s.description)
case "roundtrip":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewINIDecoder(), NewINIEncoder()), s.description)
case "decode-error":
result, err := processFormatScenario(s, NewINIDecoder(), NewINIEncoder())
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 documentINIScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
s := i.(formatScenario)
if s.skipDoc {
return
}
switch s.scenarioType {
case "encode":
documentINIEncodeScenario(w, s)
case "decode":
documentDecodeINIScenario(w, s)
case "roundtrip":
documentRoundtripINIScenario(w, s)
case "decode-error":
documentDecodeErrorINIScenario(w, s)
default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))
}
}
func documentINIEncodeScenario(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=ini '%v' sample.yml\n```\n", expression))
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```ini\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewINIEncoder())))
}
func documentDecodeErrorINIScenario(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.ini file of:\n")
writeOrPanic(w, fmt.Sprintf("```ini\n%v\n```\n", s.input))
writeOrPanic(w, "then an error is expected:\n")
writeOrPanic(w, fmt.Sprintf("```\n%v\n```\n\n", s.expectedError))
}
func TestINIScenarios(t *testing.T) {
for _, tt := range iniScenarios {
testINIScenario(t, tt)
}
genericScenarios := make([]interface{}, len(iniScenarios))
for i, s := range iniScenarios {
genericScenarios[i] = s
}
documentScenarios(t, "usage", "convert", genericScenarios, documentINIScenario)
}

View File

@ -31,24 +31,25 @@ type token struct {
}
func (t *token) toString(detail bool) string {
if t.TokenType == operationToken {
switch t.TokenType {
case operationToken:
if detail {
return fmt.Sprintf("%v (%v)", t.Operation.toString(), t.Operation.OperationType.Precedence)
}
return t.Operation.toString()
} else if t.TokenType == openBracket {
case openBracket:
return "("
} else if t.TokenType == closeBracket {
case closeBracket:
return ")"
} else if t.TokenType == openCollect {
case openCollect:
return "["
} else if t.TokenType == closeCollect {
case closeCollect:
return "]"
} else if t.TokenType == openCollectObject {
case openCollectObject:
return "{"
} else if t.TokenType == closeCollectObject {
case closeCollectObject:
return "}"
} else if t.TokenType == traverseArrayCollect {
case traverseArrayCollect:
return ".["
}

View File

@ -28,6 +28,17 @@ func GetLogger() *logging.Logger {
return log
}
func getContentValueByKey(content []*CandidateNode, key string) *CandidateNode {
for index := 0; index < len(content); index = index + 2 {
keyNode := content[index]
valueNode := content[index+1]
if keyNode.Value == key {
return valueNode
}
}
return nil
}
func recurseNodeArrayEqual(lhs *CandidateNode, rhs *CandidateNode) bool {
if len(lhs.Content) != len(rhs.Content) {
return false

11
pkg/yqlib/no_base64.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_nobase64
package yqlib
func NewBase64Decoder() Decoder {
return nil
}
func NewBase64Encoder() Encoder {
return nil
}

11
pkg/yqlib/no_csv.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_nocsv
package yqlib
func NewCSVObjectDecoder(prefs CsvPreferences) Decoder {
return nil
}
func NewCsvEncoder(prefs CsvPreferences) Encoder {
return nil
}

11
pkg/yqlib/no_ini.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_noini
package yqlib
func NewINIDecoder() Decoder {
return nil
}
func NewINIEncoder() Encoder {
return nil
}

View File

@ -6,6 +6,6 @@ func NewJSONDecoder() Decoder {
return nil
}
func NewJSONEncoder(indent int, colorise bool, unwrapScalar bool) Encoder {
func NewJSONEncoder(prefs JsonPreferences) Encoder {
return nil
}

11
pkg/yqlib/no_props.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_noprops
package yqlib
func NewPropertiesDecoder() Decoder {
return nil
}
func NewPropertiesEncoder(prefs PropertiesPreferences) Encoder {
return nil
}

7
pkg/yqlib/no_sh.go Normal file
View File

@ -0,0 +1,7 @@
//go:build yq_nosh
package yqlib
func NewShEncoder() Encoder {
return nil
}

View File

@ -0,0 +1,7 @@
//go:build yq_noshell
package yqlib
func NewShellVariablesEncoder() Encoder {
return nil
}

11
pkg/yqlib/no_uri.go Normal file
View File

@ -0,0 +1,11 @@
//go:build yq_nouri
package yqlib
func NewUriDecoder() Decoder {
return nil
}
func NewUriEncoder() Encoder {
return nil
}

View File

@ -6,6 +6,6 @@ func NewXMLDecoder(prefs XmlPreferences) Decoder {
return nil
}
func NewXMLEncoder(indent int, prefs XmlPreferences) Encoder {
func NewXMLEncoder(prefs XmlPreferences) Encoder {
return nil
}

View File

@ -38,8 +38,11 @@ func toNodes(candidate *CandidateNode, lhs *CandidateNode) []*CandidateNode {
func addOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("Add operator")
// only calculate when empty IF we are the root expression; OR
// calcWhenEmpty := expressionNode.Parent == nil || expressionNode.Parent.LHS == expressionNode
calcWhenEmpty := context.MatchingNodes.Len() > 0
return crossFunction(d, context.ReadOnlyClone(), expressionNode, add, true)
return crossFunction(d, context.ReadOnlyClone(), expressionNode, add, calcWhenEmpty)
}
func add(_ *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {

View File

@ -5,6 +5,107 @@ import (
)
var addOperatorScenarios = []expressionScenario{
{
skipDoc: true,
expression: `"foo" + "bar"`,
expected: []string{
"D0, P[], (!!str)::foobar\n",
},
},
{
skipDoc: true,
expression: `[] | .[] | "foo" + .`,
expected: []string{},
},
{
skipDoc: true,
expression: `[] | .[] | . + "foo"`,
expected: []string{},
},
{
skipDoc: true,
expression: `select(.) | "foo" + "bar"`,
expected: []string{
"D0, P[], (!!str)::foobar\n", // jq does not do this :/ - but yq has for quite some time.
},
},
{
skipDoc: true,
document: "apples: 3",
expression: `.apples + 3`,
expected: []string{
"D0, P[apples], (!!int)::6\n",
},
},
{
skipDoc: true,
document: "apples: 3",
expression: `.bobo + 3`,
expected: []string{
"D0, P[], (!!int)::3\n",
},
},
{
skipDoc: true,
expression: `select(.) | "cat" + .`,
expected: []string{},
},
{
skipDoc: true,
document: `[]`,
expression: `.[] | (.a + "|" + .b)`,
expected: []string{},
},
{
skipDoc: true,
document: `[]`,
expression: `.[] | (.a + "|")`,
expected: []string{},
},
{
skipDoc: true,
document: `[]`,
expression: `.[] | ("|" + .a)`,
expected: []string{},
},
{
skipDoc: true,
document: `resources: [foo, bar, baz]`,
expression: `.missing + .resources | .[]`,
expected: []string{
"D0, P[resources 0], (!!str)::foo\n",
"D0, P[resources 1], (!!str)::bar\n",
"D0, P[resources 2], (!!str)::baz\n",
},
},
{
skipDoc: true,
document: `resources: [foo, bar, baz]`,
expression: `. | .missing + .resources | .[]`,
expected: []string{
"D0, P[resources 0], (!!str)::foo\n",
"D0, P[resources 1], (!!str)::bar\n",
"D0, P[resources 2], (!!str)::baz\n",
},
},
{
skipDoc: true,
document: `resources: [foo, bar, baz]`,
expression: `. | .missing + .resources`,
expected: []string{
"D0, P[resources], (!!seq)::[foo, bar, baz]\n",
},
},
{
skipDoc: true,
document: `resources: [foo, bar, baz]`,
expression: `. | .missing + .resources | .[]`,
expected: []string{
"D0, P[resources 0], (!!str)::foo\n",
"D0, P[resources 1], (!!str)::bar\n",
"D0, P[resources 2], (!!str)::baz\n",
},
},
{
skipDoc: true,
document: `[{a: foo, b: bar}, {a: 1, b: 2}]`,
@ -310,6 +411,20 @@ var addOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::a: !cat Saturday, 15-Dec-01 at 6:00AM GMT\n",
},
},
{
skipDoc: true,
description: "empty add shouldn't add",
document: `[]`,
expression: `.[] | (.a + "cat")`,
expected: []string{},
},
{
skipDoc: true,
description: "empty add shouldn't add",
document: `[]`,
expression: `.[] | (.a + "cat" + .b)`,
expected: []string{},
},
{
skipDoc: true,
description: "Add to empty",

View File

@ -5,6 +5,8 @@ import (
"fmt"
)
var showMergeAnchorToSpecWarning = true
func assignAliasOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("AssignAlias operator!")
@ -138,6 +140,59 @@ func explodeOperator(d *dataTreeNavigator, context Context, expressionNode *Expr
return context, nil
}
func fixedReconstructAliasedMap(node *CandidateNode) error {
var newContent = []*CandidateNode{}
for index := 0; index < len(node.Content); index = index + 2 {
keyNode := node.Content[index]
valueNode := node.Content[index+1]
if keyNode.Tag != "!!merge" {
// always add in plain nodes
// explode both the key and value nodes
if err := explodeNode(keyNode, Context{}); err != nil {
return err
}
if err := explodeNode(valueNode, Context{}); err != nil {
return err
}
newContent = append(newContent, keyNode, valueNode)
} else {
sequence := valueNode
if sequence.Kind == AliasNode {
sequence = sequence.Alias
}
if sequence.Kind != SequenceNode {
sequence = &CandidateNode{Content: []*CandidateNode{sequence}}
}
for index := 0; index < len(sequence.Content); index = index + 1 {
// for merge anchors, we only set them if the key is not already in node or the newContent
mergeNodeSeq := sequence.Content[index]
if mergeNodeSeq.Kind == AliasNode {
mergeNodeSeq = mergeNodeSeq.Alias
}
if mergeNodeSeq.Kind != MappingNode {
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", mergeNodeSeq.Tag)
}
itemsToAdd := mergeNodeSeq.FilterMapContentByKey(func(keyNode *CandidateNode) bool {
return getContentValueByKey(node.Content, keyNode.Value) == nil &&
getContentValueByKey(newContent, keyNode.Value) == nil
})
for _, item := range itemsToAdd {
// copy to ensure exploding doesn't modify the original node
itemCopy := item.Copy()
if err := explodeNode(itemCopy, Context{}); err != nil {
return err
}
newContent = append(newContent, itemCopy)
}
}
}
}
node.Content = newContent
return nil
}
func reconstructAliasedMap(node *CandidateNode, context Context) error {
var newContent = list.New()
// can I short cut here by prechecking if there's an anchor in the map?
@ -215,6 +270,13 @@ func explodeNode(node *CandidateNode, context Context) error {
}
if hasAlias {
if ConfiguredYamlPreferences.FixMergeAnchorToSpec {
return fixedReconstructAliasedMap(node)
}
if showMergeAnchorToSpecWarning {
log.Warning("--yaml-fix-merge-anchor-to-spec is false; causing merge anchors to override the existing values which isn't to the yaml spec. This flag will default to true in late 2025.")
showMergeAnchorToSpecWarning = false
}
// this is a slow op, which is why we want to check before running it.
return reconstructAliasedMap(node, context)
}
@ -244,7 +306,7 @@ func applyAlias(node *CandidateNode, alias *CandidateNode, aliasIndex int, newCo
}
log.Debug("alias: %v", NodeToString(alias))
if alias.Kind != MappingNode {
return fmt.Errorf("merge anchor only supports maps, got %v instead", alias.Tag)
return fmt.Errorf("can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing %v", alias.Tag)
}
for index := 0; index < len(alias.Content); index = index + 2 {
keyNode := alias.Content[index]

View File

@ -34,6 +34,25 @@ thingTwo:
!!merge <<: *item_value
`
var explodeMergeAnchorsFixedExpected = `D0, P[], (!!map)::foo:
a: foo_a
thing: foo_thing
c: foo_c
bar:
b: bar_b
thing: bar_thing
c: bar_c
foobarList:
b: foobarList_b
a: foo_a
thing: foo_thing
c: foobarList_c
foobar:
c: foobar_c
a: foo_a
thing: foobar_thing
`
var explodeMergeAnchorsExpected = `D0, P[], (!!map)::foo:
a: foo_a
thing: foo_thing
@ -53,12 +72,226 @@ foobar:
thing: foobar_thing
`
var explodeWhenKeysExistDocument = `objects:
- &circle
name: circle
shape: round
- name: ellipse
!!merge <<: *circle
- !!merge <<: *circle
name: egg
`
var explodeWhenKeysExistLegacy = `D0, P[], (!!map)::objects:
- name: circle
shape: round
- name: circle
shape: round
- shape: round
name: egg
`
var explodeWhenKeysExistExpected = `D0, P[], (!!map)::objects:
- name: circle
shape: round
- name: ellipse
shape: round
- shape: round
name: egg
`
var fixedAnchorOperatorScenarios = []expressionScenario{
{
skipDoc: true,
description: "merge anchor after existing keys",
subdescription: "Does not override existing keys - note the name field in the second element is still ellipse.",
document: explodeWhenKeysExistDocument,
expression: "explode(.)",
expected: []string{explodeWhenKeysExistExpected},
},
{
description: "FIXED: Explode with merge anchors",
subdescription: "Observe that foobarList.b property is still foobarList_b.",
document: mergeDocSample,
expression: `explode(.)`,
expected: []string{explodeMergeAnchorsFixedExpected},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foo* | explode(.) | (. style="flow")`,
expected: []string{
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
"D0, P[foobarList], (!!map)::{b: foobarList_b, a: foo_a, thing: foo_thing, c: foobarList_c}\n",
"D0, P[foobar], (!!map)::{c: foobar_c, a: foo_a, thing: foobar_thing}\n",
},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foo* | explode(explode(.)) | (. style="flow")`,
expected: []string{
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
"D0, P[foobarList], (!!map)::{b: foobarList_b, a: foo_a, thing: foo_thing, c: foobarList_c}\n",
"D0, P[foobar], (!!map)::{c: foobar_c, a: foo_a, thing: foobar_thing}\n",
},
},
{
description: "FIXED: Merge multiple maps",
subdescription: "Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.",
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::x: 1\ny: 2\nr: 10\n"},
},
{
description: "FIXED: Override",
subdescription: "Taken from https://yaml.org/type/merge.html. Same values as legacy, but with the correct key order.",
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\ny: 2\nx: 1\n"},
},
{
description: "Exploding inline merge anchor",
// subdescription: "`<<` map must be exploded, otherwise `c: *b` will become invalid",
document: `{a: {b: &b 42}, <<: {c: *b}}`,
expression: `explode(.) | sort_keys(.)`,
expected: []string{
"D0, P[], (!!map)::{a: {b: 42}, c: 42}\n",
},
},
{
skipDoc: true,
description: "Exploding inline merge anchor in sequence",
subdescription: "`<<` map must be exploded, otherwise `c: *b` will become invalid",
document: `{a: {b: &b 42}, <<: [{c: *b}]}`,
expression: `explode(.) | sort_keys(.)`,
expected: []string{
"D0, P[], (!!map)::{a: {b: 42}, c: 42}\n",
},
},
{
skipDoc: true,
description: "Exploding merge anchor should not explode neighbors",
subdescription: "b must not be exploded, as `r: *a` will become invalid",
document: `{b: &b {a: &a 42}, r: *a, c: {<<: *b}}`,
expression: `explode(.c)`,
expected: []string{
"D0, P[], (!!map)::{b: &b {a: &a 42}, r: *a, c: {a: 42}}\n",
},
},
{
skipDoc: true,
description: "Exploding sequence merge anchor should not explode neighbors",
subdescription: "b must not be exploded, as `r: *a` will become invalid",
document: `{b: &b {a: &a 42}, r: *a, c: {<<: [*b]}}`,
expression: `explode(.c)`,
expected: []string{
"D0, P[], (!!map)::{b: &b {a: &a 42}, r: *a, c: {a: 42}}\n",
},
},
{
skipDoc: true,
description: "Merge anchor with inline map",
document: `{<<: {a: 42}}`,
expression: `explode(.)`,
expected: []string{
"D0, P[], (!!map)::{a: 42}\n",
},
},
{
skipDoc: true,
description: "Merge anchor with sequence with inline map",
document: `{<<: [{a: 42}]}`,
expression: `explode(.)`,
expected: []string{
"D0, P[], (!!map)::{a: 42}\n",
},
},
{
skipDoc: true,
description: "Merge anchor with aliased sequence with inline map",
document: `{s: &s [{a: 42}], m: {<<: *s}}`,
expression: `.m | explode(.)`,
expected: []string{
"D0, P[m], (!!map)::{a: 42}\n",
},
},
{
skipDoc: true,
description: "deleting after explode",
document: "x: 37\na: &a\n b: 42\n<<: *a",
expression: `explode(.) | del(.x)`,
expected: []string{
"D0, P[], (!!map)::a:\n b: 42\nb: 42\n",
},
},
}
var badAnchorOperatorScenarios = []expressionScenario{
{
skipDoc: true, // incorrect overrides
description: "LEGACY: merge anchor after existing keys",
document: explodeWhenKeysExistDocument,
expression: "explode(.)",
expected: []string{explodeWhenKeysExistLegacy},
},
{
description: "LEGACY: Explode with merge anchors", // incorrect overrides
subdescription: "Caution: this is for when --yaml-fix-merge-anchor-to-spec=false; it's not to YAML spec because the merge anchors incorrectly override the object values (foobarList.b is set to bar_b when it should still be foobarList_b). Flag will default to true in late 2025",
document: mergeDocSample,
expression: `explode(.)`,
expected: []string{explodeMergeAnchorsExpected},
},
{
skipDoc: true,
document: mergeDocSample, // incorrect overrides
expression: `.foo* | explode(.) | (. style="flow")`,
expected: []string{
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
"D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n",
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foo* | explode(explode(.)) | (. style="flow")`,
expected: []string{
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
"D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n",
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
},
},
{
description: "LEGACY: Merge multiple maps",
subdescription: "see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.",
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
},
{
description: "LEGACY: Override",
subdescription: "see https://yaml.org/type/merge.html. This has the correct data, but the wrong key order; set --yaml-fix-merge-anchor-to-spec=true to fix the key order.",
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
},
}
var anchorOperatorScenarios = []expressionScenario{
{
skipDoc: true,
description: "merge anchor to alias alias",
document: "b: &b 10\na: &a { k: *b }\nc:\n <<: [*a]",
expression: "explode(.)",
expected: []string{"D0, P[], (!!map)::b: 10\na: {k: 10}\nc:\n k: 10\n"},
},
{
skipDoc: true,
description: "merge anchor not map",
document: "a: &a\n - 0\nc:\n <<: [*a]\n",
expectedError: "merge anchor only supports maps, got !!seq instead",
expectedError: "can only use merge anchors with maps (!!map) or sequences (!!seq) of maps, but got sequence containing !!seq",
expression: "explode(.)",
},
{
@ -68,20 +301,7 @@ var anchorOperatorScenarios = []expressionScenario{
expression: ".[4] | explode(.)",
expected: []string{expectedSpecResult},
},
{
description: "Merge multiple maps",
subdescription: "see https://yaml.org/type/merge.html",
document: specDocument + "- << : [ *CENTER, *BIG ]\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
},
{
description: "Override",
subdescription: "see https://yaml.org/type/merge.html",
document: specDocument + "- << : [ *BIG, *LEFT, *SMALL ]\n x: 1\n",
expression: ".[4] | explode(.)",
expected: []string{"D0, P[4], (!!map)::r: 10\nx: 1\ny: 2\n"},
},
{
description: "Get anchor",
document: `a: &billyBob cat`,
@ -224,37 +444,22 @@ var anchorOperatorScenarios = []expressionScenario{
},
},
{
description: "Explode with alias keys",
document: `{f : {a: &a cat, *a: b}}`,
expression: `explode(.f)`,
description: "Explode with alias keys",
subdescription: "No space between alias",
skipDoc: true,
document: `{f : {a: &a cat, *a: b}}`,
expression: `explode(.f)`,
expected: []string{
"D0, P[], (!!map)::{f: {a: cat, cat: b}}\n",
},
skipForGoccy: true, // can't handle no space between alias
},
{
description: "Explode with merge anchors",
document: mergeDocSample,
expression: `explode(.)`,
expected: []string{explodeMergeAnchorsExpected},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foo* | explode(.) | (. style="flow")`,
description: "Explode with alias keys",
document: `{f : {a: &a cat, *a : b}}`,
expression: `explode(.f)`,
expected: []string{
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
"D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n",
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
},
},
{
skipDoc: true,
document: mergeDocSample,
expression: `.foo* | explode(explode(.)) | (. style="flow")`,
expected: []string{
"D0, P[foo], (!!map)::{a: foo_a, thing: foo_thing, c: foo_c}\n",
"D0, P[foobarList], (!!map)::{b: bar_b, thing: foo_thing, c: foobarList_c, a: foo_a}\n",
"D0, P[foobar], (!!map)::{c: foo_c, a: foo_a, thing: foobar_thing}\n",
"D0, P[], (!!map)::{f: {a: cat, cat: b}}\n",
},
},
{
@ -269,14 +474,47 @@ var anchorOperatorScenarios = []expressionScenario{
description: "Dereference and update a field",
subdescription: "Use explode with multiply to dereference an object",
document: simpleArrayRef,
expression: `.thingOne |= explode(.) * {"value": false}`,
expression: `.thingOne |= (explode(.) | sort_keys(.)) * {"value": false}`,
expected: []string{expectedUpdatedArrayRef},
},
{
skipDoc: true,
description: "Duplicate keys",
subdescription: "outside merge anchor",
document: `{a: 1, a: 2}`,
expression: `explode(.)`,
expected: []string{
// {a: 2} would also be fine
"D0, P[], (!!map)::{a: 1, a: 2}\n",
},
},
{
skipDoc: true,
description: "!!str << should not be treated as merge anchor",
document: `{!!str <<: {a: 37}}`,
expression: `explode(.).a`,
expected: []string{
"D0, P[a], (!!null)::null\n",
},
},
}
func TestAnchorAliasOperatorScenarios(t *testing.T) {
for _, tt := range anchorOperatorScenarios {
for _, tt := range append(anchorOperatorScenarios, badAnchorOperatorScenarios...) {
testScenario(t, &tt)
}
documentOperatorScenarios(t, "anchor-and-alias-operators", anchorOperatorScenarios)
documentOperatorScenarios(t, "anchor-and-alias-operators", append(anchorOperatorScenarios, badAnchorOperatorScenarios...))
}
func TestAnchorAliasOperatorAlignedToSpecScenarios(t *testing.T) {
ConfiguredYamlPreferences.FixMergeAnchorToSpec = true
for _, tt := range append(fixedAnchorOperatorScenarios, anchorOperatorScenarios...) {
testScenario(t, &tt)
}
for i, tt := range fixedAnchorOperatorScenarios {
fixedAnchorOperatorScenarios[i].subdescription = "Set `--yaml-fix-merge-anchor-to-spec=true` to get this correct merge behaviour (flag will default to true in late 2025).\n" + tt.subdescription
}
appendOperatorDocumentScenario(t, "anchor-and-alias-operators", fixedAnchorOperatorScenarios)
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
}

View File

@ -2,6 +2,7 @@ package yqlib
import (
"container/list"
"fmt"
)
/*
@ -34,6 +35,9 @@ func collectObjectOperator(d *dataTreeNavigator, originalContext Context, _ *Exp
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidateNode := el.Value.(*CandidateNode)
if len(candidateNode.Content) < len(first.Content) {
return Context{}, fmt.Errorf("CollectObject: mismatching node sizes; are you creating a map with mismatching key value pairs?")
}
for i := 0; i < len(first.Content); i++ {
log.Debugf("rotate[%v] = %v", i, NodeToString(candidateNode.Content[i]))

View File

@ -12,6 +12,11 @@ var collectObjectOperatorScenarios = []expressionScenario{
"D0, P[name], (!!str)::mike\n",
},
},
{
skipDoc: true,
expression: `{"c": "a", "b", "d"}`,
expectedError: "CollectObject: mismatching node sizes; are you creating a map with mismatching key value pairs?",
},
{
skipDoc: true,
expression: `{"person": {"names": ["mike"]}} | .person.names[0]`,

View File

@ -26,11 +26,12 @@ func deleteChildOperator(d *dataTreeNavigator, context Context, expressionNode *
candidatePath := candidate.GetPath()
childPath := candidatePath[len(candidatePath)-1]
if parentNode.Kind == MappingNode {
switch parentNode.Kind {
case MappingNode:
deleteFromMap(candidate.Parent, childPath)
} else if parentNode.Kind == SequenceNode {
case SequenceNode:
deleteFromArray(candidate.Parent, childPath)
} else {
default:
return Context{}, fmt.Errorf("cannot delete nodes from parent of tag %v", parentNode.Tag)
}
}

View File

@ -119,6 +119,60 @@ var deleteOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::a: []\n",
},
},
{
skipDoc: true,
description: "Delete entry appended to an array",
document: `[1,2]`,
expression: `. += [3] | del(.[2])`,
expected: []string{
"D0, P[], (!!seq)::[1, 2]\n",
},
},
{
skipDoc: true,
description: "Delete entry after sorting an array",
document: `[3,2,1]`,
expression: `sort | del(.[2])`,
expected: []string{
"D0, P[], (!!seq)::[1, 2]\n",
},
},
{
skipDoc: true,
description: "Delete entry after reversing an array",
document: `[1,2,3]`,
expression: `reverse | del(.[2])`,
expected: []string{
"D0, P[], (!!seq)::[3, 2]\n",
},
},
{
skipDoc: true,
description: "Delete entry after shuffling an array",
document: `[1,2,3]`,
expression: `shuffle | del(.[2])`,
expected: []string{
"D0, P[], (!!seq)::[3, 1]\n",
},
},
{
skipDoc: true,
description: "Delete entry from keys array",
document: `{"a": 1, "b": 2, "c": 3}`,
expression: `keys | del(.[] | select(.=="b"))`,
expected: []string{
"D0, P[], (!!seq)::- \"a\"\n- \"c\"\n",
},
},
{
skipDoc: true,
description: "Delete entry after flattening an array",
document: `[1,[2],[[3]]]`,
expression: `flatten | del(.[2])`,
expected: []string{
"D0, P[], (!!seq)::[1, 2]\n",
},
},
{
skipDoc: true,
document: `a: [10,x,10, 10, x, 10]`,

View File

@ -21,8 +21,8 @@ var flattenOperatorScenarios = []expressionScenario{
expression: `flatten[]`,
expected: []string{
"D0, P[0], (!!int)::1\n",
"D0, P[0], (!!int)::2\n",
"D0, P[0], (!!int)::3\n",
"D0, P[1], (!!int)::2\n",
"D0, P[2], (!!int)::3\n",
},
},
{
@ -40,8 +40,8 @@ var flattenOperatorScenarios = []expressionScenario{
expression: `flatten(1)[]`,
expected: []string{
"D0, P[0], (!!int)::1\n",
"D0, P[0], (!!int)::2\n",
"D0, P[0], (!!seq)::[3]\n",
"D0, P[1], (!!int)::2\n",
"D0, P[2], (!!seq)::[3]\n",
},
},
{

View File

@ -45,12 +45,13 @@ func keysOperator(_ *dataTreeNavigator, context Context, _ *ExpressionNode) (Con
candidate := el.Value.(*CandidateNode)
var targetNode *CandidateNode
if candidate.Kind == MappingNode {
switch candidate.Kind {
case MappingNode:
targetNode = getMapKeys(candidate)
} else if candidate.Kind == SequenceNode {
case SequenceNode:
targetNode = getIndices(candidate)
} else {
return Context{}, fmt.Errorf("Cannot get keys of %v, keys only works for maps and arrays", candidate.Tag)
default:
return Context{}, fmt.Errorf("cannot get keys of %v, keys only works for maps and arrays", candidate.Tag)
}
results.PushBack(targetNode)
@ -64,19 +65,20 @@ func getMapKeys(node *CandidateNode) *CandidateNode {
for index := 0; index < len(node.Content); index = index + 2 {
contents = append(contents, node.Content[index])
}
return &CandidateNode{Kind: SequenceNode, Tag: "!!seq", Content: contents}
seq := &CandidateNode{Kind: SequenceNode, Tag: "!!seq"}
seq.AddChildren(contents)
return seq
}
func getIndices(node *CandidateNode) *CandidateNode {
var contents = make([]*CandidateNode, len(node.Content))
for index := range node.Content {
contents[index] = &CandidateNode{
Kind: ScalarNode,
Tag: "!!int",
Value: fmt.Sprintf("%v", index),
}
contents[index] = createScalarNode(index, fmt.Sprintf("%v", index))
}
return &CandidateNode{Kind: SequenceNode, Tag: "!!seq", Content: contents}
seq := &CandidateNode{Kind: SequenceNode, Tag: "!!seq"}
seq.AddChildren(contents)
return seq
}

View File

@ -45,8 +45,8 @@ var keysOperatorScenarios = []expressionScenario{
document: `{dog: woof, cat: meow}`,
expression: `keys[]`,
expected: []string{
"D0, P[dog], (!!str)::dog\n",
"D0, P[cat], (!!str)::cat\n",
"D0, P[0], (!!str)::dog\n",
"D0, P[1], (!!str)::cat\n",
},
},
{

View File

@ -82,7 +82,7 @@ func loadStringOperator(d *dataTreeNavigator, context Context, expressionNode *E
contentsCandidate, err := loadString(filename)
if err != nil {
return Context{}, fmt.Errorf("Failed to load %v: %w", filename, err)
return Context{}, fmt.Errorf("failed to load %v: %w", filename, err)
}
results.PushBack(contentsCandidate)
@ -118,7 +118,7 @@ func loadOperator(d *dataTreeNavigator, context Context, expressionNode *Express
contentsCandidate, err := loadWithDecoder(filename, loadPrefs.decoder)
if err != nil {
return Context{}, fmt.Errorf("Failed to load %v: %w", filename, err)
return Context{}, fmt.Errorf("failed to load %v: %w", filename, err)
}
results.PushBack(contentsCandidate)

View File

@ -41,6 +41,10 @@ func mapOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
if err != nil {
return Context{}, err
}
if splatted.MatchingNodes.Len() == 0 {
results.PushBack(candidate.Copy())
continue
}
result, err := d.GetMatchingNodes(splatted, expressionNode.RHS)
log.Debug("expressionNode.Rhs %v", expressionNode.RHS.Operation.OperationType)

View File

@ -15,6 +15,53 @@ var mapOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::[6, 7, 8]\n",
},
},
{
description: "mapping against an empty array should do nothing",
skipDoc: true,
document: `[]`,
document2: `["cat"]`,
expression: `map(3)`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
"D0, P[], (!!seq)::[3]\n",
},
},
{
description: "mapping against an empty array should do nothing",
skipDoc: true,
document: `[[], [5]]`,
expression: `.[] |= map(3)`,
expected: []string{
"D0, P[], (!!seq)::[[], [3]]\n",
},
},
{
description: "mapping against an empty array should do nothing #2",
skipDoc: true,
document: `[]`,
document2: `[5]`,
expression: `map(3 + .)`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
"D0, P[], (!!seq)::[8]\n",
},
},
{
description: "mapping against an empty array should do nothing",
skipDoc: true,
document: `[[], [5]]`,
expression: `.[] |= map(3 + .)`,
expected: []string{
"D0, P[], (!!seq)::[[], [8]]\n",
},
},
{
skipDoc: true,
expression: `[] | map(. + 42)`,
expected: []string{
"D0, P[], (!!seq)::[]\n",
},
},
{
skipDoc: true,
document: `[1,2]`,
@ -32,6 +79,26 @@ var mapOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::[2, 3, 4]\n",
},
},
{
skipDoc: true,
document: `{}`,
document2: `{b: 12}`,
expression: `map_values(3)`,
expected: []string{
"D0, P[], (!!map)::{}\n",
"D0, P[], (!!map)::{b: 3}\n",
},
},
{
skipDoc: true,
document: `{}`,
document2: `{b: 12}`,
expression: `map_values(3 + .)`,
expected: []string{
"D0, P[], (!!map)::{}\n",
"D0, P[], (!!map)::{b: 15}\n",
},
},
{
skipDoc: true,
document: `{a: 1, b: 2, c: 3}`,

View File

@ -154,9 +154,9 @@ func repeatString(lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error
if err != nil {
return nil, err
} else if count < 0 {
return nil, fmt.Errorf("Cannot repeat string by a negative number (%v)", count)
return nil, fmt.Errorf("cannot repeat string by a negative number (%v)", count)
} else if count > 10000000 {
return nil, fmt.Errorf("Cannot repeat string by more than 100 million (%v)", count)
return nil, fmt.Errorf("cannot repeat string by more than 100 million (%v)", count)
}
target.Value = strings.Repeat(stringNode.Value, count)

View File

@ -206,7 +206,7 @@ var multiplyOperatorScenarios = []expressionScenario{
skipDoc: true,
document: `n: -4`,
expression: `"banana" * .n`,
expectedError: "Cannot repeat string by a negative number (-4)",
expectedError: "cannot repeat string by a negative number (-4)",
},
{
description: "Multiply string X by more than 100 million",
@ -214,7 +214,7 @@ var multiplyOperatorScenarios = []expressionScenario{
skipDoc: true,
document: `n: 100000001`,
expression: `"banana" * .n`,
expectedError: "Cannot repeat string by more than 100 million (100000001)",
expectedError: "cannot repeat string by more than 100 million (100000001)",
},
{
description: "Multiply int node X string",

View File

@ -58,11 +58,12 @@ func omitOperator(d *dataTreeNavigator, context Context, expressionNode *Express
var replacement *CandidateNode
if node.Kind == MappingNode {
switch node.Kind {
case MappingNode:
replacement = omitMap(node, indicesToOmit)
} else if node.Kind == SequenceNode {
case SequenceNode:
replacement = omitSequence(node, indicesToOmit)
} else {
default:
log.Debugf("Omit from type %v (%v) is noop", node.Tag, node.GetNicePath())
return context, nil
}

View File

@ -22,15 +22,16 @@ func getPathArrayFromNode(funcName string, node *CandidateNode) ([]interface{},
path := make([]interface{}, len(node.Content))
for i, childNode := range node.Content {
if childNode.Tag == "!!str" {
switch childNode.Tag {
case "!!str":
path[i] = childNode.Value
} else if childNode.Tag == "!!int" {
case "!!int":
number, err := parseInt(childNode.Value)
if err != nil {
return nil, fmt.Errorf("%v: could not parse %v as an int: %w", funcName, childNode.Value, err)
}
path[i] = number
} else {
default:
return nil, fmt.Errorf("%v: expected either a !!str or !!int in the path, found %v instead", funcName, childNode.Tag)
}

View File

@ -64,15 +64,16 @@ func pickOperator(d *dataTreeNavigator, context Context, expressionNode *Express
node := el.Value.(*CandidateNode)
var replacement *CandidateNode
if node.Kind == MappingNode {
switch node.Kind {
case MappingNode:
replacement = pickMap(node, indicesToPick)
} else if node.Kind == SequenceNode {
case SequenceNode:
replacement, err = pickSequence(node, indicesToPick)
if err != nil {
return Context{}, err
}
} else {
default:
return Context{}, fmt.Errorf("cannot pick indices from type %v (%v)", node.Tag, node.GetNicePath())
}

View File

@ -17,8 +17,8 @@ var reverseOperatorScenarios = []expressionScenario{
document: "[1, 2]",
expression: `reverse[]`,
expected: []string{
"D0, P[1], (!!int)::2\n",
"D0, P[0], (!!int)::1\n",
"D0, P[0], (!!int)::2\n",
"D0, P[1], (!!int)::1\n",
},
},
{

View File

@ -5,6 +5,12 @@ import (
)
var selectOperatorScenarios = []expressionScenario{
{
skipDoc: true,
document: `cat: pants`,
expression: `select(.nope) | key + " why though?"`,
expected: []string{},
},
{
skipDoc: true,
document: `cat`,

View File

@ -26,7 +26,12 @@ func shuffleOperator(_ *dataTreeNavigator, context Context, _ *ExpressionNode) (
a := result.Content
myRand.Shuffle(len(a), func(i, j int) { a[i], a[j] = a[j], a[i] })
myRand.Shuffle(len(a), func(i, j int) {
a[i], a[j] = a[j], a[i]
oldIndex := a[i].Key.Value
a[i].Key.Value = a[j].Key.Value
a[j].Key.Value = oldIndex
})
results.PushBack(result)
}

View File

@ -17,9 +17,9 @@ var shuffleOperatorScenarios = []expressionScenario{
document: "[1, 2, 3]",
expression: `shuffle[]`,
expected: []string{
"D0, P[2], (!!int)::3\n",
"D0, P[0], (!!int)::1\n",
"D0, P[1], (!!int)::2\n",
"D0, P[0], (!!int)::3\n",
"D0, P[1], (!!int)::1\n",
"D0, P[2], (!!int)::2\n",
},
},

View File

@ -47,11 +47,12 @@ func sortByOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
sort.Stable(sortableArray)
sortedList := candidate.CopyWithoutContent()
if candidate.Kind == MappingNode {
switch candidate.Kind {
case MappingNode:
for _, sortedNode := range sortableArray {
sortedList.AddKeyValueChild(sortedNode.Node.Key, sortedNode.Node)
}
} else if candidate.Kind == SequenceNode {
case SequenceNode:
for _, sortedNode := range sortableArray {
sortedList.AddChild(sortedNode.Node)
}

View File

@ -17,8 +17,8 @@ var sortByOperatorScenarios = []expressionScenario{
document: "[{a: banana},{a: apple}]",
expression: `sort_by(.a)[]`,
expected: []string{
"D0, P[1], (!!map)::{a: apple}\n",
"D0, P[0], (!!map)::{a: banana}\n",
"D0, P[0], (!!map)::{a: apple}\n",
"D0, P[1], (!!map)::{a: banana}\n",
},
},
{
@ -27,9 +27,9 @@ var sortByOperatorScenarios = []expressionScenario{
document: "[{a: banana},null,{a: apple}]",
expression: `sort_by(.a)[]`,
expected: []string{
"D0, P[1], (!!null)::null\n",
"D0, P[2], (!!map)::{a: apple}\n",
"D0, P[0], (!!map)::{a: banana}\n",
"D0, P[0], (!!null)::null\n",
"D0, P[1], (!!map)::{a: apple}\n",
"D0, P[2], (!!map)::{a: banana}\n",
},
},
{
@ -149,8 +149,8 @@ var sortByOperatorScenarios = []expressionScenario{
document: "[8,null]",
expression: `sort[]`,
expected: []string{
"D0, P[1], (!!null)::null\n",
"D0, P[0], (!!int)::8\n",
"D0, P[0], (!!null)::null\n",
"D0, P[1], (!!int)::8\n",
},
},
{

View File

@ -410,7 +410,7 @@ func extractMatchArguments(d *dataTreeNavigator, context Context, expressionNode
return nil, matchPrefs, fmt.Errorf(`'i' is not a valid option for match. To ignore case, use an expression like match("(?i)cat")`)
}
if len(paramText) > 0 {
return nil, matchPrefs, fmt.Errorf(`Unrecognised match params '%v', please see docs at https://mikefarah.gitbook.io/yq/operators/string-operators`, paramText)
return nil, matchPrefs, fmt.Errorf(`unrecognised match params '%v', please see docs at https://mikefarah.gitbook.io/yq/operators/string-operators`, paramText)
}
}

View File

@ -19,7 +19,7 @@ func parseStyle(customStyle string) (Style, error) {
} else if customStyle == "flow" {
return FlowStyle, nil
} else if customStyle != "" {
return 0, fmt.Errorf("Unknown style %v", customStyle)
return 0, fmt.Errorf("unknown style %v", customStyle)
}
return 0, nil
}

Some files were not shown because too many files have changed in this diff Show More