.. Copyright (c) 2026 Tobias Erbsland - Erbsland DEV. https://erbsland.dev SPDX-License-Identifier: Apache-2.0 .. index:: single: Configuration; Placeholders single: Placeholders; Configuration Text single: Configuration; Runtime Values **************************************** Using Placeholders in Configuration Text **************************************** Configuration files often need a small amount of information that belongs to the running application rather than the file itself: a deployment name, a path selected at startup, or a value held in a protected store. Placeholders let your application provide that text while the parser reads a document. You choose the available sources and transformations, and ordinary ELCL parsing resumes after the resulting text has been inserted. Placeholder expansion is an optional parser extension. Until at least one source is registered, placeholder-looking text remains literal text and has no special meaning. Reading Placeholder Expressions =============================== A placeholder begins with ``${`` and ends with ``}``. The first name selects a source, an optional colon introduces its parameter, and each pipe appends a filter: .. code-block:: elcl [academy] name: "${registry:main academy}" label: "${registry:main academy|trim|academy label:public}" Source and filter names follow the regular ELCL name rules. They are case-insensitive, and spaces and underscores are equivalent. The parser normalizes these names before calling your provider, while parameters preserve their spelling and case after ELCL escape sequences have been decoded. The colon is optional and an empty parameter is valid. A literal pipe starts the next filter, and a literal closing brace ends the expression. When either character belongs to a parameter, write it using an ELCL Unicode escape, such as ``\u007c`` for a pipe. A placeholder stays on one physical line and can contain at most 16 filters. Expansion is deliberately not recursive. If a source returns the text ``${registry:other}``, that result is inserted literally rather than interpreted as a second request. This makes every source lookup visible in the original document and prevents runtime data from introducing new operations. Where Expansion Applies ======================= The parser expands placeholders in ordinary quoted text values, including multiline text and text items in a value list. It does not expand text names, meta-values such as ``@include``, code values, or regular expressions. Parser control data therefore remains independent from values supplied at runtime. A dollar that is not immediately followed by ``{`` remains an ordinary character. Use ``\$`` when a literal dollar is directly followed by an opening brace and must never start a placeholder. Enabling Built-in Sources and Filters ===================================== The parser starts without placeholder sources or filters. For configuration values already held by the application, call :cpp:func:`Parser::setPlaceholderVariableSource() ` with a :cpp:type:`StringMap `. This registers ``var`` and replaces its complete variable map when called again. For values supplied by the process environment, call :cpp:func:`Parser::addPlaceholderEnvironmentSource() ` to register ``env``. The environment is read when a placeholder is expanded, not when the source is added. The built-in filters are a separate choice. :cpp:func:`Parser::addPlaceholderTextFilters() ` registers ``trim``, ``required``, and the other text filters. Registering filters alone does not activate expansion: every expression begins with a source. This example makes both sources and the filters available to a marine survey document. The environment assignment only makes the example repeatable; a running application normally receives that value from its parent process. .. erbsland-demo:: :source: conf/Placeholders/BuiltInProviders.cpp :show: f:builtInProviders :exec: conf/placeholders --demo BuiltInProviders :sha256: a292d13e938a9e48c5b493df4e8757b78714e4a26f0b72d2afcb087623bed76e .. code-block:: cpp void builtInProviders() { auto environment = el::system::EnvironmentVariables{}; environment.setOrThrow("ERBSLAND_DEMO_MARINE_AREA"_el, u8" 瀬戸内海 "_el); auto parser = el::conf::Parser{}; parser.addPlaceholderEnvironmentSource(); parser.setPlaceholderVariableSource({{{"species"_el, u8" アマモ "_el}}}); parser.addPlaceholderTextFilters(); const auto document = parser.parseTextOrThrow( "[survey]\n" "area: \"${env:ERBSLAND_DEMO_MARINE_AREA,required|trim}\"\n" "species: \"${var:species|trim}\"\n"_el); environment.removeOrThrow("ERBSLAND_DEMO_MARINE_AREA"_el); el::io::printLine("Area: "_el, document->getTextOrThrow("survey.area"_el)); el::io::printLine("Species: "_el, document->getTextOrThrow("survey.species"_el)); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mArea: 瀬戸内海 Species: アマモ␛[0m .. erbsland-demo-end:: The ``,required`` flag on ``env`` rejects a missing environment variable, while ``trim`` removes surrounding whitespace from either source's result. The inserted text then becomes part of the ordinary quoted ELCL value. You can assign a different source name with the optional ``name`` argument of either source method; expressions must then use that name instead of ``env`` or ``var``. Source and filter names must be unique on the parser, so adding the same environment source or filter set twice raises :cpp:class:`LogicFailure `. The :doc:`/topics/text_placeholders/built_in_sources` and :doc:`/topics/text_placeholders/built_in_filters` topics describe the complete lookup, parameter, and transformation rules shared with the standalone text replacer. Providing Application Values ============================ Implement :cpp:class:`Source ` when the application owns the lookup. The ``sourceNames()`` method announces one or more names handled by the provider, and ``resolve()`` turns a source name and parameter into replacement text. This demo provides two values from a magical academy registry. The Portuguese names are application data selected by the generated demo theme; source names, diagnostics, and code remain in English. .. erbsland-demo:: :source: conf/Placeholders/CustomSource.cpp :exec: conf/placeholders --demo CustomSource :sha256: 9ad0877c786fd401859beb3417c0c69e7004a776777c8a894c341e0b74b9c7d1 .. code-block:: cpp /// Provide application-owned values for configuration placeholders. /// /// A placeholder source publishes one or more case-insensitive ELCL names. The parser passes the normalized source name /// and the decoded, case-preserving parameter to `resolve()`. The returned text replaces the complete placeholder. class AcademySource final : public el::placeholder::Source { public: [[nodiscard]] auto sourceNames() const -> el::StringList override { return el::StringList{"academy"_el}; } [[nodiscard]] auto resolve(const el::String &, const el::String ¶meter) -> el::String override { if (parameter == "name"_el) { return "Academia da Lua"_el; } if (parameter == "library"_el) { return "Biblioteca das Estrelas"_el; } throw el::placeholder::ReplacerError{ el::placeholder::ReplacerErrorCategory::ValueNotFound, el::StringFormat{"The academy value '{}' does not exist."_el}.build(parameter)}; } }; /// Register a custom source and expand its values while parsing quoted text. void customSource() { auto parser = el::conf::Parser{}; parser.addPlaceholderSource(std::make_shared()); const auto document = parser.parseTextOrThrow( "[academy]\n" "name: \"${academy:name}\"\n" "welcome: \"Bem-vindo à ${academy:library}\"\n"_el); el::io::printLine("Academy: "_el, document->getTextOrThrow("academy.name"_el)); el::io::printLine("Message: "_el, document->getTextOrThrow("academy.welcome"_el)); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mAcademy: Academia da Lua Message: Bem-vindo à Biblioteca das Estrelas␛[0m .. erbsland-demo-end:: Register the shared provider with :cpp:func:`Parser::addPlaceholderSource() ` before parsing. One provider may announce several names when those names share a backend or lifecycle. Each announced name must be a valid, unique regular ELCL name; registration rejects null providers, empty name lists, invalid names, and collisions with an already registered source. The parser keeps the provider available for later parse operations on the same parser. Keep the shared pointer when you need to remove it with :cpp:func:`Parser::removePlaceholderSource() `. Removal uses provider identity and quietly ignores an instance that is not registered. Once the last source is removed, placeholder interpretation becomes inactive again. Transforming Values with Filters ================================ A :cpp:class:`Filter ` transforms the current replacement text. Its ``filterNames()`` method announces the handled names, while ``apply()`` receives the normalized filter name, the decoded parameter, and the text produced by the source or previous filter. Filters run from left to right. That order makes a chain readable as a small pipeline: first obtain the value, then trim it, escape it, label it, or apply another application-specific transformation. The next demo registers a filter that adds a named academy area in front of a room description. .. erbsland-demo:: :source: conf/Placeholders/CustomFilter.cpp :exec: conf/placeholders --demo CustomFilter :sha256: a03e4c922338d42097123fe62b0f22f773e4c36e3c08bf856869689c5e5688c3 .. code-block:: cpp /// Transform placeholder values with an application-owned filter. /// /// A filter publishes one or more names and receives the current text, including changes made by earlier filters in the /// chain. Its parameter is decoded but otherwise preserved. This filter adds an application label in front of a value. class AcademyLabelFilter final : public el::placeholder::Filter { public: [[nodiscard]] auto filterNames() const -> el::StringList override { return el::StringList{"academy label"_el}; } [[nodiscard]] auto apply(const el::String &, const el::String ¶meter, const el::String &value) -> el::String override { return el::join("["_el, parameter, "] "_el, value); } }; /// Register a custom filter and apply it after a placeholder source. void customFilter() { auto parser = el::conf::Parser{}; parser.addPlaceholderSource(std::make_shared()); parser.addPlaceholderFilter(std::make_shared()); const auto document = parser.parseTextOrThrow( "[academy]\n" "room: \"${literal:Biblioteca Arcana|academy label:ala norte}\"\n"_el); el::io::printLine("Room: "_el, document->getTextOrThrow("academy.room"_el)); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mRoom: [ala norte] Biblioteca Arcana␛[0m .. erbsland-demo-end:: Add a filter with :cpp:func:`Parser::addPlaceholderFilter() ` and remove the same instance with :cpp:func:`Parser::removePlaceholderFilter() `. Filter registration follows the same validity and uniqueness rules as source registration. Filters alone do not activate placeholder parsing because every expression must begin with a source. Reporting Lookup and Transformation Errors ========================================== A source or filter cannot always produce valid text. It may encounter a missing protected value, a denied lookup, or application data that fails a requirement. Throw :cpp:class:`ReplacerError ` with the category and description that best explain the failure. The parser converts this error to a location-aware :cpp:class:`ConfError `. The parser preserves that information and enriches the error with the placeholder location, the target value's name path, and a source excerpt when one is available. The following demo triggers one source failure and one filter failure so you can see the complete console diagnostics. .. erbsland-demo:: :source: conf/Placeholders/ErrorHandling.cpp :exec: conf/placeholders --demo SourceError :exec-exit-code: 1 :exec-2: conf/placeholders --demo FilterError :exec-2-exit-code: 1 :sha256: b254161ccca0f6d8979dc9119e9601ef09790b49e4b89a178c9c9c28d22cacea .. code-block:: cpp /// Create source and filter errors that the parser can enrich with configuration context. /// /// Providers should report expected lookup and transformation failures as `ReplacerError`. Choose the category that /// best describes the problem and keep the description useful without exposing protected values. class RequiredAcademySource final : public el::placeholder::Source { public: [[nodiscard]] auto sourceNames() const -> el::StringList override { return el::StringList{"academy"_el}; } [[nodiscard]] auto resolve(const el::String &, const el::String ¶meter) -> el::String override { throw el::placeholder::ReplacerError{ el::placeholder::ReplacerErrorCategory::Access, el::StringFormat{"The academy registry entry '{}' is unavailable."_el}.build(parameter)}; } }; class ErrorLiteralSource final : public el::placeholder::Source { public: [[nodiscard]] auto sourceNames() const -> el::StringList override { return el::StringList{"literal"_el}; } [[nodiscard]] auto resolve(const el::String &, const el::String ¶meter) -> el::String override { return parameter; } }; class SafeRuneFilter final : public el::placeholder::Filter { public: [[nodiscard]] auto filterNames() const -> el::StringList override { return el::StringList{"safe rune"_el}; } [[nodiscard]] auto apply(const el::String &, const el::String &, const el::String &) -> el::String override { throw el::placeholder::ReplacerError{ el::placeholder::ReplacerErrorCategory::Validation, "The academy title contains a rune that is not permitted here."_el}; } }; /// Report a source failure at the configuration value that requested it. /// /// Sources and filters can throw `ReplacerError` with a meaningful category and description. The parser adds the /// placeholder location, the target value's name path, and an available source excerpt before the error reaches the /// application. void sourceError() { auto parser = el::conf::Parser{}; parser.addPlaceholderSource(std::make_shared()); const auto document = parser.parseTextOrThrow( "[academy]\n" "name: \"${academy:secret archive}\"\n"_el); el::io::printLine("Unexpected value: "_el, document->getTextOrThrow("academy.name"_el)); } /// Report a filter failure with the same configuration context. void filterError() { auto parser = el::conf::Parser{}; parser.addPlaceholderSource(std::make_shared()); parser.addPlaceholderFilter(std::make_shared()); const auto document = parser.parseTextOrThrow( "[academy]\n" "title: \"${literal:Grimório Antigo|safe rune}\"\n"_el); el::io::printLine("Unexpected value: "_el, document->getTextOrThrow("academy.title"_el)); } .. erbsland-demo-run:: .. code-block:: console $ conf/placeholders --demo SourceError .. erbsland-ansi:: :escape-char: ␛ ␛[0m ␛[91;49m␛[1mUnknown␛[22m ␛[1mError ␛[39;49m␛[22mThe academy registry entry 'secret archive' is unavailable. ␛[1mError␛[22m ␛[1mSource: ␛[90;49m␛[22mLine:␛[39;49m 2 ␛[90;49mColumn:␛[39;49m 8 ␛[90;49mPosition:␛[39;49m 18 ␛[1mConfiguration␛[22m ␛[1mError␛[22m ␛[1mDetails: ␛[90;49m␛[22mcategory:␛[39;49m Access ␛[90;49mname path:␛[39;49m academy.name ␛[90;49m 1 │ ␛[39;49m[academy] ␛[90;49m 2 │ ␛[39;49mname: "␛[91;49m$␛[39;49m{academy:secret archive}" ␛[90;49m │ ␛[39;49m ␛[91;49m▔ ␛[90;49m╭─ Caused By: │ ␛[39;49m␛[1mThe␛[22m ␛[1macademy␛[22m ␛[1mregistry␛[22m ␛[1mentry␛[22m ␛[1m'secret␛[22m ␛[1marchive'␛[22m ␛[1mis␛[22m ␛[1munavailable. ␛[90;49m␛[22m╰─␛[0m .. erbsland-demo-run:: .. code-block:: console $ conf/placeholders --demo FilterError .. erbsland-ansi:: :escape-char: ␛ ␛[0m ␛[91;49m␛[1mUnknown␛[22m ␛[1mError ␛[39;49m␛[22mThe academy title contains a rune that is not permitted here. ␛[1mError␛[22m ␛[1mSource: ␛[90;49m␛[22mLine:␛[39;49m 2 ␛[90;49mColumn:␛[39;49m 9 ␛[90;49mPosition:␛[39;49m 19 ␛[1mConfiguration␛[22m ␛[1mError␛[22m ␛[1mDetails: ␛[90;49m␛[22mcategory:␛[39;49m Validation ␛[90;49mname path:␛[39;49m academy.title ␛[90;49m 1 │ ␛[39;49m[academy] ␛[90;49m 2 │ ␛[39;49mtitle: "␛[91;49m$␛[39;49m{literal:Grimório Antigo|safe rune}" ␛[90;49m │ ␛[39;49m ␛[91;49m▔ ␛[90;49m╭─ Caused By: │ ␛[39;49m␛[1mThe␛[22m ␛[1macademy␛[22m ␛[1mtitle␛[22m ␛[1mcontains␛[22m ␛[1ma␛[22m ␛[1mrune␛[22m ␛[1mthat␛[22m ␛[1mis␛[22m ␛[1mnot␛[22m ␛[1mpermitted␛[22m ␛[1mhere. ␛[90;49m␛[22m╰─␛[0m .. erbsland-demo-end:: Malformed expression syntax is reported as a syntax error. An unknown source or filter is an unsupported-feature error, and a chain beyond the filter limit is a limit error. These parser-generated errors receive the same value path, location, and excerpt as errors from custom providers. Restricting Placeholder Sources and Filters =========================================== Register only sources and filters that a document actually needs. A source is an application capability: if it can read secrets, inspect the environment, or query another subsystem, every configuration accepted by that parser can request those operations. Validate parameters inside the source, return only the required text, and avoid including sensitive values in error messages. The library also provides common components. :doc:`/topics/text_placeholders/built_in_sources` describes the environment-variable and application-variable sources. :doc:`/topics/text_placeholders/built_in_filters` covers reusable text transformations and checks. They use the same registration and error model as the custom providers on this page.