diff --git a/cmd/tcl/testworkflow-init/data/expressions.go b/cmd/tcl/testworkflow-init/data/expressions.go
index b347b95f727..79210715d73 100644
--- a/cmd/tcl/testworkflow-init/data/expressions.go
+++ b/cmd/tcl/testworkflow-init/data/expressions.go
@@ -85,6 +85,17 @@ var EnvMachine = expressionstcl.NewMachine().
return os.Getenv(name[4:]), true
}
return nil, false
+ }).
+ RegisterAccessor(func(name string) (interface{}, bool) {
+ if name != "env" {
+ return nil, false
+ }
+ env := make(map[string]string)
+ for _, item := range os.Environ() {
+ key, value, _ := strings.Cut(item, "=")
+ env[key] = value
+ }
+ return env, true
})
var RefSuccessMachine = expressionstcl.NewMachine().
diff --git a/docs/docs/articles/test-workflows-expressions.md b/docs/docs/articles/test-workflows-expressions.md
index 48f886f410f..1a350e6b1fb 100644
--- a/docs/docs/articles/test-workflows-expressions.md
+++ b/docs/docs/articles/test-workflows-expressions.md
@@ -94,7 +94,7 @@ spec:
# ensure that the step won't fail for 5 executions
retry:
count: 5
- until: 'self.failed'
+ until: 'self.failed'
```
#### Matrix and Shard
@@ -138,30 +138,31 @@ There are some functions that help to cast values to a different type. Additiona
### General
-| Name | Returns | Description | Example |
-|--------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
-| `join` | `string` | Join list elements | `join(["a", "b"])` is `"a,b"`
`join(["a", "b"], " - ")` is `"a - b"` |
-| `split` | `list` | Split string to list | `split("a,b,c")` is `["a", "b", "c"]`
`split("a - b - c", " - ")` is `["a", "b", "c"]` |
-| `trim` | `string` | Trim whitespaces from the string | `trim(" \nabc d ")` is `"abc d"` |
-| `len` | `int` | Length of array, map or string | `len([ "a", "b" ])` is `2`
`len("foobar")` is `6`
`len({ "foo": "bar" })` is `1` |
-| `floor` | `int` | Round value down | `floor(10.5)` is `10` |
-| `ceil` | `int` | Round value up | `ceil(10.5)` is `11` |
-| `round` | `int` | Round value to nearest integer | `round(10.5)` is `11` |
-| `at` | anything | Get value of the element | `at([10, 2], 1)` is `2`
`at({"foo": "bar"}, "foo")` is `"bar"` |
-| `tojson` | `string` | Serialize value to JSON | `tojson({ "foo": "bar" })` is `"{\"foo\":\"bar\"}"` |
-| `json` | anything | Parse the JSON | `json("{\"foo\":\"bar\"}")` is `{ "foo": "bar" }` |
-| `toyaml` | `string` | Serialize value to YAML | `toyaml({ "foo": "bar" })` is `"foo: bar\n` |
-| `yaml` | anything | Parse the YAML | `yaml("foo: bar")` is `{ "foo": "bar" }` |
-| `shellquote` | `string` | Sanitize arguments for shell | `shellquote("foo bar")` is `"\"foo bar\""`
`shellquote("foo", "bar baz")` is `"foo \"bar baz\""` |
-| `shellparse` | `[]string` | Parse shell arguments | `shellparse("foo bar")` is `["foo", "bar"]`
`shellparse("foo \"bar baz\"")` is `["foo", "bar baz"]` |
-| `map` | `list` or `map` | Map list or map values with expression; `_.value` and `_.index`/`_.key` are available | `map([1,2,3,4,5], "_.value * 2")` is `[2,4,6,8,10]` |
-| `filter` | `list` | Filter list values with expression; `_.value` and `_.index` are available | `filter([1,2,3,4,5], "_.value > 2")` is `[3,4,5]` |
-| `jq` | anything | Execute [**jq**](https://en.wikipedia.org/wiki/Jq_(programming_language)) against value | jq([1,2,3,4,5], ". | max")
is `[5]` |
-| `range` | `[]int` | Build range of numbers | `range(5, 10)` is `[5, 6, 7, 8, 9]`
`range(5)` is `[0, 1, 2, 3, 4]` |
-| `relpath` | `string` | Build relative path | `relpath("/a/b/c")` may be `./b/c`
`relpath("/a/b/c", "/a/b")` is `"./c"` |
-| `abspath` | `string` | Build absolute path | `abspath("/a/b/c")` is `/a/b/c`
`abspath("b/c")` may be `/some/working/dir/b/c` |
-| `chunk` | `[]list` | Split list to chunks of specified maximum size | `chunk([1,2,3,4,5], 2)` is `[[1,2], [3,4], [5]]` |
-| `date` | `string` | Return current date (either `2006-01-02T15:04:05.000Z07:00` format or custom argument ([**Go syntax**](https://go.dev/src/time/format.go#L101)) | `date()` may be `"2024-06-04T11:59:32.308Z"`
`date("2006-01-02")` may be `2024-06-04` |
+| Name | Returns | Description | Example |
+|--------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
+| `join` | `string` | Join list elements | `join(["a", "b"])` is `"a,b"`
`join(["a", "b"], " - ")` is `"a - b"` |
+| `split` | `list` | Split string to list | `split("a,b,c")` is `["a", "b", "c"]`
`split("a - b - c", " - ")` is `["a", "b", "c"]` |
+| `trim` | `string` | Trim whitespaces from the string | `trim(" \nabc d ")` is `"abc d"` |
+| `len` | `int` | Length of array, map or string | `len([ "a", "b" ])` is `2`
`len("foobar")` is `6`
`len({ "foo": "bar" })` is `1` |
+| `floor` | `int` | Round value down | `floor(10.5)` is `10` |
+| `ceil` | `int` | Round value up | `ceil(10.5)` is `11` |
+| `round` | `int` | Round value to nearest integer | `round(10.5)` is `11` |
+| `at` | anything | Get value of the element | `at([10, 2], 1)` is `2`
`at({"foo": "bar"}, "foo")` is `"bar"` |
+| `tojson` | `string` | Serialize value to JSON | `tojson({ "foo": "bar" })` is `"{\"foo\":\"bar\"}"` |
+| `json` | anything | Parse the JSON | `json("{\"foo\":\"bar\"}")` is `{ "foo": "bar" }` |
+| `toyaml` | `string` | Serialize value to YAML | `toyaml({ "foo": "bar" })` is `"foo: bar\n` |
+| `yaml` | anything | Parse the YAML | `yaml("foo: bar")` is `{ "foo": "bar" }` |
+| `shellquote` | `string` | Sanitize arguments for shell | `shellquote("foo bar")` is `"\"foo bar\""`
`shellquote("foo", "bar baz")` is `"foo \"bar baz\""` |
+| `shellparse` | `[]string` | Parse shell arguments | `shellparse("foo bar")` is `["foo", "bar"]`
`shellparse("foo \"bar baz\"")` is `["foo", "bar baz"]` |
+| `map` | `list` | Map list with expression; `_.value` and `_.index` are available | `map([1,2,3,4,5], "_.value * 2")` is `[2,4,6,8,10]` |
+| `entries` | `map` | Get list of entries in map | `entries({"A":"B", "C":"D"})` is `[{"key": "A", "value": "B"}, {"key": "C", "value": "D"}]` |
+| `filter` | `list` | Filter list values with expression; `_.value` and `_.index` are available | `filter([1,2,3,4,5], "_.value > 2")` is `[3,4,5]` |
+| `jq` | anything | Execute [**jq**](https://en.wikipedia.org/wiki/Jq_(programming_language)) against value | jq([1,2,3,4,5], ". | max")
is `[5]` |
+| `range` | `[]int` | Build range of numbers | `range(5, 10)` is `[5, 6, 7, 8, 9]`
`range(5)` is `[0, 1, 2, 3, 4]` |
+| `relpath` | `string` | Build relative path | `relpath("/a/b/c")` may be `./b/c`
`relpath("/a/b/c", "/a/b")` is `"./c"` |
+| `abspath` | `string` | Build absolute path | `abspath("/a/b/c")` is `/a/b/c`
`abspath("b/c")` may be `/some/working/dir/b/c` |
+| `chunk` | `[]list` | Split list to chunks of specified maximum size | `chunk([1,2,3,4,5], 2)` is `[[1,2], [3,4], [5]]` |
+| `date` | `string` | Return current date (either `2006-01-02T15:04:05.000Z07:00` format or custom argument ([**Go syntax**](https://go.dev/src/time/format.go#L101)) | `date()` may be `"2024-06-04T11:59:32.308Z"`
`date("2006-01-02")` may be `2024-06-04` |
### File System
diff --git a/pkg/tcl/expressionstcl/parse_test.go b/pkg/tcl/expressionstcl/parse_test.go
index a49bc481c27..a1cc6316ad7 100644
--- a/pkg/tcl/expressionstcl/parse_test.go
+++ b/pkg/tcl/expressionstcl/parse_test.go
@@ -264,6 +264,7 @@ a:
assert.Equal(t, `[0,2,4,6,8]`, MustCompile(`map([10,20,30,40,50], "_.index * 2")`).String())
assert.Equal(t, `[2,4,6,8,10]`, MustCompile(`map([1,2,3,4,5], "_.value * 2")`).String())
assert.Equal(t, `[0,2,4,6,8]`, MustCompile(`map([10,20,30,40,50], "_.index * 2")`).String())
+ assert.ElementsMatch(t, []interface{}{MapEntry{Key: "A", Value: "B"}, MapEntry{Key: "C", Value: 5.0}}, must(MustCompile(`entries({"A": "B", "C": 5})`).Static().SliceValue()))
assert.Equal(t, `[3,4,5]`, MustCompile(`filter([1,2,3,4,5], "_.value > 2")`).String())
assert.Equal(t, `[5]`, MustCompile(`jq([1,2,3,4,5], ". | max")`).String())
assert.Equal(t, `[{"b":{"v":2}}]`, MustCompile(`jq([{"a":{"v": 1}},{"b":{"v": 2}}], ". | max_by(.v)")`).String())
diff --git a/pkg/tcl/expressionstcl/stdlib.go b/pkg/tcl/expressionstcl/stdlib.go
index 95dc30d18c4..9f33f4a7afa 100644
--- a/pkg/tcl/expressionstcl/stdlib.go
+++ b/pkg/tcl/expressionstcl/stdlib.go
@@ -381,6 +381,22 @@ var stdFunctions = map[string]StdFunction{
return Compile(fmt.Sprintf("list(%s)", strings.Join(result, ",")))
},
},
+ "entries": {
+ Handler: func(value ...StaticValue) (Expression, error) {
+ if len(value) != 1 {
+ return nil, fmt.Errorf(`"entries" function expects 1 argument, %d provided`, len(value))
+ }
+ dict, err := value[0].MapValue()
+ if err != nil {
+ return nil, fmt.Errorf(`"entries" function expects 1st argument to be a map, %s provided: %v`, value[0], err)
+ }
+ list := make([]MapEntry, 0, len(dict))
+ for k, v := range dict {
+ list = append(list, MapEntry{Key: k, Value: v})
+ }
+ return NewValue(list), nil
+ },
+ },
"filter": {
Handler: func(value ...StaticValue) (Expression, error) {
if len(value) != 2 {
@@ -576,6 +592,11 @@ const (
floatCastStdFn = "float"
)
+type MapEntry struct {
+ Key string `json:"key"`
+ Value interface{} `json:"value"`
+}
+
func CastToString(v Expression) Expression {
if v.Static() != nil {
return NewStringValue(v.Static().Value())