mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-05 12:10:37 +00:00
Compare commits
105 Commits
64bee0fbe3
...
76c817009f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76c817009f | ||
|
|
f03c9dc599 | ||
|
|
023c85e1e2 | ||
|
|
a3d9e0172f | ||
|
|
ab3be228dc | ||
|
|
93fed3fd7a | ||
|
|
f42534ea0f | ||
|
|
b968963ed4 | ||
|
|
92309b17a4 | ||
|
|
d5757fc82b | ||
|
|
db2a4550e5 | ||
|
|
3018396ed2 | ||
|
|
84b095bbc4 | ||
|
|
f35e57d901 | ||
|
|
70ac3d6c7a | ||
|
|
904215ef4d | ||
|
|
41cc4fb4ac | ||
|
|
9c95a9f379 | ||
|
|
a4720c089a | ||
|
|
3431aebb2c | ||
|
|
ae87394f4a | ||
|
|
23a7b173bf | ||
|
|
5e75db824b | ||
|
|
08ecd39a1e | ||
|
|
b7aa711d94 | ||
|
|
a5b8ef6cb1 | ||
|
|
a47e882c8f | ||
|
|
128bf80eed | ||
|
|
4019d42d60 | ||
|
|
55daf6d93c | ||
|
|
8e731ac13c | ||
|
|
d0c897f5e6 | ||
|
|
6e8cc00030 | ||
|
|
b9d9e2fbad | ||
|
|
a98921213f | ||
|
|
0153cccda9 | ||
|
|
9b299649f7 | ||
|
|
369fe56e2d | ||
|
|
8c06478ade | ||
|
|
fa6dc5c9fb | ||
|
|
422300457c | ||
|
|
544bd9ff6f | ||
|
|
1187c954ec | ||
|
|
08d3789daa | ||
|
|
6a5e95ac84 | ||
|
|
221a5b1106 | ||
|
|
c10bfe0602 | ||
|
|
99f88df21e | ||
|
|
7696723d5c | ||
|
|
a9f7cc1ebb | ||
|
|
384d227efe | ||
|
|
25365a0f0b | ||
|
|
78c096fa8f | ||
|
|
ce9a4af0df | ||
|
|
4734be9a4d | ||
|
|
4d88d51b1b | ||
|
|
31628e7324 | ||
|
|
bfcb3fc6b7 | ||
|
|
c3782799c5 | ||
|
|
162ea5437c | ||
|
|
40808838ba | ||
|
|
176873e93a | ||
|
|
fd7e750040 | ||
|
|
ff9d2acd4a | ||
|
|
88bfbec97b | ||
|
|
b15ce77cad | ||
|
|
b84fd47934 | ||
|
|
58e0cd2600 | ||
|
|
c58d9e7da4 | ||
|
|
3ac203ebb8 | ||
|
|
b534aa9ee5 | ||
|
|
39a65b62d2 | ||
|
|
1e3006e951 | ||
|
|
22949df0fd | ||
|
|
734e2cd254 | ||
|
|
082b76affa | ||
|
|
5bc2cd03da | ||
|
|
20407a07a5 | ||
|
|
0a83da6b38 | ||
|
|
77da8b7d32 | ||
|
|
a27db3aacc | ||
|
|
ed7fd54dac | ||
|
|
2c2487c0bd | ||
|
|
2c7cce0878 | ||
|
|
2bd08ea4e8 | ||
|
|
72fa3cca98 | ||
|
|
f89605e3c0 | ||
|
|
c59fa8de59 | ||
|
|
ef8b520f89 | ||
|
|
20b5129120 | ||
|
|
89518a09b8 | ||
|
|
d01ac7801d | ||
|
|
67520b5e7b | ||
|
|
7b6e61ba2c | ||
|
|
cf8621d9e3 | ||
|
|
340b897252 | ||
|
|
4155c99a12 | ||
|
|
b26de94758 | ||
|
|
7dc5efee62 | ||
|
|
2cb7498444 | ||
|
|
649c9a083a | ||
|
|
074d785396 | ||
|
|
de2f77b49c | ||
|
|
fe06096514 | ||
|
|
40f2c2381b |
51
.github/ISSUE_TEMPLATE/bug_report_v3.md
vendored
51
.github/ISSUE_TEMPLATE/bug_report_v3.md
vendored
@ -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.
|
||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
@ -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
38
.golangci.bck.yml
Normal 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"
|
||||
@ -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$
|
||||
|
||||
@ -38,6 +38,8 @@ builds:
|
||||
- openbsd_amd64
|
||||
- windows_386
|
||||
- windows_amd64
|
||||
- windows_arm
|
||||
- windows_arm64
|
||||
|
||||
no_unique_dist_dir: true
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.1 AS builder
|
||||
FROM golang:1.24.5 AS builder
|
||||
|
||||
WORKDIR /go/src/mikefarah/yq
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.24.1
|
||||
FROM golang:1.24.5
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y npm && \
|
||||
|
||||
53
README.md
53
README.md
@ -3,7 +3,7 @@
|
||||
    
|
||||
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -2,6 +2,8 @@ package cmd
|
||||
|
||||
var unwrapScalarFlag = newUnwrapFlag()
|
||||
|
||||
var printNodeInfo = false
|
||||
|
||||
var unwrapScalar = false
|
||||
|
||||
var writeInplace = false
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
85
cmd/utils.go
85
cmd/utils.go
@ -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
1435
cmd/utils_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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, "'", "")
|
||||
}
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
Foo: 3
|
||||
apple: 1
|
||||
bar: 2
|
||||
# 001
|
||||
---
|
||||
abc: # 001
|
||||
- 1 # one
|
||||
- 2 # two
|
||||
|
||||
---
|
||||
def # 002
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
a: apple
|
||||
b: bannana
|
||||
b: banana
|
||||
---
|
||||
hello there
|
||||
apples: great
|
||||
14
examples/sample.ini
Normal file
14
examples/sample.ini
Normal 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"
|
||||
|
||||
8
examples/sample_no_sections.ini
Normal file
8
examples/sample_no_sections.ini
Normal 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
31
go.mod
@ -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
80
go.sum
@ -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=
|
||||
|
||||
@ -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, ",")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nojson
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
}
|
||||
|
||||
437
pkg/yqlib/data_tree_navigator_test.go
Normal file
437
pkg/yqlib/data_tree_navigator_test.go
Normal 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)
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nobase64
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nocsv
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -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
106
pkg/yqlib/decoder_ini.go
Normal 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
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_noprops
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nouri
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -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}, " ")
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
yaml "go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
type yamlDecoder struct {
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nobase64
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nocsv
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
113
pkg/yqlib/encoder_ini.go
Normal file
113
pkg/yqlib/encoder_ini.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nosh
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
//go:build !yq_nouri
|
||||
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
type yamlEncoder struct {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
19
pkg/yqlib/ini.go
Normal 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
187
pkg/yqlib/ini_test.go
Normal 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)
|
||||
}
|
||||
@ -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 ".["
|
||||
|
||||
}
|
||||
|
||||
@ -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
11
pkg/yqlib/no_base64.go
Normal 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
11
pkg/yqlib/no_csv.go
Normal 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
11
pkg/yqlib/no_ini.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build yq_noini
|
||||
|
||||
package yqlib
|
||||
|
||||
func NewINIDecoder() Decoder {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewINIEncoder() Encoder {
|
||||
return nil
|
||||
}
|
||||
@ -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
11
pkg/yqlib/no_props.go
Normal 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
7
pkg/yqlib/no_sh.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build yq_nosh
|
||||
|
||||
package yqlib
|
||||
|
||||
func NewShEncoder() Encoder {
|
||||
return nil
|
||||
}
|
||||
7
pkg/yqlib/no_shellvariables.go
Normal file
7
pkg/yqlib/no_shellvariables.go
Normal file
@ -0,0 +1,7 @@
|
||||
//go:build yq_noshell
|
||||
|
||||
package yqlib
|
||||
|
||||
func NewShellVariablesEncoder() Encoder {
|
||||
return nil
|
||||
}
|
||||
11
pkg/yqlib/no_uri.go
Normal file
11
pkg/yqlib/no_uri.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build yq_nouri
|
||||
|
||||
package yqlib
|
||||
|
||||
func NewUriDecoder() Decoder {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewUriEncoder() Encoder {
|
||||
return nil
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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]))
|
||||
|
||||
@ -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]`,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]`,
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user