Behaviour changes in math mode when macro is defined dynamically

If you ask \show\dynmacro after \begin{document} you’ll be answered

> \dynmacro=macro:
->\protect \unhbox \voidb@x \hbox {O}.

which is nothing you’d like, isn't it? The problem is \xdef. You're even lucky that something meaningful happens. With \textbf instead of \text you’d get several errors. You need just one step of expansion.

\documentclass{report}
\usepackage[english]{babel}
\usepackage{pgffor}
\usepackage{amsmath}

% defining a plain macro
\def\macro{\text{O}}

% defining it dynamically
\foreach \mname/\cmd in {%
    dynmacro/\text{O},%
    } {%
    \expandafter\gdef\csname\mname\expandafter\endcsname\expandafter{\cmd}%
}

\begin{document}

\show\dynmacro

\begin{equation}
    2^{\text{O}} \times % okay
    2^{\macro} \times   % okay
    2^{\dynmacro}       % this one yields a weird result
\end{equation}

\end{document}

The console now will report

> \dynmacro=macro:
->\text {O}.

Important note: \text is not the best for this, it should probably be \mathrm.


A different implementation: \dyndef wants three arguments

  1. the input form (optional, default #1=#2)
  2. the definition to perform
  3. the list of things to define

Here are a couple of examples.

\documentclass{report}
\usepackage{amsmath}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\dyndef}{O{##1=##2}mm}
 {
  \cs_set:Npn \__iagolito_dyndef_aux:w #1 \q_stop { #2 }
  \clist_map_function:nN { #3 } \iagolito_dyndef:n
 }
\cs_new:Nn \iagolito_dyndef:n { \__iagolito_dyndef_aux:w #1 \q_stop }
\ExplSyntaxOff

% defining it dynamically
\dyndef{\expandafter\newcommand\csname#1\endcsname{#2}}{
    dynmacro=\text{O},
    dynmacroA=\mathbf{0},
}

\dyndef[#1/#2/#3]{\newcommand#1{#2-#3}}{
  \foo/A/B,
}

\begin{document}

\begin{equation}
2^{\text{O}} \times 2^{\dynmacro} + 2^{\dynmacroA}+(\foo)
\end{equation}

\end{document}

enter image description here

As you see, the first example uses the default dynmacro=\text{O}; for the second example, the specification of the argument is #1/#2/#3; the same placeholders are used in the first mandatory argument.


The main problem is that \xdef completely expands its argument, and this adds an \unhbox\voidb@x to your \dynmacro. Using etoolbox's \expandonce you can make the \cmd expand once only (and using \csxdef to make it simpler):

\documentclass{report}
\usepackage[english]{babel}
\usepackage{pgffor}
\usepackage{amsmath}
\usepackage{etoolbox}

% defining a plain macro
\def\macro{\text{O}}

% defining it dynamically
\foreach \mname/\cmd in {%
    dynmacro/\text{O},%
    } {%
    \csxdef{\mname}{\expandonce\cmd}%
}

\begin{document}

\begin{equation}
    2^{\text{O}} \times % okay
    2^{\macro} \times   % okay
    2^{\dynmacro}       % okay :)
\end{equation}

\end{document}

You can use a loop not encapsulating items in macros

\documentclass{report}
\usepackage[english]{babel}
\usepackage{pgffor}
\usepackage{amsmath}
\usepackage{xinttools}

% defining a plain macro
\def\macro{\text{O}}

\makeatletter % for LaTeX's \@namedef
% defining it dynamically
\xintForpair #1#2 in {%
  (dynmacro, \text{O})%
  % add more comma-separated "pairs" if needed
  % (no ending comma, though, only to separate pairs)
}\do
{%
    \@namedef{#1}{#2}%
}
\makeatother

\begin{document}

\begin{equation}
    2^{\text{O}} \times % okay
    2^{\macro} \times   % okay
    2^{\dynmacro}        % this one is also okay
\end{equation}

% \showoutput
\end{document}

enter image description here