# parsing LaTeX to output number of arguments for each command defined

Let's say I have a file foo.sty that contains the following:

\newcommand{\a}{x}
\newcommand{\b}[2]{x}
\newcommand{\c}[2][y]{x}
\newenvironment{d}[3]{}{}


I would like to be able to run this through some software and produce a data file saying what commands and environments have been defined, and how many optional and mandatory arguments they have -- something like this:

command,a,0,0
command,b,0,2
command,c,1,1
environment,d,0,3


I could code up something in perl that would work most of the time, but I'm wondering if there's any way to do it so that it's highly reliable. The impression I get is that completely correct parsing of LaTeX is hard, and basically the only thing that does it is LaTeX. Is there some way of getting LaTeX to do this, maybe with an appropriately constructed .tex file that pulls in foo.sty, introspects, and then does write18's...?

I'm looking for a solution that's open-source and that I can run on linux in an automated way (not a GUI).

-
You can redefine \newcommand and \newenvironment to add a hook. Generally speaking, we have various ways to define macros with optional arguments, and it is a bit difficult to detect all of them. –  Leo Liu Jan 8 '12 at 2:58
The package storecmd can be adapted to do this, but time is currently a big problem for its author. Perhaps someone can take a look at the possibilities. –  Ahmed Musa Jan 8 '12 at 6:06
@Ryan: Good point. For my own application, I don't need the info about a gazillion internal macros, but it also doesn't hurt me to have that info. –  Ben Crowell Jan 8 '12 at 18:49

Put the following code in the preamble or in a .sty file

\usepackage{xparse}
\ExplSyntaxOn
% Define the command that will start the working; we'll redefine \newcommand
% so that \newcommand{\xac}[2][cccc]{abc} will execute
%
%    \command_check:w { \command_check_newcommand:w }{ command } {\xab}
%
% #2 = command|environment
% #3 = the possible *
% #4 = the argument to \newcommand
% #5 = the number of arguments
% #6 = the possible optional argument
%
% We check if the last argument is missing and take the appropriate action
\NewDocumentCommand{ \command_check:w }{m m s m O{0} o}
{
\IfBooleanTF{#3}
{ \tl_gset:Nn \g_command_check_star_tl { * } \bool_gset_true:N \g_command_check_star_bool }
{ \tl_gset:Nn \g_command_check_star_tl { } \bool_gset_false:N \g_command_check_star_bool }
\IfNoValueTF{#6}
{ \command_check_noopt:nnnn {#1} {#2} {#4} {#5} }
{ \command_check_opt:nnnnn {#1} {#2} {#4} {#5} {#6} }
}
% \StartSaveCommands sets up the checks, first by creating aliases for the kernel commands
% and then redefining them as explained above
\NewDocumentCommand{\StartSaveCommands}{}
{
\cs_set_eq:NN \command_check_newenvironment:w \newenvironment
\cs_set_eq:NN \command_check_renewenvironment:w \renewenvironment
\cs_set:Npn \newenvironment { \command_check:w {\command_check_newenvironment:w}{environment} }
\cs_set:Npn \renewenvironment { \command_check:w {\command_check_renewenvironment:w}{environment} }
}
% \StopSaveCommands restores the kernel commands
\NewDocumentCommand{\StopSaveCommands}{}
{
\cs_set_eq:NN \newenvironment \command_check_newenvironment:w
\cs_set_eq:NN \renewenvironment \command_check_renewenvironment:w
}
% \WriteSaveCommands will take care of writing out the list of commands
% with their number of arguments
\NewDocumentCommand{\WriteSaveCommands}{}
{
\iow_open:Nn \g_command_check_write { \jobname.cmd }
\seq_map_inline:Nn \g_command_check_seq {\iow_now:Nx \g_command_check_write { ##1 } }
}
% Allocate a write stream
\iow_new:N \g_command_check_write
% If there's no optional argument, say \newcommand{\xab}[1]{aaa},
% we want to write out "command,\xab ,1,0", so we store that
% string into an item appended to the sequence \g_command_check_seq
\cs_new:Npn \command_check_noopt:nnnn #1 #2 #3 #4
{
\seq_gput_right:Nx \g_command_check_seq
{
#2 \bool_if:NT \g_command_check_star_bool { (*) },
\tl_to_str:n {#3} , #4, 0
}
\exp_after:wN #1 \g_command_check_star_tl {#3} [#4]
}
% If there's an optional argument, say \newcommand{\xac}[2][cccc]{abc},
% we want to write out "command,\xac,1,1[cccc]; everything as before,
% but we decrease by 1 the number of stated arguments
\cs_new:Npn \command_check_opt:nnnnn #1 #2 #3 #4 #5
{
\seq_gput_right:Nx \g_command_check_seq
{
#2 \bool_if:NT \g_command_check_star_bool { (*) }, \tl_to_str:n {#3} ,
\int_to_arabic:n {#4-1} , 1[\tl_to_str:n{#5}]
}
\exp_after:wN #1 \g_command_check_star_tl {#3}[#4][#5]
}
% Allocate the sequence, the token list variable and the boolean
\seq_new:N \g_command_check_seq
\tl_new:N \g_command_check_star_tl
\bool_new:N \g_command_check_star_bool
\ExplSyntaxOff


Now you can put your personal commands between \StartSaveCommands and \StopSaveCommands, say

\documentclass{article}

<the above code>

\StartSaveCommands

\newcommand{\xaa}{abc}
\newcommand*{\xab}[1]{abc}
\newcommand{\xac}[2][cccc]{abc}
\renewcommand{\phi}{\varphi}
\newenvironment{xae}[1]{}{}
\newenvironment{xaf}[2][sss]{}{}

\StopSaveCommands
\WriteSaveCommands

\begin{document}

<text>

\end{document}


This will define the commands and environments and write a file with extension .cmd; in our example it would contain

command,\xaa ,0,0
command(*),\xab ,1,0
command,\xac ,1,1[cccc]
command,\phi ,0,0
environment,xae,1,0
environment,xaf,1,1[sss]



(Note: edited to take care also of \newcommand* and \newenvironment*)

-
I'd personally not bother defining \command_check_write:n, instead using \seq_map_inline:Nn and putting the code inline inside the def of \WriteSaveCommands (with hash doubled of course). Also, the code could be simply \iow_now:Nn \command_check_write {#1}, not sure why you x-expand twice. And, nitpicking, I would add g_ in front of that _write variable. –  Bruno Le Floch Jan 8 '12 at 12:07
@BrunoLeFloch Fixed, thanks. –  egreg Jan 8 '12 at 13:25
Very cool, thanks! But I had some trouble figuring out how to make this into a minimal working example. I tried adding \documentclass{minimal} at the top and \begin{document}...\end{document} at the bottom, and then uncommenting the commented-out section and putting it after \begin{document}. That produced the following error: l.64 \seq_show:N \g_command_check_seq –  Ben Crowell Jan 8 '12 at 14:27
@BenCrowell I've reworked the code to make it easier to create an example. –  egreg Jan 8 '12 at 15:01
@egreg Can you please add a bit of commentary on the code? Code is ok, but somehow it needs a bit of finesse. –  Yiannis Lazarides Jan 8 '12 at 15:08

in the logfile you'll find:

command, ,\A,0,0
command,*,\AB,0,0
command, ,\B,2,0
command,*,\BA,2,0
command, ,\C,2,y
command,*,\CBA,2,y
environment, ,D,3,0
environment,*,DD,3,0
environment, ,foobar,3,0
environment,*,FooBar,3,y


it also handels a star version of \newcommand and \newenvironment

which can also be saved in an own file. The code can also be extended to \renewcommand and so on ...

\documentclass{minimal}

\makeatletter
\newif\if@star
\newif\if@command
\let\NewCommand\newcommand
\let\NewEnvironment\newenvironment
\def\newcommand{\global\@commandtrue%
\@ifnextchar*{\global\@startrue\test@CommEnv}{\global\@starfalse\test@CommEnv*}}
\def\newenvironment{\global\@commandfalse%
\@ifnextchar*{\global\@startrue\test@CommEnv}{\global\@starfalse\test@CommEnv*}}
\def\test@CommEnv*#1{\@ifnextchar[{\test@CommEnv@i{#1}}{%
\if@command
\typeout{command,\if@star*\else\space\fi,\string#1,0,0}%
\def\next{\NewCommand{#1}}%
\else
\typeout{environment,\if@star*\else\space\fi,\string#1,0,0}%
\def\next{\NewEnvironment{#1}}
\fi\next}}
\def\test@CommEnv@i#1[#2]{%
\@ifnextchar[{\test@CommEnv@ii{#1}[#2]}{%
\if@command
\typeout{command,\if@star*\else\space\fi,\string#1,#2,0}%
\def\next{\NewCommand{#1}[#2]}%
\else
\typeout{environment,\if@star*\else\space\fi,#1,#2,0}%
\def\next{\NewEnvironment{#1}[#2]}
\fi\next}}
\def\test@CommEnv@ii#1[#2][#3]{%
\if@command
\typeout{command,\if@star*\else\space\fi,\string#1,#2,#3}%
\def\next{\NewCommand{#1}[#2][#3]}%
\else
\typeout{environment,\if@star*\else\space\fi,#1,#2,#3}%
\def\next{\NewEnvironment{#1}[#2][#3]}
\fi\next}

\newcommand{\A}{x}
\newcommand*{\AB}{x}
\newcommand{\B}[2]{x}
\newcommand*{\BA}[2]{x}
\newcommand{\C}[2][y]{x}
\newcommand*{\CBA}[2][y]{x}
\newenvironment{D}[3]{}{}
\newenvironment*{DD}[3]{}{}
\newenvironment{foobar}[3]{}{}
\newenvironment*{FooBar}[3][y]{}{}

\begin{document}
\A \B{1}{2} \C[1]{2} \begin{D}{1}{2}{3} D \end{D}
\end{document}

-
Awesome, thanks! I think there's a bug, though. If I change \newenvironment{D}[3]{}{} to \newenvironment{foobar}[3]{}{}, I get Missing \begin{document}. If I change it to \newenvironment{ddddd}[3]{}{} , I get Command \d already defined. It seems like the hook for newenvironment is only eating the first character of the environment name...? (Newcommand seems OK.) –  Ben Crowell Jan 8 '12 at 14:22
@BenCrowell: \d is an already existing command, the reason why I used \D. And \newenvironment{foobar} is now working. I edited my code, had forgotten to set #1 into braces –  Herbert Jan 8 '12 at 16:57
That works. Thanks! –  Ben Crowell Jan 8 '12 at 18:54
Thanks for pointing out the *-variants. –  egreg Jan 8 '12 at 19:14