Expressions

Expressions occur in a number of places, most often the right-hand side of a let ... = statement.

Every expression starts with a value:

There are no functions or loops in Werk, but values can be transformed or inspected by "piping" them through an expression chain, imitating shell piping. Any value can be piped through of the built-in operators to transfrom the value in some way.

Abstract syntax:

atomic-expression = string-expr
                  | list-expr
                  | builtin-expr
                  | identifier
                  | '(' expression ')'
                  ;

expression = atomic-expression ('|' builtin-operator)*;

builtin-expr = 'which' string-expr
             | 'env' string-expr
             | ...
             ;

builtin-operator = 'match' match-body
                 | 'join' string-expr
                 | ...
                 ;

Querying the system

The operators obtain a value from the system or runtime environment of the werk process, and produce a value directly. They may initiate an expression chain.

which

Determine the native OS path of an executable, by way of the PATH environment variable. If the program cannot be found, this expression causes an error to be reported, and werk aborts.

The result of this expression participates in outdatedness checks.

Syntax:

which <string-expr>

Example:

let cc = which "clang"     # e.g. "C:\Program Files\LLVM\bin\clang.EXE"

env

Read environment variable. If the variable is not set, this evaluates to the empty string.

The result of this expression participates in outdatedness checks.

Syntax:

env <string-expr>

Example:

let rust-log = env "RUST_LOG"    # e.g. "trace"

glob

Glob workspace files. Given a standard globbing pattern (e.g., **/*.txt), produces a list of all files in the workspace that match the pattern. All names in the result are "absolute workspace paths", i.e. they start with / and are relative to the workspace root.

This expression takes .gitignore into account, and will never return a path to a file covered by .gitignore.

Note that standard glob pattern syntax collides with string interpolation syntax, so capture groups must be escaped: *.\{frag,vert,comp\}.

The result of this expression participates in outdatedness checks.

Syntax:

glob <string-expr>

Example:

let source-files = glob "src/**/*.c"      # ["src/foo.c", "src/bar.c"]

shell

Run a program during evaluation, producing its standard output. If the command fails, evaluation fails and werk will abort. If the command produces output that is not valid UTF-8, this expression also fails.

Caution: In the global scope, a shell expression will cause a program to be run even in --dry-run mode. The intent is that this type of expression can be used to query information about the project or system in a non-destructive manner, such as obtaining the git HEAD or similar.

Note: Even though the expression is named "shell", the command is not passed through the user's shell (like sh, bash, or PowerShell).

The result of this expression participates in outdatedness checks.

Syntax:

shell <run-expr>

Example:

let sha1 = shell "git rev-parse --short HEAD"    # e.g., "0be81c6"

read

Read a workspace file during evaluation, producing its contents as a string. If the file contains invalid UTF-8, this expression fails, and werk will abort.

This will only read files in the workspace - never the output directory.

The result of this expression participates in outdatedness checks.

Syntax:

read <string-expr>

Example:

let contents = read "my-file.txt"    # contents of "my-file.txt"

Expression chaining / piping

There are no functions in werk, but expressions can be followed by a chain of operators, transforming the value in some way. For example, an expression that evaluates to a list can be passed to the join ", " operator to produce a string, where each element of the list is separated by a comma.

The chaining operator is |, as a nod to shell piping syntax. Expression chains can be arbitrarily long.

Syntax:

<first> | <then> | <last>

Example:

let words = ["Hello", "World"]
let message = words | join ", "

# Prints "Hello, World!"
info message

Built-in operators

match

Perform string pattern-matching and substitution. The "input" to the match operation is whatever value is piped into it, which is to say, match can only meaningfully appear as the right-hand side of a chaining expression.

If the "input" value is a list, the pattern substitution is performed recursively. If no patterns match the input, the string is passed through unchanged. The catch-all pattern "%" can be used to provide a fallback, potentially using an error operation to fail early.

Syntax:

match {
    <pattern> => <expression>
    <pattern> => <expression>
}

Example:

let source-file = "foo.c"
let object-file = source-file | match {
  "%.c" => "{%}.o"
  "%.cpp" => "{%}.o"
  "%" => "unsupported source file extension: {}"
}

join

Given a list of values, convert the list to a string (recursively), where each element is separated by a separator.

When given a string, returns the string unmodified.

The "input" to the operation is whatever value it receives via expression piping.

Syntax:

join <separator>

Example:

let cflags = ["-O0", "-g"]
let arguments = cflags | join " "   # "-O0 -g"

split

Given a string, convert it to a list by splitting it by some separator. The separator is a pattern expression. If the separator is not present in the string, returns a list with one entry containing the original string.

See also lines.

Example:

let split = "Hello World" | split " "    # ["Hello", "World]

lines

Given a string, convert it to a list by splitting it into separate lines.

This is similar to split "\n", except it also handles CRLF line breaks.

Example:

let split = "a\r\nb\nc" | lines    # ["a", "b", "c"]

flatten

Given a list containing other lists, return a flat list containing all strings of the left-hand-side.

When given a string, returns a list with a single element containing that string (equivalent to [string]).

May only appear on the right-hand-side of a piping expression.

Example:

let flattened = ["a", ["b", ["c"]]]   # ["a", "b", "c"]

filter

Given a list, filter elements (recursively) through a pattern, keeping only the elements that matched the pattern. This also flattens the list if it contains other lists.

Always produces a list.

Example:

let filtered = ["a.c", "b.cpp"] | filter "%.cpp"   # ["b.cpp"]

filter-match

Given a list, filter elements (recursively) through a pattern, and replace each match with the right-hand side of the pattern match, keeping only the elements that matched the pattern. This also flattens the list if it contains other lists.

Always produces a list. When given a string, the string is filtered as if it was the element of a list.

This is a combination of the filter and map expressions, or the match. Compared to filter | map, the difference is that the mapping operation has access to pattern-match stem, capture groups, etc., and not just the string that matched. Compared to filter | match or match | filter, the difference is that the filter condition is that the pattern failed to match.

Example:

let mapped = ["a.c", "b.cpp"]
           | filter-match "%.c" => "{%}.o"  # ["a.o"]

discard

Inverse of filter: Retains only elements that do not match the pattern(s).

Always produces a list.

Example:

let filtered = ["a.c", "b.cpp"] | detain "%.cpp"   # ["a.c"]

dedup

Deduplicate strings in a list (recursively), preserving the original order. This implies flatten.

When given a single string, returns the string unmodified.

Example:

let deduplicated = ["a", ["a"], "b", "a"] | dedup    # ["a", "b"]

map

Given a list expression, pass each element through a string expression where the "implied" value is the entry in the original list. Produces a list.

Given a string, evaluate the right-hand string expression once with the string as the implied value. Produces a string.

Example:

let mapped = ["a", "b"] | map "hello {}"    # ["hello a", "hello b"]
let mapped = "a" | map "hello {}"           # "hello a"

info

Print an informational message during evaluation. This can appear within an expression chain to introspect values along the way.

Syntax:

info <string-expression>

warn

Print a warning. This can appear within an expression chain.

Syntax:

warn <string-expression>

error

Unconditionally fail evaluation. If this is reached during evaluation in the global scope, the workspace will fail to initialize. If this is reached in a recipe scope, the recipe will fail to build.

In general, error expressions only meaningfully appear within match expressions, but syntactically they can appear anywhere. For example, it may be useful to do "printf-style debugging" of a werkfile by unconditionally failing early.

Syntax:

error <string-expression>

Example:

let profile = "debug"
let cflags = profile | match {
    "debug" => "-O0"
    "release" => "-O3"
    "%" => error "Invalid profile: {profile}. Valid values are \"debug\" and \"release\"."
}

assert-eq expression

When this appear as part of an expression chain, fail evaluation if the two sides are not equal.

Returns the value unchanged.

Syntax:

... | assert-eq <expr>

Example:

let input = ["a", "b"]
let result = input | map "{}.c" | assert-eq ["a.c", "b.c"]

assert-match expression

When this appear as part of an expression chain, fail evaluation if the left side does not match the pattern.

When the left hand side is a list, the pattern is evaluated for all strings in the list (recursively).

Returns the value unchanged.

Syntax:

... | assert-match <pattern>

Example:

let input = ["a.c", "b.c"]
let result = input | assert-match "%.c"