class

Krikri::VarSubstitutor

Inherits Reference < Object

VariableSubstitutor - Main class for variable substitution Uses modular components from variable_substitutor/ directory

Constants

MAX_BLOCK_TAG_ESCALATION_DEPTH = 50
STRICT_REF_IDENT_REGEX = /\b[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*|\[[^\[\]]+\])*/

Constructors

new(vars : Hash(String, JSON::Any), host_name : String | Nil = nil, facts : Hash(String, JSON::Any) = {} of String => JSON::Any)

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.

Source
new(vars : Hash(String, String | JSON::Any) = {} of String => String | JSON::Any, host_name : String | Nil = nil, facts : Hash(String, JSON::Any) = {} of String => JSON::Any)
Source

Class methods

add_resolved_var_name(host_name : String, name : String) : Nil

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.

Source
conditional_eval_active?
Source
current_deprecation_origin(name : String | Nil = nil) : String | Nil

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.

Source
current_fact_deprecation_op
Source
current_loop_source_site
Source
current_task_param
Source
current_template_file
Source
current_when_site
Source
deferred_leaf_leak(rendered : String) : String | Nil

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.

Source
deferred_walk_active?
Source
deprecation_site_names?(name : String) : Bool

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).

Source
emit_fact_deprecation(origin : String | Nil) : Nil

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.

Source
enter_conditional_eval
Source
enter_deferred_leaf_scope
Source
enter_deferred_walk
Source
enter_span_memo_scope
Source
enter_strict_span
Source
exit_conditional_eval
Source
exit_deferred_leaf_scope
Source
exit_deferred_walk
Source
exit_span_memo_scope
Source
exit_strict_span
Source
expression_root(expr : String) : String | Nil

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).

Source
fact_read_origin(name : String) : String | Nil

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).
Source
failing_span
Source
host_from_vars(vars : Hash(String, JSON::Any) | Nil) : String | Nil

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.

Source
hydrate_unsafe_registry_from_config(config : JSON::Any) : Nil

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).

Source
play_hosts_read_origin
Source
pop_current_task_param
Source
pop_current_template_file
Source
pop_current_when_site
Source
pop_loop_source_site
Source
pop_preparing_var
Source
published_var_origin(host_name : String | Nil, name : String) : String | Nil

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.

Source
push_current_template_file(path : String) : Nil
Source
push_loop_source_site(source_file : String | Nil, source_line : Int32, key : String) : Nil
Source
push_preparing_var(host_name : String | Nil, name : String, text : String | Nil) : Nil
Source
record_deferred_leaf(raw : String, message : String) : Nil
Source
record_failing_span(span : String) : Nil
Source
register_var_origin_lookup(host_name : String, &block : String -> String | Nil) : Nil
Source
reset_failing_span
Source
resolved_names_snapshot

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.

Source
resolved_var_name?(host_name : String | Nil, name : String) : Bool
Source
set_current_task_param(source_file : String | Nil, source_line : Int32, key : String, text : String | Nil = nil, origin : String | Nil = nil) : Nil

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.

Source
set_current_when_site(source_file : String | Nil, source_line : Int32, exact_col : Int32, text : String | Nil = nil) : Nil
Source
set_resolved_var_names(host_name : String, names : Enumerable(String)) : Nil
Source
single_top_level_span?(text : String) : Bool

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.

Source
span_memo
Source
span_memo_key(vars : Hash(String, JSON::Any), raw : String) : String

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.

Source
strict_span_active?
Source
top_level_fact_name?(name : String, vars : Hash(String, JSON::Any)) : Bool

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.

Source
unsafe_root?(vars : Hash(String, JSON::Any) | Nil, source_expr : String | Nil) : Bool

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.

Source

Instance methods

check_strict_undefined(inner : String) : Nil

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.

Source
evaluate_structured_strict(expr : String) : JSON::Any | Nil

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.

Source
has_variable?(name : String) : Bool
Source
host_name
Source
scan_strict_expression_refs(text : String) : Nil

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).

Source
scan_strict_include_vars_path(text : String, raw_task_vars : Hash(String, JSON::Any)) : Nil

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).

Source
set_variable(name : String, value : String | JSON::Any) : Nil
Source
strict_render(raw : String) : String

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.

Source
substitute(text : String, strict : Bool = false, output : Bool = false, native : Bool = false) : String
Source
substitute_array(array : Array(String)) : Array(String)
Source
substitute_hash(hash : Hash(String, String)) : Hash(String, String)
Source
unresolvable_template?(raw : String) : Bool

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.

Source
vars
Source

Nested types