1

I was trying to use a macro inside of \cite optional argument. I expected the optional argument to be ignored if the macro expands into nothing (i.e, get something like [1]), but sometimes a supposedly empty argument still gives something like [1,].

%\usepackage{natbib,xparse}

\NewDocumentCommand\foo{m}{#1}
%\newcommand\foo[1]{#1}         % similar results
foo
\cite[\foo{}]{ref}              % [1]

\NewDocumentCommand\fooOpt{o m}{#2}
%\newcommand\fooOpt[2][dummy]{#2}  % similar results
fooOpt
\cite[\fooOpt{}]{ref}           % [1,]
\cite[\fooOpt[useless]{}]{ref}  % this one is broken, I suppose because of the nested []

\NewDocumentCommand\fooStar{s m}{\IfBooleanF{#1}{#2}}
fooStar
\cite[\fooStar{}]{ref}          % [1,]
\cite[\fooStar*{ignored}]{ref}  % [1,]

I found out that natbib uses the \if command to discriminate between empty and non-empty argument. If you look at the sources of natbib, you will find \if*#2*\else\NAT@cmt#2\fi (which implies that the command will have unexpected behavior if the optional argument starts with *). The problem seems to be that some of the macros I defined still created a token, despite the result being empty.

Based on that, I created this test case:

\def\YN[#1]{\if*#1*Y\else N\fi}
\YN[]           % Y
\YN[{}]         % Y
\YN[{{}}]       % N (weirdly, \cite[{{}}]{ref} produces [1], not [1,]
\YN[{{{}}}]     % N
\YN[\foo{}]     % Y
\YN[\fooOpt{}]  % N
\YN[\fooStar{}] % N

How can I change \fooOpt and \fooStar (and potentially any macro with a complex signature that contains at least one optional argument) such that this simple test returns Y? I am particularly interested in the optional star argument (\NewDocumentCommand{…}{s …}{…}). The def of \YN cannot be changed (because I can't edit the natbib package).

I am somewhat in the dark about how tokens work, and I don't understand the mechanisms behind this weird behavior.

1
  • 1
    Try using \NewExpandableDocumentCommand instead of \NewDocumentCommand.
    – Skillmon
    Commented Jan 25, 2023 at 5:35

1 Answer 1

3

The issue is that the parsing for an optional star or argument is not implemented expandably if you don't explicitly ask for it. In case of the first macro \foo the m-argument is actually grabbed expandably (even though the macro itself is defined \protected, but \if doesn't care for \protected and will expand them regardless -- however things that use \futurelet or any other assignment will not be expanded and result in some tokens that are not equal to * for \if).

You can change the implementation used to parse for optional arguments to a fully expandable one by using \NewExpandableDocumentCommand, note however that the macro has to take one mandatory argument after all the optional stuff with this. Also parsing is a slight bit less stable with it (no way to distinguish [ from {[} in expandable parsing, so \fooOpt{[} will break).

At least your test cases work out that way:

\documentclass{article}

\NewExpandableDocumentCommand\foo{m}{#1}
\NewExpandableDocumentCommand\fooOpt{o m}{#2}
\NewExpandableDocumentCommand\fooStar{s m}{\IfBooleanF{#1}{#2}}

% for things outside of this MWE I wouldn't suggest using `r` arguments,
% instead stick to `m` for required arguments
\NewDocumentCommand\YN{r[]}{\if*#1*Y\else N\fi\par}

\begin{document}
\YN[]           % Y
\YN[{}]         % Y
\YN[{{}}]       % N (weirdly, \cite[{{}}]{ref} produces [1], not [1,]
\YN[{{{}}}]     % N
\YN[\foo{}]     % Y
\YN[\fooOpt{}]  % Y and no longer N
\YN[\fooStar{}] % Y and no longer N
\end{document}
1
  • This worked indeed! To bad that I was actually using commands with optional arguments at the end, but I guess this is the price to pay.
    – User9123
    Commented Jan 25, 2023 at 6:23

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .