9

Here's a MWE that creates spurious whitespace:

\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\newcommand\aetest[2][\relax]{%%
  \def\aejunk{<#2>}%%
  \ifx\relax#1
  \else
    \def\aejunk{(#2:#1)}%%
  \fi
  \fbox{\aejunk\rule[-0.5ex]{0pt}{2.5ex}}%%
  }

\begin{document}

  \aetest[world]{hello}%%
  \aetest{ciao}

\end{document}

enter image description here

I know how to fix this. I just need to rewrite the \ifx\relax#1 as

\ifx\relax#1%%

But I would have thought, since the only time this test is true is when #1 is \relax, that the following whitespace would have been absorbed and the end of line comments should be unnecessary. I assume there's something about the tokenization process I'm not understanding. Could someone explain what's happening here that allows whitespace to creep in?

Mico
  • 506,678
A.Ellett
  • 50,533
  • The space is added when you invoke \aetest{ciao}, since \relax tests true, and the space is then added, prior to the <ciao> fbox. You can test this by replacing the line-ending "space" with a .% and seeing when the dot shows up. When you comment the ciao line, the dot goes away. So that is its source, and indeed, as I said, it makes sense because the \ifx\relax#1 test is true in the ciao case. – Steven B. Segletes Jan 22 '15 at 21:07
  • haven't tried it, but i think the space is the "unprotected" #1 in \ifx\relax#1. i'd reverse this: \ifx#1\relax. – barbara beeton Jan 22 '15 at 21:08
  • 1
    @barbarabeeton If I reverse, I have larger headaches: such as when #1 is aa. – A.Ellett Jan 22 '15 at 21:17
  • @StevenB.Segletes I tested something similar. But where is the whitespace coming from. As I understand expansion, \aetest{ciao} should expand to \def\aejunk{<ciao>}%%\ifx\relax\relax.... and consequently I would expect that the second \relax will gobble up the trailing whitespace of that line. – A.Ellett Jan 22 '15 at 21:22
  • @A.Ellett -- yes, wasn't paying attention. i retract that suggestion -- much safer to add % after #1. – barbara beeton Jan 22 '15 at 21:31
  • No, \ifx\ifx\relax does not gobble up additional white space if true, only if false. Try \ifx\relax\relax This is true\fi to see that what follows a true condition is not gobbled. – Steven B. Segletes Jan 22 '15 at 22:13

2 Answers2

6

OK, I think I see what's happening.

When I define \aetest

\newcommand\aetest[2][\relax]{%%
  \def\aejunk{<#2>}%%
  \ifx\relax#1
  \else
    \def\aejunk{(#2:#1)}%%
  \fi
  \fbox{\aejunk\rule[-0.5ex]{0pt}{2.5ex}}%%
  }

There is, as of yet, no expansion of #1 or #2 yet to happen. So, when \aetest is defined, the end of line character after #1 gets tokenized. Since once the space has been tokenized, it will not disappear later: such as when

\aejunk{ciao}

gets expanded.

A.Ellett
  • 50,533
  • 1
    It was quite obvious from the beginning: 1 is not a control sequence, nor TeX is looking for a <number>, so the end-of-line is converted to a space. Don't use that test in order to see whether the optional argument hasn't be supplied: [] is much safer and the test can be \ifx\hfuzz#1\hfuzz or my preferred \if\relax\detokenize{#1}\relax – egreg Jan 22 '15 at 21:42
  • @egreg obviously ouch! I forgot about the \if\relax\detokenize{#1}\relax trick. – A.Ellett Jan 22 '15 at 22:13
6

When TeX is absorbing the replacement text for a definition, it does no expansion whatsoever (unless you're using \edef or \xdef). In your replacement text you have

\ifx\relax#1

and the end of line counts as a space, because it is converted to a space during tokenization since it doesn't follow a control word. The space will not be ignored during expansion at macro usage time, because TeX isn't looking for <one optional space>, which it does after numeric constants or after the = in assignments where = is optional and some other cases where even entire strings of space tokens are ignored.

The fact that in the “no optional argument” case #1 is replaced by \relax is completely irrelevant, because spaces are ignored after control words only during tokenization: space tokens that creep in after control words are not ignored.

Note also that in the definition you are doing, TeX actually does two \def commands. First it does

\expandafter\def\expandafter\aetest\expandafter{%
  \expandafter\@protected@testopt
  \expandafter\aetest
  \csname\string\aetest\endcsname{\relax}%
}

and then

\expandafter\def\csname\string\aetest\endcsname[#1]#2{%
  \def\aejunk{<#2>}%%
  \ifx\relax#1
  \else
    \def\aejunk{(#2:#1)}%%
  \fi
  \fbox{\aejunk\rule[-0.5ex]{0pt}{2.5ex}}%%
  }

In the second definition, there's no trace of #1 being \relax in the “no optional argument” case. But, as I said, this is irrelevant to begin with.

If you want a safe test for the “no optional argument”, use xparse:

\usepackage{xparse}

\NewDocumentCommand{\aetest}{om}{%
  \IfNoValueTF{#1}
    {\def\aejunk{<#2>}}
    {\def\aejunk{(#1:#2)}}%
   \fbox{\aejunk\rule[-0.5ex]{0pt}{2.5ex}}%
}

so \aetest{ciao} and \aetest[]{ciao} would give different results.

On the other hand, if you just want to test if the optional argument is not supplied or it is empty, the usual

\if\relax\detokenize{#1}\relax

test is better.

egreg
  • 1,121,712