[Tex/LaTex] What’s the best practice way to test whether parameter is empty

best practicesparameterstex-core

I have a macro that might work either in LaTeX or in plain TeX and I'd like to test whether one of its parameters, which presumably should be just text, is empty. Currently I do it this way:

\bgroup
\setbox0=\hbox{#1}
\ifdim\wd0=0pt
    it is empty
\else
    it is not empty
\fi
\egroup

And I wonder whether this is “proper” way or whether there is some other, more “best practices like”, one.

Thx.

Best Answer

The method of the question

\bgroup
\setbox0=\hbox{#1}
\ifdim\wd0=0pt
    it is empty
\else
    it is not empty
\fi
\egroup

has several problems, if used for a general test:

  • \setbox0\hbox{#1} leak color specials, when #1 contains top level \color commands, because the color macros use \aftergroup to reset the color after the current group (\hbox). This can be fixed for both plain TeX and LaTeX by using an additional group:

    \setbox0=\hbox{\begingroup#1\endgroup}
    

    In LaTeX \sbox0{#1} can be used.

  • #1 can contain material, but the overall width is zero, examples:

    \hbox{$\not$}% the width of the glyph `\not` is zero so that `\not=` forms the "not equals" symbols
    \hbox{\rlap{Text}}
    
  • #1 can contain so much material, that the width overflows. TeX does not throw an error, but the width can be zero accidentally again.

  • The width of the \hbox depends on the font. For example, TikZ sets \nullfont in its environments, thus the width would always be zero.

  • \bgroup and \egroup are the macro form for { and }. They have a serious side effect in math, that they form a math subformula, which behaves as math ordinary atom and influences the horizontal spacing. This can be fixed by using \begingroup and \endgroup.

Test based on macro definition

#1 can be put into a macro and then the macro can be tested for emptiness:

\def\param{#1}%
\ifx\param\empty
  The parameter is empty.%
\else
  The parameter is not empty.%
\fi

This is not perfect yet, because #1 might contain #, which is problematic in macro definitions, because they need to be doubled and numbered. This can be avoided by the use of a token register:

\toks0={#1}%
\edef\param{\the\toks0}% No full expansion, the token register is unpacked only
\ifx\param\empty
...

In LaTeX \@empty can be used, but it also provides plain TeX's \empty.

The side effects of setting \toks0 and defining \param can be removed by using a group:

\begingroup
  \toks0={#1}%
  \edef\param{\the\toks0}%
\expandafter\endgroup
\ifx\param\empty
...

This solution works in both plain TeX and LaTeX; e-TeX is not used. Because of the assignment and definition the code is not expandable.

Expandable tests

If e-TeX is available, \detokenize allows a safe expandable method, see also the answer of PhilipPirrip:

\if\relax\detokenize{#1}\relax
  The parameter is empty.%
\else
  The parameter is not empty.%
\fi

Because \detokenize converts is argument to simple characters with category code other (same as digits) and the space (in case of the space), it does not contain any command tokens and other problematic stuff, which could break \if.

Without e-TeX the test should not use \if, but \ifx:

\ifx\relax#1\relax
...

However, a macro with meaning \relax might be present at the start of parameter #1. Therefore \relax should be replaced by something, which is unlikely to be used in #1, examples:

\def\TestEmptyFence{TestEmptyFence}

\ifx\TestEmptyFence#1\TestEmptyFence
...

Or Donald Arseneau (url.sty, ...) often uses a character with unusual catcode:

\begingroup
  \catcode`Q=3 %
  \gdef\MyEmptyTestMacro#1{%
    \ifxQ#1Q%
    ...
   }%
 \endgroup % restore catcode of Q

However, these expandable tests without e-TeX can be broken, e.g., if #1 contains unmatched \if commands. To reduce these problems, see the much more elaborate answer of Ulrich Diez.

I have marked the best solutions, non-expandable without e-TeX and expandable with e-TeX, by adding a quote environment to the code block.

Improvement of the if branches

\def\foobar#1#2#3{%
  \if...
    #2% if true
  \else
    #3% otherwise
  \fi

can be improved, because the code has the limitation, that #2 is followed by \else and #3 is followed by \fi. Thus both #2 and #3 cannot contain macros at the end, which expect following parameters, e.g.:

\foobar{...}{\textit}{\textbf}{Hello}

Instead of Hello, \textit gets \else and \textbf gets\fi` as parameter, breaking the code.

The standard way is finishing the \if construct first and selecting the argument via \@firstoftwo and \@secondoftwo:

\def\foobar#1{%
  \if...
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi
}

The \expandafter closes the current if branch first. The macros \@firstoftwo and \@secondoftwo are defined in LaTeX:

\long\def\@firstoftwo#1#2{#1}
\long\def\@secondoftwo#1#2{#2}

Plain TeX does not have an equivalent, thus they need to be defined there.