4

I'm trying to create a command \placeDate to print something on the first call and then print nothing and generate an error if I call it once again.

The idea is to print the macro as usual and at the end call another macro (\inhibit) to redefine the macro and generate an error that could work for any macro. In the error message I'd like to see the name of the macro that we called several times (here placeDate).

So as I have to renew the command I have to send \placeDate to \inhibit as an argument. When the error is generated I need to remove the backslash to write the description message. \removebs is the command for that. The point is I'd like to save the string before renewing the so that we can call the string in the message.

\documentclass[a4paper,12pt]{report}
\usepackage[english]{babel}
\usepackage[utf8x]{inputenc}
\usepackage[T1]{fontenc}

\newcommand{\removebs}[1]{%
  {\catcode92=9 \endlinechar-1 \scantokens{#1}}%
}

\newcommand{\inhibit}[2]
{
    \newcommand{\obj}{\removebs{#1}}
    \renewcommand#1[#2]
    {
        \GenericError{}{Error: multiple \obj instantiation}{Only one \obj possible.}{}
    }
}

\newcommand{\placeDate}[1]
{
    \noindent #1, \today.\\\\
    \inhibit{\placeDate}{1}
}

\begin{document}
    \placeDate{City} %ok
    \placeDate{City} %generates error
\end{document}

Obviously here it doesn't work because when \obj is called then \removebs{#1} becomes \removebs{\placeDate} and then I suspect that \placeDate is expanded before the process of \removebs. So I'd like some help to solve that problem.

Here is what we get in Overleaf :

Overleaf log

Thank you in advance.

EDIT : In fact I have some similar macros in my code that i need to inhibit the same way. So each of these macros would inihibt themselves by calling a generic inhibit macro.

Example: \macroA

Result expected:

Error: multiple macroA instantiation. Only one macroA possible.

3 Answers 3

4

If you want that the error message prints the macro name without the backslash you can do

\documentclass[a4paper,12pt]{report}
\usepackage[english]{babel}
%\usepackage[utf8x]{inputenc} % don't use utf8x
\usepackage[T1]{fontenc}

\makeatletter
\newcommand{\inhibit}[2]{%
  \renewcommand{#1}[#2]{%
    \GenericError{}{Oops!}{%
      Error: multiple \expandafter\@gobble\string#1\space instantiation%
    }{Only one \expandafter\@gobble\string#1\space possible.}%
  }%
  \global\let#1#1% in case the macro appears in a group
}
\makeatother

\newcommand{\placeDate}[1]{%
  \noindent #1, \today.\par
  \inhibit{\placeDate}{1}%
}

\begin{document}

\placeDate{City} %ok
\placeDate{City} %generates error

\end{document}

Console output:

! Oops!.

Error: multiple placeDate instantiation
Type  H <return>  for immediate help.
 ...

l.25 \placeDate{City}
                      %generates error
? h
Only one placeDate possible.

But I'd just use \string#1

\documentclass[a4paper,12pt]{report}
\usepackage[english]{babel}
%\usepackage[utf8x]{inputenc} % don't use utf8x
\usepackage[T1]{fontenc}

\newcommand{\inhibit}[2]{%
  \renewcommand{#1}[#2]{%
    \GenericError{}{Oops!}{%
      Error: multiple \string#1 instantiation%
    }{Only one \string#1 possible.}%
  }%
  \global\let#1#1% in case the macro appears in a group
}

\newcommand{\placeDate}[1]{%
  \noindent #1, \today.\par
  \inhibit{\placeDate}{1}%
}

\begin{document}

\placeDate{City} %ok
\placeDate{City} %generates error

\end{document}

Console output

! Oops!.

Error: multiple \placeDate instantiation
Type  H <return>  for immediate help.
 ...

l.23 \placeDate{City}
                      %generates error
? h
Only one \placeDate possible.

You might want an abstraction layer, which can be done with expl3.

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\newonetimecommand}{mO{0}+m}
 {% #1 = command name, #2 = number of parameters, #3 = replacement text
  \vivier_otc_define:nnn { #1 } { #2 } { #3 }
 }

\cs_new_protected:Nn \vivier_otc_define:nnn
 {
  % define the internal name
  \cs_new_protected:cn { vivier_otc_\cs_to_str:N #1 : \prg_replicate:nn { #2 } { n } }
   {
    % the first time use the given replacement text
    #3
    % and globally redefine to issue an error and gobble the arguments
    \cs_gset_protected:Npn #1
     {
      % issue the error
      \msg_error:nnx { vivier } { multiple-instance } { \token_to_str:N #1 }
      % gobble the arguments
      \use:c { use_none: \prg_replicate:nn { #2 } { n } }
     }
   }
  % define the external command
  \cs_gset_eq:Nc #1 { vivier_otc_\cs_to_str:N #1 : \prg_replicate:nn { #2 } { n } }
 }

\msg_new:nnnn { vivier } { multiple-instance }
 {
  multiple~#1~instantiation
 }
 {
  Only~one~#1~possible
 }

\ExplSyntaxOff

\newonetimecommand{\placeDate}[1]{%
  \par\noindent #1, \today.\par
}

\begin{document}

\placeDate{City} %ok
\placeDate{City} %generates error

\end{document}

Console output:

! Package vivier Error: multiple \placeDate instantiation

For immediate help type H <return>.
 ...

l.47 \placeDate
               {City} %generates error
? h

Only one \placeDate possible
3
  • Nice answer for both w and w/o the backslash. I appreciate details thank's.
    – vivier
    Dec 31, 2021 at 18:01
  • @vivier I added an abstraction layer to ease the work of defining several such commands.
    – egreg
    Dec 31, 2021 at 22:35
  • The result is stunning, I will take my time to analyze it. It goes beyond what I thought possible.
    – vivier
    Dec 31, 2021 at 22:50
4

From your use case description I think you can use much simpler code without the catcode and scantokens complications.

\documentclass[a4paper,12pt]{report}
\showhyphens{entdimensionalisiert}
\usepackage[english]{babel}
% avoid utf8x unless needed for compatbility with old documents \usepackage[utf8x]{inputenc}
\usepackage[T1]{fontenc}

\newcommand{\placeDate}[1]
{% dont generate spurious space
    \noindent #1, \today.\par % don't use consecutive \\
    \gdef\placeDate{\inhibit\placeDate}%
}

\newcommand\inhibit[1]{%
\GenericError{}{Error: multiple \string#1 instantiation}%
{Only one \string#1  possible.}{}}

\begin{document}
    \placeDate{City} %ok
    \placeDate{City} %generates error
\end{document}
4
  • Thanks a lot for your time. I think I was not clear enough so I'll edit the question. In fact it's a sample, I'd like to avoid to create a \macroError several time. I have some similar macros in my code that i need to inhibit the same way. So each of these macros would inihibt themselves by calling a generic inhibit macro.
    – vivier
    Dec 31, 2021 at 17:04
  • @vivier sure but you could still do that without catcode changes surely, I'll refactor it a bit... Dec 31, 2021 at 17:20
  • @vivier refactored with a generic \inhibit command. Dec 31, 2021 at 17:23
  • That's exactly what I was looking for, thanks a lot. I didn't know the \string trick that's really nice.
    – vivier
    Dec 31, 2021 at 18:00
1

Some issues:

  • All solutions presented so far rely on the command in question not being renamed / not being copied before being used for the first time.

    E.g., if you do \let\bar=\foo before carrying out \foo the first time, then carrying out \foo more than once will trigger the desired error-message but carrying out \bar (several times) will never trigger the desired error-message as \bar will not be redefined to deliver the error-message but \foo will be redefined to deliver the error-message.

  • A generic command for redefining whatsoever other command to deliver an error-message if used for the second time needs to find out about

    • parameters/arguments processed by the command to redefine.
    • the macro-mechanism underlying the command to redefine.

    (Macro-mechanism: E.g., commands defined in terms of \newcommand that process optional arguments or defined in terms of \DeclareRobustCommand or defined in terms of \NewDocumentCommand actually are macro-mechanisms consisting of a user-level-macro which calls internal macros. If you wish to redefine such a command, you may need to redefine the user-level-macro and the internal macros. Finding out about the kind of underlying macro-mechanism, i.e., finding out about macros belonging to the macro-mechanism might not be trivial.)

In order to circumvent these issues I suggest implementing flags denoting whether a command was already used.

\NewInhibitflag{⟨control sequence token⟩}

defines a command whose name is obtained by prepending to the name of the ⟨control sequence token⟩ the sequence MyPrefix@ReservedForDefinedFlags@ and appending the sequence @flag@WeirdPostfix to be equal to the character-token 0.

\CheckInhibitflag{⟨control sequence token⟩}%
                 {⟨tokens if control sequence token is used the 1st time⟩}

checks if a command whose name is obtained by prepending to the name of the ⟨control sequence token⟩ the sequence MyPrefix@ReservedForDefinedFlags@ and appending the sequence @flag@WeirdPostfix equals the character-token 1.
If not so, ⟨control sequence token⟩ was not used yet, thus that command is defined to equal the character-token 1 and ⟨tokens if control sequence token is used the 1st time⟩ are delivered.
If so, ⟨control sequence token⟩ was already used and an error-message is delivered.

\CheckInhibitflag can be used in the definition-text of whatsoever command that does not need to be expandable—no matter what arguments/parameters it processes and no matter if it is defined in terms of \newcommand or \DeclareRobustCommand or \NewDocumentCommand.
E.g., with macros that process arguments but shall not produce visible output you can wrap \CheckInhibitflag{...}{...} between \@bsphack...\@esphack.

\makeatletter
% \NewInhibitflag{<control sequence token>}%
\DeclareRobustCommand\NewInhibitflag[1]{%
  \begingroup
  \escapechar=-1
  \@ifundefined{MyPrefix@ReservedForDefinedFlags@\string#1@flag@WeirdPostfix}%
               {\global\expandafter\let\csname MyPrefix@ReservedForDefinedFlags@\string#1@flag@WeirdPostfix\endcsname=0}%
               {%
                 \GenericError{}{Control sequence underlying flag for \@backslashchar\string#1 already defined}%
                              {Control sequence underlying flag for \@backslashchar\string#1 already defined.}{}%
               }%
  \endgroup
}
% \CheckInhibitflag{<control sequence token>}%
%                  {<tokens if control sequence token is used the 1st time>}
\DeclareRobustCommand\CheckInhibitflag[1]{%
  \begingroup
  \escapechar=-1
  \@ifundefined{MyPrefix@ReservedForDefinedFlags@\string#1@flag@WeirdPostfix}{%
    \GenericError{}{Control sequence underlying flag for \@backslashchar\string#1 undefined}%
                   {Use \@backslashchar\string\NewInhibitflag{\@backslashchar\string#1} for defining it.}{}%
    \endgroup\@firstofone
    % the argument of \@firstofone is
    % {<tokens if control sequence token is used the 1st time>}.
    % This is provided by the user with the call to \CheckInhibitflag
  }{%
    \expandafter\ifx\csname MyPrefix@ReservedForDefinedFlags@\string#1@flag@WeirdPostfix\endcsname1%
    \expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
    {\@firstoftwo}%
    {\global\expandafter\let\csname MyPrefix@ReservedForDefinedFlags@\string#1@flag@WeirdPostfix\endcsname=1\endgroup\@secondoftwo}%
    {%
      \GenericError{}{Error: multiple \@backslashchar\string#1 instantiation}{Only one \@backslashchar\string#1 possible.}{}%
      \endgroup
    }%
    % the 2nd argument of \@firstoftwo/\@secondoftwo is
    % {<tokens if control sequence token is used the 1st time>}.
    % This is provided by the user with the call to \CheckInhibitflag
  }%
}%
\makeatother

\documentclass{report}

\NewInhibitflag{\placeDate}%
\newcommand\placeDate[1]{%
  \CheckInhibitflag{\placeDate}{%
    #1, \today.\par
  }%
}%

\begin{document}
\noindent\placeDate{City}%ok
\noindent Text

%\noindent\placeDate{Town}%generates error
\noindent Text
\end{document}

enter image description here

Don't use commands where only one-time-usage is allowed within moving arguments like the arguments of sectioning- or \caption-commands which might also end up in the table of contents, page-headers/footers or pdf-bookmarks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.