Configuring Named-Key Parameter Lists

A named-key field needs a clear vocabulary: which names can appear, which names mean the same thing, and which kinds of entry are valid here? Format records those decisions for the parser.

You will learn how to register keys, choose between named and positional entries, and set useful limits on values. If you are new to the parser, begin with Parsing Compact Named-Key Parameters for a complete reading example.

Key Identifiers and Accepted Spellings

A spelling is what a reader types; an identifier is what your application uses after parsing. Keeping these separate lets you accept aliases without giving the rest of your code several names for one option. Each registered spelling maps to a nonnegative integer identifier.

For example, topic and subject can share one identifier. A parsed entry then has the same meaning whichever spelling appeared in the input. The first spelling registered for an identifier is its preferred name: keyName() returns that spelling when you need to display it.

Registering Spellings

The key table uses normalized names such as topic and quiz_level. Readers need not match the letter case of those names: SUBJECT finds a registered subject alias. Write the registered spelling in normalized form, and give every alias a unique name.

setKeys() replaces the whole table when you know the vocabulary at once. addKey() appends one spelling when you are building it step by step. Both validate new entries; for example, a second registration of the same alias is rejected even if it has a different identifier.

Looking Up a Name

The table also works in both directions after registration. keyIndex() returns an optional identifier for an input spelling, so you can check whether that spelling is known. keyName() takes an identifier and returns its first registered spelling; it throws if the identifier is absent.

The following example registers a preferred name and an alias, then prints parsed entries using the preferred name. Its small vocabulary is only an illustration; the same pattern works for any compact field.

text/NamedKeyParsing/RegisterQuizKeys.cpp

/// Register a vocabulary for a compact quiz request.
///
/// `Format` stores normalized key spellings and integer identifiers. Several spellings can share one identifier,
/// while the first registered spelling is the name returned by `keyName()`.
void registerQuizKeys() {
    enum class QuizKey { Topic, Level };
    auto format = el::named_key::Format{}.setKeys({
        {"topic"_el, static_cast<int>(QuizKey::Topic)},
        {"subject"_el, static_cast<int>(QuizKey::Topic)},
    });
    format.addKey("level"_el, static_cast<int>(QuizKey::Level));
    format.setValueListAllowed(false);

    // The spelling in the input is normalized before lookup.
    auto reader = el::StringCharReader{"SUBJECT=gezegen,level=2"_el};
    for (const auto &entry : el::named_key::Parser{reader, format}.readAllEntries()) {
        el::io::printLine(format.keyName(entry.keyIndex()), ": "_el, entry.value());
    }
    el::io::printLine("Registered aliases: "_el, format.keys().count());
}

Output

topic: gezegen
level: 2
Registered aliases: 3

Although the input says SUBJECT, the output says topic. The parser resolves the alias to its identifier, and keyName() turns that identifier back into the preferred spelling. The output also reports three registered aliases, even though they represent only two options. keys() exposes the spellings in the table, so its count is an alias count.

The example also disables positional values because this field accepts named entries only. That choice prevents an unknown bare word at the start from becoming a positional value. The next section explains when to make that choice.

Choose the Forms a Field Accepts

Once the keys are known, decide what a valid entry looks like in this particular field. Does a key need a value? Can the same option appear twice? Could the field instead be a list of unnamed values?

By default, a format accepts bare keys, keyed values, and positional values. It allows each semantic key once. The four settings below change those rules independently, so the key vocabulary can stay the same while different fields accept different forms.

Repeated keys

Most fields should reject a repeated option, and that is the default. setUniqueKeysRequired(false) is useful when repetition has a clear meaning, such as requesting several topics with topic=gezegen,topic=yıldız.

Uniqueness follows the identifier, not the spelling. If topic and subject are aliases, topic=gezegen,subject=yıldız is also a repeat. When you allow this, collect the entries or define how your application chooses among them; otherwise a later value could silently compete with an earlier one.

Bare keys

A bare key can act like a switch: hint may be enough to request a hint. If every recognized key in the field needs a value, setKeysWithoutValuesAllowed(false) makes a bare key a parse error. For example, it accepts topic=gezegen but rejects topic.

This is a field-wide rule. When only some keys need values, leave bare keys allowed and check the returned entry kind for each key in your application, as the Parsing Compact Named-Key Parameters example does.

Keyed values

The opposite field shape is a set of switches. setValuesAllowed(false) lets it accept a bare hint while rejecting hint=on.

This rule affects values attached to recognized keys. It does not disable positional values. For a field containing only recognized switches, disable positional values separately.

Positional values

A positional list gives values without keys, as in Mars,Venüs. Keep that form enabled when the position or order of each value is meaningful. For a field that requires names, setValueListAllowed(false) turns an unrecognized bare entry into an error.

The first entry determines how the rest of the list is read. After a recognized key, an unknown key is an error. After a positional value, later entries stay positional; Mars,topic=gezegen is two positional values rather than a mixture of two entry kinds. Choose separate field grammars if such an input would surprise your readers.

The following demo starts from a format that requires named entries. It changes one policy at a time, then explicitly enables positional values for its answer-list variant.

text/NamedKeyParsing/ChooseEntryPolicies.cpp

/// Choose which forms of quiz options a field accepts.
///
/// The entry-form switches are independent: repeated keys, bare keys, keyed values, and positional values can
/// each be enabled or disabled according to the field's grammar.
void chooseEntryPolicies() {
    const auto base = el::named_key::Format{}.setKeys({{"topic"_el, 0}, {"hint"_el, 1}}).setValueListAllowed(false);

    // A repeated semantic key can represent several requested topics.
    const auto repeated = el::named_key::Format{base}.setUniqueKeysRequired(false).setValueListAllowed(false);
    auto repeatedReader = el::StringCharReader{"topic=gezegen,topic=yıldız"_el};
    el::io::printLine("Topics: "_el, el::named_key::Parser{repeatedReader, repeated}.readAllEntries().count());

    // Require a value after each recognized key.
    const auto valued = el::named_key::Format{base}.setKeysWithoutValuesAllowed(false);
    auto valuedReader = el::StringCharReader{"topic=gezegen"_el};
    el::io::printLine("Valued key: "_el, el::named_key::Parser{valuedReader, valued}.readEntry().isKeyWithValue());

    // A flags-only field accepts a bare key but no keyed value.
    const auto flags = el::named_key::Format{base}.setValuesAllowed(false);
    auto flagReader = el::StringCharReader{"hint"_el};
    el::io::printLine("Bare flag: "_el, el::named_key::Parser{flagReader, flags}.readEntry().isKey());

    // A positional-answer field accepts a value without a registered key.
    const auto answers = el::named_key::Format{base}.setValueListAllowed(true);
    auto answerReader = el::StringCharReader{"gezegen"_el};
    el::io::printLine("Positional answer: "_el, el::named_key::Parser{answerReader, answers}.readEntry().isValue());
}

Output

Topics: 2
Valued key: true
Bare flag: true
Positional answer: true

Bound Positional Lists and Value Length

The entry-form policies decide what a value looks like. Limits decide how much of it you will accept. They are especially useful when a short field comes from input you do not control.

Counting Positional Entries

setMaximumValues() caps the number of entries in a positional list. Its default is unlimited. Named entries do not count toward this limit, because they belong to the key-based form of the field.

Limiting Each Value

setMaximumValueLength() applies to every value, whether it follows a key or appears on its own. It counts Unicode code points, rather than the number of encoded bytes. The default limit is 200 code points; key names are not counted as values.

The example below permits two short positional answers. It also shows that a too-long value is rejected when attached to a registered key.

text/NamedKeyParsing/LimitAnswerValues.cpp

/// Bound the number and length of positional quiz answers.
///
/// `setMaximumValues()` counts positional entries, while `setMaximumValueLength()` measures the decoded value of
/// every positional or keyed entry in Unicode code points.
void limitAnswerValues() {
    const auto format =
        el::named_key::Format{}.setMaximumValues(el::unit::ItemCount{2U}).setMaximumValueLength(el::unit::CpCount{5U});
    auto acceptedReader = el::StringCharReader{"Mars,Venüs"_el};
    el::io::printLine("Accepted answers: "_el, el::named_key::Parser{acceptedReader, format}.readAllEntries().count());

    // A third positional entry exceeds the count limit.
    auto extraReader = el::StringCharReader{"Mars,Venüs,Ay"_el};
    try {
        const auto entries = el::named_key::Parser{extraReader, format}.readAllEntries();
        el::io::printLine("Unexpected accepted answers: "_el, entries.count());
    } catch (const el::err::ParseError &) {
        el::io::printLine("Third answer rejected"_el);
    }

    // The length limit also applies to a value attached to a key.
    auto keyedFormat = format;
    keyedFormat.addKey("topic"_el, 0);
    auto longReader = el::StringCharReader{"topic=gezegen"_el};
    try {
        const auto entry = el::named_key::Parser{longReader, keyedFormat}.readEntry();
        el::io::printLine("Unexpected accepted topic: "_el, entry.value());
    } catch (const el::err::ParseError &) {
        el::io::printLine("Long topic rejected"_el);
    }
}

Output

Accepted answers: 2
Third answer rejected
Long topic rejected

The third answer exceeds the count limit. The keyed topic value exceeds the length limit. Both failures become ParseError with an input position, so a caller can explain where the field went wrong.

Choose limits that fit the surrounding document or protocol. That way the field accepts useful input while still bounding work on unexpected input.

When the field’s punctuation or compact spelling differs from the defaults, continue with Customizing Named-Key Syntax.

Complete demo sources

text/NamedKeyParsing/RegisterQuizKeys.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/named_key/Format.hpp>
#include <erbsland/named_key/Parser.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringCharReader.hpp>

namespace demo {

/// Register a vocabulary for a compact quiz request.
///
/// `Format` stores normalized key spellings and integer identifiers. Several spellings can share one identifier,
/// while the first registered spelling is the name returned by `keyName()`.
void registerQuizKeys() {
    enum class QuizKey { Topic, Level };
    auto format = el::named_key::Format{}.setKeys({
        {"topic"_el, static_cast<int>(QuizKey::Topic)},
        {"subject"_el, static_cast<int>(QuizKey::Topic)},
    });
    format.addKey("level"_el, static_cast<int>(QuizKey::Level));
    format.setValueListAllowed(false);

    // The spelling in the input is normalized before lookup.
    auto reader = el::StringCharReader{"SUBJECT=gezegen,level=2"_el};
    for (const auto &entry : el::named_key::Parser{reader, format}.readAllEntries()) {
        el::io::printLine(format.keyName(entry.keyIndex()), ": "_el, entry.value());
    }
    el::io::printLine("Registered aliases: "_el, format.keys().count());
}

}

text/NamedKeyParsing/ChooseEntryPolicies.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/named_key/Format.hpp>
#include <erbsland/named_key/Parser.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringCharReader.hpp>

namespace demo {

/// Choose which forms of quiz options a field accepts.
///
/// The entry-form switches are independent: repeated keys, bare keys, keyed values, and positional values can
/// each be enabled or disabled according to the field's grammar.
void chooseEntryPolicies() {
    const auto base = el::named_key::Format{}.setKeys({{"topic"_el, 0}, {"hint"_el, 1}}).setValueListAllowed(false);

    // A repeated semantic key can represent several requested topics.
    const auto repeated = el::named_key::Format{base}.setUniqueKeysRequired(false).setValueListAllowed(false);
    auto repeatedReader = el::StringCharReader{"topic=gezegen,topic=yıldız"_el};
    el::io::printLine("Topics: "_el, el::named_key::Parser{repeatedReader, repeated}.readAllEntries().count());

    // Require a value after each recognized key.
    const auto valued = el::named_key::Format{base}.setKeysWithoutValuesAllowed(false);
    auto valuedReader = el::StringCharReader{"topic=gezegen"_el};
    el::io::printLine("Valued key: "_el, el::named_key::Parser{valuedReader, valued}.readEntry().isKeyWithValue());

    // A flags-only field accepts a bare key but no keyed value.
    const auto flags = el::named_key::Format{base}.setValuesAllowed(false);
    auto flagReader = el::StringCharReader{"hint"_el};
    el::io::printLine("Bare flag: "_el, el::named_key::Parser{flagReader, flags}.readEntry().isKey());

    // A positional-answer field accepts a value without a registered key.
    const auto answers = el::named_key::Format{base}.setValueListAllowed(true);
    auto answerReader = el::StringCharReader{"gezegen"_el};
    el::io::printLine("Positional answer: "_el, el::named_key::Parser{answerReader, answers}.readEntry().isValue());
}

}

text/NamedKeyParsing/LimitAnswerValues.cpp

// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0

#include <DemoCommon.hpp>
#include <erbsland/CpCount.hpp>
#include <erbsland/ItemCount.hpp>
#include <erbsland/named_key/Format.hpp>
#include <erbsland/named_key/Parser.hpp>
#include <erbsland/ParseError.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringCharReader.hpp>

namespace demo {

/// Bound the number and length of positional quiz answers.
///
/// `setMaximumValues()` counts positional entries, while `setMaximumValueLength()` measures the decoded value of
/// every positional or keyed entry in Unicode code points.
void limitAnswerValues() {
    const auto format =
        el::named_key::Format{}.setMaximumValues(el::unit::ItemCount{2U}).setMaximumValueLength(el::unit::CpCount{5U});
    auto acceptedReader = el::StringCharReader{"Mars,Venüs"_el};
    el::io::printLine("Accepted answers: "_el, el::named_key::Parser{acceptedReader, format}.readAllEntries().count());

    // A third positional entry exceeds the count limit.
    auto extraReader = el::StringCharReader{"Mars,Venüs,Ay"_el};
    try {
        const auto entries = el::named_key::Parser{extraReader, format}.readAllEntries();
        el::io::printLine("Unexpected accepted answers: "_el, entries.count());
    } catch (const el::err::ParseError &) {
        el::io::printLine("Third answer rejected"_el);
    }

    // The length limit also applies to a value attached to a key.
    auto keyedFormat = format;
    keyedFormat.addKey("topic"_el, 0);
    auto longReader = el::StringCharReader{"topic=gezegen"_el};
    try {
        const auto entry = el::named_key::Parser{longReader, keyedFormat}.readEntry();
        el::io::printLine("Unexpected accepted topic: "_el, entry.value());
    } catch (const el::err::ParseError &) {
        el::io::printLine("Long topic rejected"_el);
    }
}

}