Custom template functions in the overlay

Updated 5 November 2026

Besides the built-in functions (mergepkg(), pkg(), exists(), ini(), grep(), cut(), …), the utilities let you define your own - an ordinary script in the overlay. This is a way to bring into the template a check that cannot be expressed as a condition: whether a network interface exists, whether a provider is reachable, which profile is active.

Where to place a function

A function is a file in the scripts/functions/ directory of the overlay repository:

<location>/scripts/functions/checkdev.sh

Here <location> is the repository path from the location parameter of the /etc/portage/repos.conf section; the templates of this repository live in the same <location>/profiles/templates, so the function is available in every template of the repository. The function name is the part of the file name up to the first dot, so the extension does not affect the name: checkdev.sh is called as checkdev(), and cflang.py as cflang(). It is called as #-name(arguments)-# or, on the left side of a header condition, simply name(arguments):

# Calculate checkdev(wan0)!= path=/etc/conf.d

Choosing the language of the file

The function file can be written in any language whose interpreter is present on the system: the system itself reads the first line, and the engine does not need to run the file - it reads its text and passes it to the interpreter as input, so the file does not need to be executable. The #! line is optional too: if it is missing, the file is run through sh. For example, the cflang.py file can look like this:

#!/usr/bin/env python3
import sys
print("PY-OK " + (sys.argv[1] if len(sys.argv) > 1 else "?"))

It is more reliable to set the interpreter through env - #!/usr/bin/env python3, and not by an absolute path: if the file named by an absolute path does not exist, the merge does not just complain, it stops and does not finish; with the env form a missing interpreter is visible in the same warning as the other troubles.

You call such a file with the #-cflang(hello)-# construct, and it substitutes PY-OK hello into the template. The arguments reach the script as separate parameters after --, so sys.argv sees both the function name and its values. A useful consequence: the function text first goes through ordinary template processing, so variables and #-…-# calls written inside the function file are substituted before the script even starts.

Overriding the built-in functions

A function from scripts/functions/ is checked before the built-in one and replaces it: if you put a file with the name of
a standard function, the label will call your script. An exists script with echo SHADOW-OK returns to the template
SHADOW-OK instead of the normal answer of the built-in function, and a Python script returns PY-OK hello. Treat
the built-in function names with care: the substitution acts on
every template of this repository, and it is easy to miss.

Calling protocol

A function is an ordinary script, and the return protocol is this:

  • stdout - the value of the text to substitute;
  • file descriptor 3 - a warning (it lands in the output as WARNING);
  • file descriptor 4 - a success message (SUCCESS);
  • a non-zero exit code - an error.

An example from a real overlay - checking that an interface exists:

scripts/functions/checkdev.sh

#!/bin/bash
# Check whether a network interface exists
# parameters: name
# Returns that same name if the interface exists
if ip link show $1 &>/dev/null; then
        echo $1
        exit 0
fi

Such a function in a template gives the name of the interface if it exists and nothing if it does not, and together with the condition checkdev(wan0)!= полностью отключает целый шаблон на машинах без этого интерфейса.

Two different outcomes: a function not found and a function that failed

It is important to tell these apart when debugging.

  • Function name not found (the name is neither among the built-ins nor in scripts/functions of any repository) - the template is aborted with the message function of templates '<name>' not found. A typo in a function name costs more than it looks: the whole template stops working.
  • The script exists but ended with an error (a non-zero exit code, a missing interpreter, text the interpreter cannot run) - the utilities report the warning Failed to apply custom function … and substitute an empty value; the template continues. If the function stood in a condition, the result will usually be "the condition is false", and you will get a template that was quietly not applied, not an error.

The practical conclusion: make the function "safe by default" - on any trouble it should print what matches the behaviour you want when nothing is known (usually an empty line and exit 0), and you will be able to tell "no data" from "it failed" by the warning.

Names that are not functions

Some #-…-# constructs are variables, not functions, and checking for them in scripts/functions is pointless. For example, #-cl_pass_file-# is the name of the template file being processed, which the utilities substitute themselves (for a directory, the name of the directory); that is why the #-cut(1)-# construct is useful: it takes cl_pass_file by default and splits it on -, turning the template name runlevel-<service> into <service> (numbering from zero: field 1 is the second).

The distinction is simple: a function differs from a variable by the two argument parentheses (), even if there are no arguments. cl_pass_file in #-cl_pass_file-# is a variable; #-cl_pass_file()-# is already a call to a function with that name. A construct without parentheses is never counted as a function.

Your own script outranks a built-in function

The scripts/functions/ directory is looked through before the list of built-in functions: if it holds an executable file whose name matches a built-in function, the call goes to your script and not to the code of the utilities. A practical consequence: a script such as ini.sh or cut.sh shadows the built-in ini() and cut() in every template of this repository - and instead of "I overrode a function" you get behaviour that quietly changed where those names were not expected. So a built-in function name is a bad name for your own script; when something behaves suspiciously, check whether scripts/functions/ holds a file with that name.

If scripts with one and the same name exist in several repositories, the last one in the repository list is run: first come the Calculate repositories from the main repos.conf (in the order of their sections), then the other non-Portage repositories from repos.conf.d/*.conf.

A trick worth copying

Functions go well with cut() and ini(): the template name carries the data (link.<file>, runlevel-<service>, <index>-<name>), and the function unpacks it. That way one short template yields a family of links and service entries, and the list of "what to configure" is kept in ini.env - so the administrator edits it instead of patching the overlay.

See also