Conditional values
A YAML mapping where a value is expected means branches. Anything else is the literal value.
packages: - name: fd from: macos: brew default: [apt, zypper] # a list is still just a value version: latest # a scalar is still just a valueThere is no when:, no select:, no matcher:. The shape carries the
meaning, so the common case stays one line and the conditional case stays four.
Arm names come from a closed set
Section titled “Arm names come from a closed set”Arm keys are built-in names, or names you declare under targets:. Anything
else is a parse error with a did-you-mean:
bedouin.yaml:6:7: packages[0].from: unknown arm `mcaos` did you mean: macos?That is what the closed vocabulary buys: the vocabulary does not depend on the machine, so a config is valid or invalid identically everywhere. Only which arm wins varies. A typo cannot become a branch that silently never matches on a machine you are not sitting at.
Built-ins:
| Kind | Names |
|---|---|
| os | macos, linux |
| distro | ubuntu, debian, fedora, opensuse, arch, other-distro |
| family | debian-like, rhel-like, suse-like, arch-like |
| arch | x86_64, arm64 |
| combined | every name above, plus -x86_64 or -arm64 |
The more specific arm wins
Section titled “The more specific arm wins”Written order is irrelevant. Arms are compared by the facts they imply:
version: ubuntu: "1.80" # implies os=linux, distro=ubuntu, distro_like=debian linux: stable # implies os=linux default: latestubuntu contains everything linux contains and more, so it wins on Ubuntu —
and the file can be sorted by a formatter without changing what it means.
Only genuinely incomparable arms are ambiguous, and those are a parse error:
version: macos: "1.80" # os axis arm64: nightly # arch axis -- neither contains the otherarms `macos` and `arm64` can both be true on one machine and neither is morespecific, so which one wins would be arbitrary. Write `macos-arm64:` for the machine where both holdDeclared targets
Section titled “Declared targets”For axes no enum can know — a distro version, a hostname, an environment variable — name the condition once:
targets: - name: noble match: { distro: ubuntu, distro_version: ">=24.04" } - name: work match: { env: { BEDOUIN_PROFILE: work } }
packages: - name: neovim from: { noble: apt, default: cargo } # apt's nvim is stale before 24.04A declared target beats every built-in — you named it deliberately. Among several active targets, the one declared first wins.
match: accepts os, distro, distro_like, distro_version, arch,
hostname and env. Version strings may lead with >=, >, <= or <.
only: — when an item should not exist at all
Section titled “only: — when an item should not exist at all”Arms choose between values. They cannot make an item not exist, and without that one config cannot cover Ubuntu and macOS:
packages: - name: xclip from: apt only: linux - name: mas from: brew only: [macos]A pruned item’s other fields are never evaluated, so from: apt on a
Linux-only package costs nothing on a Mac.
Saying it once
Section titled “Saying it once”from: { macos: brew, debian-like: apt, suse-like: zypper } on twenty packages
is twenty chances to get one wrong. Two ways out, both of which already work:
A variable holding the mapping. Variable values take arms like anything else, so this resolves per platform and reads well at the use site:
vars: pm: { macos: brew, debian-like: apt, suse-like: zypper }
packages: - name: jq from: "{{ vars.pm }}" - name: tree from: "{{ vars.pm }}" - name: zellij from: ["{{ vars.pm }}", cargo] # still works in a fallback listA YAML anchor, if you would rather not involve a variable. This is plain YAML and Bedouin never sees the difference:
packages: - name: jq from: &pm { macos: brew, debian-like: apt, suse-like: zypper } - name: tree from: *pmThe variable is usually the better one: it has a name, it shows up in
plan -v, and an anchor has to be declared before its first use.
Templates
Section titled “Templates”Values are minijinja templates, rendered after arm selection — so a losing arm is never evaluated, and a template only valid on one platform costs nothing on the others.
path: ["{{ home }}/.cargo/bin"]version: "{{ env.ZELLIJ_VERSION | default('latest') }}"Undefined names are an error, not an empty string: {{ hom }} would otherwise
render /.cargo/bin and ship a wrong PATH entry without a word.