Skip to content

Commit

Permalink
Rewrite default template to handle nested ifs, != and env vars in if
Browse files Browse the repository at this point in the history
The awk script now performs all processing in the BEGIN block using an
implementation that is capable of handling if statements which contain nested
if statments (fixes #436). To make nested ifs look better, if, else and endif
lines can now have optional whitespace before {%.

Includes are now handled in the same way as the main file which means that
included files can both include other files and have if statements in addition
to variables (fixes #406). Include lines can now also have optional whitespace
before {%.

All variables are handled in the same way now so it's now possible to use env
variables in if statements (fixes #488).

Also add support for != in addition to == (fixes #358). Thus it's now
e.g. possible to check if a variable is set (#477) by doing:

{% if yadm.class != ""%}
Class is set to {{ yadm.class }}
{% endif %}

Possible breaking change: An error will be issued if a non-existent yadm or env
variable is referenced in an if statement or in a variable substitution.
  • Loading branch information
erijo committed Oct 27, 2024
1 parent 76ce3de commit d2cd138
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 77 deletions.
73 changes: 70 additions & 3 deletions test/test_unit_template_default.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Unit tests: template_default"""

import os

FILE_MODE = 0o754
Expand All @@ -12,6 +13,7 @@
LOCAL_USER = "default_Test+@-!^User"
LOCAL_DISTRO = "default_Test+@-!^Distro"
LOCAL_DISTRO_FAMILY = "default_Test+@-!^Family"
ENV_VAR = "default_Test+@-!^Env"
TEMPLATE = f"""
start of template
default class = >{{{{yadm.class}}}}<
Expand All @@ -30,6 +32,9 @@
{{% if yadm.class == "wrongclass1" %}}
wrong class 1
{{% endif %}}
{{% if yadm.class != "wronglcass" %}}
Included section from !=
{{% endif\t\t %}}
{{% if yadm.class == "{LOCAL_CLASS}" %}}
Included section for class = {{{{yadm.class}}}} ({{{{yadm.class}}}} repeated)
Multiple lines
Expand Down Expand Up @@ -97,6 +102,12 @@
{{% if yadm.distro_family == "wrongfamily2" %}}
wrong family 2
{{% endif %}}
{{% if env.VAR == "{ENV_VAR}" %}}
Included section for env.VAR = {{{{env.VAR}}}} ({{{{env.VAR}}}} again)
{{% endif %}}
{{% if env.VAR == "wrongenvvar" %}}
wrong env.VAR
{{% endif %}}
end of template
"""
EXPECTED = f"""
Expand All @@ -111,6 +122,7 @@
classes = >{LOCAL_CLASS2}
{LOCAL_CLASS}<
Included section from else
Included section from !=
Included section for class = {LOCAL_CLASS} ({LOCAL_CLASS} repeated)
Multiple lines
Included section for second class
Expand All @@ -121,6 +133,7 @@
Included section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again)
Included section for distro_family = \
{LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again)
Included section for env.VAR = {ENV_VAR} ({ENV_VAR} again)
end of template
"""

Expand All @@ -138,7 +151,7 @@
An empty file removes the line above
{%include basic%}
{% include "./variables.{{ yadm.os }}" %}
{% include dir/nested %}
{% include dir/nested %}
Include basic again:
{% include basic %}
"""
Expand All @@ -154,6 +167,42 @@
basic
"""

TEMPLATE_NESTED_IFS = """\
{% if yadm.user == "me" %}
print1
{% if yadm.user == "me" %}
print2
{% else %}
no print1
{% endif %}
{% else %}
{% if yadm.user == "me" %}
no print2
{% else %}
no print3
{% endif %}
{% endif %}
{% if yadm.user != "me" %}
no print4
{% if yadm.user == "me" %}
no print5
{% else %}
no print6
{% endif %}
{% else %}
{% if yadm.user == "me" %}
print3
{% else %}
no print7
{% endif %}
{% endif %}
"""
EXPECTED_NESTED_IFS = """\
print1
print2
print3
"""


def test_template_default(runner, yadm, tmpdir):
"""Test template_default"""
Expand Down Expand Up @@ -182,7 +231,7 @@ def test_template_default(runner, yadm, tmpdir):
local_distro_family="{LOCAL_DISTRO_FAMILY}"
template_default "{input_file}" "{output_file}"
"""
run = runner(command=["bash"], inp=script)
run = runner(command=["bash"], inp=script, env={"VAR": ENV_VAR})
assert run.success
assert run.err == ""
assert output_file.read() == EXPECTED
Expand Down Expand Up @@ -243,12 +292,30 @@ def test_include(runner, yadm, tmpdir):
assert os.stat(output_file).st_mode == os.stat(input_file).st_mode


def test_nested_ifs(runner, yadm, tmpdir):
"""Test nested if statements"""

input_file = tmpdir.join("input")
input_file.write(TEMPLATE_NESTED_IFS, ensure=True)
output_file = tmpdir.join("output")

script = f"""
YADM_TEST=1 source {yadm}
set_awk
local_user="me"
template_default "{input_file}" "{output_file}"
"""
run = runner(command=["bash"], inp=script)
assert run.success
assert run.err == ""
assert output_file.read() == EXPECTED_NESTED_IFS


def test_env(runner, yadm, tmpdir):
"""Test env"""

input_file = tmpdir.join("input")
input_file.write("{{env.PWD}}", ensure=True)
input_file.chmod(FILE_MODE)
output_file = tmpdir.join("output")

script = f"""
Expand Down
174 changes: 100 additions & 74 deletions yadm
Original file line number Diff line number Diff line change
Expand Up @@ -368,87 +368,113 @@ function template_default() {
# the explicit "space + tab" character class used below is used because not
# all versions of awk seem to support the POSIX character classes [[:blank:]]
read -r -d '' awk_pgm << "EOF"
# built-in default template processor
BEGIN {
blank = "[ ]"
c["class"] = class
c["classes"] = classes
c["arch"] = arch
c["os"] = os
c["hostname"] = host
c["user"] = user
c["distro"] = distro
c["distro_family"] = distro_family
c["source"] = source
ifs = "^{%" blank "*if"
els = "^{%" blank "*else" blank "*%}$"
end = "^{%" blank "*endif" blank "*%}$"
skp = "^{%" blank "*(if|else|endif)"
vld = conditions()
inc_start = "^{%" blank "*include" blank "+\"?"
inc_end = "\"?" blank "*%}$"
inc = inc_start ".+" inc_end
prt = 1
err = 0
}
END { exit err }
{ replace_vars() } # variable replacements
$0 ~ vld, $0 ~ end {
if ($0 ~ vld || $0 ~ end) prt=1;
if ($0 ~ els) prt=0;
if ($0 ~ skp) next;
}
($0 ~ ifs && $0 !~ vld), $0 ~ end {
if ($0 ~ ifs && $0 !~ vld) prt=0;
if ($0 ~ els || $0 ~ end) prt=1;
if ($0 ~ skp) next;
}
{ if (!prt) next }
$0 ~ inc {
file = $0
sub(inc_start, "", file)
sub(inc_end, "", file)
sub(/^[^\/].*$/, source_dir "/&", file)
while ((res = getline <file) > 0) {
replace_vars()
print
}
if (res < 0) {
printf "%s:%d: error: could not read '%s'\n", FILENAME, NR, file | "cat 1>&2"
err = 1
}
close(file)
next
}
{ print }
function replace_vars() {
for (label in c) {
gsub(("{{" blank "*yadm\\." label blank "*}}"), c[label])
yadm["class"] = class
yadm["classes"] = classes
yadm["arch"] = arch
yadm["os"] = os
yadm["hostname"] = host
yadm["user"] = user
yadm["distro"] = distro
yadm["distro_family"] = distro_family
yadm["source"] = source
VARIABLE = "(env|yadm)\\.[a-zA-Z0-9_]+"
current = 0
filename[current] = ARGV[1]
line[current] = 0
level = 0
skip[level] = 0
for (; current >= 0; --current) {
while ((res = getline <filename[current]) > 0) {
++line[current]
if ($0 ~ "^[ \t]*\\{%[ \t]*if[ \t]+" VARIABLE "[ \t]*[!=]=[ \t]*\".*\"[ \t]*%\\}$") {
if (skip[level]) { skip[++level] = 1; continue }
match($0, VARIABLE)
lhs = substr($0, RSTART, RLENGTH)
match($0, /[!=]=/)
op = substr($0, RSTART, RLENGTH)
match($0, /".*"/)
rhs = replace_vars(substr($0, RSTART + 1, RLENGTH - 2))
if (lhs == "yadm.class") {
lhs = "not" rhs
split(classes, cls_array, "\n")
for (idx in cls_array) {
if (rhs == cls_array[idx]) { lhs = rhs; break }
}
}
else {
lhs = replace_vars("{{" lhs "}}")
}
if (op == "==") { skip[++level] = lhs != rhs }
else { skip[++level] = lhs == rhs }
}
else if (/^[ \t]*\{%[ \t]*else[ \t]*%\}$/) {
if (level == 0 || skip[level] < 0) { error("else without matching if") }
skip[level] = skip[level] ? skip[level - 1] : -1
}
else if (/^[ \t]*\{%[ \t]*endif[ \t]*%\}$/) {
if (--level < 0) { error("endif without matching if") }
}
else if (!skip[level]) {
$0 = replace_vars($0)
if (match($0, /^[ \t]*\{%[ \t]*include[ \t]+("[^"]+"|[^"]+)[ \t]*%\}$/)) {
include = $0
sub(/^[ \t]*\{%[ \t]*include[ \t]+"?/, "", include)
sub(/"?[ \t]*%\}$/, "", include)
if (index(include, "/") != 1) {
include = source_dir "/" include
}
filename[++current] = include
line[current] = 0
}
else { print }
}
}
if (res >= 0) { close(filename[current]) }
else if (current == 0) { error("could not read input file") }
else { --current; error("could not read include file '" filename[current + 1] "'") }
}
for (label in ENVIRON) {
gsub(("{{" blank "*env\\." label blank "*}}"), ENVIRON[label])
if (level > 0) {
current = 0
error("unterminated if")
}
exit 0
}
function condition_helper(label, value) {
gsub(/[\\.^$(){}\[\]|*+?]/, "\\\\&", value)
return sprintf("yadm\\.%s" blank "*==" blank "*\"%s\"", label, value)
function error(text) {
printf "%s:%d: error: %s\n",
filename[current], line[current], text > "/dev/stderr"
exit 1
}
function conditions() {
pattern = ifs blank "+("
for (label in c) {
if (label != "class") {
value = c[label]
pattern = sprintf("%s%s|", pattern, condition_helper(label, value));
function replace_vars(input) {
output = ""
while (match(input, "\\{\\{[ \t]*" VARIABLE "[ \t]*\\}\\}")) {
if (RSTART > 1) {
output = output substr(input, 0, RSTART - 1)
}
data = substr(input, RSTART + 2, RLENGTH - 4)
input = substr(input, RSTART + RLENGTH)
gsub(/[ \t]+/, "", data)
split(data, fields, /\./)
if (fields[1] == "env" && fields[2] in ENVIRON) {
output = output ENVIRON[fields[2]]
}
else if (fields[1] == "yadm" && fields[2] in yadm) {
output = output yadm[fields[2]]
}
else {
error("'" data "' is not a known variable")
}
}
split(classes, cls_array, "\n")
for (idx in cls_array) {
value = cls_array[idx]
pattern = sprintf("%s%s|", pattern, condition_helper("class", value));
}
sub(/\|$/, ")" blank "*%}$", pattern)
return pattern
return output input
}
EOF

Expand Down

0 comments on commit d2cd138

Please sign in to comment.