7

I am trying to set up a macro that defines a macro that is to be executed only once.

With what I learned from Define a Macro via Macro if Given Macro is not defined I can do that via the \expandafter\newcommand syntax. And indeed the MWE as below works and produces the desired output:

enter image description here

where the subsequent \HelloWorld and \HelloDude are ignored.

What I would like to do, is to learn how to use \NewDocumentCommand (instead of \newcommand) to define this new macro via a macro. My attempt (requires you to uncomment the \def at the top of the file) fails with

Illegal parameter number in definition of \reserved@a

Furthermore, am wondering if there is a way to define repeated parameters. That is instead of {m m} can I use something similar to with tabular where one can say \begin{tabular}{r*{3}{l}r} which is the identical to \begin{tabular}{rlllr}. This would save me from having to use \IfEqCasefor each possible number of parameters. I personally only need up to 3 so am ok with the slight duplication of code below, but though I'd ask in case there was some other syntax that I am not aware of.

Notes:

  • You need to uncomment the \def at the top to get the MWE below to fail.

Refernces

Code:

%% Uncomment the following \def to get the failing test case.
%\def\UseXparseForDefiningMacro{}% Works if commented out (in which case \newcommand{}{} is used to define macro)
\documentclass{article}
\usepackage{etoolbox}
\usepackage{xstring}
\usepackage{xparse}
\usepackage{xcolor}

\makeatletter
\NewDocumentCommand{\DefineMeAMacroThatExecutesOnlyOnce}{m m m}{%
    % #1 = csname to use
    % #2 = number of parameters
    % #3 = code to execute
    \newtoggle{AlreadyIssued#1}%
    \togglefalse{AlreadyIssued#1}%
    \ifdefined\UseXparseForDefiningMacro
        \IfEqCase{#2}{%
            {0}{\expandafter\NewDocumentCommand\csname#1\endcsname{     }{% No paramater version
                    \iftoggle{AlreadyIssued#1}{}{%
                        #3%                           Never executed this macro so go ahead an execute it,
                        \toggletrue{AlreadyIssued#1}% and remember that we did (so we don't do it again).
                    }%
                }}%
            {1}{\expandafter\NewDocumentCommand\csname#1\endcsname{  m  }{% 1 paramater version
                    \iftoggle{AlreadyIssued#1}{}{%
                        #3%                           Never executed this macro so go ahead an execute it,
                        \toggletrue{AlreadyIssued#1}% and remember that we did (so we don't do it again).
                    }%
                }}%
            {2}{\expandafter\NewDocumentCommand\csname#1\endcsname{ m m }{% 2 paramater version
                    \iftoggle{AlreadyIssued#1}{}{%
                        #3%                           Never executed this macro so go ahead an execute it,
                        \toggletrue{AlreadyIssued#1}% and remember that we did (so we don't do it again).
                    }%
                }}%
        }%
    \else% ---------------------------------------------  This works!!
        \expandafter\newcommand\csname#1\endcsname[#2]{%
            \iftoggle{AlreadyIssued#1}{}{%
                #3%                           Never executed this macro so go ahead an execute it,
                \toggletrue{AlreadyIssued#1}% and remember that we did (so we don't do it again).
            }%
        }%
    \fi
}
\makeatother

\DefineMeAMacroThatExecutesOnlyOnce{HelloWorld}{0}{% Does not take any parameters
    Hello World!%
}

\DefineMeAMacroThatExecutesOnlyOnce{HelloDude}{1}{% Take 1 parameter
    Hello \textcolor{red}{#1}.%
}

\begin{document}

%% Section 1: Works if \UseXparseForDefiningMacro is NOT defined
\HelloWorld
\HelloWorld

%% Section 2: Works if \UseXparseForDefiningMacro is NOT defined
\HelloDude{Peter}
\HelloDude{John}

\end{document}
2
  • Out of curiosity, why don't you use the expl3 syntax? It makes much more sense when using xparse, in my opinion. I would imagine the solution would also be more concise. Jun 30, 2014 at 21:42
  • @SeanAllred: Am embarrassed to admit it, but I can't quite read expl3 syntax yet. Jun 30, 2014 at 22:34

2 Answers 2

8

You could do this but it's so wrong, why mix expl3 and etoolbox tests and why all the toggle stuff, if you want \foo to just execute once define it to be

 \def\foo{hello\let\foo\@empty}

No need for a separate toggle macro.

but anyway:

%% Uncomment the following \def to get the failing test case.
\def\UseXparseForDefiningMacro{}% Works if commented out (in which case \newcommand{}{} is used to define macro)
\documentclass{article}
\usepackage{etoolbox}
\usepackage{xstring}
\usepackage{xparse}
\usepackage{xcolor}

\makeatletter
\NewDocumentCommand{\DefineMeAMacroThatExecutesOnlyOnce}{m m m}{%
    % #1 = csname to use
    % #2 = number of parameters
    % #3 = code to execute
    \newtoggle{AlreadyIssued#1}%
    \togglefalse{AlreadyIssued#1}%
    \ifdefined\UseXparseForDefiningMacro
        \expandafter\NewDocumentCommand\csname#1\expandafter\endcsname
              \expandafter{\romannumeral#2000}{%
            \iftoggle{AlreadyIssued#1}{}{%
                #3%                           Never executed this macro so go ahead an execute it,
                \toggletrue{AlreadyIssued#1}% and remember that we did (so we don't do it again).
            }%
        }%
    \else% ---------------------------------------------  This works!!
        \expandafter\newcommand\csname#1\endcsname[#2]{%
            \iftoggle{AlreadyIssued#1}{}{%
                #3%                           Never executed this macro so go ahead an execute it,
                \toggletrue{AlreadyIssued#1}% and remember that we did (so we don't do it again).
            }%
        }%
    \fi
}
\makeatother

\DefineMeAMacroThatExecutesOnlyOnce{HelloWorld}{0}{% Does not take any parameters
    Hello World!%
}

\DefineMeAMacroThatExecutesOnlyOnce{HelloDude}{1}{% Take 1 parameter
    Hello \textcolor{red}{#1}.%
}

\begin{document}

%% Section 1: Works if \UseXparseForDefiningMacro is NOT defined
\HelloWorld
\HelloWorld

%% Section 2: Works if \UseXparseForDefiningMacro is NOT defined
\HelloDude{Peter}
\HelloDude{John}

\end{document}
4
  • Yeah, I always forget about the \def thing to stop things from happening again. Can you elaborate on the \expandafter{\romannumeral#2000} magic that is happening here. How does that become m m if #2=2? Jun 30, 2014 at 22:38
  • 1
    \romannumeral2000 expands to mm
    – egreg
    Jun 30, 2014 at 23:07
  • @egreg: Wow, that is very clever!! An I was thinking that a loop would be needed... Jul 1, 2014 at 3:11
  • @PeterGrill yes well it was fortunate you wanted m arguments (although using romannumeral and then changing m to whatever you need is a viable technique for replicating in general) Jul 1, 2014 at 9:14
8

Here is a solution for this problem, using (perhaps abusing) only expl3 features. I'm not really sure about the usefulness of this code. ;-)

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewDocumentCommand{\NewOnceMacro}{m m m}
 {
  \grill_new_once_macro:Nnn #1 { #2 } { #3 }
 }

% an addition to the kernel functions
\cs_set_eq:NN \use_none: \prg_do_nothing:

\cs_new_protected:Npn \grill_new_once_macro:Nnn #1 #2 #3
 {
  \cs_set_eq:cc
   {% the corresponding "do nothing" macro
    \cs_to_str:N #1 -disabled
   }
   {% generate \use_none:<as many n's as #2>
    use_none: \prg_replicate:nn { #2 } { n }
   }
  \use:x
   {
    \exp_not:n { \NewDocumentCommand { #1 } }
      { \prg_replicate:nn { #2 } { m } } % the right number of m's
   }
   {
    % the first usage definition
    #3
    % then redefine the macro to do nothing
    \cs_gset_eq:Nc #1 { \cs_to_str:N #1 -disabled }
   }
 }

\ExplSyntaxOff

\NewOnceMacro{\HelloWorld}{0}{Hello world}
\NewOnceMacro{\Hello}{1}{Hello #1}
\NewOnceMacro{\Foo}{2}{Your #1 is full of #2}

\begin{document}

\HelloWorld
\HelloWorld

\Hello{Peter}
\Hello{Grill}

\Foo{hovercraft}{eels}
\Foo{head}{ideas}

\end{document}

enter image description here

4
  • I read in interface3 about \use:x “Any hash characters (#) in the argument must be doubled.” What does that mean?
    – Manuel
    Jun 30, 2014 at 22:46
  • @Manuel I'm not really using # inside \use:x, because #1 and #2 will be replaced with the arguments before \use:x is executed. Think to it as doing a \def inside a \def: if you want to use parameters to the inner \def you have to double #.
    – egreg
    Jun 30, 2014 at 22:51
  • Okey, but then why is necessary that comment? Isn't that applied to every command? -- EDIT: Understood.
    – Manuel
    Jun 30, 2014 at 22:53
  • 1
    I will not upvote this question, it is scratched. :P Jul 1, 2014 at 10:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .