From 8698433d44db9614b45ff0cbdf7203636d8518df Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Sun, 27 Feb 2022 11:56:46 +1100 Subject: [PATCH] Added line and column operators --- pkg/yqlib/doc/operators/column.md | 66 ++++++++++++++++++++++ pkg/yqlib/doc/operators/headers/column.md | 3 + pkg/yqlib/doc/operators/headers/line.md | 3 + pkg/yqlib/doc/operators/line.md | 68 +++++++++++++++++++++++ pkg/yqlib/expression_tokeniser.go | 2 + pkg/yqlib/lib.go | 3 + pkg/yqlib/operator_column.go | 23 ++++++++ pkg/yqlib/operator_column_test.go | 47 ++++++++++++++++ pkg/yqlib/operator_line.go | 23 ++++++++ pkg/yqlib/operator_line_test.go | 47 ++++++++++++++++ 10 files changed, 285 insertions(+) create mode 100644 pkg/yqlib/doc/operators/column.md create mode 100644 pkg/yqlib/doc/operators/headers/column.md create mode 100644 pkg/yqlib/doc/operators/headers/line.md create mode 100644 pkg/yqlib/doc/operators/line.md create mode 100644 pkg/yqlib/operator_column.go create mode 100644 pkg/yqlib/operator_column_test.go create mode 100644 pkg/yqlib/operator_line.go create mode 100644 pkg/yqlib/operator_line_test.go diff --git a/pkg/yqlib/doc/operators/column.md b/pkg/yqlib/doc/operators/column.md new file mode 100644 index 0000000000..3902933a06 --- /dev/null +++ b/pkg/yqlib/doc/operators/column.md @@ -0,0 +1,66 @@ +# Column + +Returns the column of the matching node. Starts from 1, 0 indicates there was no column data. + +{% hint style="warning" %} +Note that versions prior to 4.18 require the 'eval/e' command to be specified. + +`yq e ` +{% endhint %} + +## Returns column of _value_ node +Given a sample.yml file of: +```yaml +a: cat +b: bob +``` +then +```bash +yq '.b | column' sample.yml +``` +will output +```yaml +4 +``` + +## Returns column of _key_ node +Pipe through the key operator to get the column of the key + +Given a sample.yml file of: +```yaml +a: cat +b: bob +``` +then +```bash +yq '.b | key | column' sample.yml +``` +will output +```yaml +1 +``` + +## First column is 1 +Given a sample.yml file of: +```yaml +a: cat +``` +then +```bash +yq '.a | key | column' sample.yml +``` +will output +```yaml +1 +``` + +## No column data is 0 +Running +```bash +yq --null-input '{"a": "new entry"} | column' +``` +will output +```yaml +0 +``` + diff --git a/pkg/yqlib/doc/operators/headers/column.md b/pkg/yqlib/doc/operators/headers/column.md new file mode 100644 index 0000000000..6c9997466a --- /dev/null +++ b/pkg/yqlib/doc/operators/headers/column.md @@ -0,0 +1,3 @@ +# Column + +Returns the column of the matching node. Starts from 1, 0 indicates there was no column data. diff --git a/pkg/yqlib/doc/operators/headers/line.md b/pkg/yqlib/doc/operators/headers/line.md new file mode 100644 index 0000000000..4b9bca0371 --- /dev/null +++ b/pkg/yqlib/doc/operators/headers/line.md @@ -0,0 +1,3 @@ +# Line + +Returns the line of the matching node. Starts from 1, 0 indicates there was no line data. diff --git a/pkg/yqlib/doc/operators/line.md b/pkg/yqlib/doc/operators/line.md new file mode 100644 index 0000000000..dcbf6b6eb0 --- /dev/null +++ b/pkg/yqlib/doc/operators/line.md @@ -0,0 +1,68 @@ +# Line + +Returns the line of the matching node. Starts from 1, 0 indicates there was no line data. + +{% hint style="warning" %} +Note that versions prior to 4.18 require the 'eval/e' command to be specified. + +`yq e ` +{% endhint %} + +## Returns line of _value_ node +Given a sample.yml file of: +```yaml +a: cat +b: + c: cat +``` +then +```bash +yq '.b | line' sample.yml +``` +will output +```yaml +3 +``` + +## Returns line of _key_ node +Pipe through the key operator to get the line of the key + +Given a sample.yml file of: +```yaml +a: cat +b: + c: cat +``` +then +```bash +yq '.b | key| line' sample.yml +``` +will output +```yaml +2 +``` + +## First line is 1 +Given a sample.yml file of: +```yaml +a: cat +``` +then +```bash +yq '.a | line' sample.yml +``` +will output +```yaml +1 +``` + +## No line data is 0 +Running +```bash +yq --null-input '{"a": "new entry"} | line' +``` +will output +```yaml +0 +``` + diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index c3c31a5653..ac8b7f8ea4 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -315,6 +315,8 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`,`), opToken(unionOpType)) lexer.Add([]byte(`:\s*`), opToken(createMapOpType)) lexer.Add([]byte(`length`), opToken(lengthOpType)) + lexer.Add([]byte(`line`), opToken(lineOpType)) + lexer.Add([]byte(`column`), opToken(columnOpType)) lexer.Add([]byte(`eval`), opToken(evalOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 0adc308633..d57e5d4e48 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -80,6 +80,9 @@ var createMapOpType = &operationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: var shortPipeOpType = &operationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence: 45, Handler: pipeOperator} var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator} +var lineOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Handler: lineOperator} +var columnOpType = &operationType{Type: "LINE", NumArgs: 0, Precedence: 50, Handler: columnOperator} + var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator} var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator} var evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 50, Handler: evalOperator} diff --git a/pkg/yqlib/operator_column.go b/pkg/yqlib/operator_column.go new file mode 100644 index 0000000000..ac7d339c0d --- /dev/null +++ b/pkg/yqlib/operator_column.go @@ -0,0 +1,23 @@ +package yqlib + +import ( + "container/list" + "fmt" + + yaml "gopkg.in/yaml.v3" +) + +func columnOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + log.Debugf("columnOperator") + + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", candidate.Node.Column), Tag: "!!int"} + result := candidate.CreateReplacement(node) + results.PushBack(result) + } + + return context.ChildContext(results), nil +} diff --git a/pkg/yqlib/operator_column_test.go b/pkg/yqlib/operator_column_test.go new file mode 100644 index 0000000000..3684588303 --- /dev/null +++ b/pkg/yqlib/operator_column_test.go @@ -0,0 +1,47 @@ +package yqlib + +import ( + "testing" +) + +var columnOperatorScenarios = []expressionScenario{ + { + description: "Returns column of _value_ node", + document: "a: cat\nb: bob", + expression: `.b | column`, + expected: []string{ + "D0, P[b], (!!int)::4\n", + }, + }, + { + description: "Returns column of _key_ node", + subdescription: "Pipe through the key operator to get the column of the key", + document: "a: cat\nb: bob", + expression: `.b | key | column`, + expected: []string{ + "D0, P[b], (!!int)::1\n", + }, + }, + { + description: "First column is 1", + document: "a: cat", + expression: `.a | key | column`, + expected: []string{ + "D0, P[a], (!!int)::1\n", + }, + }, + { + description: "No column data is 0", + expression: `{"a": "new entry"} | column`, + expected: []string{ + "D0, P[], (!!int)::0\n", + }, + }, +} + +func TestColumnOperatorScenarios(t *testing.T) { + for _, tt := range columnOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "column", columnOperatorScenarios) +} diff --git a/pkg/yqlib/operator_line.go b/pkg/yqlib/operator_line.go new file mode 100644 index 0000000000..1535e059a0 --- /dev/null +++ b/pkg/yqlib/operator_line.go @@ -0,0 +1,23 @@ +package yqlib + +import ( + "container/list" + "fmt" + + yaml "gopkg.in/yaml.v3" +) + +func lineOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + log.Debugf("lineOperator") + + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + node := &yaml.Node{Kind: yaml.ScalarNode, Value: fmt.Sprintf("%v", candidate.Node.Line), Tag: "!!int"} + result := candidate.CreateReplacement(node) + results.PushBack(result) + } + + return context.ChildContext(results), nil +} diff --git a/pkg/yqlib/operator_line_test.go b/pkg/yqlib/operator_line_test.go new file mode 100644 index 0000000000..e3765249a9 --- /dev/null +++ b/pkg/yqlib/operator_line_test.go @@ -0,0 +1,47 @@ +package yqlib + +import ( + "testing" +) + +var lineOperatorScenarios = []expressionScenario{ + { + description: "Returns line of _value_ node", + document: "a: cat\nb:\n c: cat", + expression: `.b | line`, + expected: []string{ + "D0, P[b], (!!int)::3\n", + }, + }, + { + description: "Returns line of _key_ node", + subdescription: "Pipe through the key operator to get the line of the key", + document: "a: cat\nb:\n c: cat", + expression: `.b | key| line`, + expected: []string{ + "D0, P[b], (!!int)::2\n", + }, + }, + { + description: "First line is 1", + document: "a: cat", + expression: `.a | line`, + expected: []string{ + "D0, P[a], (!!int)::1\n", + }, + }, + { + description: "No line data is 0", + expression: `{"a": "new entry"} | line`, + expected: []string{ + "D0, P[], (!!int)::0\n", + }, + }, +} + +func TestLineOperatorScenarios(t *testing.T) { + for _, tt := range lineOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "line", lineOperatorScenarios) +}