Terraform 0.12.7 adds regular expression extraction
The new regex and regexall functions return structured captures, with different behavior for missing matches and different result types for each capture style.
A configuration sometimes receives structured information inside a string. Extracting that information is different from replacing part of the string or passing it unchanged to a provider. Terraform 0.12.7, released on August 22, adds regex and regexall to return the substrings matched by a regular expression.
The interesting design choice is the shape of the result. A pattern can produce a string, positional captures, or named captures. The richer value model in 0.12 lets the functions express those alternatives directly. Configuration can then use the result as data instead of inventing another delimiter convention.
I like Terraform modules to have explicit interfaces. Regular expression extraction can help when the outside interface really is a string, but the resulting module should still make clear which pieces it consumes. The new functions give that extraction a defined type and failure behavior.
Capture groups determine the result
The regex documentation at the release tag specifies three result forms. With no capture groups, regex returns the matching substring as a single string. With unnamed captures it returns a list of captured substrings. Named captures produce a map whose keys are the capture names.
The whole match and the captures are therefore different interfaces. Adding parentheses for grouping can change the result from a string to a list, even when the pattern matches exactly the same characters. A noncapturing group allows grouping without introducing that result change.
For example, this expression extracts two fields from an illustrative deployment label. The anchors require the pattern to cover the complete input.
locals {
deployment = regex("^([a-z]+)-([0-9]+)$", "worker-12")
component = local.deployment[0]
sequence = local.deployment[1]
}
The captures are worker and 12. Both are strings, including the numeric-looking capture. The regular expression has identified text; it has not established a separate numeric type for that field. A consumer that needs a number must account for that distinction.
Named groups can express the same extraction with field names. This syntax follows the named-capture form supported by the release.
locals {
deployment = regex("^(?P<component>[a-z]+)-(?P<sequence>[0-9]+)$", "worker-12")
component = local.deployment.component
}
The function does not allow a pattern to mix named and unnamed captures. That restriction keeps one invocation from needing two different capture-addressing conventions. The pattern itself determines the result structure, so changes to capture style deserve the same attention as changes to other module data shapes.
No match can be a normal result
regex raises an error when its pattern does not match. This is suitable when the configuration requires a match before it can proceed. It is less convenient when a failed match is an expected condition, such as deciding whether to include an entry in a derived collection.
The regexall documentation supplies the alternative. It returns a list of matches, which is empty when there are none. Testing its length gives a boolean result without requiring a successful extraction.
locals {
valid_label = length(regexall("^[a-z]+-[0-9]+$", "worker-12")) > 0
}
This computes a boolean in ordinary configuration. It does not introduce a custom input validation block or automatically reject a module argument. Any further effect depends on how the configuration uses the computed value. The release adds matching functions, so claims about new validation syntax would go beyond this change.
Anchors also affect the question being asked. Without them, finding one valid-looking substring does not establish that the whole input follows the label format. With the anchors shown here, a nonempty result requires the complete string to satisfy the pattern. That is a property of this particular example, not of every use of regexall.
Repeated matches add a collection level
The result from regexall is a list of the result that regex would return for an individual match. A pattern with no captures therefore returns a list of strings. Unnamed captures produce a list of lists, and named captures produce a list of maps.
That extra collection level is easy to overlook. Selecting the first match and selecting its first captured field are separate operations. A caller that needs all matches should preserve the outer collection and transform each entry deliberately, instead of assuming the function returns one flat list of every captured substring.
The choice between the two functions is therefore about both cardinality and absence. regex represents an expected single extraction with an error on failure. regexall represents a collection that can contain no matches. Their return shapes follow the capture structure inside each match.
Pattern syntax has its own limits
Terraform documents the RE2 regular expression language for these functions and explicitly excludes backreferences. A pattern copied from another regular expression engine may rely on operations this engine does not support. Compatibility needs to be assessed at the pattern level, not inferred from the shared function name.
Quoted Terraform strings also interpret backslashes before the regular expression engine sees them. The documentation requires doubling backslashes intended for the pattern. Using [0-9] in the examples above avoids that extra escaping for digits, but more complicated patterns still need to account for both parsers.
I prefer a specialized parser when the language already supplies one for the format involved. The function documentation makes the same recommendation because regular expressions can obscure intent. The useful addition in 0.12.7 is a structured extraction mechanism for cases that need it, with explicit capture types and an empty-collection option when missing matches are expected.