Krikri::VarSubstitutor
VariableSubstitutor - Main class for variable substitution Uses modular components from variable_substitutor/ directory
Constants
Constructors
SUGGESTED_PERFORMANCE_IMPROVEMENTS.md item #18: TaskExecutor
constructs a VarSubstitutor from an already-Hash(String, JSON::Any) vars_context at 28+ call sites - every when:,
param substitution, changed_when:/failed_when:, delegate_to:
resolution, and (the item's own focus) once per loop iteration.
The general initialize below exists for callers that may still
be handing over mixed String | JSON::Any values and needs a
per-key case/when to coerce each one - real, necessary work
for THAT input shape, but pure waste when the input is already
exactly Hash(String, JSON::Any) (every TaskExecutor call site,
checked directly via vars_context's own declared type), where a
plain bulk Hash#dup produces an identical result without walking
every entry through a type-dispatch branch and rebuilding the hash
key-by-key. Still a full .dup, not a bare reference - #add_ magic_variables below mutates @vars in place
(inventory_hostname/ansible_hostname/ansible_host), so
aliasing the caller's own hash would leak that mutation back into
it; .dup keeps the identical "private copy" semantics the
general path already has, just built via one bulk copy instead of
N individual inserts.
Instance methods
Strict scan of a bare Jinja expression (a {{ }} span's inner text,
no surrounding braces) for variable references that real Ansible
fails on when templating it strictly: a chain-shaped reference
(d['k'], d.attr, groups[name].x) whose ROOT is undefined, or
whose resolution bottoms out in a dict-subscript miss on a
resolvable dict ("object of type 'dict' has no attribute 'k'").
Written for meta/argument_specs.yml default: templating - real
Ansible templates the entire spec strictly, and its expressions are
arbitrary compound shapes (rke2's 'server' if inventory_hostname in groups[rke2_servers_group_name] else ... ternary) that the bare/
chained checks in raise_if_strict_undefined don't reach. Tolerance
idioms are honored exactly like the block-tag scan: | default(...),
is defined/is undefined, filter calls, function calls, kwarg
names. Quoted string literals are skipped as reference sources but
keep their contents (a bracket key is a string literal).
Strict finalization of an include_vars path expression: everything
scan_strict_expression_refs checks, PLUS recursion into the RAW
(unrendered) task-vars values the expression references. The recursion
is needed because a task's own vars: dict is deep-rendered LENIENTLY
when the execution context is built (render_task_vars), so by the time
the path expression itself is substituted strictly, a candidate like
'{{ ansible_facts.os_family }}.yml' has already collapsed to the
literal "undefined.yml" and the strict check has nothing left to catch.
Real Ansible templates the lookup's dict args strictly as part of
finalizing include_vars's own _raw_params (verified live against
2.19.4: gantsign.oh-my-zsh's include_vars: "{{ lookup('first_found', params) }}" with files: ['{{ ansible_facts.os_family }}.yml', 'default.yml'] and no gathered facts FAILS the include_vars task with
"object of type 'dict' has no attribute 'os_family'" - it does not
silently fall through to default.yml and load nothing).
The raising twin of #unresolvable_template?: renders raw (known to
contain Jinja markers) the way a strict caller - a task-level when:/
assert: - needs it rendered, raising UndefinedVariableError with real
Ansible's INNERMOST-missing-name message ("'system_user' is undefined",
not the outer variable that merely holds the template text) when the
value bottoms out at a name set nowhere. Everything
substitute_impl(raw, true) already tolerates - default(),
| d(...), omit, Jinja keywords, tolerant nested chains - stays
tolerated here, because the strictness is substitute_impl's own.
output: marks the FINAL, user-facing rendering of a value - a
module argument, a debug message, anything whose text a human or a
target host actually sees. Only there is a container rendered in
Python's repr form (['a', 'b'], matching real Ansible);
every INTERNAL caller leaves it false and keeps the JSON-compact
form, because this engine renders sub-expressions to text and
JSON.parsees them back all over the place (loop sources,
with_fileglob, nested-template re-rendering, the omit sentinel
sweep) and Python-repr text is not valid JSON. See
CrinjaRenderer#evaluate_value!'s comment for the same trap found
from the other side.
Public form of the same probe, for the Crinja-context conversion
side (CrinjaRenderer.convert_var) - see its call site for why
that path needs to ASK rather than raise: it hands the answer to
Crinja as a real Undefined, whose own default()/is defined
semantics are what a lenient caller wants, instead of failing a
task the lenient caller never wanted failed.