2

I am confused about how expansion works in \addtocontents, or more precisely in \protected@write. As I understand it, \addtocontents should essentially expand its second argument with \protected@edef and write the result to the aux file. With this understanding, I would expect the behavior to be (up to a \protected@) the same as

\iow_shipout:Ne \@auxout
  {
    \exp_not:N \@writefile { #1 } { #2 }
  }

But as the following MWE shows, they behave differently when trying to prevent expansion inside their argument with \exp_not:n.

\documentclass{article}

\makeatletter

\ExplSyntaxOn \NewDocumentCommand { \naiveaddtocontents } { m m } { \iow_shipout:Ne @auxout { \exp_not:N @writefile { #1 } { #2 } } } \ExplSyntaxOff

\def\abc{some text}

\begin{document}

bla

\ExplSyntaxOn

% this does what I expect \protected@edef \l_tmpa_tl { \exp_not:n { \abc } } \tl_show:N \l_tmpa_tl

% so does this \naiveaddtocontents{ lof }{ \exp_not:n { \abc } }

% this does not \addtocontents{ lof }{ \exp_not:n { \abc } }

\ExplSyntaxOff

\end{document}

The aux file looks like

\relax 
\@writefile {lof}{\abc }
\@writefile{lof}{some text}
\gdef \@abspage@last{1}

So \naiveaddtocontents is preventing expansion with \exp_not:n, but \addtocontents is not. For my use case where I don't want the argument expanded at all, I can just use this \naiveaddtocontents. But why doesn't \exp_not:n work as I incorrectly expected in \addtocontents?

mbert
  • 4,171

2 Answers2

3

Let's look at the definition of \addtocontents:

% latex.ltx, line 14289:
\long\def\addtocontents#1#2{%
  \protected@write\@auxout
      {\let\label\@gobble \let\index\@gobble \let\glossary\@gobble}%
      {\string\@writefile{#1}{#2}}}

With your naive definition you'd have big problems if you have a \label in the second argument to \addtocontents. But let's leave this out, because it's a technicality.

Suppose you want \addtocontents{toc}{Hey, this is \textbf{boldface}}.

\documentclass{article}

\makeatletter \ExplSyntaxOn \NewDocumentCommand { \naiveaddtocontents } { m m } { \iow_shipout:Ne @auxout { \exp_not:N @writefile { #1 } { #2 } } } \ExplSyntaxOff \makeatother

\begin{document}

Some text

\addtocontents{toc}{Hey, this is \textbf{boldface}}

\naiveaddtocontents{toc}{Hey, this is \textbf{boldface}}

\end{document}

The console will print

(\end occurred when \ifx on line 21 was incomplete)
(\end occurred when \ifx on line 21 was incomplete)
(\end occurred when \ifx on line 21 was incomplete)
(\end occurred when \ifmmode on line 21 was incomplete)</usr/local/texlive/2023
/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb>

and the aux file will have

\relax
\@writefile{toc}{Hey, this is \textbf  {boldface}}
\@writefile {toc}{Hey, this is \protect \unhbox \voidb@x \bgroup \edef l3backend-pdftex.def{boldface}\let \futurelet \@let@token \let \protect \relax \edef cmr{cmr}\edef cmss{cmss}\edef cmtt{cmtt}\def ##1,b,{}\series@check@toks {,ulm,elm,lm,slm,mm,sbm,bm,ebm,ubm,muc,mec,mc,msc,msx,mx,mex,mux,{}{},b,}\edef {}\edef b{b}\def ##1,m,{}\series@check@toks {,ulm,elm,lm,slm,mm,sbm,bm,ebm,ubm,muc,mec,mc,msc,msx,mx,mex,mux,{}{},m,}\edef {}\edef m{m}\protect \let }
\gdef \@abspage@last{1}

Not really what you want to see, do you? Let's fix it.

\documentclass{article}

\makeatletter \ExplSyntaxOn \NewDocumentCommand { \naiveaddtocontents } { m m } { \iow_shipout:Ne @auxout { \exp_not:N @writefile { #1 } { \text_expand:n { #2 } } } } \ExplSyntaxOff \makeatother

\newcommand{\abc}{some text}

\begin{document}

Some text

\addtocontents{toc}{Hey, this is \textbf{boldface} and \abc}

\naiveaddtocontents{toc}{Hey, this is \textbf{boldface} and \abc}

\end{document}

Now the aux file will have

\relax
\@writefile{toc}{Hey, this is \textbf  {boldface} and some text}
\@writefile {toc}{Hey, this is \textbf {boldface} and some text}
\gdef \@abspage@last{1}

Basically, \text_expand:n will do roughly the same as \protected@edef does, but leaving the resulting token list wrapped in \unexpanded.

If you do \exp_not:n (that is, \unexpanded), TeX will do no expansion, so you get \abc and not its expansion.

Without \makeatletter and \makeatother:

\ExplSyntaxOn
\NewDocumentCommand { \naiveaddtocontents } { m m }
  {
    \iow_shipout:ce { @auxout }
      {
        \token_to_str:c { @writefile } { #1 } { \text_expand:n { #2 } }
      }
  }
\ExplSyntaxOff

Instead of \token_to_str:c you can use \exp_not:c

egreg
  • 1,121,712
2

The command \addtocontents uses \protected@write. Below is the definition of \protected@write from source2e:

\long\def \protected@write#1#2#3{%
      \begingroup
       \let\thepage\relax
       #2%
       \let\protect\@unexpandable@protect
       \edef\reserved@a{\write#1{#3}}%
       \reserved@a
      \endgroup
      \if@nobreak\ifvmode\nobreak\fi\fi
}

The relevant part is \edef\reserved@a{\write#1{#3}}%. If the above definition of \protected@write would be added in the document and in the previous line, \edef is replaced by \def then \addtocontents{ lof }{ \exp_not:n { \abc } } gives \@writefile{lof}{\abc } in the .aux file.

However with \edef, an additional expansion occurs such that \addtocontents{ lof }{ \exp_not:n { \abc } } gives \@writefile{lof}{some text} in the .aux file.

Therefore with \edef, the example works if an additional \exp_not:n is added: \addtocontents{ lof }{ \exp_not:n { \exp_not:n { \abc } } } gives \@writefile{lof}{\abc } in the .aux file.

matexmatics
  • 4,819