39

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)?

Ulrike Fischer
  • 327,261
  • 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
  • 1
    Another “nice” example: first \edef\hm{\string#}\let\xp\expandafter and then \xp\def\xp\x\xp#\xp\hm2{Bummer!}. Now \message{\meaning\x} prints macro:#1#2->Bummer! – egreg Apr 22 '16 at 16:52
  • @egreg: catcodes can get heavily in the way. Just imagine some expl3 context. The chance to get a correct result is higher if one simply count the # until the first -> and hope ;-). But it is interesting how some things are hidden in tex. – Ulrike Fischer Apr 22 '16 at 16:55
  • 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
  • 4
    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=\Ybut @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
  • @DavidCarlisle The 1 got lost in translation – egreg Apr 22 '16 at 19:54
  • @DavidCarlisle: When I use \escapechar=\Y ` in my example I still get identical output for my two macros. – Ulrike Fischer Apr 22 '16 at 20:15
  • 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
  • @DavidCarlisle: Looks as if I got confused. I didn't realize that my both commands have only one argument. But it is still impossible to see where the arguments end and the macro text begins. – Ulrike Fischer Apr 22 '16 at 20:32
  • I understand that this is not what you are asking, but wouldn't it be way easier to just track the number of arguments at \definition? – Henri Menke May 12 '16 at 16:30
  • @HenriMenke: Who should track it? The engine? Sure, that would be better, using \meaning to guess is only an (not reliable) work around. – Ulrike Fischer May 12 '16 at 16:36
  • @UlrikeFischer It would be most convenient if it happens on engine level, but I don't think that this is implemented. You probably have to overload \def. With \newcommand (which essentially is an overload for \def) you give the number of parameters as actual number. Adding storing capabilities should be trivial. – Henri Menke May 12 '16 at 17:14
  • 1
    I think it is doable in theory but if you allow for any catcode permutation without placing restrictions there the code would be horribly complex. However, you could determine the max possible param string and then rebuild the macro and then compare it to the original: if it fails you the change catcodes and or reduce the param string (as it will end at some ->). With some restriction on catcodes it gets simpler. In any case not practical as the number of possibilities to check might be HUGE – Frank Mittelbach May 12 '16 at 17:22
  • @HenriMenke: I don't think that it is trivial. Not if you have to consider catcode changes, weird argument patterns, and if all expandable definitions should remain expandable. – Ulrike Fischer May 12 '16 at 17:29
  • 1
    @FrankMittelbach: I got to the same conclusion. If you add it as an answer I could accept it and get it from the list of unanswered questions. – Ulrike Fischer May 12 '16 at 17:30
  • @UlrikeFischer I'm sorry for the confusion. I meant it is trivial for \newcommand. For \def it is surely nontrivial, because you can inject any catcode permutation in the macro signature. – Henri Menke May 13 '16 at 07:47
  • 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. –  Aug 05 '16 at 03:54
  • This question is loosely related: http://tex.stackexchange.com/questions/271607/parse-argument-by-character-while-executing-embedded-macros – Steven B. Segletes Jan 06 '17 at 17:21
  • See also another answer in the question tex core - How to get number of arguments in a macro? - TeX - LaTeX Stack Exchange which works with newcommand, but does not require redefining it. ■ (for Google searches, hopefully) This question is a subset of [How can I get the parameter text of a macro at TeX runtime?] – user202729 Dec 10 '21 at 06:16
  • The task is not trivial at all. E.g., arguments might be delimited by hashes that are not of catcode 6: \expandafter\def\expandafter\testA\expandafter#\expandafter1\string#2{#1/blabla}\typeout{\meaning\testA}\def\testB#1#2{#1/blabla}\typeout{\meaning\testB}\typeout{meanings \ifx\testA\testB are equal\else differ\fi}\stop Here the macros \testA and \testB do exactly the same but are considered different: \catcode`\Y=6\relax\def\testA#1#2{}\def\testBY1Y2{}\typeout{\meaning\testA}\typeout{\meaning\testB}\typeout{meanings \ifx\testA\testB are equal\else differ\fi}\stop – Ulrich Diez Dec 23 '21 at 12:50
  • Interestingly parameter-text and definition-text containing implicit parameter-characters doesn't matter: \let\para=#\def\testA#1#2{arg1: #1 arg2: #2}\def\testB\para1\para2{arg1: \para1 arg2: \para2}\typeout{\testA{1}{2}}\typeout{\testB{1}{2}}\typeout{\meaning\testA}\typeout{\meaning\testB}\typeout{meanings \ifx\testA\testB are equal\else differ\fi}\stop – Ulrich Diez Dec 23 '21 at 13:04
  • For cross-linking,, there are a few more solutions in [Around the bend] exercise #6 (can be found on CTAN) – user202729 Jul 12 '22 at 09:59
  • • In LuaTeX there's token.get_macro which gets only the ⟨replacement text⟩ but not the ⟨parameter text⟩, so you can determine what's the boundary of the ⟨parameter text⟩ which distinguish the 2 cases in the question;; but this does not solve the problem completely there are other complications e.g. # not of catcode 6, or catcode 6 but char code not #, # as part of macro name etc.. – user202729 Jul 12 '22 at 11:22

1 Answers1

5

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 05 '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 05 '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 05 '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 05 '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 05 '17 at 13:15