37

While pondering over a test to distinguish two versions of a command I came up with the following example:

\documentclass{article}
\begin{document}
\def\testA#1{\#2->blabla}
\typeout{\meaning\testA}
\def\testB#1->\#2{blabla}
\typeout{\meaning\testB}
\end{document}

The output in the log is

macro:#1->\#2->blabla
macro:#1->\#2->blabla

So it is imho not possible to use \meaning to count the number of arguments of a command. Is there some other way (without executing the command)?

  • 3
    If there is just one -> you are sure that the parameter text is what's before it; in the case there's more than one, try splitting at each of them and to rebuild the macro with \scantokens and compare with the original (via \ifx). With some luck you'll be able to get away, but of course catcodes get into the way. – egreg Apr 22 '16 at 16:44
  • 2
    My impression is that the problem is not solvable in full generality. In regexpatch I have a “rebuild” based test; if it's not passed, the patching macros signal failure and don't touch the command. – egreg Apr 22 '16 at 16:58
  • 3
    you can tell your examples have 1 parameter by discounting the \#2 as you get macro:#1->\#2->blabla but then macro:#1->Y#2->blabla if \escapechar=`Y but @egreg's example (if corrected to \edef\hm{\string#}\let\xp\expandafter \xp\def\xp\x\xp#\xp1\hm2{Bummer!} is a bit harder – David Carlisle Apr 22 '16 at 19:51
  • 3
    @UlrikeFischer yes but both of them have 1 parameter the fact that \#2 changes to Y#2 shows that that is the csname starting with # not a catcode 12 \ followed by #2 for a second parameter – David Carlisle Apr 22 '16 at 20:18
  • 3
    Could you use the l3regex package to expand the macro, search for all of the #1, #2,..., strip out the #'s and then return the largest integer you find? Personally, expl3 scares me so I don't know if this is feasible. – Andrew Aug 5 '16 at 3:54
3

NOTE: This only works for how many arguments are defined by way of \newcommand, and not by way of \def. And even then, it only works for newly defined \newcommands, not ones that are built into LaTeX. That may not be a problem, since those built in commands are thoroughly documented.

Note though, that packages loaded following the preamble setup will be processed with this information remembered. Thus, my MWE can and does tell me about the arguments of the loaded stackengine package macros.

EDITED to work with \newcommand* invocations, as well.

I'm sure redefining \newcommand is, in general, a really bad thing to do, but I basically redefine it to save the values of the number of arguments and optional arguments, as I feed the definition to a saved version of \newcommand.

For a macro, say \x that is created with \newcommand, the macro \xARGS contains the number of arguments and \xOPTARGS contains the number of optional arguments (0 or 1). And to Ulrike's point, these extra macros providing the argument count are available prior to the execution of macro, (but obviously after the \newcommand that defined it).

EDITED to show it also works with \csname definitions, too.

\documentclass{article}
\let\SVnewcommand\newcommand
\makeatletter
\renewcommand\newcommand{\@ifstar%
  {\gdef\StarVer{T}\newcommandaux}%
  {\gdef\StarVer{F}\newcommandaux}%
}
\def\newcommandaux#1{\saverootname{#1}\futurelet\testchar\MaybeHasArgsCom}
\def\saverootname#1{\xdef\Mname{\expandafter\@gobble\string#1}}
%
\def\MaybeHasArgsCom{\ifx[\testchar \let\next\HasArgsCom
  \else%
    \expandafter\xdef\csname\Mname ARGS\endcsname{0}%
    \expandafter\xdef\csname\Mname OPTARGS\endcsname{0}%
    \if T\StarVer%
      \let\next\HasNoArgsStarCom%
    \else%
      \let\next\HasNoArgsCom%
    \fi%
  \fi%
  \next}
\def\HasArgsCom[#1]{%
  \xdef\SaveArgs{#1}%
  \expandafter\xdef\csname\Mname ARGS\endcsname{#1}%
  \futurelet\testchar\MaybeHasOptArgCom%
}
\def\MaybeHasOptArgCom{%
  \ifx[\testchar%
    \expandafter\xdef\csname\Mname OPTARGS\endcsname{1}%
    \if T\StarVer%
      \let\next\HasOptArgStarCom%
    \else
      \let\next\HasOptArgCom%
    \fi%
  \else%
    \expandafter\xdef\csname\Mname OPTARGS\endcsname{0}%
    \if T\StarVer%
      \let\next\HasNoOptArgStarCom%
    \else
      \let\next\HasNoOptArgCom%
    \fi%
  \fi%
  \expandafter\next\expandafter[\SaveArgs]}
%
\long\def\HasOptArgCom[#1][#2]#3{%
  \expandafter\SVnewcommand\csname\Mname\endcsname[#1][#2]{#3}%
}
\def\HasOptArgStarCom[#1][#2]#3{%
  \expandafter\SVnewcommand\expandafter*\csname\Mname\endcsname[#1][#2]{#3}%
}
\long\def\HasNoOptArgCom[#1]#2{%
  \expandafter\SVnewcommand\csname\Mname\endcsname[#1]{#2}%
}
\long\def\HasNoOptArgStarCom[#1]#2{%
  \expandafter\SVnewcommand\expandafter*\csname\Mname\endcsname[#1]{#2}%
}
\long\def\HasNoArgsCom#1{%
  \expandafter\SVnewcommand\csname\Mname\endcsname{#1}%
}
\long\def\HasNoArgsStarCom#1{%
  \expandafter\SVnewcommand\expandafter*\csname\Mname\endcsname{#1}%
}
%
\makeatother
\parskip 1em
\usepackage[T1]{fontenc}
\usepackage{lmodern,stackengine}
\begin{document}
\newcommand*\XYZ{xyz}
\string\XYZ, whose value is \XYZ, 
has \XYZARGS{} arguments
and \XYZOPTARGS{} optional arguments.

\newcommand\PDQ[1]{p#1d#1q}
\detokenize{\PDQ{123\par123}}, whose value is \PDQ{123\par123}, has \PDQARGS{} argument(s)
and \PDQOPTARGS{} optional arguments.

\newcommand*\ggg[2]{g#1g#2gx}
\string\ggg\{1\}\{2\}, whose value is \ggg{1}{2}, has \gggARGS{} argument(s)
and \gggOPTARGS{} optional arguments.

\newcommand\mytest[1][Q]{#1}
\string\mytest, whose value is \mytest, has \mytestARGS{} argument(s)
and \mytestOPTARGS{} optional arguments.

\string\mytest[X], whose value is \mytest[X], has \mytestARGS{} argument(s)
and \mytestOPTARGS{} optional arguments.

\newcommand\othertest[3][I]{([#1]#2,#3)}
\string\othertest\{j\}\{k\}, whose value is \othertest{j}{k}, has 
\othertestARGS{} argument(s) and \othertestOPTARGS{} optional arguments.

\string\othertest[i]\{j\}\{k\}, whose value is \othertest[i]{j}{k}, has 
\othertestARGS{} argument(s) and \othertestOPTARGS{} optional arguments.

\expandafter\newcommand\csname X1\endcsname[1]{*#1*}
\string\csname X1\string\endcsname\{OOO\}, whose value is 
  \csname X1\endcsname{OOO}, has \csname X1ARGS\endcsname{} argument(s) and 
  \csname X1OPTARGS\endcsname{} optional arguments.

\string\stackengine{} has \stackengineARGS{} arguments 

\string\stackon{} has \stackonARGS{} arguments and \stackonOPTARGS{} optional argument.

\end{document}

enter image description here

If one wanted to also save whether or not the \newcommand was invoked with a starred version, then \newcommandaux could be revised as

\def\newcommandaux#1{%
  \saverootname{#1}%
  \expandafter\xdef\csname\Mname STAR\endcsname{\StarVer}%
  \futurelet\testchar\MaybeHasArgsCom%
}

Then, a \newcommand\x... would result in \xSTAR being defined as F, while an invocation of \newcommand*\x... would result in \xSTAR being defined as T.

  • Woudln't you just need to check only until the first optional? I.e., check for *, grab the command to define, and then see the following optional. And then save that number. – Manuel Apr 5 '17 at 12:56
  • @Manuel If i understand the thrust of your question, one could just check for the 1st optional, which would indicate, for example, that \stackon has 3 arguments. But because of the use of square brackets for optional argument invocation, it is also very useful to know if \stackon has any optional argument usage. And to do this, I need to check if the \newcommand invocation uses two sets of optional arguments. – Steven B. Segletes Apr 5 '17 at 13:06
  • Ah, okey, didn't read the answer but seemed to long. So you are not only saving the number of arguments but also the information about if the command has an optional argument. – Manuel Apr 5 '17 at 13:07
  • @Manuel Correct. And I am considering on whether to also save whether the \newcommand was invoked with a * or not. That is potentially useful. – Steven B. Segletes Apr 5 '17 at 13:09
  • With xparse it's easy easy easy :) A few ifs and you define anything that is necessary, and then put the original \newcommand there. – Manuel Apr 5 '17 at 13:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.