Compare commits

...

3 Commits

Author SHA1 Message Date
Mark
b6528393a3
Merge 82f3e1497e into 4532346e13 2025-09-12 02:43:51 +08:00
Mike Farah
4532346e13 Adding first operator 2025-09-10 18:47:52 +10:00
mark.barzali
82f3e1497e feat: configure colors of output with env variables
Signed-off-by: mark.barzali <mark.barzali@heureka.group>
2025-09-08 12:35:07 +02:00
9 changed files with 713 additions and 7 deletions

View File

@ -423,6 +423,30 @@ Flags:
Use "yq [command] --help" for more information about a command.
```
## Environment Variables
### Color Customization
You can customize the colors used in YAML output by setting these environment variables:
- `YQ_COLOR_BOOL` - Color for boolean values (default: hi-magenta)
- `YQ_COLOR_NUMBER` - Color for numeric values (default: hi-magenta)
- `YQ_COLOR_MAP_KEY` - Color for map keys (default: cyan)
- `YQ_COLOR_ANCHOR` - Color for YAML anchors (default: hi-yellow)
- `YQ_COLOR_ALIAS` - Color for YAML aliases (default: hi-yellow)
- `YQ_COLOR_STRING` - Color for string values (default: green)
- `YQ_COLOR_COMMENT` - Color for comments (default: hi-black)
Supported color values: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, and their `hi-` prefixed variants (e.g., `hi-red`).
Example:
```bash
export YQ_COLOR_STRING=red
export YQ_COLOR_MAP_KEY=hi-blue
yq -C '.data' file.yaml
```
## Known Issues / Missing Features
- `yq` attempts to preserve comment positions and whitespace as much as possible, but it does not handle all scenarios (see https://github.com/go-yaml/yaml/tree/v3 for details)
- Powershell has its own...[opinions on quoting yq](https://mikefarah.gitbook.io/yq/usage/tips-and-tricks#quotes-in-windows-powershell)

93
pkg/yqlib/color_config.go Normal file
View File

@ -0,0 +1,93 @@
package yqlib
import (
"fmt"
"github.com/fatih/color"
"os"
)
type ColorConfig struct {
Bool color.Attribute
Number color.Attribute
MapKey color.Attribute
Anchor color.Attribute
Alias color.Attribute
String color.Attribute
Comment color.Attribute
}
func NewColorConfig() *ColorConfig {
config := &ColorConfig{
Bool: color.FgHiMagenta,
Number: color.FgHiMagenta,
MapKey: color.FgCyan,
Anchor: color.FgHiYellow,
Alias: color.FgHiYellow,
String: color.FgGreen,
Comment: color.FgHiBlack,
}
colorMappings := map[string]*color.Attribute{
"YQ_COLOR_BOOL": &config.Bool,
"YQ_COLOR_NUMBER": &config.Number,
"YQ_COLOR_MAP_KEY": &config.MapKey,
"YQ_COLOR_ANCHOR": &config.Anchor,
"YQ_COLOR_ALIAS": &config.Alias,
"YQ_COLOR_STRING": &config.String,
"YQ_COLOR_COMMENT": &config.Comment,
}
for envVar, configField := range colorMappings {
if colorStr := os.Getenv(envVar); colorStr != "" {
if attr, err := parseColorAttribute(colorStr); err == nil {
*configField = attr
}
}
}
return config
}
// parseColorAttribute converts a color string to a color.Attribute.
//
// Supports three types of color specifications:
// 1. Standard color names: "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black"
// 2. High-intensity variants: "hi-red", "hi-green", "hi-blue", etc.
func parseColorAttribute(colorStr string) (color.Attribute, error) {
switch colorStr {
case "black":
return color.FgBlack, nil
case "red":
return color.FgRed, nil
case "green":
return color.FgGreen, nil
case "yellow":
return color.FgYellow, nil
case "blue":
return color.FgBlue, nil
case "magenta":
return color.FgMagenta, nil
case "cyan":
return color.FgCyan, nil
case "white":
return color.FgWhite, nil
case "hi-black":
return color.FgHiBlack, nil
case "hi-red":
return color.FgHiRed, nil
case "hi-green":
return color.FgHiGreen, nil
case "hi-yellow":
return color.FgHiYellow, nil
case "hi-blue":
return color.FgHiBlue, nil
case "hi-magenta":
return color.FgHiMagenta, nil
case "hi-cyan":
return color.FgHiCyan, nil
case "hi-white":
return color.FgHiWhite, nil
default:
return color.Reset, fmt.Errorf("unknown color: %s", colorStr)
}
}

View File

@ -19,46 +19,47 @@ func format(attr color.Attribute) string {
func colorizeAndPrint(yamlBytes []byte, writer io.Writer) error {
tokens := lexer.Tokenize(string(yamlBytes))
config := NewColorConfig()
var p printer.Printer
p.Bool = func() *printer.Property {
return &printer.Property{
Prefix: format(color.FgHiMagenta),
Prefix: format(config.Bool),
Suffix: format(color.Reset),
}
}
p.Number = func() *printer.Property {
return &printer.Property{
Prefix: format(color.FgHiMagenta),
Prefix: format(config.Number),
Suffix: format(color.Reset),
}
}
p.MapKey = func() *printer.Property {
return &printer.Property{
Prefix: format(color.FgCyan),
Prefix: format(config.MapKey),
Suffix: format(color.Reset),
}
}
p.Anchor = func() *printer.Property {
return &printer.Property{
Prefix: format(color.FgHiYellow),
Prefix: format(config.Anchor),
Suffix: format(color.Reset),
}
}
p.Alias = func() *printer.Property {
return &printer.Property{
Prefix: format(color.FgHiYellow),
Prefix: format(config.Alias),
Suffix: format(color.Reset),
}
}
p.String = func() *printer.Property {
return &printer.Property{
Prefix: format(color.FgGreen),
Prefix: format(config.String),
Suffix: format(color.Reset),
}
}
p.Comment = func() *printer.Property {
return &printer.Property{
Prefix: format(color.FgHiBlack),
Prefix: format(config.Comment),
Suffix: format(color.Reset),
}
}

View File

@ -0,0 +1,345 @@
## First matching element from array
Given a sample.yml file of:
```yaml
- a: banana
- a: cat
- a: apple
```
then
```bash
yq 'first(.a == "cat")' sample.yml
```
will output
```yaml
a: cat
```
## First matching element from array with multiple matches
Given a sample.yml file of:
```yaml
- a: banana
- a: cat
- a: apple
- a: cat
```
then
```bash
yq 'first(.a == "cat")' sample.yml
```
will output
```yaml
a: cat
```
## First matching element from array with numeric condition
Given a sample.yml file of:
```yaml
- a: 10
- a: 100
- a: 1
```
then
```bash
yq 'first(.a > 50)' sample.yml
```
will output
```yaml
a: 100
```
## First matching element from array with boolean condition
Given a sample.yml file of:
```yaml
- a: false
- a: true
- a: false
```
then
```bash
yq 'first(.a == true)' sample.yml
```
will output
```yaml
a: true
```
## First matching element from array with null values
Given a sample.yml file of:
```yaml
- a: null
- a: cat
- a: apple
```
then
```bash
yq 'first(.a != null)' sample.yml
```
will output
```yaml
a: cat
```
## First matching element from array with complex condition
Given a sample.yml file of:
```yaml
- a: dog
b: 5
- a: cat
b: 3
- a: apple
b: 7
```
then
```bash
yq 'first(.b > 4)' sample.yml
```
will output
```yaml
a: dog
b: 5
```
## First matching element from map
Given a sample.yml file of:
```yaml
x:
a: banana
y:
a: cat
z:
a: apple
```
then
```bash
yq 'first(.a == "cat")' sample.yml
```
will output
```yaml
a: cat
```
## First matching element from map with numeric condition
Given a sample.yml file of:
```yaml
x:
a: 10
y:
a: 100
z:
a: 1
```
then
```bash
yq 'first(.a > 50)' sample.yml
```
will output
```yaml
a: 100
```
## First matching element from nested structure
Given a sample.yml file of:
```yaml
items:
- a: banana
- a: cat
- a: apple
```
then
```bash
yq '.items | first(.a == "cat")' sample.yml
```
will output
```yaml
a: cat
```
## First matching element with no matches
Given a sample.yml file of:
```yaml
- a: banana
- a: cat
- a: apple
```
then
```bash
yq 'first(.a == "dog")' sample.yml
```
will output
```yaml
```
## First matching element from empty array
Given a sample.yml file of:
```yaml
[]
```
then
```bash
yq 'first(.a == "cat")' sample.yml
```
will output
```yaml
```
## First matching element from scalar node
Given a sample.yml file of:
```yaml
hello
```
then
```bash
yq 'first(. == "hello")' sample.yml
```
will output
```yaml
```
## First matching element from null node
Given a sample.yml file of:
```yaml
null
```
then
```bash
yq 'first(. == "hello")' sample.yml
```
will output
```yaml
```
## First matching element with string condition
Given a sample.yml file of:
```yaml
- a: banana
- a: cat
- a: apple
```
then
```bash
yq 'first(.a | test("^c"))' sample.yml
```
will output
```yaml
a: cat
```
## First matching element with length condition
Given a sample.yml file of:
```yaml
- a: hi
- a: hello
- a: world
```
then
```bash
yq 'first(.a | length > 4)' sample.yml
```
will output
```yaml
a: hello
```
## First matching element from array of strings
Given a sample.yml file of:
```yaml
- banana
- cat
- apple
```
then
```bash
yq 'first(. == "cat")' sample.yml
```
will output
```yaml
cat
```
## First matching element from array of numbers
Given a sample.yml file of:
```yaml
- 10
- 100
- 1
```
then
```bash
yq 'first(. > 50)' sample.yml
```
will output
```yaml
100
```
## First element with no RHS from array
Given a sample.yml file of:
```yaml
- 10
- 100
- 1
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
10
```
## First element with no RHS from array of maps
Given a sample.yml file of:
```yaml
- a: 10
- a: 100
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
a: 10
```
## No RHS on empty array returns nothing
Given a sample.yml file of:
```yaml
[]
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
```
## No RHS on scalar returns nothing
Given a sample.yml file of:
```yaml
hello
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
```
## No RHS on null returns nothing
Given a sample.yml file of:
```yaml
null
```
then
```bash
yq 'first' sample.yml
```
will output
```yaml
```

View File

@ -54,6 +54,12 @@ func (p *expressionParserImpl) createExpressionTree(postFixPath []*Operation) (*
switch numArgs {
case 1:
if len(stack) < 1 {
// Allow certain unary ops to accept zero args by interpreting missing RHS as nil
// TODO - make this more general on OperationType
if Operation.OperationType == firstOpType {
// no RHS provided; proceed without popping
break
}
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]

View File

@ -118,6 +118,7 @@ var participleYqRules = []*participleYqRule{
simpleOp("sort_?by", sortByOpType),
simpleOp("sort", sortOpType),
simpleOp("first", firstOpType),
simpleOp("reverse", reverseOpType),

View File

@ -143,6 +143,7 @@ var delPathsOpType = &operationType{Type: "DEL_PATHS", NumArgs: 1, Precedence: 5
var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 52, Handler: explodeOperator, CheckForPostTraverse: true}
var sortByOpType = &operationType{Type: "SORT_BY", NumArgs: 1, Precedence: 52, Handler: sortByOperator, CheckForPostTraverse: true}
var firstOpType = &operationType{Type: "FIRST", NumArgs: 1, Precedence: 52, Handler: firstOperator, CheckForPostTraverse: true}
var reverseOpType = &operationType{Type: "REVERSE", NumArgs: 0, Precedence: 52, Handler: reverseOperator, CheckForPostTraverse: true}
var sortOpType = &operationType{Type: "SORT", NumArgs: 0, Precedence: 52, Handler: sortOperator, CheckForPostTraverse: true}
var shuffleOpType = &operationType{Type: "SHUFFLE", NumArgs: 0, Precedence: 52, Handler: shuffleOperator, CheckForPostTraverse: true}

View File

@ -0,0 +1,51 @@
package yqlib
import "container/list"
func firstOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
results := list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)
// If no RHS expression is provided, simply return the first entry in candidate.Content
if expressionNode == nil || expressionNode.RHS == nil {
if len(candidate.Content) > 0 {
results.PushBack(candidate.Content[0])
}
continue
}
splatted, err := splat(context.SingleChildContext(candidate), traversePreferences{})
if err != nil {
return Context{}, err
}
for splatEl := splatted.MatchingNodes.Front(); splatEl != nil; splatEl = splatEl.Next() {
splatCandidate := splatEl.Value.(*CandidateNode)
// Create a new context for this splatted candidate
splatContext := context.SingleChildContext(splatCandidate)
// Evaluate the RHS expression against this splatted candidate
rhs, err := d.GetMatchingNodes(splatContext, expressionNode.RHS)
if err != nil {
return Context{}, err
}
includeResult := false
for resultEl := rhs.MatchingNodes.Front(); resultEl != nil; resultEl = resultEl.Next() {
result := resultEl.Value.(*CandidateNode)
includeResult = isTruthyNode(result)
if includeResult {
break
}
}
if includeResult {
results.PushBack(splatCandidate)
break
}
}
}
return context.ChildContext(results), nil
}

View File

@ -0,0 +1,184 @@
package yqlib
import "testing"
var firstOperatorScenarios = []expressionScenario{
{
description: "First matching element from array",
document: "[{a: banana},{a: cat},{a: apple}]",
expression: `first(.a == "cat")`,
expected: []string{
"D0, P[1], (!!map)::{a: cat}\n",
},
},
{
description: "First matching element from array with multiple matches",
document: "[{a: banana},{a: cat},{a: apple},{a: cat}]",
expression: `first(.a == "cat")`,
expected: []string{
"D0, P[1], (!!map)::{a: cat}\n",
},
},
{
description: "First matching element from array with numeric condition",
document: "[{a: 10},{a: 100},{a: 1}]",
expression: `first(.a > 50)`,
expected: []string{
"D0, P[1], (!!map)::{a: 100}\n",
},
},
{
description: "First matching element from array with boolean condition",
document: "[{a: false},{a: true},{a: false}]",
expression: `first(.a == true)`,
expected: []string{
"D0, P[1], (!!map)::{a: true}\n",
},
},
{
description: "First matching element from array with null values",
document: "[{a: null},{a: cat},{a: apple}]",
expression: `first(.a != null)`,
expected: []string{
"D0, P[1], (!!map)::{a: cat}\n",
},
},
{
description: "First matching element from array with complex condition",
document: "[{a: dog, b: 5},{a: cat, b: 3},{a: apple, b: 7}]",
expression: `first(.b > 4)`,
expected: []string{
"D0, P[0], (!!map)::{a: dog, b: 5}\n",
},
},
{
description: "First matching element from map",
document: "x: {a: banana}\ny: {a: cat}\nz: {a: apple}",
expression: `first(.a == "cat")`,
expected: []string{
"D0, P[y], (!!map)::{a: cat}\n",
},
},
{
description: "First matching element from map with numeric condition",
document: "x: {a: 10}\ny: {a: 100}\nz: {a: 1}",
expression: `first(.a > 50)`,
expected: []string{
"D0, P[y], (!!map)::{a: 100}\n",
},
},
{
description: "First matching element from nested structure",
document: "items: [{a: banana},{a: cat},{a: apple}]",
expression: `.items | first(.a == "cat")`,
expected: []string{
"D0, P[items 1], (!!map)::{a: cat}\n",
},
},
{
description: "First matching element with no matches",
document: "[{a: banana},{a: cat},{a: apple}]",
expression: `first(.a == "dog")`,
expected: []string{
// No output expected when no matches
},
},
{
description: "First matching element from empty array",
document: "[]",
expression: `first(.a == "cat")`,
expected: []string{
// No output expected when array is empty
},
},
{
description: "First matching element from scalar node",
document: "hello",
expression: `first(. == "hello")`,
expected: []string{
// No output expected when node is scalar (no content to splat)
},
},
{
description: "First matching element from null node",
document: "null",
expression: `first(. == "hello")`,
expected: []string{
// No output expected when node is null (no content to splat)
},
},
{
description: "First matching element with string condition",
document: "[{a: banana},{a: cat},{a: apple}]",
expression: `first(.a | test("^c"))`,
expected: []string{
"D0, P[1], (!!map)::{a: cat}\n",
},
},
{
description: "First matching element with length condition",
document: "[{a: hi},{a: hello},{a: world}]",
expression: `first(.a | length > 4)`,
expected: []string{
"D0, P[1], (!!map)::{a: hello}\n",
},
},
{
description: "First matching element from array of strings",
document: "[banana, cat, apple]",
expression: `first(. == "cat")`,
expected: []string{
"D0, P[1], (!!str)::cat\n",
},
},
{
description: "First matching element from array of numbers",
document: "[10, 100, 1]",
expression: `first(. > 50)`,
expected: []string{
"D0, P[1], (!!int)::100\n",
},
},
// New tests for no RHS (return first child)
{
description: "First element with no RHS from array",
document: "[10, 100, 1]",
expression: `first`,
expected: []string{
"D0, P[0], (!!int)::10\n",
},
},
{
description: "First element with no RHS from array of maps",
document: "[{a: 10},{a: 100}]",
expression: `first`,
expected: []string{
"D0, P[0], (!!map)::{a: 10}\n",
},
},
{
description: "No RHS on empty array returns nothing",
document: "[]",
expression: `first`,
expected: []string{},
},
{
description: "No RHS on scalar returns nothing",
document: "hello",
expression: `first`,
expected: []string{},
},
{
description: "No RHS on null returns nothing",
document: "null",
expression: `first`,
expected: []string{},
},
}
func TestFirstOperatorScenarios(t *testing.T) {
for _, tt := range firstOperatorScenarios {
testScenario(t, &tt)
}
documentOperatorScenarios(t, "first", firstOperatorScenarios)
}