§ Migrate · Annexes

Function map

Every OpenTofu built-in function, with the CUE expression that replaces it, or the statement that none does.

Time
15 min
Level
intermediate
Needs
The concept map read.
You will learn
Which CUE builtin or expression does each OpenTofu function become?
Done when
Each function in your specification has a row here, and its CUE expression evaluates.

View as Markdown

On this page

OpenTofu 1.13 has 134 built-in functions, and tofu metadata functions -json lists them. CUE has a standard library of packages and a few builtins, and a comprehension replaces what neither covers. This page maps each function to its replacement, or says that there is none. pravo eval -c evaluates every CUE expression in the tables at the end of the page. The result column is that output.

Use a function by importing its package. An import that nothing uses is an error. The names after the imports are the ones the tables use.

Package Import Holds
strings "strings" ToUpper, Join, Split, Replace, TrimSpace, SliceRunes
list "list" Range, Slice, Concat, Contains, Sort, Max, Sum, MinItems, MaxItems
struct "struct" MinFields, MaxFields
math "math" Abs, Ceil, Floor, Log, Pow
strconv "strconv" Atoi, ParseInt, ParseFloat, ParseBool
regexp "regexp" Find, FindAll, ReplaceAll, Match
net "net" AddIP, ParseCIDR, InCIDR, QueryEscape, QueryUnescape, IPCIDR
path, time, uuid "path", "time", "uuid" Base, Dir, FormatString, SHA1
encoding "encoding/base64", "encoding/json", "encoding/yaml", "encoding/csv", "encoding/hex" Encode, Marshal, Unmarshal, Decode
crypto "crypto/md5", "crypto/sha1", "crypto/sha256", "crypto/sha512" Sum, Sum256, Sum512
text/template "text/template" Execute

The CUE builtins need no import: len, div, mod, quo, rem, and the operators. A row that says No equivalent gives a replacement in the note when there is one.

The functions

Numeric

OpenTofu CUE Result Note
abs(-5) math.Abs(-5) 5
ceil(1.2) math.Ceil(1.2) 2
floor(1.8) math.Floor(1.8) 1
log(8, 2) math.Log(8) / math.Log(2) 3.0 math.Log is the natural logarithm; the result is a float.
max(1, 3, 2) list.Max([1, 3, 2]) 3 list.Max takes one list.
min(1, 3, 2) list.Min([1, 3, 2]) 1 list.Min takes one list.
parseint("ff", 16) strconv.ParseInt("ff", 16, 64) 255
pow(2, 3) math.Pow(2, 3) 8
signum(-5) [if _n > 0 {1}, if _n < 0 {-1}, 0][0] -1 No builtin; the list picks the first branch that holds. Here _n: -5.

String

OpenTofu CUE Result Note
chomp("hello\n") strings.TrimRight("hello\n", "\r\n") "hello"
endswith("hello", "llo") strings.HasSuffix("hello", "llo") true
format("%05d", 42) template.Execute("{{printf \"%05d\" .n}}", {n: 42}) "00042" text/template uses Go verbs and Go template syntax. For %s and %d alone, use interpolation: "\(a)-\(b)".
formatlist("web-%s", ["a", "b"]) [for x in ["a", "b"] {"web-\(x)"}] ["web-a", "web-b"]
indent(2, "a\nb") strings.Replace("a\nb", "\n", "\n ", -1) Neither indents the first line.
join(",", ["a", "b"]) strings.Join(["a", "b"], ",") "a,b"
lower("AbC") strings.ToLower("AbC") "abc"
regex("[a-z]+", "123abc456") regexp.Find("[a-z]+", "123abc456") "abc" regexp.FindSubmatch returns the groups.
regexall("[0-9]+", "a1b22c333") regexp.FindAll("[0-9]+", "a1b22c333", -1) ["1", "22", "333"]
replace("a-b-c", "-", "_") strings.Replace("a-b-c", "-", "_", -1) "a_b_c" -1 replaces every match.
replace("a1b22", "/[0-9]+/", "#") regexp.ReplaceAll("[0-9]+", "a1b22", "#") "a#b#" The HCL form is replace with a pattern between slashes.
split(",", "a,b,c") strings.Split("a,b,c", ",") ["a", "b", "c"] The string comes first in CUE.
startswith("hello", "he") strings.HasPrefix("hello", "he") true
strcontains("hello", "ell") strings.Contains("hello", "ell") true
strrev("abc") strings.Join(list.Reverse(strings.Split("abc", "")), "") "cba" No builtin; the string is split into characters.
substr("hello", 1, 2) strings.SliceRunes("hello", 1, 3) "el" The end of SliceRunes is exclusive.
templatestring("Hello, $${name}!", {name = "pravo"}) template.Execute("Hello, {{.name}}!", {name: "pravo"}) "Hello, pravo!" templatestring renders a template that a value holds. The placeholders of text/template are {{.name}}.
title("hello world") strings.ToTitle("hello world") "Hello World"
trim("xxhixx", "x") strings.Trim("xxhixx", "x") "hi"
trimprefix("hello", "he") strings.TrimPrefix("hello", "he") "llo"
trimsuffix("hello", "lo") strings.TrimSuffix("hello", "lo") "hel"
trimspace(" hi ") strings.TrimSpace(" hi ") "hi"
upper("abc") strings.ToUpper("abc") "ABC"

Collection

OpenTofu CUE Result Note
alltrue([true, false]) !list.Contains([true, false], false) false
anytrue([false, true]) list.Contains([false, true], true) true
chunklist([1, 2, 3, 4, 5], 2) [for i in list.Range(0, 5, 2) {list.Slice([1, 2, 3, 4, 5], i, [if i+2 < 5 {i+2}, 5][0])}] [[1, 2], [3, 4], [5]] No builtin.
coalesce(null, "", "b", "c") [for x in [null, "", "b", "c"] if x != null && x != "" {x}][0] "b"
coalescelist([], ["x"], ["y"]) [for l in [[], ["x"], ["y"]] if len(l) > 0 {l}][0] ["x"]
compact(["a", "", "b"]) [for x in ["a", "", "b"] if x != "" {x}] ["a", "b"]
concat([1, 2], [3]) list.Concat([[1, 2], [3]]) [1, 2, 3]
contains(["a", "b"], "b") list.Contains(["a", "b"], "b") true
distinct([1, 2, 1]) [for i, x in [1, 2, 1] if !list.Contains(list.Slice([1, 2, 1], 0, i), x) {x}] [1, 2] No builtin. list.UniqueItems checks that a list has no duplicates and does not remove them.
element(["a", "b", "c"], 4) _l[mod(4, len(_l))] "b" Both wrap around. Here _l: ["a", "b", "c"].
flatten([[1], [2, [3]]]) list.FlattenN([[1], [2, [3]]], -1) [1, 2, 3]
index(["a", "b", "c"], "b") [for i, x in _l if x == "b" {i}][0] 1 Fails when the value is not in the list.
keys({a = 1, b = 2}) [for k, _ in _m {k}] ["a", "b"] Keys come in the order written. OpenTofu sorts them. Wrap with list.SortStrings to sort. Here _m: {a: 1, b: 2}.
length(["a", "b", "c"]) len(_l) 3 For a string len counts bytes. Count characters with len(strings.Split(s, "")).
lookup({a = 1}, "z", 0) *_m["z"] | 0 0 The default marker goes on the lookup. With _m: {a: 1} the result is 0, and for a present key the value.
matchkeys([1, 2, 3], ["a", "b", "c"], ["a", "c"]) [for i, k in ["a", "b", "c"] if list.Contains(["a", "c"], k) {[1, 2, 3][i]}] [1, 3]
merge({a = 1, b = 2}, {c = 3}) _m & {c: 3} Unification, not an override: two values for one key are a conflict. See the note below.
one([5]) [if len([5]) == 1 {[5][0]}, null][0] 5
range(0, 5, 2) list.Range(0, 5, 2) [0, 2, 4] range(3) is list.Range(0, 3, 1).
reverse(["a", "b", "c"]) list.Reverse(_l) ["c", "b", "a"]
setintersection([1, 2, 3], [2, 3, 4]) [for x in [1, 2, 3] if list.Contains([2, 3, 4], x) {x}] [2, 3]
setproduct([1, 2], ["x", "y"]) [for a in [1, 2] for b in ["x", "y"] {[a, b]}] [[1, "x"], [1, "y"], [2, "x"], [2, "y"]]
setsubtract([1, 2, 3], [2]) [for x in [1, 2, 3] if !list.Contains([2], x) {x}] [1, 3]
setunion([1, 2], [2, 3]) [for i, x in list.Concat([[1, 2], [2, 3]]) if !list.Contains(list.Slice(list.Concat([[1, 2], [2, 3]]), 0, i), x) {x}] [1, 2, 3] A concatenation without duplicates.
slice(["a", "b", "c"], 1, 3) list.Slice(_l, 1, 3) ["b", "c"]
sort(["b", "a", "c"]) list.SortStrings(["b", "a", "c"]) ["a", "b", "c"] list.Sort(l, list.Ascending) sorts numbers.
sum([1, 2, 3]) list.Sum([1, 2, 3]) 6
transpose({a = ["1", "2"], b = ["2"]}) {for k, v in {a: ["1", "2"], b: ["2"]} for x in v {"\(x)": [for k2, v2 in {a: ["1", "2"], b: ["2"]} if list.Contains(v2, x) {k2}]}} The result is a map from each value to the keys that hold it.
values({a = 1, b = 2}) [for _, v in _m {v}] [1, 2] In the order written.
zipmap(["a", "b"], [10, 20]) {for i, k in ["a", "b"] {"\(k)": [10, 20][i]}}

Encoding

OpenTofu CUE Result Note
base64encode("hello") base64.Encode(null, "hello") "aGVsbG8="
base64decode("aGVsbG8=") "\(base64.Decode(null, "aGVsbG8="))" "hello" Decode returns bytes; interpolation makes a string.
base64gunzip(x) No equivalent Decode the value outside and pass it as a tag.
base64gzip("hello") No equivalent Compute the value outside and pass it as a tag.
csvdecode("a,b\n1,2\n") csv.Decode("a,b\n1,2\n") [["a", "b"], ["1", "2"]] The result is a list of rows. Build maps from the header row with a comprehension.
jsondecode("{\"a\": 1}") json.Unmarshal("{\"a\": 1}") a: 1
jsonencode({a = 1, b = [true]}) json.Marshal({a: 1, b: [true]}) "{\"a\":1,\"b\":[true]}" Keys come in the order written. OpenTofu sorts them.
textdecodebase64("aGk=", "UTF-8") No equivalent Only UTF-8 is read: use base64decode.
textencodebase64("hi", "UTF-8") base64.Encode(null, "hi") "aGk=" Only UTF-8 is written.
urldecode("a+b%26c") net.QueryUnescape("a+b%26c") "a b&c"
urlencode("a b&c") net.QueryEscape("a b&c") "a+b%26c"
yamldecode("a: 1\nb: [x]") yaml.Unmarshal("a: 1\nb: [x]")
yamlencode({a = 1, b = ["x"]}) yaml.Marshal({a: 1, b: ["x"]}) The text can differ in layout from the OpenTofu output.

Filesystem

OpenTofu CUE Result Note
abspath("x") No equivalent A specification is pure. Pass the path as a tag, or use -T for cwd.
basename("/a/b/c.txt") path.Base("/a/b/c.txt") "c.txt"
dirname("/a/b/c.txt") path.Dir("/a/b/c.txt") "/a/b"
pathexpand("~/x") No equivalent Pass the expanded path as a tag.
file("hello.txt") An embedded file @embed(file=hello.txt, type=text). See below.
fileexists("hello.txt") No equivalent A file that is not there leaves its embedded value incomplete, and pravo eval -c fails.
fileset(".", "*.txt") An embedded file @embed(glob=*.txt, type=text) gives a struct of file names to content.
filebase64("hello.txt") An embedded file base64.Encode(null, _file) of an embedded file.
templatefile("x.tpl", {n = 1}) An embedded file template.Execute(_tpl, {n: 1}) with Go template syntax.

Date and time

OpenTofu CUE Result Note
formatdate("YYYY-MM-DD", "2023-11-14T22:13:20Z") time.FormatString("2006-01-02", "2023-11-14T22:13:20Z") "2023-11-14" The layout is the Go reference time, not YYYY-MM-DD.
plantimestamp() No equivalent Pass the time as a tag: now: string @tag(now).
timeadd("2023-11-14T22:13:20Z", "1h") No equivalent Pass the result as a tag: until: string @tag(until).
timecmp("2023-11-14T22:13:20Z", "2023-11-15T00:00:00Z") "2023-11-14T22:13:20Z" < "2023-11-15T00:00:00Z" true For two UTC timestamps in the same layout. The result is a boolean, not -1, 0 or 1.
timestamp() No equivalent Pass the time as a tag: now: string @tag(now).

Hash and crypto

OpenTofu CUE Result Note
base64sha256("hello") base64.Encode(null, sha256.Sum256("hello")) "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
base64sha512("hello") base64.Encode(null, sha512.Sum512("hello"))
bcrypt("secret") No equivalent The bcrypt_hash attribute of random_password holds a hash.
filebase64sha256("hello.txt") An embedded file base64.Encode(null, sha256.Sum256(_file)).
filebase64sha512("hello.txt") An embedded file base64.Encode(null, sha512.Sum512(_file)).
filemd5("hello.txt") An embedded file hex.Encode(md5.Sum(_file)).
filesha1("hello.txt") An embedded file hex.Encode(sha1.Sum(_file)).
filesha256("hello.txt") An embedded file hex.Encode(sha256.Sum256(_file)).
filesha512("hello.txt") An embedded file hex.Encode(sha512.Sum512(_file)).
md5("hello") hex.Encode(md5.Sum("hello")) "5d41402abc4b2a76b9719d911017c592"
rsadecrypt(x, key) No equivalent
sha1("hello") hex.Encode(sha1.Sum("hello")) "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
sha256("hello") hex.Encode(sha256.Sum256("hello"))
sha512("hello") hex.Encode(sha512.Sum512("hello"))
uuid() No equivalent Evaluation is pure. The random_uuid resource of hashicorp/random makes a UUID once and records it.
uuidv5("dns", "example.com") uuid.SHA1("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "example.com") "cfbff0d1-9375-5685-968c-48ce8b15ae17" The first argument is the namespace UUID. This one is the DNS namespace.

IP network

OpenTofu CUE Result Note
cidrcontains("10.0.0.0/16", "10.0.1.5") net.InCIDR("10.0.1.5", "10.0.0.0/16") true The arguments change places. InCIDR takes an address and not a prefix.
cidrhost("10.0.0.0/24", 5) net.AddIP(strings.Split("10.0.0.0/24", "/")[0], 5) "10.0.0.5" Valid when the prefix is the network address.
cidrnetmask("10.0.0.0/24") net.ParseCIDR("10.0.0.0/24").prefix_mask "255.255.255.0" ParseCIDR also gives prefix_len, prefix_addr and broadcast_addr.
cidrsubnet("10.0.0.0/16", 8, 2) net.AddIP(strings.Split("10.0.0.0/16", "/")[0], 2 * (math.Pow(2, 32-(strconv.Atoi(strings.Split("10.0.0.0/16", "/")[1])+8)) & int)) + "/\(strconv.Atoi(strings.Split("10.0.0.0/16", "/")[1])+8)" "10.0.2.0/24" IPv4. Write it once as a definition when you use it often.
cidrsubnets("10.0.0.0/16", 4, 4) No equivalent Write a comprehension over the cidrsubnet expression.

Assertions

An assume function returns its value when the value meets a condition and fails otherwise. A constraint joined to the value does the same.

OpenTofu CUE Result Note
assumeequal(1, 1) 1 & 1 1 Two values that differ are a conflict.
assumenotnull("x") "x" & !=null "x"
assumestringprefix("pre-x", "pre-") "pre-x" & =~"^pre-" "pre-x"
assumelistlength([1, 2], 1, 3) [1, 2] & list.MinItems(1) & list.MaxItems(3) [1, 2]
assumelistlengthmax([1, 2], 3) [1, 2] & list.MaxItems(3) [1, 2]
assumelistlengthmin([1, 2], 1) [1, 2] & list.MinItems(1) [1, 2]
assumemaplength({a = 1}, 1, 3) {a: 1} & struct.MinFields(1) & struct.MaxFields(3) a: 1
assumemaplengthmax({a = 1}, 3) {a: 1} & struct.MaxFields(3) a: 1
assumemaplengthmin({a = 1}, 1) {a: 1} & struct.MinFields(1) a: 1
assumesetlength(toset([1, 2]), 1, 3) [1, 2] & list.MinItems(1) & list.MaxItems(3) [1, 2] CUE has lists and no sets.
assumesetlengthmax(toset([1, 2]), 3) [1, 2] & list.MaxItems(3) [1, 2]
assumesetlengthmin(toset([1, 2]), 1) [1, 2] & list.MinItems(1) [1, 2]

Type conversion

OpenTofu CUE Result Note
can(local.x) (_m["z"] != _|_) false Tests whether a reference or an expression is defined.
convert("5", number) strconv.Atoi("5") 5 strconv.ParseFloat parses a float, strconv.ParseBool a boolean, and "\(x)" makes a string.
ephemeralasnull(x) No equivalent pravo never records an ephemeral value. See Secrets and ephemeral values.
issensitive(x) No equivalent Sensitivity belongs to the provider schema and to sensitive: true on an output.
nonsensitive(x) No equivalent Sensitivity belongs to the provider schema and to sensitive: true on an output.
sensitive(x) No equivalent Mark an output sensitive: true.
tobool("true") strconv.ParseBool("true") true
tolist(x) No equivalent Not needed. CUE has one list type.
tomap(x) No equivalent Not needed. CUE has one struct type.
tonumber("3.5") strconv.ParseFloat("3.5", 64) 3.5 strconv.Atoi parses an integer.
toset([1, 2, 1]) [for i, x in [1, 2, 1] if !list.Contains(list.Slice([1, 2, 1], 0, i), x) {x}] [1, 2] Only needed to remove duplicates. A loop over a list needs no set.
tostring(5) "\(5)" "5"
try(local.m.z, "d") *_m["z"] | "d" "d" The default marker goes on the first expression.
type(x) No equivalent Test a value against a type: (x & string) != _|_. OpenTofu has type in tofu console only.

Differences that change a result

Five differences matter when a result feeds a resource.

  • Order of keys. keys, values and jsonencode sort the keys in OpenTofu. A CUE struct keeps the order you wrote. Sort with list.SortStrings when the order must not depend on the source.
  • Conflicts. merge lets the last map win. & unifies, and two different values for one key are an error. Mark the value that may be replaced with *: {encrypted: *true | bool}. Pitfalls shows the error and the fix.
  • Defaults. lookup(m, k, d) and try(a, b) are *m[k] | d and *a | b. The marker goes on the expression that may fail. The form m[k] | *d returns d even when the key exists.
  • Length. len counts bytes. length("héllo") is 5 in OpenTofu. len("héllo") is 6.
  • No clock, no randomness. There is no timestamp() and no uuid(). A tag carries a time. A resource of hashicorp/random carries a random value, recorded once.

Provider-defined functions

provider::kubernetes::manifest_decode(yaml) is a call in OpenTofu. In pravo a call is a declaration, built through the provider like a data source. The arguments go in args, and the answer is result. The function is pure, its result is never recorded, and it needs no cluster.

main.tf
locals {
  manifest = provider::kubernetes::manifest_decode(file("cm.yaml"))
}

output "kind" {
  value = local.manifest.kind
}
calls/main.cue
package calls

import (
	"pravo.dev/pravo"
	k8s "pravo.dev/providers/hashicorp/kubernetes"
)

$k8s: k8s.#Provider

manifest: $k8s.#Function.manifest_decode & {
	args: ["apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: demo\n"]
}

kind: pravo.#Output & {value: manifest.result.kind}
terminal
$pravo -C calls init > /dev/null
$pravo -C calls plan 2>&1 | tail -n 4
Graph: planned 0 of 1, 1 functions called.

Outputs:
  kind: "ConfigMap"

In a root that declares the provider, pravo schema kubernetes/manifest_decode prints the parameters and the documentation of the function.

Files

A function that reads a file becomes an embedded file. The directory must be a CUE module, because @embed reads files relative to the module. The first line of the file is @extern(embed). The file name stays a plain string: @embed(file=hello.txt, type=text), and @embed(glob=*.txt, type=text) embeds a set.

files/hello.txt
hello
files/greeting.tpl
Hello, {{.name}}! n={{.n}}
files/cue.mod/module.cue
module: "example.com/files@v0"
language: version: "v0.14.0"
files/main.cue
@extern(embed)
package files

import (
	"encoding/base64"
	"encoding/hex"
	"crypto/md5"
	"crypto/sha256"
	"text/template"
)

_file: _ @embed(file=hello.txt, type=text)
_tpl:  _ @embed(file=greeting.tpl, type=text)
_all:  _ @embed(glob=*.txt, type=text)

file:         _file
fileset:      [for name, _ in _all {name}]
filebase64:   base64.Encode(null, _file)
filemd5:      hex.Encode(md5.Sum(_file))
filesha256:   hex.Encode(sha256.Sum256(_file))
templatefile: template.Execute(_tpl, {name: "pravo", n: 3})
terminal
$pravo -C files eval -c
file: """
	hello

	"""
fileset: ["hello.txt"]
filebase64: "aGVsbG8K"
filemd5:    "b1946ac92492d2347c6235b4d2611184"
filesha256: "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
templatefile: """
	Hello, pravo! n=3

	"""

The digests equal the MD5 and SHA-256 digests of the file. The template uses Go template syntax. A template written for templatefile with ${name} needs {{.name}}, and a %{ for } directive needs {{range}}.

Evaluate every row

The file below holds each CUE expression of the tables as a field, with the helper values _n, _l and _m. pravo eval -c fails when a field is not concrete. A row that stops evaluating fails the page.

functions.cue
package fn

import (
	"math"
	"strings"
	"strconv"
	"list"
	"struct"
	"regexp"
	"net"
	"path"
	"time"
	"uuid"
	"text/template"
	"encoding/base64"
	"encoding/csv"
	"encoding/hex"
	"encoding/json"
	"encoding/yaml"
	"crypto/md5"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
)

_n: -5
_l: ["a", "b", "c"]
_m: {a: 1, b: 2}

fn_abs: math.Abs(-5)
fn_ceil: math.Ceil(1.2)
fn_floor: math.Floor(1.8)
fn_log: math.Log(8) / math.Log(2)
fn_max: list.Max([1, 3, 2])
fn_min: list.Min([1, 3, 2])
fn_parseint: strconv.ParseInt("ff", 16, 64)
fn_pow: math.Pow(2, 3)
fn_signum: [if _n > 0 {1}, if _n < 0 {-1}, 0][0]
fn_chomp: strings.TrimRight("hello\n", "\r\n")
fn_endswith: strings.HasSuffix("hello", "llo")
fn_format: template.Execute("{{printf \"%05d\" .n}}", {n: 42})
fn_formatlist: [for x in ["a", "b"] {"web-\(x)"}]
fn_indent: strings.Replace("a\nb", "\n", "\n  ", -1)
fn_join: strings.Join(["a", "b"], ",")
fn_lower: strings.ToLower("AbC")
fn_regex: regexp.Find("[a-z]+", "123abc456")
fn_regexall: regexp.FindAll("[0-9]+", "a1b22c333", -1)
fn_replace: strings.Replace("a-b-c", "-", "_", -1)
fn_replace_regex: regexp.ReplaceAll("[0-9]+", "a1b22", "#")
fn_split: strings.Split("a,b,c", ",")
fn_startswith: strings.HasPrefix("hello", "he")
fn_strcontains: strings.Contains("hello", "ell")
fn_strrev: strings.Join(list.Reverse(strings.Split("abc", "")), "")
fn_substr: strings.SliceRunes("hello", 1, 3)
fn_templatestring: template.Execute("Hello, {{.name}}!", {name: "pravo"})
fn_title: strings.ToTitle("hello world")
fn_trim: strings.Trim("xxhixx", "x")
fn_trimprefix: strings.TrimPrefix("hello", "he")
fn_trimsuffix: strings.TrimSuffix("hello", "lo")
fn_trimspace: strings.TrimSpace("  hi ")
fn_upper: strings.ToUpper("abc")
fn_alltrue: !list.Contains([true, false], false)
fn_anytrue: list.Contains([false, true], true)
fn_chunklist: [for i in list.Range(0, 5, 2) {list.Slice([1, 2, 3, 4, 5], i, [if i+2 < 5 {i+2}, 5][0])}]
fn_coalesce: [for x in [null, "", "b", "c"] if x != null && x != "" {x}][0]
fn_coalescelist: [for l in [[], ["x"], ["y"]] if len(l) > 0 {l}][0]
fn_compact: [for x in ["a", "", "b"] if x != "" {x}]
fn_concat: list.Concat([[1, 2], [3]])
fn_contains: list.Contains(["a", "b"], "b")
fn_distinct: [for i, x in [1, 2, 1] if !list.Contains(list.Slice([1, 2, 1], 0, i), x) {x}]
fn_element: _l[mod(4, len(_l))]
fn_flatten: list.FlattenN([[1], [2, [3]]], -1)
fn_index: [for i, x in _l if x == "b" {i}][0]
fn_keys: [for k, _ in _m {k}]
fn_length: len(_l)
fn_lookup: *_m["z"] | 0
fn_matchkeys: [for i, k in ["a", "b", "c"] if list.Contains(["a", "c"], k) {[1, 2, 3][i]}]
fn_merge: _m & {c: 3}
fn_one: [if len([5]) == 1 {[5][0]}, null][0]
fn_range: list.Range(0, 5, 2)
fn_reverse: list.Reverse(_l)
fn_setintersection: [for x in [1, 2, 3] if list.Contains([2, 3, 4], x) {x}]
fn_setproduct: [for a in [1, 2] for b in ["x", "y"] {[a, b]}]
fn_setsubtract: [for x in [1, 2, 3] if !list.Contains([2], x) {x}]
fn_setunion: [for i, x in list.Concat([[1, 2], [2, 3]]) if !list.Contains(list.Slice(list.Concat([[1, 2], [2, 3]]), 0, i), x) {x}]
fn_slice: list.Slice(_l, 1, 3)
fn_sort: list.SortStrings(["b", "a", "c"])
fn_sum: list.Sum([1, 2, 3])
fn_transpose: {for k, v in {a: ["1", "2"], b: ["2"]} for x in v {"\(x)": [for k2, v2 in {a: ["1", "2"], b: ["2"]} if list.Contains(v2, x) {k2}]}}
fn_values: [for _, v in _m {v}]
fn_zipmap: {for i, k in ["a", "b"] {"\(k)": [10, 20][i]}}
fn_base64encode: base64.Encode(null, "hello")
fn_base64decode: "\(base64.Decode(null, "aGVsbG8="))"
fn_csvdecode: csv.Decode("a,b\n1,2\n")
fn_jsondecode: json.Unmarshal("{\"a\": 1}")
fn_jsonencode: json.Marshal({a: 1, b: [true]})
fn_textencodebase64: base64.Encode(null, "hi")
fn_urldecode: net.QueryUnescape("a+b%26c")
fn_urlencode: net.QueryEscape("a b&c")
fn_yamldecode: yaml.Unmarshal("a: 1\nb: [x]")
fn_yamlencode: yaml.Marshal({a: 1, b: ["x"]})
fn_basename: path.Base("/a/b/c.txt")
fn_dirname: path.Dir("/a/b/c.txt")
fn_formatdate: time.FormatString("2006-01-02", "2023-11-14T22:13:20Z")
fn_timecmp: "2023-11-14T22:13:20Z" < "2023-11-15T00:00:00Z"
fn_base64sha256: base64.Encode(null, sha256.Sum256("hello"))
fn_base64sha512: base64.Encode(null, sha512.Sum512("hello"))
fn_md5: hex.Encode(md5.Sum("hello"))
fn_sha1: hex.Encode(sha1.Sum("hello"))
fn_sha256: hex.Encode(sha256.Sum256("hello"))
fn_sha512: hex.Encode(sha512.Sum512("hello"))
fn_uuidv5: uuid.SHA1("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "example.com")
fn_cidrcontains: net.InCIDR("10.0.1.5", "10.0.0.0/16")
fn_cidrhost: net.AddIP(strings.Split("10.0.0.0/24", "/")[0], 5)
fn_cidrnetmask: net.ParseCIDR("10.0.0.0/24").prefix_mask
fn_cidrsubnet: net.AddIP(strings.Split("10.0.0.0/16", "/")[0], 2 * (math.Pow(2, 32-(strconv.Atoi(strings.Split("10.0.0.0/16", "/")[1])+8)) & int)) + "/\(strconv.Atoi(strings.Split("10.0.0.0/16", "/")[1])+8)"
fn_assumeequal: 1 & 1
fn_assumenotnull: "x" & !=null
fn_assumestringprefix: "pre-x" & =~"^pre-"
fn_assumelistlength: [1, 2] & list.MinItems(1) & list.MaxItems(3)
fn_assumelistlengthmax: [1, 2] & list.MaxItems(3)
fn_assumelistlengthmin: [1, 2] & list.MinItems(1)
fn_assumemaplength: {a: 1} & struct.MinFields(1) & struct.MaxFields(3)
fn_assumemaplengthmax: {a: 1} & struct.MaxFields(3)
fn_assumemaplengthmin: {a: 1} & struct.MinFields(1)
fn_assumesetlength: [1, 2] & list.MinItems(1) & list.MaxItems(3)
fn_assumesetlengthmax: [1, 2] & list.MaxItems(3)
fn_assumesetlengthmin: [1, 2] & list.MinItems(1)
fn_can: (_m["z"] != _|_)
fn_convert: strconv.Atoi("5")
fn_tobool: strconv.ParseBool("true")
fn_tonumber: strconv.ParseFloat("3.5", 64)
fn_toset: [for i, x in [1, 2, 1] if !list.Contains(list.Slice([1, 2, 1], 0, i), x) {x}]
fn_tostring: "\(5)"
fn_try: *_m["z"] | "d"
terminal
$pravo eval -c
fn_abs:      5
fn_ceil:     2
fn_floor:    1
fn_log:      3.0
fn_max:      3
fn_min:      1
fn_parseint: 255
fn_pow:      8
fn_signum:   -1
fn_chomp:    "hello"
fn_endswith: true
fn_format:   "00042"
fn_formatlist: ["web-a", "web-b"]
fn_indent: """
	a
	  b
	"""
fn_join:  "a,b"
fn_lower: "abc"
fn_regex: "abc"
fn_regexall: ["1", "22", "333"]
fn_replace:       "a_b_c"
fn_replace_regex: "a#b#"
fn_split: ["a", "b", "c"]
fn_startswith:     true
fn_strcontains:    true
fn_strrev:         "cba"
fn_substr:         "el"
fn_templatestring: "Hello, pravo!"
fn_title:          "Hello World"
fn_trim:           "hi"
fn_trimprefix:     "llo"
fn_trimsuffix:     "hel"
fn_trimspace:      "hi"
fn_upper:          "ABC"
fn_alltrue:        false
fn_anytrue:        true
fn_chunklist: [[1, 2], [3, 4], [5]]
fn_coalesce: "b"
fn_coalescelist: ["x"]
fn_compact: ["a", "b"]
fn_concat: [1, 2, 3]
fn_contains: true
fn_distinct: [1, 2]
fn_element: "b"
fn_flatten: [1, 2, 3]
fn_index: 1
fn_keys: ["a", "b"]
fn_length: 3
fn_lookup: 0
fn_matchkeys: [1, 3]
fn_merge: {
	a: 1
	b: 2
	c: 3
}
fn_one: 5
fn_range: [0, 2, 4]
fn_reverse: ["c", "b", "a"]
fn_setintersection: [2, 3]
fn_setproduct: [[1, "x"], [1, "y"], [2, "x"], [2, "y"]]
fn_setsubtract: [1, 3]
fn_setunion: [1, 2, 3]
fn_slice: ["b", "c"]
fn_sort: ["a", "b", "c"]
fn_sum: 6
fn_transpose: {
	"1": ["a"]
	"2": ["a", "b"]
}
fn_values: [1, 2]
fn_zipmap: {
	a: 10
	b: 20
}
fn_base64encode: "aGVsbG8="
fn_base64decode: "hello"
fn_csvdecode: [["a", "b"], ["1", "2"]]
fn_jsondecode: {
	a: 1
}
fn_jsonencode:       "{\"a\":1,\"b\":[true]}"
fn_textencodebase64: "aGk="
fn_urldecode:        "a b&c"
fn_urlencode:        "a+b%26c"
fn_yamldecode: {
	a: 1
	b: ["x"]
}
fn_yamlencode: """
	a: 1
	b:
	  - x

	"""
fn_basename:           "c.txt"
fn_dirname:            "/a/b"
fn_formatdate:         "2023-11-14"
fn_timecmp:            true
fn_base64sha256:       "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
fn_base64sha512:       "m3HSJL1i83hdltRq0+o9czGb+8KJDKra4t/3JRlnPKcjI8PZm6XBHXx6zG4UuMXaDEZjR1wuXDre9G9zvN7AQw=="
fn_md5:                "5d41402abc4b2a76b9719d911017c592"
fn_sha1:               "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
fn_sha256:             "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
fn_sha512:             "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
fn_uuidv5:             "cfbff0d1-9375-5685-968c-48ce8b15ae17"
fn_cidrcontains:       true
fn_cidrhost:           "10.0.0.5"
fn_cidrnetmask:        "255.255.255.0"
fn_cidrsubnet:         "10.0.2.0/24"
fn_assumeequal:        1
fn_assumenotnull:      "x"
fn_assumestringprefix: "pre-x"
fn_assumelistlength: [1, 2]
fn_assumelistlengthmax: [1, 2]
fn_assumelistlengthmin: [1, 2]
fn_assumemaplength: {
	a: 1
}
fn_assumemaplengthmax: {
	a: 1
}
fn_assumemaplengthmin: {
	a: 1
}
fn_assumesetlength: [1, 2]
fn_assumesetlengthmax: [1, 2]
fn_assumesetlengthmin: [1, 2]
fn_can:      false
fn_convert:  5
fn_tobool:   true
fn_tonumber: 3.5
fn_toset: [1, 2]
fn_tostring: "5"
fn_try:      "d"

Recap

  • Import the package, call the function, and check the result with pravo eval -e.
  • A function with no builtin is a comprehension, a resource of a provider, or a tag that carries the value.
  • Order, conflicts and defaults differ from OpenTofu: read the five differences before you replace merge, lookup and try.

Check yourself

How do you write lookup(var.m, "k", "d")?

*m.k | "d", with the default marker on the lookup. The form m.k | *"d" returns the default even when k exists.

What replaces timestamp()?

A tag such as now: string @tag(now) carries a time that the caller supplies.

Next: Command map maps the OpenTofu commands.