17

When I thought I understood the concept of the doc package, LaTeX proved me wrong.

I was trying to make a file to use with ltxdoc when I realised I could not use conditionals in the preamble because they would break things by making TeX read stuff in the preamble from that point on, as it shouldn't. Then I tried to MWE-ify that code and stumbled upon a bigger problem.

When I thought I understood the concept of conditionals and expansion, TeX proved me wrong :)

Consider the following code (lines numbered to make reading easier):

01 % \tracingall
02 % \iffalse meta-comment    ^^A This /iffalse
03 \documentclass{ltxdoc}
04 \def\showfi{\immediate\write0{{/fi\ on line \the\inputlineno}}}%
05 \title{Doc Test}%
06 \newif\ifitsme\itsmetrue
07 \ifitsme\author{Me :)}%
08 \else   \author{Not me :/}%
09 \fi\showfi
10 \begin{document}%
11   \DocInput{\jobname}%
12   \tracingnone
13 \end{document}%
14 % \fi\showfi               ^^A should match this /fi
15 % \fi\showfi               ^^A but it's matching this one
16 % \tracingnone
17 % \maketitle
18 % \endinput

When LaTeX starts, comments are comments, so everything goes normally: load ltxdoc; define the \showfi (to log on what line the \fi appears); give it a title to avoid an error; define a new \ifitsme, set it to true, which makes Me :) the author. Then I call \showif just in case (not important now).

So far so good...

Then I \DocInput this same file. Not comments aren't comments anymore and things go off the rails :)

\DocInput starts reading the file from start. I turn on full-logging with \tracingall and then comes the usual \iffalse meta-comment from .dtx files. This \iffalse usually goes up to the \fi right after the \end{document} to continue reading the source.

What I think it should do:

This won't be the case because when TeX finds a false conditional it skips code without expansion until the next \else or \fi, which here is the \else in line 08 (because TeX doesn't expand \ifitsme to find out it's an \iftrue). I would expect TeX to read that \else and execute \author{Not me :/}, then read the \fi from line 09 and show {/fi\ on line 9} in the log (Then another \begin{document} would come up and break things, but this is an error for another day).

What it does:

TeX doesn't skip to the \fi on line 09, neither to the one on line 14, but to an extra \fi (that I added because it would throw an error otherwise) on line 15. The log shows:

{\iffalse: (level 1) entered on line 2}
{false}
{\fi: \iffalse (level 1) entered on line 2}

\showfi ->\immediate \write 0{{/fi\ on line \the \inputlineno }}
{\immediate}
\write->{/fi\ on line \the \inputlineno }
{/fi\ on line 15}

What:

What is TeX actually doing that it does not behave as I expected? Why does it skip not only the 08 \else... 09 \if, but also the 14 \fi, requiring an additional \fi inserted there for no (apparent) good reason?


Code without line-numbers for copy-pasting:

% \tracingall
% \iffalse meta-comment    ^^A This /iffalse
\documentclass{ltxdoc}
\def\showfi{\immediate\write0{{/fi\ on line \the\inputlineno}}}%
\title{Doc Test}%
\newif\ifitsme\itsmetrue
\ifitsme\author{Me :)}%
\else   \author{Not me :/}%
\fi\showfi
\begin{document}%
  \DocInput{\jobname}%
  \tracingnone
\end{document}%
% \fi\showfi               ^^A should match this /fi
% \fi\showfi               ^^A but it's matching this one
% \tracingnone
% \maketitle
% \endinput
12

As you correctly say, when you do pdflatex test.dtx, the file is read with % a comment character. When TeX finds \DocInput, it will basically ignore % at the start of lines and make ^^A a comment character. So your input is essentially

% \iffalse meta-comment    ^^A This /iffalse
\documentclass{ltxdoc}
\def\showfi{\immediate\write0{{/fi\ on line \the\inputlineno}}}%
\title{Doc Test}%
\newif\ifitsme\itsmetrue
\ifitsme\author{Me :)}%
\else   \author{Not me :/}%
\fi\showfi
\begin{document}%
\iffalse meta-comment    ^^A This /iffalse
\documentclass{ltxdoc}
\def\showfi{\immediate\write0{{/fi\ on line \the\inputlineno}}}%
\title{Doc Test}%
\newif\ifitsme\itsmetrue
\ifitsme\author{Me :)}%
\else   \author{Not me :/}%
\fi\showfi
\begin{document}%
  \DocInput{\jobname}%
\end{document}%
\fi\showfi               ^^A should match this /fi
\fi\showfi               ^^A but it's matching this one
\maketitle
\endinput
\end{document}%
% \fi\showfi               ^^A should match this /fi
% \fi\showfi               ^^A but it's matching this one
% \maketitle
% \endinput

Now \iffalse is seen, but \ifitsme is a conditional found twice in skipped text, so two matching \fi are looked for. And this is where things go wrong, because there is no more a matching \fi for \iffalse, unless you add one, like you do.

The \fi in the uncommented line with “should match this /fi” matches \ifitsme after \newif.

Defining conditionals inside conditionals is always open to this kind of problems. Use

\expandafter\newif\csname ifitsme\endcsname

so when the code is inside skipped text there will be no conditional at all.

  • Thanks, I almost got it. I just don't understand one thing: You say The \fi in the uncommented line with “should match this /fi” matches \ifitsme. But \ifitsme is not a proper conditional, but an alias for an \iftrue, right? So it would require TeX to expand \ifitsme to find out it's a conditional, therefore search for a matching \fi. In my understanding TeX doesn't know that \ifitsme is a conditional, so it should use the \fi on line 9 to match the \iffalse, ignoring the \ifitsme as any other control sequence. What is wrong with this statement? – Phelype Oleinik Jun 21 '18 at 14:26
  • 1
    @PhelypeOleinik naturally TeX must know after \newif that \ifitsme is a conditional. conditionals would not be usable (nestable) it this weren't the case. – Ulrike Fischer Jun 21 '18 at 14:31
  • 1
    @PhelypeOleinik No, TeX knows \ifitsme is either \iftrue or \iffalse, because \itsmetrue does \let\ifitsme\iftrue and similarly for the false setting. It's a feature of the language. If you say \let\repeat\fi (which is actually done in the LaTeX kernel), then you can use \repeat wherever \fi is expected. – egreg Jun 21 '18 at 14:31
  • Oooohhh, I got it, the assignment is done with \let, so it doesn't require expansion, then TeX searches for the \fi. Indeed Ulrike, if this weren't the case, conditionals would be useless. Thanks a lot, to all of you :) – Phelype Oleinik Jun 21 '18 at 14:37
12

Your \newif command is inside a \iffalse so it is not seen, and \ifitsme is now either not an "if" at all, or if the \newif has been executed earlier, the \ifitsme behind the \newif counts too and you have one "if" too much:

This works:

\documentclass{article}
\begin{document}

\newif\ifitsme
\iffalse

\ifitsme hello \fi

\fi 
\end{document}

This fails

\documentclass{article}
\begin{document}


\iffalse
\newif\ifitsme
\ifitsme hello \fi

\fi 
\end{document}

This fails too

\documentclass{article}
\begin{document}

\newif\ifitsme

\iffalse
\newif\ifitsme %\newif not seen, but \ifitsme so one fi missing
\ifitsme hello \fi

\fi 
\end{document}

Morale: Better don't use \newif inside \if--\fi.

  • Thanks Ulrike, but I don't follow :/ I understand what happens in your example (maybe because it's much simpler :), the \iffalse is closed by the hello \fi and the other \fi throws an Extra \fi error. But in my example, the \newif is seen because in the "% is comment" pass, the \iffalse on line 1 is not seen and (I presume) everything goes as it should. But then in the "% is ignored" pass, the \iffalse on line 1 should match with the \else on line 8 and the \fi on line 9. But it doesn't, instead it goes two more \fis ahead... – Phelype Oleinik Jun 21 '18 at 13:53
  • @PhelypeOleinik I see what you mean. I added an edit for the docinput case. – Ulrike Fischer Jun 21 '18 at 14:01
  • After egreg's answer I (finally) understood your third example. I need lots of words to understand things :P. Thanks a lot Ulrike :) – Phelype Oleinik Jun 21 '18 at 14:41
9

Here is a technique for more safely skipping stuff. Put it between

\bgroup\catcode2 0 \catcode`\\ 12 ^^Biffalse

and

^^Bfi^^Begroup

What this does is to inhibit \ starting control sequences. So you are sure that whatever you do with \if, \fi, \else, or \if... etc.. in-between this will have nil influenc.

And it assigns to ascii byte 0x02 this meaning of starting control sequences. we can do a safe ^^Biffalse/^^Bfi thing.

I am not using ^^A as doc assigns to it a use as comment character and we want to keep that (especially if the editor highlights appropriately).

I guess with your MWE it would look like this

% \tracingall
% \bgroup\catcode2 0 \catcode`\\ 12 ^^Biffalse
\documentclass{ltxdoc}
\def\showfi{\immediate\write0{{/fi\ on line \the\inputlineno}}}%
\title{Doc Test}%
\newif\ifitsme\itsmetrue
\ifitsme\author{Me :)}%
\else   \author{Not me :/}%
\fi\showfi
\begin{document}%
  \DocInput{\jobname.dtx}%
  \tracingnone
\end{document}%
% \fi\showfi
% ^^Bfi^^Begroup
% \tracingnone
% \maketitle
% \endinput

with the file being saved as test.dtx.

Caveat: although I use doc for my dtx I never use \DocInput (because it led into endless problems with the way I wanted to handle guards, and because I don't want the documentation part (not the one of code implementation, but the user manual) to be "commented-out") so I don't know if that would be your use case.

  • Thanks, this looks safer indeed :) Could you name one package of yours that doesn't use \DocInput as you mentioned? I'm interested in options. I use \DocIput basically because it's the way everyone does, so there are plenty examples to learn from... – Phelype Oleinik Jun 21 '18 at 14:45
  • Most of my packages have a rather complicated dtx structure because it is most of the time self-extracting or with various things such as README.md, or Makefile. Notice that if you want to refer in a README or a CHANGE LOG to a TeX conditional \iffoo your are not going to be able to do it with \csname.... One with a relatively simple dtx is bnumexpr but it does use \iffalse/\fi. etoc uses the technique as explained here (it starts at line 67 in etoc.dtx and skipping ends at line 409). – user4686 Jun 21 '18 at 14:57
  • Okay, I'll take a look. Thanks a lot :) – Phelype Oleinik Jun 21 '18 at 15:09

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.