[Tex/LaTex] Why isn’t a command defined by \newcommand with an optional argument expandable

expansionmacrostex-core

In trying to fix a problem with a nested macro repeatedly calling itself, I tried expanding the inner call before passing it as an argument to the outer one. This failed miserably because the macro was defined using \newcommand with optional arguments. So my questions:

Why isn't something defined as \newcommand{\test}[2][]{} expandable?

Is there an expandable alternative?

Here's a simple example:

\documentclass{article}

\newcommand{\test}[2]{got #1 and #2}
\newcommand{\testopt}[2][nothing]{got #1 and #2}

\begin{document}

\edef\result{\test{one}{two}}
\result

\edef\result{\testopt{two}}
\result

\end{document}

Best Answer

As egreg explains, a fully robust check for optional arguments must use \futurelet and cannot be expandable (in fact, \newcommand does not make a fully correct check: try \let\lbrack[\testopt\lbrack... with your definition of \testopt, but it is possible to fix it).

Expandably, there are two possibilities to look ahead: either grab an undelimited argument, or grab a delimited argument. The first one removes braces: \testopt{[}a]{b} will be misrecognized as identical to \testopt[a]{b}. For the second method we need to decide until what tokens we should grab, and those tokens must be present. One possibility, since the second argument of \testopt is mandatory, is to force the user to put it in braces, and grab until the first open brace.

  • Undelimited argument: we need a test for emptyness (\detokenize sets all the catcodes to 12 or 10, different from the catcode of $). Then define \testopt to grab one argument, and compare it with [: if it does not contain [, then there was no optional argument, and #1 is the mandatory argument. Otherwise, test if it is alone in #1 (slightly dirty code to cater for the case \testopt{{}[}), in which case we consider that there was an optional argument.

    \makeatletter
    \newcommand{\@ifstrempty}[1]{%
      \ifcat$\detokenize{#1}$%
        \expandafter\@firstoftwo
      \else
        \expandafter\@secondoftwo
      \fi}
    \newcommand{\testopt@do}[2]{got #1 and #2}
    \newcommand{\testopt}[1]{%
      \expandafter\@ifstrempty
      \expandafter{\testopt@i#1[}
        {\testopt@do{nothing}{#1}}
        {\testopt@ii{#1}}}
    \long\def\testopt@i#1[{}
    \newcommand{\testopt@ii}[1]{%
      \expandafter\expandafter\expandafter\@ifstrempty
      \expandafter\expandafter\expandafter
        {\expandafter\testopt@iii\detokenize{#1}}
        {\testopt@iv#1}
        {\testopt@do{nothing}{#1}}
    \long\def\testopt@iii#1[{#1}
    \long\def\testopt@iv[#1]{\testopt@do{#1}}
    
  • Delimited argument: grab until a left brace, test if that starts with a [.

    \long\def\testopt#1#{%
      \@ifstrempty{#1}{\testopt@do{nothing}}
        {%
          \expandafter\@ifstrempty\expandafter{\testopt@i#1[}
            {\testopt@do{nothing}#1}
            {\testopt@iv#1}%
        }%
    }