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.
Class methods
Write-time complement of #set_resolved_var_names: a register:/set_fact:
write adds its name to the host's registry IMMEDIATELY, instead of
waiting for that host's next vars-context build. Matters for
cross-host reads: hostvars[x] hands another host's registered
results/set_facts to a host that may be the only one executing from
here on, and a value written by a host that never executes again
would otherwise reach the re-render funnels unregistered (the hostile
{{ lookup('pipe', ...) }} stdout executed on the controller instead
of passing through verbatim). The next context build still recomputes
the full set and makes this redundant from then on.
The formatted Origin block for a deprecation raised while a template RESOLVES a variable (the play_hosts read alias, the top-level fact alias): a template-file render in progress wins (the read belongs to the file), then the conditional clause being evaluated, then the loop source being rendered, then the templated task param being finalized; nil when none applies - reads whose Origin krikri does not reproduce (a vars file) stay silent rather than mis-attribute the warning.
The message of the first deferred leaf whose raw template text is
still present in rendered, or nil when the span never consumed
one. See #enter_deferred_leaf_scope. An occurrence that is still
quoted - a dict/list serialization holding the raw leaf as one of
its own values - is NOT a read: the container passed through
untouched (the BOOTSTRAP dict that the when: merely indexes into,
issue #422), and only an UNQUOTED occurrence means the leaf's text
was combined into a scalar result.
Whether the template being rendered by the CURRENT fiber site names
name - the guard that ties a fact read to the template text that
named it, for the reads no variable value accounts for (a when:
clause, a task param, a template file). A read reached only through
another variable's value is attributed to that value's declaration
instead (see #fact_read_origin).
Emits the INJECT_FACTS_AS_VARS deprecation for a placeable Origin while keeping ansible's one-block-per-templating-operation record. A read whose Origin krikri cannot place (nil) stays silent and does not spend the operation's block, so a later placeable read in the same operation still gets it - real keeps the first ACCESS, whichever fact it named.
The leading identifier of an expression - r for r.stdout,
r.stdout | length > 0, r['stdout_lines'], ansible_local.evil.v;
nil when the expression does not start with a plain identifier
(a quoted literal, a (-headed parenthesized expression, a
dict/list literal, &...). The registry is keyed on these roots:
an unsafe root makes EVERY value reachable through it unsafe, at
any nesting depth, exactly like Ansible's own taint model
(AnsibleUnsafe wraps whole structures).
The Origin block for one top-level fact READ: the value krikri is re-rendering at that moment when its stored text names the fact, otherwise the site whose own template text names it; nil when neither places the read, so it stays silent rather than mis-attribute the warning.
ansible-core tags every stored YAML value with its DEFINING position
and lazily templating that value is what reads the fact, so a read
reached through another variable's value (docker_yum_repo_url: https://...{{ ansible_distribution }}... read as {{ docker_yum_ repo_url }}, or a when: on such a variable) reports that value's
own declaration - the role's defaults/main.yml line and value column
- not the template that merely used the variable (issue #961, adzumatokaku.docker_role; live-verified against 2.21.6, including a two-level chain where the INNERMOST value's line wins).
The host a vars hash belongs to - every executor-built context
carries inventory_hostname, and the evaluator components
(VariableLookup/FilterEngine/ComparisonEvaluator/ConditionalEvaluator,
the shared Rerender helpers) are constructed with the vars hash
alone, so this is how they reach the per-host registry without
threading a host parameter through every constructor.
The child-process counterpart of #resolved_names_snapshot: rebuilds
the per-host unsafe-name registry from a plugin config's
unsafe_registry field and records the exact text of every
brace-bearing string under those names (the value-level registry,
UnsafeValues) - the current host's values from the config's own
vars, every other host's from its hostvars entry. No-op when the
field is absent (configs built before this field existed, or plugin
invocations that never carried a vars context).
The defining Origin block of name's own stored value for host (see #register_var_origin_lookup), or nil when the executor cannot place it.
Snapshot of every host's unsafe-name registry - the serialized form
travels inside a debug:/assert: plugin config (see
build_plugin_config) so the DETACHED plugin process an async: task
spawns (__async_run, or the module binary uploaded to a remote
target) can rebuild the same registry via
#hydrate_unsafe_registry_from_config: the registry is process memory,
and a fresh child process starts with it empty, which would leave
that process's own re-render gates blind.
origin is the formatted Origin block when the caller already knows
the exact token the read belongs to (a task's templated action name,
whose directive can sit on the task's own - action: ... line, where
the key-based text scan below finds nothing) - it wins over the scan.
Whether text is exactly ONE {{ }} span spanning its whole
length, ignoring {{/}} sequences that sit INSIDE a quoted
string literal within the expression. The naive whole-text
scan("{{").size == 1 count treats the double-templated idiom
({{ query('flattened', '{{ pkg_list }}') }} - rhaamo.snmpd,
round 5460199) as mixed multi-span text, which costs the param
its native typing (the output:true repr form instead of the JSON
container the plugin wire expects) even though the whole value is
one interpolation whose nested braces are merely string-literal
TEXT, exactly what ansible-core's own parser sees.
Memo entries are keyed by the variable SCOPE as well as the template
text: one templating operation can render the same text in two
scopes ({{ who }} {{ hostvars['h2'].who }} renders {{ myname }}
for the current host AND for h2), and a text-only key returned the
first scope's value for the second.
Whether name is one of the gathered facts' TOP-LEVEL
ansible_-prefixed spellings - the aliases ansible-core 2.21
deprecates (the INJECT_FACTS_AS_VARS default): reading one from a
template prints the [DEPRECATION WARNING] naming the reading
template's Origin, once per Origin, whichever template kind read it.
The injected copy real tags is clean_facts(facts), which drops
ansible_local plus every ansible_*_interpreter key before
injecting, so those spellings never deprecate even though krikri's
own fact store holds them (ansible_python_interpreter for a local
connection) - live-verified against 2.21.6: ansible_hostname
warns, ansible_python_interpreter and ansible_host do not.
THE single shared re-render gate: whether the value an expression resolved to must NOT be re-rendered because the expression's root variable is execution-resolved (unsafe). Every evaluation path that re-renders a resolved value's own template text (Rerender.if_ templated / Rerender.render_raw, VariableLookup's per-site guards, FilterEngine, ComparisonEvaluator, ConditionalEvaluator, ExpressionEvaluator#retemplated_lookup_value, the debug action plugin's var= renderer) funnels through this predicate - fail-safe by construction: a call site that forgets to pass its expression simply loses the gate, so new sites should always thread it.
Instance methods
Public entry to the strict-undefined check substitute(strict: true)
runs before evaluating a whole {{ }} span - for callers that
evaluate a span STRUCTURALLY instead of through #substitute (the
set_fact native-typing path), so the evaluation happens once and the
strict-undefined failure still fires exactly as it would have.
Lazy: build the ExpressionEvaluator / JinjaRenderer only after
magic variables have been added to @vars. The dup happens here if
and only if any of these private getters is reached, which is the
case for every templated substitute() call - but explicitly NOT
the case for a substitute() that returns on the no-placeholder
early-exit, which is the win #20 targets.
Structured (typed) evaluation of a whole-single-span expression with
the strict-undefined probe applied - the native-typed task-arg /
set_fact path's strictness seam. evaluate_structured is LENIENT
about undefined values (a consumed missing attribute silently
becomes nil/"undefined"), which let {{ d.missing == 'x' }},
{{ 'a' if d.missing else 'b' }}, {{ not d.missing }},
{{ d.missing is none }} etc. finalize natively to a bool where
ansible-core 2.19 fails the task with the attribute-shaped error
(live-verified matrix vs 2.19.11). Same signal-gated probe as
#evaluate_stripped_span: the lenient evaluation runs first,
unchanged; the strict probe re-evaluates only when a miss signal
appeared, and only its verdict can raise.
Strict scan of a bare Jinja expression (a {{ }} span's inner text,
no surrounding braces) for variable references that 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 -
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.
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
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.
Public form of the same probe, for the Crinja-context conversion
side (JinjaRenderer.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.