10

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

4
  • 1
    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, 2012 at 2:58
  • 1
    What do you mean by "commands and environments"? Most macros define secret internal macros behind the scenes; do you want to hit those too? If not, then your job is easy: just scan for \newcommand, \renewcommand, etc. in the top-level files. Or, perhaps better, do something trivial like \outer\let\countednewcommand=\newcommand and so on: then use these "counted" commands for the macros you want to track, and since they are \outer, you can be sure they are all visible to a naive scanner.
    – Ryan Reich
    Jan 8, 2012 at 3:31
  • 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, 2012 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.
    – user6853
    Jan 8, 2012 at 18:49

2 Answers 2

6

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} 
%
% #1 = \newcommand|\renewcommand|\newenvironment|\renewenvironment (in new form)
% #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_newcommand:w \newcommand
   \cs_set_eq:NN \command_check_renewcommand:w \renewcommand
   \cs_set_eq:NN \command_check_newenvironment:w \newenvironment
   \cs_set_eq:NN \command_check_renewenvironment:w \renewenvironment
   \cs_set:Npn \newcommand { \command_check:w {\command_check_newcommand:w}{command} }
   \cs_set:Npn \renewcommand { \command_check:w {\command_check_renewcommand:w}{command} }
   \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 \newcommand \command_check_newcommand:w
   \cs_set_eq:NN \renewcommand \command_check_renewcommand:w
   \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{xad}{}{}
\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,xad,0,0
environment,xae,1,0
environment,xaf,1,1[sss]

All commands defined with \newcommand or \renewcommand and environments created between \StartSaveCommands and \StopSaveCommands will create an entry in the .cmd file.

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

8
  • 1
    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. Jan 8, 2012 at 12:07
  • 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
    – user6853
    Jan 8, 2012 at 14:27
  • @BenCrowell I've reworked the code to make it easier to create an example.
    – egreg
    Jan 8, 2012 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.
    – yannisl
    Jan 8, 2012 at 15:08
  • @YiannisLazarides Done
    – egreg
    Jan 8, 2012 at 16:17
8

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}
2
  • 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.)
    – user6853
    Jan 8, 2012 at 14:22
  • 1
    @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
    – user2478
    Jan 8, 2012 at 16:57

You must log in to answer this question.