envoke-cli 1.0.0

Resolve variables from literals, commands, scripts, and templates — output as env vars, .env files, or custom formats
envoke-cli-1.0.0 is not a library.

envoke

Resolve variable values from a declarative YAML configuration.

envoke reads an envoke.yaml file, resolves variables from multiple sources (literals, commands, shell scripts, and minijinja templates) in dependency order, and renders the results as shell-safe VAR='value' lines — ready to source as environment variables, write to .env files, or feed into custom output templates.

Installation

From crates.io

cargo install envoke-cli

With cargo-binstall

cargo binstall envoke-cli

With mise

mise use -g cargo:envoke-cli

From source

cargo install --git https://github.com/glennib/envoke envoke-cli

From GitHub releases

Pre-built binaries are available on the releases page for:

  • Linux (x86_64, aarch64)
  • macOS (x86_64, Apple Silicon)
  • Windows (x86_64)

Quick start

Create an envoke.yaml:

variables:
  DB_HOST:
    default:
      literal: localhost
    envs:
      prod:
        literal: db.example.com

  DB_USER:
    default:
      literal: app

  DB_PASS:
    envs:
      local:
        literal: devpassword
      prod:
        sh: vault kv get -field=password secret/db

  DB_URL:
    default:
      template: "postgresql://{{ DB_USER }}:{{ DB_PASS | urlencode }}@{{ DB_HOST }}/mydb"

Generate variables for an environment:

$ envoke local
# @generated by `envoke local` at 2025-06-15T10:30:00+02:00
# Do not edit manually. Modify envoke.yaml instead.

DB_HOST='localhost'
DB_PASS='devpassword'
DB_URL='postgresql://app:devpassword@localhost/mydb'
DB_USER='app'

Note: All output includes an @generated header with the invocation command and timestamp. Examples below omit this header for brevity.

Source them into your shell:

eval "$(envoke local --prepend-export)"

Or write them to a file:

envoke local --output .env

Configuration

The config file (default: envoke.yaml) has a single top-level key variables that maps variable names to their definitions.

Variable definition

Each variable can have:

Field Description
description Optional. Rendered as a # comment above the variable in output.
tags Optional. List of tags for conditional inclusion. Variable is only included when at least one of its tags is passed via --tag. Untagged variables are always included.
default Optional. Fallback source used when the target environment has no entry in envs.
envs Map of environment names to sources.
overrides Optional. Map of override names to alternative source definitions (each with its own default/envs). Activated via --override.

A variable must have either an envs entry matching the target environment or a default. If neither exists, resolution fails with an error.

Source types

Each source specifies exactly one of the following fields:

literal

A fixed string value.

DB_HOST:
  default:
    literal: localhost

cmd

Run a command and capture its stdout (trimmed). The value is a list where the first element is the executable and the rest are arguments.

GIT_SHA:
  default:
    cmd: [git, rev-parse, --short, HEAD]

sh

Run a shell script via sh -c and capture its stdout (trimmed).

TIMESTAMP:
  default:
    sh: date -u +%Y-%m-%dT%H:%M:%SZ

template

A minijinja template string, compatible with Jinja2. Reference other variables with {{ VAR_NAME }}. Dependencies are automatically detected and resolved first via topological sorting.

DB_URL:
  default:
    template: "postgresql://{{ DB_USER }}:{{ DB_PASS }}@{{ DB_HOST }}/{{ DB_NAME }}"

The urlencode filter is available for escaping special characters:

CONN_STRING:
  default:
    template: "postgresql://{{ USER | urlencode }}:{{ PASS | urlencode }}@localhost/db"

skip

Omit this variable from the output. Useful for conditionally excluding a variable in certain environments while including it in others.

DEBUG_TOKEN:
  default:
    skip: true
  envs:
    local:
      literal: debug-token-value

Environments and defaults

envoke selects the source for each variable by checking the envs map for the target environment. If no match is found, it falls back to default. This lets you define shared defaults and override them per environment:

LOG_LEVEL:
  default:
    literal: info
  envs:
    local:
      literal: debug
    prod:
      literal: warn

Tags

Variables can be tagged for conditional inclusion. Tagged variables are only included when at least one of their tags is passed via --tag. Untagged variables are always included. This is useful for gating expensive-to-resolve variables (e.g. vault lookups) or optional components behind explicit opt-in.

variables:
  DB_HOST:
    default:
      literal: localhost

  VAULT_SECRET:
    tags: [vault]
    envs:
      prod:
        sh: vault kv get -field=secret secret/app
      local:
        literal: dev-secret

  OAUTH_CLIENT_ID:
    tags: [oauth]
    envs:
      prod:
        sh: vault kv get -field=client_id secret/oauth
      local:
        literal: local-client-id
# Without --tag, only untagged variables are included:
$ envoke local
DB_HOST='localhost'

# Include vault-tagged variables (and all untagged ones):
$ envoke local --tag vault
DB_HOST='localhost'
VAULT_SECRET='dev-secret'

# Include everything:
$ envoke local --tag vault --tag oauth
DB_HOST='localhost'
OAUTH_CLIENT_ID='local-client-id'
VAULT_SECRET='dev-secret'

Variables without tags are always included regardless of which --tag flags are passed. Tagged variables require explicit opt-in.

Overrides

Overrides add a third dimension for varying values alongside environments and tags. A variable can declare named overrides, each with its own default/envs sources. Activate them with --override:

variables:
  DATABASE_HOST:
    default:
      literal: localhost
    envs:
      prod:
        literal: 172.10.0.1
    overrides:
      read-replica:
        default:
          literal: localhost-ro
        envs:
          prod:
            literal: 172.10.0.2

  CACHE_STRATEGY:
    envs:
      prod:
        literal: lru
    overrides:
      aggressive-cache:
        envs:
          prod:
            literal: lfu-with-prefetch

  DATABASE_PORT:
    default:
      literal: "5432"
    # No overrides -- unaffected by --override flag
# Base values:
$ envoke prod
CACHE_STRATEGY='lru'
DATABASE_HOST='172.10.0.1'
DATABASE_PORT='5432'

# Activate an override:
$ envoke prod --override read-replica
CACHE_STRATEGY='lru'
DATABASE_HOST='172.10.0.2'
DATABASE_PORT='5432'

# Multiple overrides on disjoint variables:
$ envoke prod --override read-replica --override aggressive-cache
CACHE_STRATEGY='lfu-with-prefetch'
DATABASE_HOST='172.10.0.2'
DATABASE_PORT='5432'

When an override is active for a variable, the source is selected using a 4-level fallback chain:

  1. Override envs[environment]
  2. Override default
  3. Base envs[environment]
  4. Base default

Variables without a matching override definition are unaffected and use the normal base fallback. If multiple active overrides are defined on the same variable, envoke reports an error. Unknown override names (not defined on any variable) produce a warning on stderr.

CLI usage

envoke [OPTIONS] [ENVIRONMENT]
Option Description
ENVIRONMENT Target environment name (e.g. local, prod). Required unless --schema is used.
-c, --config <PATH> Path to config file. Default: envoke.yaml.
-o, --output <PATH> Write output to a file instead of stdout.
-t, --tag <TAG> Only include tagged variables with a matching tag. Repeatable. Untagged variables are always included.
-O, --override <NAME> Activate a named override for source selection. Repeatable. Per variable, at most one active override may be defined.
--prepend-export Switches to a built-in template that prefixes each variable with export . Ignored when --template is used.
--template <PATH> Use a custom output template file instead of the built-in format.
--schema Print the JSON Schema for envoke.yaml and exit.

JSON Schema

Generate a JSON Schema for editor autocompletion and validation:

envoke --schema > envoke-schema.json

Use it in your envoke.yaml with a schema comment for editors that support it:

# yaml-language-server: $schema=envoke-schema.json
variables:
  # ...

Alternatively, point directly at the hosted schema without writing a local file:

# yaml-language-server: $schema=https://raw.githubusercontent.com/glennib/envoke/refs/heads/main/envoke.schema.json
variables:
  # ...

How it works

  1. Parse the YAML config file.
  2. Filter out variables excluded by --tag flags (if any).
  3. For each remaining variable, select the source matching the target environment (or the default), applying the override fallback chain if --override flags are active.
  4. Extract template dependencies and topologically sort all variables using Kahn's algorithm.
  5. Resolve values in dependency order -- literals are used as-is, commands and shell scripts are executed, templates are rendered with already-resolved values.
  6. Render output using a built-in or custom Jinja2 template (see Custom templates). The default template produces an @generated header followed by sorted VAR='value' lines with shell-safe escaping.

Circular dependencies and references to undefined variables are detected before any resolution begins and reported as errors.

Custom templates

By default, envoke outputs shell VAR='value' lines with an @generated header. You can supply your own minijinja (Jinja2-compatible) template via --template:

envoke local --template my-template.j2

Template context

The template receives the following variables:

Name Type Description
variables map of name -> {value, description} Rich access: {{ variables.DB_URL.value }}. Iteration: {% for name, var in variables | items %}. Sorted alphabetically.
v map of name -> value string Flat shorthand: {{ v.DATABASE_URL }}.
meta.timestamp string RFC 3339 timestamp of invocation.
meta.invocation string Full CLI invocation as a single string.
meta.invocation_args list of strings CLI args as individual elements.
meta.environment string Target environment name.
meta.config_file string Path to the config file used.

Filters

  • shell_escape -- escapes single quotes for shell safety (' -> '\'').

Example: JSON output

{
{% for name, var in variables | items %}  "{{ name }}": "{{ var.value }}"{% if not loop.last %},{% endif %}
{% endfor %}}
envoke local --template json.j2

Example: Docker .env format

# Generated for {{ meta.environment }}
{% for name, var in variables | items -%}
{{ name }}={{ v[name] }}
{% endfor -%}

Development

This project uses mise as a task runner. After installing mise:

mise install       # Install tool dependencies
mise run build     # Build release binary
mise run test      # Run tests (via cargo-nextest)
mise run clippy    # Run lints
mise run fmt       # Format code
mise run ci        # Run all checks (fmt, clippy, test, build)

Run a single test:

cargo nextest run -E 'test(test_name)'

License

MIT OR Apache-2.0