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}

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.
->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\scantokensand 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\edef\hm{\string#}\let\xp\expandafterand then\xp\def\xp\x\xp#\xp\hm2{Bummer!}. Now\message{\meaning\x}printsmacro:#1#2->Bummer!– egreg Apr 22 '16 at 16:52regexpatchI 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\#2as you getmacro:#1->\#2->blablabut thenmacro:#1->Y#2->blablaif\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:511got lost in translation – egreg Apr 22 '16 at 19:54\escapechar=\Y ` in my example I still get identical output for my two macros. – Ulrike Fischer Apr 22 '16 at 20:15\followed by#2for a second parameter – David Carlisle Apr 22 '16 at 20:18\definition? – Henri Menke May 12 '16 at 16:30\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->). 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\newcommand. For\defit is surely nontrivial, because you can inject any catcode permutation in the macro signature. – Henri Menke May 13 '16 at 07:47l3regexpackage 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\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}\stopHere the macros\testAand\testBdo 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\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:04token.get_macrowhich 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