Index duplication error caused by tokenizing an optional argument in a macro

I'm trying to create a command \term for printing a definition and then indexing it. It has one optional argument and one mandatory argument; if the optional argument is absent, the mandatory argument is used in its place. Here's the code I have:

% .tex file
\documentclass{article}
\usepackage{makeidx}
\usepackage{xparse}

\makeindex
\NewDocumentCommand{\term}{o m}{
{\bfseries #2}
\IfNoValueTF{#1}{
\index{#2}
}{
\index{#1}
}
}

\begin{document}
A \term[pistar-boojum@$\pi_*$-boojum]{$\pi_*$-boojum} is\dots

Now I'm going to index $\pi_*$-boojum again.\index{pistar-boojum@$\pi_*$-boojum}

\printindex
\end{document}


The problem is, these two entries are displayed separately in the index:

Looking at the .ind file pinpoints the problem:

% .ind file
\begin{theindex}

\item $\pi _*$-boojum, 1
\item $\pi_*$-boojum, 1

\end{theindex}


I think the source of this issue is that \IfNoValueTF tokenizes the optional argument, inserting the space between \pi and _*. Then, makeindex treats \pi _* and \pi_* as different things.

Hence, my question is: is there a way to use the mandatory argument as the default value of an optional argument without tokenizing the optional argument? A priori it should be possible to check whether a string is empty without processing it, but I'd also be content to know that it's impossible in LaTeX.

Here are some things I tried:

• Using \IfNoValueTF as above
• Using \@dblarg as in this answer
• Using the ifmtarg package.
• Wrapping in \mbox as in this answer
• Using \detokenize as in this answer
• Aside: with an up-to-date xparse you can do {O{#2}m}. – Joseph Wright Jun 27 '17 at 17:31
• @JosephWright this code is part of a project with multiple collaborators, not all of whom have an up-to-date version of xparse. If nothing else works, I'll use that, but I'd prefer a solution that works on a wider range of distributions. Thank you for the suggestion! – Arun Debray Jun 27 '17 at 17:35
• Won't help with the matter at hand, in terms of tokenization, but won't hurt either: I was just noting in case you have simpler use cases where it's useful. – Joseph Wright Jun 27 '17 at 17:37

Your problem here isn't xparse, it's \index, or rather trying to read already-tokenize material. The latter quite deliberately reads arguments more-or-less verbatim. As with all verbatim things, this only works if the input has not previously been tokenized. To do that and wrap \index into another command, you will have to do a verbatim read. We don't have 'verbatim optional arguments' in xparse (at least at the moment!), so one might do something like

\NewDocumentCommand{\term}{t[}{%
\IfBooleanTF#1{\termauxi]}{\termauxii}%
}
\NewDocumentCommand\termauxi{vm}{%
\index{#1}%
{\bfseries #2}%
}
\NewDocumentCommand\termauxii{v}{%
\index{#1}%
{\bfseries\scantokens{#1}}%
}


The above yields .idx file

\indexentry{pistar-boojum@$\pi_*$-boojum}{1}
\indexentry{pistar-boojum@$\pi_*$-boojum}{1}
\indexentry{$\pi_*$-boojum}{1}


from the input in the question plus a third test line:

A \term[pistar-boojum@$\pi_*$-boojum]{$\pi_*$-boojum} is\dots

Now I'm going to index $\pi_*$-boojum again.\index{pistar-boojum@$\pi_*$-boojum}

A \term{$\pi_*$-boojum} is \dots


Of course, \scantokens can be a bit risky but it does allow us to have a more-or-less verbatim mandatory argument ...

• This does not solve my problem. I would like both entries to map to the same index entry pistar-boojum@$\pi_*$-boojum (e.g. if they were on different pages, I would get a single index entry listing those two pages), rather than one mapping to $\pi_*$-boojum (indexed under $) and the other mapping to pistar-boojum@$\pi_*\$-boojum (indexed under p). – Arun Debray Jun 27 '17 at 17:57
• @ArunDebray See updated approach – Joseph Wright Jun 27 '17 at 18:10
• I've used xparse here as it was in the question but since we need low-level verbatim manipulation one might simply go with \@ifnextchar plus \@sanitize and do it all 'by hand'. – Joseph Wright Jun 27 '17 at 18:12
• The updated version looks like it works. Thanks so much! – Arun Debray Jun 27 '17 at 18:38