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

Instance methods

get_vars
Source
has_variable?(name : String) : Bool
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 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).

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

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

Source
substitute(text : String, strict : Bool = false, output : Bool = false, native : Bool = false) : String

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.

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

Source
vars
Source

Nested types