Jinja expressions are allowed in renderable string fields, including variables,
target fields, and rule recipes. Structural Jinja blocks cannot reshape the
YAML document. Use the dedicated foreach and when keys for manifest-time
expansion.
Generate targets with `foreach` and `when`
The next complete manifest creates two targets and excludes the disabled item:
netsuke_version: "1.0.0"
vars:
reports:
- daily
- weekly
- disabled
targets:
- foreach: reports
when: item != 'disabled'
name: "{{ item }}.txt"
command: "echo {{ index }} > {{ outs }}"
defaults:
- daily.txt
- weekly.txt
Each expansion receives item and a zero-based index. Target-local variables
take precedence over global variables, and iteration values take precedence
over both.
when is evaluated while Netsuke loads the manifest. It does not create a
runtime branch. Runtime decisions belong in a command or script.
Top-level actions support the same foreach and when keys.
Diagnose manifest expansion
Pass --verbose to a normal manifest-loading command to inspect manifest-time
filtering. Netsuke emits a debug event with the exact number of filtered
targets and actions, including entries whose metadata is not retained. It also
emits at most 64 per-entry events for each expansion. The aggregate event's
omitted_filtered_entries field reports how many additional filtered entries
were excluded from those bounded records.
Normal manifest loading also records these aggregate counters:
netsuke_manifest_filtered_targets_totalnetsuke_manifest_filtered_actions_totalnetsuke_manifest_omitted_filtered_entries_total
The counters have no labels and never contain raw manifest values.
Each retained event contains only the entry's section, an eight-character
entry_name_hash, an optional zero-based iteration_index, and the byte
length of its when_expression in when_expression_len. Raw entry names,
foreach item values, and when expressions are never emitted. Manifest query
loading, including netsuke help targets, remains side-effect-free and emits
neither these expansion events nor expansion metrics.
Discover files with `glob`
glob(pattern) expands a shell-style pattern to the sorted list of matching
files while Netsuke loads the manifest, so the results become part of the
static build graph. It pairs naturally with foreach to generate one target
per matched file; a target list such as foreach: glob('src/*.c') produces one
expansion per matching source.
Matching is case-sensitive. * and ? do not cross directory separators; use
** to descend into subdirectories. Directories are excluded, so only files
are returned. Relative patterns resolve against the directory containing the
manifest — the workspace root — independent of the directory Netsuke is invoked
from, so glob('src/*.c') in a Netsukefile at the project root matches
<project>/src/*.c. The quick-start guide shows a complete
runnable example.
Patterns may be absolute or relative to the manifest directory, including
parent-relative patterns such as glob('../shared/*.h'). Relative results
retain their pattern-relative spelling after Netsuke removes the workspace
base; absolute patterns remain absolute. Expansion is scoped to the pattern's
longest literal directory prefix — the text up to the first *, ?, [ or
{, trimmed back to the last separator, so src/ for src/**/*.c. If that
prefix does not exist, or names something that is not a directory, the call
returns an empty list rather than failing. A symbolic-link literal prefix, such
as src/link/*.c, cannot establish the capability and causes expansion to
fail. A match is skipped rather than reported as an error when the metadata
lookup cannot resolve a symbolic link — the match itself or a directory reached
on the way to it — because it is unreadable within the prefix, dangling, or
resolves outside that prefix. A cyclic symbolic link is reported as an error
rather than skipped, since it describes a broken tree rather than a missing
file.
Patterns with unmatched braces are rejected during validation. When an opening brace remains unclosed, the diagnostic points to the outermost unmatched opening brace; an unmatched closing brace is reported at that closing brace.
The Jinja helper rejects a matched path unless it can be inserted as one
portable unquoted shell word. ASCII letters, digits, /, :, comma, full
stop, underscore, and hyphen are accepted; whitespace, control characters, and
shell punctuation are rejected. This prevents an untrusted checkout filename
from becoming shell syntax when item is interpolated into a command or
script.
Define reusable macros
Macros return rendered text and can accept default arguments:
netsuke_version: "1.0.0"
vars:
greeting: Hello
macros:
- signature: "say(name, punctuation='!')"
body: "{{ greeting }}, {{ name }}{{ punctuation }}"
targets:
- name: greeting.txt
command: "echo '{{ say('Netsuke') }}' > {{ outs }}"
defaults:
- greeting.txt
Select optional tools
which(name, **kwargs) returns an executable path and fails when the command
is absent. The same helper is also available as a filter.
On Windows, a name without an extension is matched against the effective
PATHEXT, the same list the shell uses — so which('cargo') finds cargo.exe
provided .exe is among those entries. A custom PATHEXT may legitimately
omit it, in which case it is not a candidate.
PATHEXT falls back to the built-in list only when it is unset or when no
entry survives normalization — that is, every entry is empty or whitespace. Any
other value is used as given, however unusual. The built-in list, in order:
.com, .exe, .bat, .cmd, .vbs, .vbe, .js, .jse, .wsf, .wsh,
.msc
The fallback exists because an empty effective list would match nothing and report every command missing. Entries are matched case-insensitively and tried in the order the list gives them. A name that already carries an extension is used as written.
command_available(name, **kwargs) returns a boolean and is better for
complementary branches:
netsuke_version: "1.0.0"
actions:
- name: test-fast
command: "cargo nextest run"
deps:
- config/test-profile.toml
when: command_available("cargo-nextest")
- name: test-fast
command: "cargo test"
deps:
- config/test-profile.toml
when: not command_available("cargo-nextest")
targets: []
defaults:
- test-fast
Netsuke evaluates both guards while loading the manifest, without running
either recipe, so exactly one test-fast action enters the build graph. The
selected action's deps become Ninja implicit dependencies: changes to
config/test-profile.toml make the action stale, but the path is not appended
to cargo nextest run or cargo test as a recipe argument.
Both helpers accept:
all=true: return allwhichmatches. It does not change the boolean result fromcommand_available.canonical=true: canonicalize matching paths.fresh=true: bypass the resolver cache for this lookup.cwd_mode="auto"|"always"|"never"|"workspace-recursive": choose the executable search domain.autosearches onlyPATHentries, including empty components in a non-emptyPATH; it reports the command missing whenPATHis empty or unset.alwaysprepends the workspace root/current directory,neverexcludes it, andworkspace-recursiveexplicitly scans the workspace tree after the normalPATHpass. Recursive search can resolve a checkout-controlled executable, so use it only when that trust boundary is intended.
The env(name) function reads one environment variable, and
env(name, default='...') supplies the value to use when it is absent. A
present variable always takes precedence over the default. An empty string is a
present value, so it yields '' rather than the default. The default is
consulted only for a missing variable: a non-Unicode value is still an error,
and a variable refused by the access policy is still refused. The default must
be a string; a number, boolean, list, or map is rejected rather than
stringified. Omitting the default, passing default=none, or passing an
undefined default supplies no fallback at all: each behaves exactly as if the
argument had not been written.
which resolver observability
which and command_available record two bounded counters, both emitted in
the drained metrics snapshot described in
Diagnose configuration selection:
netsuke_stdlib_which_cache_total— a counter that records cache outcomes with thecwd_modeandoutcomelabels.outcomeishit,miss, orbypass, wherebypassis afresh=truelookup.netsuke_stdlib_which_resolution_total— a counter with two label shapes that counts resolution outcomes. A success series carriescwd_modeandoutcome=found. A failure series carriescwd_mode, anoutcomeofnot_foundorerror, and a boundedcategory. Thecategorylabel occurs only on failure series, so a query matching on it selects exactly the failures.
The cwd_mode label is drawn from a closed vocabulary of auto, always,
never, and workspace_recursive. It records how the current directory was
asked to contribute to the search: Netsuke reads it from the options before the
cache probe and before the lookup, so it does not indicate whether recursive
workspace lookup ran or which search produced the result.
The label's spelling is not the template's. A manifest writes
cwd_mode="workspace-recursive", with the hyphen. The underscore spelling
workspace_recursive is the label's value and is not a value a template may
supply.
The category label is the resolver's bounded failure taxonomy: not_found,
direct_not_found, args, canonicalize, is_executable,
canonicalize_non_utf8, workspace_non_utf8, walkdir, cwd_resolve, or
cwd_non_utf8. Every label on both counters is drawn from a closed set fixed
by the resolver rather than by manifest content. The cwd_mode label does
record which search policy a manifest requested — that is the fact the
vocabulary exists to carry — but it names it as one of the fixed spellings
rather than quoting the template. Nothing else is recorded: command names,
filesystem paths, workspace names, and PATH or PATHEXT values never reach a
label.