| author | Benjamin Linskey | 2026-06-11 03:23:27 -0400 |
|---|---|---|
| committer | Benjamin Linskey | 2026-06-11 07:02:09 -0400 |
| commit | b10392c52dc06489b65e638b8ca71771b763bf03 (patch) | |
| tree | 082453b9c713caf654ee3b2ad929906faec800d5 | |
| parent | 653365c2366785a7fb3b012fe39dbd3ca9707606 (diff) | |
| download | tpl-b10392c52dc06489b65e638b8ca71771b763bf03.tar.gz | |
Fix brace escaping in regexes
The POSIX extended regular expression spec requires that the left brace character be escaped when it appears outside of an interval expression. The results are undefined if the brace is not escaped.1
The ERE spec permits right braces to be escaped, though this isn’t required.2 But the standard ERE escaping rules are superseded in the POSIX awk specification. The awk spec does not specify a right brace escape sequence, so the meaning of an escaped right brace is undefined.3
We have a number of escaped right braces and one instance of a string variable containing unescaped left braces being passed to a function as a regex. These don’t cause any problems in practice, but to ensure maximum portability and pedantry, we bring everything into strict compliance with the spec here.
-
POSIX.1-2024 XBD 9.4.3: <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap09.html#tag_09_04_03> ↩
-
POSIX.1-2024 XBD 9.4.2: <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap09.html#tag_09_04_02> ↩
-
POSIX.1-2024 awk: <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/awk.html#tag_20_06_13_04> ↩
| -rwxr-xr-x | tpl | 15 |
@@ -27,18 +27,15 @@ BEGIN { FS = "[[:space:]]*=[[:space:]]*" } FILENAME == ARGV[1] && /^#/ { next } FILENAME == ARGV[1] && NF == 2 { # Replace template variables with previously defined values. - while (match($2, /\{\{[[:space:]]*[^\{\}[:space:]]+[[:space:]]\}\}/)) { + while (match($2, /\{\{[[:space:]]*[^\{}[:space:]]+[[:space:]]}}/)) { matched_text = substr($2, RSTART, RLENGTH) # Extract the actual var name, without braces and space. - match(matched_text, /[^\{\}[:space:]]+/) + match(matched_text, /[^\{}[:space:]]+/) var_name = substr(matched_text, RSTART, RLENGTH) - # Even though matched_text contains the { and } regex - # metacharacters, we don't need to escape them because they're - # not used in a way that matches the (r){m,n} syntax. - # (We don't need to escape them elsewhere in this program - # either, but we do so for maximum clarity.) + # Left braces must be escaped for strict POSIX compliance. + gsub(/\{/, "\\{", matched_text) sub(matched_text, vals[var_name], $2) } @@ -46,9 +43,9 @@ FILENAME == ARGV[1] && NF == 2 { } FILENAME == ARGV[1] { next } -/\{\{[[:space:]]*[^\{\}[:space:]]+[[:space:]]*\}\}/ { +/\{\{[[:space:]]*[^\{}[:space:]]+[[:space:]]*}}/ { for (key in vals) { - gsub("\{\{[[:space:]]*" key "[[:space:]]*\}\}", vals[key]) + gsub("\{\{[[:space:]]*" key "[[:space:]]*}}", vals[key]) } } |