1
0
Fork 0
mirror of synced 2024-11-16 06:15:34 -05:00

Rewrite default template to handle nested ifs, != and env vars in if

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 %}

A non-existing yadm or env variable is now replaced with the empty string.
This commit is contained in:
Erik Flodin 2024-10-27 13:38:12 +01:00
parent 76ce3defea
commit 8ba9823407
No known key found for this signature in database
GPG key ID: 420A7C865EE3F85F
2 changed files with 168 additions and 76 deletions

View file

@ -1,4 +1,5 @@
"""Unit tests: template_default""" """Unit tests: template_default"""
import os import os
FILE_MODE = 0o754 FILE_MODE = 0o754
@ -12,6 +13,7 @@ LOCAL_HOST = "default_Test+@-!^Host"
LOCAL_USER = "default_Test+@-!^User" LOCAL_USER = "default_Test+@-!^User"
LOCAL_DISTRO = "default_Test+@-!^Distro" LOCAL_DISTRO = "default_Test+@-!^Distro"
LOCAL_DISTRO_FAMILY = "default_Test+@-!^Family" LOCAL_DISTRO_FAMILY = "default_Test+@-!^Family"
ENV_VAR = "default_Test+@-!^Env"
TEMPLATE = f""" TEMPLATE = f"""
start of template start of template
default class = >{{{{yadm.class}}}}< default class = >{{{{yadm.class}}}}<
@ -30,6 +32,9 @@ Included section from else
{{% if yadm.class == "wrongclass1" %}} {{% if yadm.class == "wrongclass1" %}}
wrong class 1 wrong class 1
{{% endif %}} {{% endif %}}
{{% if yadm.class != "wronglcass" %}}
Included section from !=
{{% endif\t\t %}}
{{% if yadm.class == "{LOCAL_CLASS}" %}} {{% if yadm.class == "{LOCAL_CLASS}" %}}
Included section for class = {{{{yadm.class}}}} ({{{{yadm.class}}}} repeated) Included section for class = {{{{yadm.class}}}} ({{{{yadm.class}}}} repeated)
Multiple lines Multiple lines
@ -97,6 +102,13 @@ Included section for distro_family = \
{{% if yadm.distro_family == "wrongfamily2" %}} {{% if yadm.distro_family == "wrongfamily2" %}}
wrong family 2 wrong family 2
{{% endif %}} {{% 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 %}}
yadm.no_such_var="{{{{ yadm.no_such_var }}}}" and env.NO_SUCH_VAR="{{{{ env.NO_SUCH_VAR }}}}"
end of template end of template
""" """
EXPECTED = f""" EXPECTED = f"""
@ -111,6 +123,7 @@ default distro_family = >{LOCAL_DISTRO_FAMILY}<
classes = >{LOCAL_CLASS2} classes = >{LOCAL_CLASS2}
{LOCAL_CLASS}< {LOCAL_CLASS}<
Included section from else Included section from else
Included section from !=
Included section for class = {LOCAL_CLASS} ({LOCAL_CLASS} repeated) Included section for class = {LOCAL_CLASS} ({LOCAL_CLASS} repeated)
Multiple lines Multiple lines
Included section for second class Included section for second class
@ -121,6 +134,8 @@ Included section for user = {LOCAL_USER} ({LOCAL_USER} repeated)
Included section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again) Included section for distro = {LOCAL_DISTRO} ({LOCAL_DISTRO} again)
Included section for distro_family = \ Included section for distro_family = \
{LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again) {LOCAL_DISTRO_FAMILY} ({LOCAL_DISTRO_FAMILY} again)
Included section for env.VAR = {ENV_VAR} ({ENV_VAR} again)
yadm.no_such_var="" and env.NO_SUCH_VAR=""
end of template end of template
""" """
@ -138,7 +153,7 @@ The first line
An empty file removes the line above An empty file removes the line above
{%include basic%} {%include basic%}
{% include "./variables.{{ yadm.os }}" %} {% include "./variables.{{ yadm.os }}" %}
{% include dir/nested %} {% include dir/nested %}
Include basic again: Include basic again:
{% include basic %} {% include basic %}
""" """
@ -154,6 +169,42 @@ Include basic again:
basic 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): def test_template_default(runner, yadm, tmpdir):
"""Test template_default""" """Test template_default"""
@ -182,7 +233,7 @@ def test_template_default(runner, yadm, tmpdir):
local_distro_family="{LOCAL_DISTRO_FAMILY}" local_distro_family="{LOCAL_DISTRO_FAMILY}"
template_default "{input_file}" "{output_file}" 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.success
assert run.err == "" assert run.err == ""
assert output_file.read() == EXPECTED assert output_file.read() == EXPECTED
@ -243,12 +294,30 @@ def test_include(runner, yadm, tmpdir):
assert os.stat(output_file).st_mode == os.stat(input_file).st_mode 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): def test_env(runner, yadm, tmpdir):
"""Test env""" """Test env"""
input_file = tmpdir.join("input") input_file = tmpdir.join("input")
input_file.write("{{env.PWD}}", ensure=True) input_file.write("{{env.PWD}}", ensure=True)
input_file.chmod(FILE_MODE)
output_file = tmpdir.join("output") output_file = tmpdir.join("output")
script = f""" script = f"""

169
yadm
View file

@ -368,87 +368,110 @@ function template_default() {
# the explicit "space + tab" character class used below is used because not # the explicit "space + tab" character class used below is used because not
# all versions of awk seem to support the POSIX character classes [[:blank:]] # all versions of awk seem to support the POSIX character classes [[:blank:]]
read -r -d '' awk_pgm << "EOF" read -r -d '' awk_pgm << "EOF"
# built-in default template processor
BEGIN { BEGIN {
blank = "[ ]" yadm["class"] = class
c["class"] = class yadm["classes"] = classes
c["classes"] = classes yadm["arch"] = arch
c["arch"] = arch yadm["os"] = os
c["os"] = os yadm["hostname"] = host
c["hostname"] = host yadm["user"] = user
c["user"] = user yadm["distro"] = distro
c["distro"] = distro yadm["distro_family"] = distro_family
c["distro_family"] = distro_family yadm["source"] = source
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) { VARIABLE = "(env|yadm)\\.[a-zA-Z0-9_]+"
replace_vars()
print 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] "'") }
} }
if (res < 0) { if (level > 0) {
printf "%s:%d: error: could not read '%s'\n", FILENAME, NR, file | "cat 1>&2" current = 0
err = 1 error("unterminated if")
} }
close(file) exit 0
next
} }
{ print } function error(text) {
function replace_vars() { printf "%s:%d: error: %s\n",
for (label in c) { filename[current], line[current], text > "/dev/stderr"
gsub(("{{" blank "*yadm\\." label blank "*}}"), c[label]) exit 1
}
for (label in ENVIRON) {
gsub(("{{" blank "*env\\." label blank "*}}"), ENVIRON[label])
}
} }
function condition_helper(label, value) { function replace_vars(input) {
gsub(/[\\.^$(){}\[\]|*+?]/, "\\\\&", value) output = ""
return sprintf("yadm\\.%s" blank "*==" blank "*\"%s\"", label, value) while (match(input, "\\{\\{[ \t]*" VARIABLE "[ \t]*\\}\\}")) {
} if (RSTART > 1) {
function conditions() { output = output substr(input, 0, RSTART - 1)
pattern = ifs blank "+(" }
for (label in c) { data = substr(input, RSTART + 2, RLENGTH - 4)
if (label != "class") { input = substr(input, RSTART + RLENGTH)
value = c[label]
pattern = sprintf("%s%s|", pattern, condition_helper(label, value)); gsub(/[ \t]+/, "", data)
split(data, fields, /\./)
if (fields[1] == "env") {
output = output ENVIRON[fields[2]]
}
else {
output = output yadm[fields[2]]
} }
} }
split(classes, cls_array, "\n") return output input
for (idx in cls_array) {
value = cls_array[idx]
pattern = sprintf("%s%s|", pattern, condition_helper("class", value));
}
sub(/\|$/, ")" blank "*%}$", pattern)
return pattern
} }
EOF EOF