1

Edit 2: At the bottom I have updated the question after incorporating the answer from here: newcommand for index inserts unwanted spaces when using {}

I created a function to display a command in bold with optionally giving the man page section in parenthesis.

\usepackage{xparse}
\newcommand*{\man}[2][]{%
   \textbf{#2}\IfNoValueF{#1}{(#1)}%
}

Now I also want to be able to create an index entry for these commands. So I created a wrapper command:

\newcommand*{\mani}[2][]{%
   \man[#1]{#2}%
   \index{#2@\man[#1]{#2}}%
}

However, sometimes I want to create an index entry using the same formatting, but not print the command in the running text. Here is a MWE:

\documentclass{memoir}
\usepackage{xparse}
\newcommand*{\man}[2][]{%
   \textbf{#2}\IfNoValueF{#1}{(#1)}%
}
\newcommand*{\mani}[2][]{%
   \man[#1]{#2}%
   \index{#2@\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}

\printindex
\end{document}

The file output.idx contains two lines, the second containing an additional space. The question thus is, why is there an additional space and how can I fix it so there is no additional space?

\indexentry{ssh-keygen@\man[1]{ssh-keygen}}{1}
\indexentry{ssh-keygen@\man [1]{ssh-keygen}}{1}

Edit 1: I've added a screenshot of the result of this extra space: two identical index entries are printed.

Double index entry

Update

I have updated my code as per the provided solution but I now get an error message which I do not understand.

\documentclass{memoir}
\usepackage{xparse}
\makeindex
\NewDocumentCommand\man{om}{%
   \textbf{#2}\IfNoValueF{#1}{(#1)}%
}
\makeatletter
\newcommand*{\mani}{%
   \@bsphack
   \begingroup
   \@sanitize
   \@mani
}
\newcommand*{\@mani}[2][]{%
   \man[#1]{#2}%
   \@wrindex{#2@\string\man[#1]{#2}}%
}
\makeatletter


\begin{document}
Two separate commands: \man[1]{ssh-keygen}\index{ssh-keygen@\man[1]{ssh-keygen}}
Hello world

One command: \mani[1]{ssh-keygen}

\printindex
\end{document}

OverLeaf states:

The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \usepackage{...}.

The log file reports:

! Missing number, treated as zero.
<to be read again> 
                   {
l.25     One command: \mani[1]{ssh-keygen}
                                          
A number should have been here; I inserted `0'.
(If you can't figure out why I needed to see a number,
look up `weird error' in the index to The TeXbook.)
5
  • 1
    Does the space actually cause an issue somewhere? Aug 21, 2020 at 8:31
  • @PeterGrill: yes, I've updated my question with a screenshot of the issue at hand.
    – Tommiie
    Aug 21, 2020 at 8:36
  • 1
    Unrelated to the question: \newcommand does not provide the marker NoValue, so your use of \IfNoValueF might work by happy coincidence but you shouldn't do that. If you load xparse and use IfNoValue then you should use \NewDocumentCommand and its syntax.
    – campa
    Aug 21, 2020 at 8:37
  • 1
    I added the memoir tag, since the solution from the suggested duplicate breaks with memoir. If you switched to article + \usepackage{makeidx} as in the linked answer, things work. I tried chasing the relevant macros from memoir, but had to give up.
    – moewe
    Aug 22, 2020 at 14:39
  • 1
    Not a duplicate, but one' explanation of the extra space in the .idx file: tex.stackexchange.com/q/145309 Aug 22, 2020 at 19:06

2 Answers 2

1

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}
1

The name of the \write-handle used for the .idx-file with memoir differs from the name of the \write-handle used with the LaTeX 2ε-macro \@wrindex.

Thus you need to map the name of the "kernel-\write-handle" for the index to the "memoir-\write-handle" for the index:

\documentclass{memoir}
\usepackage{xparse}
\makeindex
\NewDocumentCommand\man{om}{%
   \textbf{#2}\IfNoValueF{#1}{(#1)}%
}
\makeatletter
\newcommand*{\mani}{%
   \@bsphack
   \begingroup
   \@sanitize
   \@mani
}
\NewDocumentCommand{\@mani}{om}{%
   \@ifundefined{@indexfile}{%
     \expandafter\let\expandafter\@indexfile\csname\jobname @idxfile\endcsname
   }{}%
   \IfNoValueTF{#1}{%
     \man{#2}%
     \@wrindex{#2@\string\man{#2}}%
   }{%
     \man[#1]{#2}%
     \@wrindex{#2@\string\man[#1]{#2}}%
   }%
}
\makeatletter

\begin{document}
Two separate commands: \man[1]{ssh-keygen}\index{ssh-keygen@\man[1]{ssh-keygen}}
Hello world

One command: \mani[1]{ssh-keygen}

\printindex
\end{document}

The .idx-file looks like this:

\indexentry{ssh-keygen@\man[1]{ssh-keygen}}{1}
\indexentry{ssh-keygen@\man[1]{ssh-keygen}}{1}

Be aware that—unlike the approach of stringification after tokenization presented in my other answer—\@sanitize does not handle the balancing of curly braces in case the arguments of \mani are to contain the control-symbol-token \{ and/or the control-symbol-token \}.

You must log in to answer this question.

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