Replace Fragile Hand-Written Option Parsing with LaTeX3's l3keys
Hand-rolled LaTeX option parsing breaks on commas and stray spaces. Learn how to use the supported l3keys key-value interface to build a robust memo package, and understand the trade-offs before you ship.
04 Feb 2026, 00:47 UTC

If you maintain a LaTeX class or package, you have probably written a macro that splits a comma-separated option list by hand. It works until a user passes title=Q3 Report, Final and the comma silently truncates the value, or until a stray space after a key produces a confusing “missing equals sign” error. The supported alternative is LaTeX3’s key-value interface, l3keys, which treats options as structured data with typed destinations rather than raw text to be chopped apart.
The practical takeaway: declare keys once with a type such as .tl_set:N or .bool_set:N, process them with \keys_set:nn, and expose only a single setup command to users. You still need to respect TeX expansion rules, but the parsing layer stops being your bug farm.
Why hand-rolled parsing fails
TeX macros do not natively understand key-value pairs. A typical hand-rolled parser loops over a list and looks for an equals sign, but it has no built-in protection for punctuation inside values. Four failure modes show up repeatedly:
- Commas inside values. A title like
Q3 Report, Finalis split into two fragments, so the second half is either discarded or misinterpreted as a new key. - Stray spaces. A space before or after the equals sign becomes part of the key or value, causing silent mismatches or “missing equals” diagnostics.
- Boolean flags without values. A bare
secretis ambiguous: should it mean true, or is it a malformed key? - Missing equals signs. A typo such as
title Q3 Reportis often swallowed silently, leaving the variable unset.
These are not exotic edge cases; they are the first thing a user tries when a value contains natural punctuation.
How l3keys changes the model
l3keys is the key-value layer of the LaTeX3 programming layer, expl3. In current LaTeX kernels, expl3 is preloaded; if you are maintaining an older document or class, you can add \RequirePackage{expl3} at the top of your file to load it explicitly.
Instead of writing a parser, you declare keys with a type that tells TeX where to store the result. The most common types are:
| Key type | Purpose | Typical use |
|---|---|---|
.tl_set:N | Store an arbitrary token list | Title, author, free text |
.bool_set:N | Store a boolean (true/false) | Flags such as secret |
.str_set:N | Store a string with spaces ignored | Dates, identifiers |
.code:n | Execute code immediately when the key is set | Formatting switches |
You then process a list with \keys_set:nn { <module> } { key = value, flag }. The module name is a namespace you choose; it prevents collisions with other packages. Values that contain commas or equals signs should be wrapped in braces: title = {Q3 Report, Final}. Keep user-level key names free of underscores and colons; those characters belong to expl3 internal naming conventions.
A worked memo package
Suppose you want a small memo package with four options: title, author, date, and a boolean secret flag. Keep the key declarations internal and expose only \memo_setup:n to users.
Declaring the keys
Create a file named memo.sty in your working directory (or a local texmf tree). You need write permission to that directory.
\RequirePackage{expl3}
\ProvidesExplPackage{memo}{2026/11/12}{0.1}{Memo package}
\keys_define:nn { memo }
{
title .tl_set:N = \l_memo_title_tl,
author .tl_set:N = \l_memo_author_tl,
date .tl_set:N = \l_memo_date_tl,
secret .bool_set:N = \l_memo_secret_bool,
secret .default:n = true,
}
\NewDocumentCommand \memo_setup:n { m }
{ \keys_set:nn { memo } { #1 } }
\NewDocumentCommand \memo_print { }
{
\noindent\textbf{\l_memo_title_tl}\par
\noindent\l_memo_author_tl\par
\noindent\l_memo_date_tl\par
\bool_if:NT \l_memo_secret_bool
{ \noindent\textbf{Confidential}\par }
}
The l and _tl / _bool suffixes follow expl3 conventions: l marks the variable as local to the package, and the suffix records its type. This makes accidental use from outside the package easier to spot.
Using the setup command
In a document, load the package and call the setup command:
\documentclass{article}
\usepackage{memo}
\memo_setup:n {
title = {Q3 Report, Final},
author = Platform Team,
date = 2026-11-12,
secret
}
\begin{document}
\memo_print
\end{document}
Compile from the command line with pdflatex memo-test.tex (or run it from your editor). Expected checks: the title block should show Q3 Report, Final with the comma intact, and the Confidential line should appear because secret was given as a bare flag. If you remove secret or write secret=false, the confidential line should disappear.
One risk: if you forget the braces around the title, the comma will still split the list, and l3keys will treat Final as an unknown key. That is a better failure mode than silent corruption, but it still requires user discipline.
Trade-offs and limitations
l3keys is a real interface, not a magic parser. Three limitations matter in practice:
.code:nexecutes immediately. If a key runs heavy assignments or fragile expansions at set-time, you can break later processing. Prefer.tl_set:Nto store the value and expand it at use-time.- Error messages feel low-level. Unknown keys are not silently ignored by default. For class or package options, decide explicitly whether unknown keys should error, warn, or be forwarded to another handler.
- Version sensitivity. Exact diagnostics and available key properties vary by distribution. Verify against the
l3keysandinterface3documentation installed with your TeX distribution, and check the LaTeX kernel version in the log file.
Also remember that l3keys does not suspend TeX expansion rules. A token list and a string behave differently when expanded, so choose the type that matches how you intend to use the value.
How to verify before shipping
Before you release the package, run a minimal test that exercises the failure modes you care about:
- Create a scratch
.texfile that calls\memo_setup:nwith a comma inside the title and the boolean flag omitted. Compile and compare the output to the expected title block. - Add a deliberately unknown key to the same scratch file and confirm that your chosen unknown-key behavior fires (error, warning, or forward).
- Inspect the log for undefined control sequences or unknown-key errors, and confirm the kernel version reported there matches your assumptions.
These checks take minutes and catch the exact class of bugs that hand-rolled parsers hide until a user reports them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.