diff --git a/pkg/yqlib/doc/operators/headers/shuffle.md b/pkg/yqlib/doc/operators/headers/shuffle.md new file mode 100644 index 0000000000..3fc6648118 --- /dev/null +++ b/pkg/yqlib/doc/operators/headers/shuffle.md @@ -0,0 +1,4 @@ +# Shuffle + +Shuffles an array. Note that this command does _not_ use a cryptographically secure random number generator to randomise the array order. + diff --git a/pkg/yqlib/doc/operators/shuffle.md b/pkg/yqlib/doc/operators/shuffle.md new file mode 100644 index 0000000000..953b513fd3 --- /dev/null +++ b/pkg/yqlib/doc/operators/shuffle.md @@ -0,0 +1,51 @@ +# Shuffle + +Shuffles an array. Note that this command does _not_ use a cryptographically secure random number generator to randomise the array order. + + +## Shuffle array +Given a sample.yml file of: +```yaml +- 1 +- 2 +- 3 +- 4 +- 5 +``` +then +```bash +yq 'shuffle' sample.yml +``` +will output +```yaml +- 5 +- 2 +- 4 +- 1 +- 3 +``` + +## Shuffle array in place +Given a sample.yml file of: +```yaml +cool: + - 1 + - 2 + - 3 + - 4 + - 5 +``` +then +```bash +yq '.cool |= shuffle' sample.yml +``` +will output +```yaml +cool: + - 5 + - 2 + - 4 + - 1 + - 3 +``` + diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index 7e2f22843a..33756fef08 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -49,6 +49,7 @@ var participleYqRules = []*participleYqRule{ simpleOp("to_?unix", toUnixOpType), simpleOp("with_dtf", withDtFormatOpType), simpleOp("error", errorOpType), + simpleOp("shuffle", shuffleOpType), simpleOp("sortKeys", sortKeysOpType), simpleOp("sort_?keys", sortKeysOpType), diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 0325f6af56..8b61eae3b3 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -135,6 +135,7 @@ var explodeOpType = &operationType{Type: "EXPLODE", NumArgs: 1, Precedence: 50, var sortByOpType = &operationType{Type: "SORT_BY", NumArgs: 1, Precedence: 50, Handler: sortByOperator} var reverseOpType = &operationType{Type: "REVERSE", NumArgs: 0, Precedence: 50, Handler: reverseOperator} var sortOpType = &operationType{Type: "SORT", NumArgs: 0, Precedence: 50, Handler: sortOperator} +var shuffleOpType = &operationType{Type: "SHUFFLE", NumArgs: 0, Precedence: 50, Handler: shuffleOperator} var sortKeysOpType = &operationType{Type: "SORT_KEYS", NumArgs: 1, Precedence: 50, Handler: sortKeysOperator} diff --git a/pkg/yqlib/operator_shuffle.go b/pkg/yqlib/operator_shuffle.go new file mode 100644 index 0000000000..73a76965dc --- /dev/null +++ b/pkg/yqlib/operator_shuffle.go @@ -0,0 +1,37 @@ +package yqlib + +import ( + "container/list" + "fmt" + "math/rand" + + yaml "gopkg.in/yaml.v3" +) + +func shuffleOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + + // ignore CWE-338 gosec issue of not using crypto/rand + // this is just to shuffle an array rather generating a + // secret or something that needs proper rand. + myRand := rand.New(rand.NewSource(Now().UnixNano())) // #nosec + + results := list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + + candidateNode := unwrapDoc(candidate.Node) + + if candidateNode.Kind != yaml.SequenceNode { + return context, fmt.Errorf("node at path [%v] is not an array (it's a %v)", candidate.GetNicePath(), candidate.GetNiceTag()) + } + + result := deepClone(candidateNode) + + a := result.Content + + myRand.Shuffle(len(a), func(i, j int) { a[i], a[j] = a[j], a[i] }) + results.PushBack(candidate.CreateReplacement(result)) + } + return context.ChildContext(results), nil +} diff --git a/pkg/yqlib/operator_shuffle_test.go b/pkg/yqlib/operator_shuffle_test.go new file mode 100644 index 0000000000..23a3748b6c --- /dev/null +++ b/pkg/yqlib/operator_shuffle_test.go @@ -0,0 +1,30 @@ +package yqlib + +import "testing" + +var shuffleOperatorScenarios = []expressionScenario{ + { + description: "Shuffle array", + document: "[1, 2, 3, 4, 5]", + expression: `shuffle`, + expected: []string{ + "D0, P[], (!!seq)::[5, 2, 4, 1, 3]\n", + }, + }, + + { + description: "Shuffle array in place", + document: "cool: [1, 2, 3, 4, 5]", + expression: `.cool |= shuffle`, + expected: []string{ + "D0, P[], (doc)::cool: [5, 2, 4, 1, 3]\n", + }, + }, +} + +func TestShuffleByOperatorScenarios(t *testing.T) { + for _, tt := range shuffleOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "shuffle", shuffleOperatorScenarios) +}