5

I'm currently trying to update some LaTeX code. I want to replace \newcommand with \NewDocumentCommand. Most of the time it works fine, but I have problems with the following example:

\documentclass{article}
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}

\newcommand{\coords}{(1,1)}  % works
%\NewDocumentCommand{\coords}{}{(1,1)}  % does not work

\begin{document}
\begin{tikzpicture}
    \begin{axis}
        \addplot coordinates {(0,0) \coords};
    \end{axis}
\end{tikzpicture}
\end{document}

With \newcommand it works fine, but with \NewDocumentCommand I get the error

Incomplete \ifx; all text was ignored after line 11.

What is the difference between \newcommand and \NewDocumentCommand, that causes the error? If I understand correctly, \NewDocumentCommand always declares robust commands. Is this the problem here?

How can I still use \NewDocumentCommand? (Or something similar that allows to define commands with multiple optional arguments with custom delimiters)

4
  • 1
    How about \NewExpandableDocumentCommand? ;-)
    – gusbrs
    Jun 7, 2022 at 16:28
  • @gusbrs Thanks, but that doesn't seem to work either :/
    – user263192
    Jun 7, 2022 at 16:34
  • \NewDocumentCommand is robust and typically not suited for this kind of commands. Jun 7, 2022 at 16:37
  • @user187803 Indeed it doesn't. I don't know the culprit then.
    – gusbrs
    Jun 7, 2022 at 16:39

2 Answers 2

3

pgfplots does one-step expansion when it encounters a control sequence in <coordinates list> from input \addplot ... coordinates {<coordinates list>}, see \pgfplots@foreach@plot@coord@ITERATE@.

This looks fine and fits commands defined by \NewExpandableDocumentCommand just right, but in general one-step expansion is not enough. When a macro (or the expansion of its replacement text) contains conditionals like \ifx, one-step expansion will leave incomplete \ifxxx.

\documentclass{article}
\begin{document}
% show result of one-step expansion of `\ifx\a\b true\else false\fi`
\ttfamily
\detokenize\expandafter{\ifx\a\b true\else false\fi} % leave "false\fi"
\end{document}

Unfortunately, \NewExpandableDocumentCommand-defined commands do use conditionals. In OP's example, for \NewExpandableDocumentCommand{\coords}{}{(1,1)} the first encountered conditional is \token_if_eq_charcode:NNT.

Since <coordinates list> ignores spaces and a coordinate won't start with digits, +, or -, it seems safe to do a \romannumeral expansion. Explanations of \romannumeral-`\0 used below can be found on this site.

\documentclass{article}
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}

\usepackage{xpatch}
\makeatletter
% originally defined in pgfplotscoordprocessing.code.tex
\xpatchcmd\pgfplots@foreach@plot@coord@ITERATE@
  {
    \if\noexpand\pgfplots@foreach@plot@coord@ITERATE@tok\noexpand\anymacro
          % Ah. #1 has the same character (!) code as \anymacro,
          % that means it is a macro! Expand it:
      \def\pgfplots@loop@next{%
        \expandafter\pgfplots@foreach@plot@coord@ITERATE#1}%
  }{%
    \if\noexpand\pgfplots@foreach@plot@coord@ITERATE@tok\noexpand\anymacro
      \def\pgfplots@loop@next{%
        \expandafter\pgfplots@foreach@plot@coord@ITERATE\romannumeral-`\0#1}%
  }
  {}{\PatchFailed}
\makeatother

%\newcommand{\coords}{(1,1)}  % works
\NewExpandableDocumentCommand{\coords}{}{(1,1)} % works now

\begin{document}
\begin{tikzpicture}
    \begin{axis}
        \addplot coordinates {(0,0) \coords};
    \end{axis}
\end{tikzpicture}
\end{document}

enter image description here

PS: I'm curious about why you'd like to define \coord by xparse (or, as it's now part of the kernel, the ltcmd module) commands, when \def or \newcommand already fits the requirement.

5
  • 1
    Thank you for your detailed answer. About your PS: The example I give in my question is what I identified as one core problem, so it is stripped down very much. However, eventually, I want to have a macro \foo that accepts (optional) arguments of the form \foo<...>[...]{...} (hence \NewDocumentCommand) and, also, \foo will be used more generally, not only in pgfplots. (Actually, I'm talking about the macro \py in the pyluatex package of which I'm the author. Currently, it doesn't accept optional arguments).
    – user263192
    Jun 7, 2022 at 19:53
  • 1
    My suggestion is to construct a new no-arg macro and append needed data to it (in \py). Supporting arbitrary arg-spec xparse commands in anywhere is a nightmare, but supporting a no-arg macro is much easier. Jun 7, 2022 at 20:23
  • With the no-arg approach you mean something similar to @frougon's answer? If I get your answer right, without patching pgfplots, there is also no way to get it working with an optional argument to \newcommand, because with an optional argument, one-step expansion does no longer expand the macro fully. Do you agree?
    – user263192
    Jun 8, 2022 at 7:30
  • Yes, I mean sth like \def\macro{<result of python code>}. The opt-arg detection used by \newcommand-defined commands relies on \futurelet hence is unexpandable. \NewExpandableDocumentCommand-defined commands use an expandable way to detect opt-arg, with some restrictions (see texdoc xparse, sec. 3.3). But I believe that new way relies on conditionals (like \if) hence still won't work in pgfplots, without patching. Jun 8, 2022 at 18:13
  • Thanks a lot for the insights
    – user263192
    Jun 8, 2022 at 18:42
2

\NewDocumentCommand defines \protected macros. As such, they aren't expanded inside \edef, \write, etc. The macros defined by \NewExpandableDocumentCommand don't have this property, so it is a natural idea to try it here... but this doesn't work, as mentioned in the comments. Finding out why may be complicated, I'll leave this to the experts!

But here is a strategy that essentially always works when you can't do what you want in a “sensitive place” (typically, an expansion-only context):

  1. You do the “complex work” before TeX reaches the sensitive place. You should choose a place where TeX performs expansion and execution; this way, you can use all the tools you want for the task (including \NewDocumentCommand or any other non-expandable tooling).

  2. From this place, you store the result in a macro (in expl3, the “result macro” in *_set_* functions like \seq_set_split:Nnn is almost always the first argument; I keep this convention, this helps memory).

  3. You put the “result macro” (a single control sequence token) in the “sensitive place”. Expanding a macro once is a very basic operation; this works in every context where expansion takes place.

Let's apply this strategy to your problem:

\documentclass[tikz,border=2mm]{standalone}
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}

\NewDocumentCommand{\prepareStuff}{ m O{#3} m }{%
  \def#1{(#2,#3)}% you could compute things here using \edef or \cs_new:Npx
}

\begin{document}
\begin{tikzpicture}
  \prepareStuff{\coords}{1}
  \begin{axis}
    \addplot coordinates {(0,0) \coords};
  \end{axis}
\end{tikzpicture}
\end{document}

enter image description here

Of course, you could do \prepareStuff{\coords}[1]{1} with the same effect here (showing you can use optional args as requested, or anything \NewDocumentCommand allows) and put this call in other places, like before \begin{document}.

Note: I used \def inside \prepareStuff in case you want to call \prepareStuff several times with the same “result macro” (here, \coords). For cases where this doesn't make sense, you can of course use \newcommand.

Also, \def performs a local definition (its effect is limited to the current TeX group); if you want a global one, use \gdef instead.

2
  • Nice! You could use O{#3} instead of hard-coding the default there.
    – gusbrs
    Jun 7, 2022 at 17:50
  • This is in, thanks for the idea!
    – frougon
    Jun 7, 2022 at 18:11

You must log in to answer this question.