The command \index
is expected to get its argument by reading and tokenizing things from the .tex-input file under \@sanitize
-category-code-régime. (\@sanitize
-category-code-régime means: Space-character, \
, $
, &
, #
, ^
, _
, %
and ~
have category code 12(other).) This is for several reasons—e.g.,
- avoiding unwanted expansion of expandable tokens.
- avoiding the appending of a space-character when unexpanded-writing a control-word-token to external file.
But with your command \mani
the command \index
does get its argument passed from \mani
. When \mani
gathers/composes \index
's argument, the tokens forming that argument are not tokenized under \@sanitize
-category-code-régime but are tokenized under normal category-code-régime.
Beneath other things tokenizing under normal category-code-régime implies that phrases like \man
get tokenized as control-word-tokens, not as character-sequences \
, m
, a
, n
. When control-word-tokens get written unexpanded to text-file, e.g., some .idx
-file belonging to the process of creating the index, a space-character will be appended. I.e., the character-sequence \
, m
, a
, n
, ⟨space character⟩
will be written.
Within the definition of \mani
you can apply \string
to the command \man
in order to turn it into a sequence of character-tokens. (Hereby it is relied on only one input-character having category code 0 (escape) and the value of the integer-parameter \escapechar
being equal to the number of the code-point of that character in the TeX-engine's internal character-encoding-scheme. Usually the backslash-character \
is the only character of category code 0(escape) and usually \escapechar
has the value 92 which is the number of the backslash-character's code-point in the TeX-engine's internal character-encoding-scheme.)
\documentclass{memoir}
\usepackage{xparse}
\NewDocumentCommand{\man}{om}{%
\textbf{#2}\IfNoValueF{#1}{(#1)}%
}
\NewDocumentCommand{\mani}{om}{%
\IfNoValueTF{#1}{%
\man{#2}%
\index{#2@\string\man{#2}}%
}{%
\man[#1]{#2}%
\index{#2@\string\man[#1]{#2}}%
}%
}%
\makeindex
\begin{document}
Only an index entry: \index{ssh-keygen@\man[1]{ssh-keygen}}
Hello world
Command and index entry: \mani[1]{ssh-keygen}
Only an index entry: \index{ssh-keygen-no-optional-argument@\man{ssh-keygen-no-optional-argument}}
Hello world
Command and index entry: \mani{ssh-keygen-no-optional-argument}
\printindex
\end{document}
With the example above the resulting .idx-file looks like this:
\indexentry{ssh-keygen@\man[1]{ssh-keygen}}{1}
\indexentry{ssh-keygen@\man[1]{ssh-keygen}}{1}
\indexentry{ssh-keygen-no-optional-argument@\man{ssh-keygen-no-optional-argument}}{1}
\indexentry{ssh-keygen-no-optional-argument@\man{ssh-keygen-no-optional-argument}}{1}
With this approach, the only thing "stringified" within the argument of \index
, which is supplied via the macro \mani
, is the phrase \man
.
Things that come from \mani
's first or second argument are not stringified. In case the sets of tokens supplied via these arguments also contain control-word-tokens, you might get unwanted spaces here, too.
I can offer a routine \StringifyNAct
which applies \string
to each token within its argument:
\StringifyNAct{⟨action⟩}{⟨token 1⟩⟨token 2⟩...⟨token n⟩}
yields:
⟨action⟩{⟨stringification of token 1⟩}%
⟨action⟩{⟨stringification of token 2⟩}%
...
⟨action⟩{⟨stringification of token n⟩}%
whereby "stringification of token" means the result of applying \string
to the token in question.
Due to \romannumeral
-expansion the result is delivered by triggering two expansion-steps (e.g., via two \expandafter
-chains).
I suggest the command \mani
to read and tokenize its arguments under normal category-code-régime, but with the space-character (and probably also the horizontal-tab-character, addressable as ^^I
with TeX's ^^
-notation) being of category-code 12(other), then to apply \StringifyNAct
to the arguments, then passing the result thereof to the \index
-command and—nested in \scantokens
—to the \man
-command:
\documentclass{memoir}
\makeatletter
%%========================Code for \StringifyNAct==============================
%%
%% Copyright (C) 2019, 2020 by Ulrich Diez ([email protected])
%%
%% This work may be distributed and/or modified under the
%% conditions of the LaTeX Project Public Licence (LPPL), either
%% version 1.3 of this license or (at your option) any later
%% version. (The latest version of this license is in:
%% http://www.latex-project.org/lppl.txt
%% and version 1.3 or later is part of all distributions of LaTeX
%% version 1999/12/01 or later.)
%% The author of this work is Ulrich Diez.
%% This work has the LPPL maintenance status 'not maintained'.
%% Usage of any/every component of this work is at your own risk.
%% There is no warranty - neither for probably included
%% documentation nor for any other part/component of this work.
%% If something breaks, you usually may keep the pieces.
%%
%%=============================================================================
%% Paraphernalia:
%% \UD@firstoftwo, \UD@secondoftwo,
%% \UD@PassFirstToSecond, \UD@Exchange, \UD@removespace
%% \UD@CheckWhetherNull, \UD@CheckWhetherBrace,
%% \UD@CheckWhetherLeadingSpace, \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\newcommand\UD@removespace{}\UD@firstoftwo{\def\UD@removespace}{} {}%
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
\romannumeral0\expandafter\UD@secondoftwo\string{\expandafter
\UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
\UD@secondoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \UD@CheckWhetherBrace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has leading
%% catcode-1-token>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has no leading
%% catcode-1-token>}%
\newcommand\UD@CheckWhetherBrace[1]{%
\romannumeral0\expandafter\UD@secondoftwo\expandafter{\expandafter{%
\string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
\UD@firstoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingSpace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked>'s 1st token is a
%% space-token>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked>'s 1st token is not
%% a space-token>}%
\newcommand\UD@CheckWhetherLeadingSpace[1]{%
\romannumeral0\UD@CheckWhetherNull{#1}%
{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
{\expandafter\UD@secondoftwo\string{\UD@CheckWhetherLeadingSpaceB.#1 }{}}%
}%
\newcommand\UD@CheckWhetherLeadingSpaceB{}%
\long\def\UD@CheckWhetherLeadingSpaceB#1 {%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@secondoftwo#1{}}%
{\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
{\UD@Exchange{ }{\expandafter\expandafter\expandafter\expandafter
\expandafter\expandafter\expandafter}\expandafter\expandafter
\expandafter}\expandafter\UD@secondoftwo\expandafter{\string}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%
%% \UD@ExtractFirstArg{ABCDE} yields {A}
%%
%% \UD@ExtractFirstArg{{AB}CDE} yields {AB}
%%.............................................................................
\newcommand\UD@RemoveTillUD@SelDOm{}%
\long\def\UD@RemoveTillUD@SelDOm#1#2UD@SelDOm{{#1}}%
\newcommand\UD@ExtractFirstArg[1]{%
\romannumeral0%
\UD@ExtractFirstArgLoop{#1UD@SelDOm}%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{ #1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillUD@SelDOm#1}}%
}%
%%-----------------------------------------------------------------------------
%% In case an argument's first token is an opening brace, stringify that and
%% add another opening brace before that and remove everything behind the
%% matching closing brace:
%% \UD@StringifyOpeningBrace{{Foo}bar} yields {{Foo} whereby the second
%% opening brace is stringified:
%%.............................................................................
\newcommand\UD@StringifyOpeningBrace[1]{%
\romannumeral0%
\expandafter\UD@ExtractFirstArgLoop\expandafter{%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\expandafter
\expandafter {%
\expandafter\UD@firstoftwo
\expandafter{%
\expandafter}%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\string
\expandafter}%
\string#1%
UD@SelDOm}%
}%
%%-----------------------------------------------------------------------------
%% In case an argument's first token is an opening brace, remove everything till
%% finding the corresponding closing brace. Then stringify that closing brace:
%% \UD@StringifyClosingBrace{{Foo}bar} yields: {}bar} whereby the first closing
%% brace is stringified:
%%.............................................................................
\newcommand\UD@StringifyClosingBrace[1]{%
\romannumeral0\expandafter\expandafter\expandafter
\UD@StringifyClosingBraceloop
\UD@ExtractFirstArg{#1}{#1}%
}%
\newcommand\UD@CheckWhetherStringifiedOpenBraceIsSpace[1]{%
%% This can happen when character 32 (space) has catcode 1...
\expandafter\UD@CheckWhetherLeadingSpace\expandafter{%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\UD@secondoftwo
\expandafter{%
\expandafter}%
\expandafter{%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\UD@firstoftwo
\expandafter{%
\expandafter}%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\string
\expandafter}%
\string#1%
}%
}%
\newcommand\UD@TerminateStringifyClosingBraceloop[2]{%
\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter\expandafter
\expandafter{%
\expandafter\string
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter#1%
\string#2%
}%
}%
\newcommand\UD@StringifyClosingBraceloopRemoveElement[4]{%
\expandafter\UD@PassFirstToSecond\expandafter{\expandafter
{\romannumeral0\expandafter\UD@secondoftwo\string}{}%
\UD@CheckWhetherStringifiedOpenBraceIsSpace{#4}{%
\UD@Exchange{\UD@removespace}%
}{%
\UD@Exchange{\UD@firstoftwo\expandafter{\expandafter}}%
}{%
\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter#1%
\romannumeral0\UD@Exchange{ }{\expandafter\expandafter\expandafter}%
\expandafter
}%
\string#4%
}{\expandafter\UD@StringifyClosingBraceloop\expandafter{#2#3}}%
}%
\newcommand\UD@StringifyClosingBraceloop[2]{%
\UD@CheckWhetherNull{#1}{%
\UD@CheckWhetherStringifiedOpenBraceIsSpace{#2}{%
\UD@TerminateStringifyClosingBraceloop{\UD@removespace}%
}{%
\UD@TerminateStringifyClosingBraceloop{\UD@firstoftwo\expandafter{\expandafter}}%
}%
{#2}%
}{%
\UD@CheckWhetherLeadingSpace{#1}{%
\UD@StringifyClosingBraceloopRemoveElement
{\UD@removespace}{\UD@removespace}%
}{%
\UD@StringifyClosingBraceloopRemoveElement
{\UD@firstoftwo\expandafter{\expandafter}}{\UD@firstoftwo{}}%
}%
{#1}{#2}%
}%
}%
%%-----------------------------------------------------------------------------
%% Apply <action> to the stringification of each token of the argument:
%%
%% \StringifyNAct{<action>}{<token 1><token 2>...<token n>}
%%
%% yields: <action>{<stringification of token 1>}%
%% <action>{<stringification of token 2>}%
%% ...
%% <action>{<stringification of token n>}%
%%
%% whereby "stringification of token" means the result of applying \string
%% to the token in question.
%% Due to \romannumeral-expansion the result is delivered after two
%% \expandafter-chains.
%% If you leave <action> empty, you can apply a loop on the list formed by
%% {<stringification of token 1>}%
%% {<stringification of token 2>}%
%% ...
%% {<stringification of token n>}%
%%
%% Below a macro \ConcatenateStringifiedtokens is implemented which loops
%% on that list for concatenating.
%%.............................................................................
\newcommand\StringifyNAct{%
\romannumeral0\StringifyNActLoop{}%
}%
%%.............................................................................
%% \StringifyNActLoop{{<stringification of token 1>}...{<stringification of token k-1>}}%
%% {<action>}%
%% {<token k>...<token n>}
%%.............................................................................
\newcommand\StringifyNActLoop[3]{%
\UD@CheckWhetherNull{#3}{%
\UD@firstoftwo{ }{}#1%
}{%
\UD@CheckWhetherBrace{#3}{%
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\UD@StringifyClosingBrace{#3}%
}{%
\expandafter\StringifyNActLoop\expandafter{%
\romannumeral0%
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{\UD@StringifyOpeningBrace{#3}}{\StringifyNActLoop{#1}{#2}}%
}{#2}%
}%
}{%
\UD@CheckWhetherLeadingSpace{#3}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@removespace#3}{%
\StringifyNActLoop{#1#2{ }}{#2}%
}%
}{%
\expandafter\UD@PassFirstToSecond\expandafter{\UD@firstoftwo{}#3}{%
\expandafter\StringifyNActLoop\expandafter{%
\romannumeral0%
\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\UD@PassFirstToSecond
\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter{%
\expandafter\expandafter\expandafter\string
\expandafter\UD@Exchange
\romannumeral0\UD@ExtractFirstArgLoop{#3UD@SelDOm}{}%
}{ #1#2}%
}%
{#2}%
}%
}%
}%
}%
}%
%% The promised loop for concatenating stringified tokens - apply as:
%%
%% \romannumeral0%
%% \expandafter\expandafter\expandafter
%% \ConcatenateStringifiedtokens
%% \StringifyNAct{}{<tokens to stringify>}\relax
%%
\newcommand*\ConcatenateStringifiedtokens{%
\ConcatenateStringifiedtokensloop{ }%
}%
\newcommand\ConcatenateStringifiedtokensloop[2]{%
\ifx\relax#2\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
{#1}{%
\ConcatenateStringifiedtokensloop{#1#2}%
}%
}%
%%=================== End of code for \StringifyNAct ==========================
\makeatother
\usepackage{xparse}
\makeatletter
\NewDocumentCommand{\man}{om}{%
\UD@CheckWhetherNull{#2}{}{%
\toks@{#2}%
\textbf{\the\toks@}%
}%
\IfNoValueF{#1}{(#1)}%
}%
\NewDocumentCommand{\mani}{}{%
\begingroup
\catcode`\ =12\relax
\catcode`\^^I=12\relax
\maniinner
}%
\NewDocumentCommand{\maniinner}{om}{%
\endgroup
\IfNoValueTF{#1}{%
\expandafter\maniinnerinner\expandafter{%
\romannumeral0%
\expandafter\expandafter\expandafter
\ConcatenateStringifiedtokens
\StringifyNAct{}{#2}\relax
}%
}{%
\expandafter\UD@PassFirstToSecond\expandafter{%
\romannumeral0%
\expandafter\expandafter\expandafter
\ConcatenateStringifiedtokens
\StringifyNAct{}{#2}\relax
}{%
\expandafter\maniinnerinner\expandafter[\expandafter{%
\romannumeral0%
\expandafter\expandafter\expandafter
\ConcatenateStringifiedtokens
\StringifyNAct{}{#1}\relax
}]%
}%
}%
}%
\makeatother
\begingroup
\newcommand\maniinnerinner[1]{%
\endgroup
\NewDocumentCommand{\maniinnerinner}{om}{%
\IfNoValueTF{##1}{%
\scantokens{\man{##2}#1}%
\index{##2@\string\man{##2}}%
}{%
\scantokens{\man[##1]{##2}#1}%
\index{##2@\string\man[##1]{##2}}%
}%
}%
}%
\catcode`\%=12\relax
\maniinnerinner{%}%
\makeindex
\begin{document}
Only an index entry: \index{ssh-keygen@\man[1]{ssh-keygen}}
Hello world
Command and index entry: \mani[1]{ssh-keygen}
Only an index entry: \index{ssh-keygen-no-optional-argument@\man{ssh-keygen-no-optional-argument}}
Hello world
Command and index entry: \mani{ssh-keygen-no-optional-argument}
\newcommand\ke{ke}%
\newcommand\one{1}%
Only an index entry: \index{ssh-\ke y\string#gen@\man[\one]{ssh-\ke y\string#gen}}
Hello world
Command and index entry: \mani[\one]{ssh-\ke y\string#gen}
\printindex
\end{document}
With the example above the resulting .idx-file looks like this:
\indexentry{ssh-keygen@\man[1]{ssh-keygen}}{1}
\indexentry{ssh-keygen@\man[1]{ssh-keygen}}{1}
\indexentry{ssh-keygen-no-optional-argument@\man{ssh-keygen-no-optional-argument}}{1}
\indexentry{ssh-keygen-no-optional-argument@\man{ssh-keygen-no-optional-argument}}{1}
\indexentry{ssh-\ke y\string#gen@\man[\one]{ssh-\ke y\string#gen}}{1}
\indexentry{ssh-\ke y\string#gen@\man[\one]{ssh-\ke y\string#gen}}{1}
\newcommand
does not provide the markerNoValue
, so your use of\IfNoValueF
might work by happy coincidence but you shouldn't do that. If you loadxparse
and useIfNoValue
then you should use\NewDocumentCommand
and its syntax.memoir
tag, since the solution from the suggested duplicate breaks withmemoir
. If you switched toarticle
+\usepackage{makeidx}
as in the linked answer, things work. I tried chasing the relevant macros frommemoir
, but had to give up..idx
file: tex.stackexchange.com/q/145309