This commit is contained in:
Mike Farah 2021-11-16 15:29:16 +11:00
parent 76841fab3b
commit 356eff3b0b
9 changed files with 272 additions and 8 deletions

11
acceptance_tests/load-file.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
setUp() {
rm test*.yml || true
}
testLoadFileNotExist() {
}
testStrLoadFileNotExist() {
}

View File

@ -1,8 +1,2 @@
# welcome! # cat
# bob
---
# bob
a: cat # meow
# have a great day
a:
include: 'data2.yaml'

2
examples/thing.yml Normal file
View File

@ -0,0 +1,2 @@
a: apple is included
b: cool.

View File

@ -0,0 +1,13 @@
# Load
The `load`/`strload` operator allows you to load in content from another file referenced in your yaml document.
Note that you can use string operators like `+` and `sub` to modify the value in the yaml file to a path that exists in your system.
Lets say there is a file `../../examples/thing.yml`:
```yaml
a: apple is included
b: cool
```

93
pkg/yqlib/doc/load.md Normal file
View File

@ -0,0 +1,93 @@
# Load
The `load`/`strload` operator allows you to load in content from another file referenced in your yaml document.
Note that you can use string operators like `+` and `sub` to modify the value in the yaml file to a path that exists in your system.
Lets say there is a file `../../examples/thing.yml`:
```yaml
a: apple is included
b: cool
```
## Simple example
Given a sample.yml file of:
```yaml
myFile: ../../examples/thing.yml
```
then
```bash
yq eval 'load(.myFile)' sample.yml
```
will output
```yaml
a: apple is included
b: cool.
```
## Replace node with referenced file
Note that you can modify the filename in the load operator if needed.
Given a sample.yml file of:
```yaml
something:
file: thing.yml
```
then
```bash
yq eval '.something |= load("../../examples/" + .file)' sample.yml
```
will output
```yaml
something:
a: apple is included
b: cool.
```
## Replace _all_ nodes with referenced file
Recursively match all the nodes (`..`) and then filter the ones that have a 'file' attribute.
Given a sample.yml file of:
```yaml
something:
file: thing.yml
over:
here:
- file: thing.yml
```
then
```bash
yq eval '(.. | select(has("file"))) |= load("../../examples/" + .file)' sample.yml
```
will output
```yaml
something:
a: apple is included
b: cool.
over:
here:
- a: apple is included
b: cool.
```
## Replace node with referenced file as string
This will work for any text based file
Given a sample.yml file of:
```yaml
something:
file: thing.yml
```
then
```bash
yq eval '.something |= strload("../../examples/" + .file)' sample.yml
```
will output
```yaml
something: |-
a: apple is included
b: cool.
```

View File

@ -336,6 +336,8 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`from_json`), opToken(decodeOpType))
lexer.Add([]byte(`sortKeys`), opToken(sortKeysOpType))
lexer.Add([]byte(`load`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: false}))
lexer.Add([]byte(`strload`), opTokenWithPrefs(loadOpType, nil, loadPrefs{loadAsString: true}))
lexer.Add([]byte(`select`), opToken(selectOpType))
lexer.Add([]byte(`has`), opToken(hasOpType))
lexer.Add([]byte(`unique`), opToken(uniqueOpType))

View File

@ -95,6 +95,8 @@ var captureOpType = &operationType{Type: "CAPTURE", NumArgs: 1, Precedence: 50,
var testOpType = &operationType{Type: "TEST", NumArgs: 1, Precedence: 50, Handler: testOperator}
var splitStringOpType = &operationType{Type: "SPLIT", NumArgs: 1, Precedence: 50, Handler: splitStringOperator}
var loadOpType = &operationType{Type: "LOAD", NumArgs: 1, Precedence: 50, Handler: loadYamlOperator}
var keysOpType = &operationType{Type: "KEYS", NumArgs: 0, Precedence: 50, Handler: keysOperator}
var collectObjectOpType = &operationType{Type: "COLLECT_OBJECT", NumArgs: 0, Precedence: 50, Handler: collectObjectOperator}

View File

@ -0,0 +1,97 @@
package yqlib
import (
"bufio"
"container/list"
"fmt"
"io/ioutil"
"os"
"gopkg.in/yaml.v3"
)
type loadPrefs struct {
loadAsString bool
}
func loadString(filename string) (*CandidateNode, error) {
// ignore CWE-22 gosec issue - that's more targetted for http based apps that run in a public directory,
// and ensuring that it's not possible to give a path to a file outside that directory.
filebytes, err := ioutil.ReadFile(filename) // #nosec
if err != nil {
return nil, err
}
return &CandidateNode{Node: &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: string(filebytes)}}, nil
}
func loadYaml(filename string) (*CandidateNode, error) {
file, err := os.Open(filename) // #nosec
if err != nil {
return nil, err
}
reader := bufio.NewReader(file)
documents, err := readDocuments(reader, filename, 0)
if err != nil {
return nil, err
}
if documents.Len() == 0 {
// return null candidate
return &CandidateNode{Node: &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null"}}, nil
} else if documents.Len() == 1 {
return documents.Front().Value.(*CandidateNode), nil
} else {
sequenceNode := &CandidateNode{Node: &yaml.Node{Kind: yaml.SequenceNode}}
for doc := documents.Front(); doc != nil; doc = doc.Next() {
sequenceNode.Node.Content = append(sequenceNode.Node.Content, doc.Value.(*CandidateNode).Node)
}
return sequenceNode, nil
}
}
func loadYamlOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
log.Debugf("loadYamlOperator")
loadPrefs := expressionNode.Operation.Preferences.(loadPrefs)
// need to evaluate the 1st parameter against the context
// and return the data accordingly.
var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs)
if err != nil {
return Context{}, err
}
if rhs.MatchingNodes.Front() == nil {
return Context{}, fmt.Errorf("Filename expression returned nil")
}
nameCandidateNode := rhs.MatchingNodes.Front().Value.(*CandidateNode)
filename := nameCandidateNode.Node.Value
var contentsCandidate *CandidateNode
if loadPrefs.loadAsString {
contentsCandidate, err = loadString(filename)
} else {
contentsCandidate, err = loadYaml(filename)
}
if err != nil {
return Context{}, fmt.Errorf("Failed to load %v: %w", filename, err)
}
results.PushBack(contentsCandidate)
}
return context.ChildContext(results), nil
}

View File

@ -0,0 +1,50 @@
package yqlib
import (
"testing"
)
var loadScenarios = []expressionScenario{
{
description: "Simple example",
document: `{myFile: "../../examples/thing.yml"}`,
expression: `load(.myFile)`,
expected: []string{
"D0, P[], (doc)::a: apple is included\nb: cool.\n",
},
},
{
description: "Replace node with referenced file",
subdescription: "Note that you can modify the filename in the load operator if needed.",
document: `{something: {file: "thing.yml"}}`,
expression: `.something |= load("../../examples/" + .file)`,
expected: []string{
"D0, P[], (doc)::{something: {a: apple is included, b: cool.}}\n",
},
},
{
description: "Replace _all_ nodes with referenced file",
subdescription: "Recursively match all the nodes (`..`) and then filter the ones that have a 'file' attribute. ",
document: `{something: {file: "thing.yml"}, over: {here: [{file: "thing.yml"}]}}`,
expression: `(.. | select(has("file"))) |= load("../../examples/" + .file)`,
expected: []string{
"D0, P[], (!!map)::{something: {a: apple is included, b: cool.}, over: {here: [{a: apple is included, b: cool.}]}}\n",
},
},
{
description: "Replace node with referenced file as string",
subdescription: "This will work for any text based file",
document: `{something: {file: "thing.yml"}}`,
expression: `.something |= strload("../../examples/" + .file)`,
expected: []string{
"D0, P[], (doc)::{something: \"a: apple is included\\nb: cool.\"}\n",
},
},
}
func TestLoadScenarios(t *testing.T) {
for _, tt := range loadScenarios {
testScenario(t, &tt)
}
documentScenarios(t, "load", loadScenarios)
}