grml

April 28, 2026 · View on GitHub

grml is a Makefile alternative. Build targets are declared in a grml.yaml file at the project root, using YAML syntax. Running grml opens an interactive shell that exposes those targets as commands; passing a target on the command line runs it once and exits.

A worked example lives in sample/cd sample && grml.

asciicast

Installation

From source

go install github.com/desertbit/grml@latest

Usage

grml [flags] [command...]
FlagDescription
-d, --directoryroot directory containing the grml file (default: current directory)
-f, --filegrml file relative to the root (default: grml.yaml)
-v, --verbosetrace each shell command as it runs (set -x)

The -f flag lets you keep multiple manifests side by side — e.g. grml.yaml for in-container work and grml.host.yaml for tasks that must run on the host.

Without a target, grml drops into an interactive shell with tab completion. Built-in commands:

CommandDescription
reloadre-read the grml file (preserves option values)
verbose <bool>toggle verbose mode at runtime
optionsprint current option values
options checktoggle bool options interactively
options set <name>pick a value for a choice option

Manifest reference

Top-level keys

KeyDescription
versionmanifest schema version, currently 3 (required)
projectproject name, exposed as ${PROJECT} (required)
envordered map of environment variables, supporting ${VAR} interpolation
optionsuser-tweakable options: bools (check) or lists of strings (single choice)
interpretersh (default) or bash
importshell files sourced before every exec body
commandscommand tree

Per-command keys

KeyDescription
helphelp text (supports ${VAR} interpolation from env)
aliaslist of alternative names
argspositional arguments, exposed as env vars of the same name
envenv vars for an included subgrml file, scoped to the commands in that file (see Per-include env)
optionsoptions for an included subgrml file, with their own options check / options set UI under that command (see Per-include options)
importshell files for an included subgrml file, sourced only when running commands in that file (see Per-include imports)
depsother commands to run first; see Dep paths for the syntax
execshell body to run
commandsnested sub-commands
includeload the rest of this command's definition from another YAML file

Implicit environment variables

The process environment is inherited and the following are always set:

VariableValue
ROOTabsolute path to the root directory containing the grml file
PROJECTproject name from the manifest
NUMCPUnumber of CPU cores
LOCAL_ROOTabsolute path to the directory of the current subgrml file — only set inside included subtrees (in root commands, use ${ROOT} instead)

Each option is also exported: bools as true/false, choices as the active value. Each args entry is exported when the command runs.

Variable interpolation

${VAR} is expanded by grml inside env values, import paths, and help strings. Inside exec bodies, expansion is performed by the shell at runtime — env vars, options, args, and any other shell-visible variables are all available there.

Dep paths

A deps entry is one of:

SyntaxResolves to
foo.barabsolute path from the root command tree
.barrelative to the current command (i.e. its child bar)
~.barrelative to the nearest enclosing include point — its child bar (root if no include)

~. lets an included subgrml file reference its own siblings without knowing the name the root manifest gave it. For example, commands/release.yaml can say deps: [~.tag] whether the root mounts it as release:, rel:, or anything else.

Per-include env

An included subgrml file can declare its own env: block at the top. Those values layer on top of the root env (root values stay visible) and apply only to commands defined inside that file. Same-named root keys are overridden within the included file; commands outside it are unaffected. LOCAL_ROOT is auto-defined to the included file's directory, so a subgrml can refer to its own files via ${LOCAL_ROOT}/<file> without hard-coding the path. Subgrml commands also run with their working directory set to ${LOCAL_ROOT}, so exec bodies can reference sibling files by relative path. Root commands keep ${ROOT} as their cwd.

# commands/release.yaml — included from the root manifest as the 'release' command
env:
    DESTBIN:      ${PROJECT}-${VERSION}-release   # overrides root DESTBIN, only inside this file
    RELEASE_NOTE: ${PROJECT} ${VERSION} release   # only visible to commands in this file

help: cut a ${VERSION} release
commands:
    publish:
        help: publish ${DESTBIN} artifacts        # uses the per-include DESTBIN
        exec: |
            echo "publishing ${BINDIR}/${DESTBIN}"

Per-include options

An included subgrml file can declare its own options: block. Each subgrml's options live in their own namespace — two subgrmls can each have a debug option without colliding, and there's no need to prefix names manually.

The interactive shell exposes a separate options UI under each subgrml's command:

grml » options                  # root manifest's options
grml » labrat options           # labrat's options
grml » labrat options check     # toggle labrat's bool options
grml » labrat options set foo   # pick a value for labrat's choice option
grml » closer options           # closer's options (independent of labrat's)

When running a command, the env vars exported are the merged options from every applicable scope: root first, then each ancestor scope down to the command's own scope. Inner scopes shadow outer scopes for same-named options, so a command inside labrat always sees its own debug, never the root one.

Per-include imports

An included subgrml file can declare its own import: block, parallel to the root manifest's import:. Listed scripts are sourced only when running commands defined inside that file (and any descendants), and they run after the env is in place — so top-level statements in the script can use the per-include env.

Paths are written relative to the included file's own directory, so a self-contained subgrml can ship its helpers alongside its YAML:

commands/
  release.yaml      # subgrml: import: [release.sh]
  release.sh        # sourced only when running release.* commands

Sourcing order for any given command: root manifest's import: first, then per-include import: from outermost ancestor down to the command's own scope. Last-sourced wins for function/variable definitions.

Shell builtins

grml injects helpers under the grml_* namespace into every exec body and import script. They work under both sh and bash.

HelperDescription
grml_option <name>exit 0 if the named option/env var equals true (bool option check)
grml_option <name> <value>exit 0 if the named option/env var equals <value> (choice check)
grml_if <name> <if-str> <else-str>print if-str when option is true, else else-str (bool form)
grml_if <name> <value> <if-str> <else-str>print if-str when option equals value, else else-str (choice form)

Examples:

# Branch on a bool option:
if grml_option debug; then
    go build -gcflags="all=-N -l" -o "${BINDIR}/${DESTBIN}"
else
    go build -o "${BINDIR}/${DESTBIN}"
fi

# Inline-if for picking between two strings (avoids a temporary if/else):
suffix=$(grml_if debug '-debug' '')
echo "publishing ${BINDIR}/${DESTBIN}${suffix}"

# Choice-form: pick a string based on the active value of a choice option.
gpu_flag=$(grml_if runtime cuda '--cuda' '--cpu')