10

The Problem

There are several packages I would like to write which require me to redefine the \newcommand (\renewcommand, etc.) command so I can track or change the commands that authors subsequently define. But I have no idea how to go about it. My naive attempts so far have failed. Probably because I am still very weak at the TeX level.

I'm surprised that I couldn't find more than vague hints towards this concept. It feels like such an obvious thing to do.

Motivation

A simple usecase, and the first package I plan write, is to ensure the following: that using \newcommand to define a command - say: \cmd - that has already been defined does not immediately generate an error. \cmd would simply be in a state of conflict. Subsequently trying to expand \cmd would then generate an error (since it's ambiguous which of the two definitions you want).

The conflict could be resolved by subsequently redefining \cmd using \renewcommand, after which \cmd can once again safely be expanded. Example:

\newcommand{\cmd}{FIRST}   %
\cmd                       % outputs FIRST
\newcommand{\cmd}{SECOND}  % no problem yet
\cmd                       % error: expanding ambiguous command
\renewcommand{\cmd}{THIRD} %
\cmd                       % outputs THIRD

This could, for example, be used to mediate conflicts between packages (that use \newcommand). Of course, this concept is still quite weak (for example, what to do if two packages independently use \renewcommand on the same command?). But it is enough to serve as a usecase for my question.

After I learn more about this, I plan to exercise more fine-grained control.

Pseudo Code Solution

It feels like I have to do something like this (ignoring the optional argument for now):

\let\old@newcommand\newcommand
\MetaRenewCommand{\newcommand}{
    \ifdefined#1
        \old@newcommand{#1}{
            \PackageError{lazyfail}{Expanding ambiguous command \protect #1}
        }
    \else
        \old@newcommand{#1}{#3}
    \fi
}

Of course, there are many things wrong with this code. There is no \MetaRenewCommand and I still have to handle the optional argument of \newcommand.

So, how do I start? It feels like it must be possible, as \newcommand and friends are not primitives of LaTeX, but defined in terms of lower level commands.

Further Motivation

Here's another use-case I have in mind for this. When including a package, I would like to ignore all commands it provides except for a small list which I specify:

\usepackagefor[\Lightning]{marvosym}

Here I am loading the marvosym package, but only to use \Ligntning. I choose this example because the marvosym \CheckedBox command conflicts with the one in the llncs class.

I already routinely specify the commands I plan to use a package for with a comment, but it would be nice to actually enforce that.

11
  • 2
    \providecommand provides your "no problem yet" requirement... but not generating an error upon subsequent usage.
    – Werner
    Sep 19, 2012 at 21:35
  • It might be useful to (perhaps separately) ask about the wider thing you're trying to do. You mention 'many packages', so it would be handy to see if there is some better way to achieve the bigger goal than fiddling around with LaTeX kernel commands.
    – Joseph Wright
    Sep 19, 2012 at 21:38
  • Werner: Assume that I am the package writer, and have no control over the code in the 'Motivation' section.
    – mhelvens
    Sep 19, 2012 at 21:39
  • Joseph: Well, the 'Motivation' section already describes a standalone usecase, I think. I want to expand it, but that's only going to require more control over how the author's commands are defined. Also, this is a good opportunity for me to learn more about the guts of TeX. Would you humour me? :-)
    – mhelvens
    Sep 19, 2012 at 21:42
  • 2
    @mhelvens My point is that your use case should give an error: that's the point of \newcommand (if you just want to force the issue, you use \def in a package). I'm not sure I see what you are really up to: if two packages define \foo, then trouble will ensue, hence the existence of \newcommand to warn about this and allow some action to be taken. (That said, at a technical level this is doable, certainly as an exercise.)
    – Joseph Wright
    Sep 19, 2012 at 21:45

2 Answers 2

6

Tackling the problem as posed is tricky due to the way \newcommand works. Heiko's approach is probably more elegant, but one possible method is to use xparse to deal with the syntax of \newcommand, and letltxmacro to deal with the way \newcommand is set up

\documentclass{article}
\usepackage{letltxmacro,xparse}
\makeatletter
\LetLtxMacro{\saved@newcommand}{\newcommand}
\DeclareDocumentCommand{\newcommand}{s+mo+o+m}{%
  \begingroup
  \edef\x{%
    \endgroup
    \ifdefined#2%
      \unexpanded{%
          \renewcommand{#2}%
            {%
              \PackageError{lazyfail}
                {Expanding ambiguous command \protect #2}%
                \@ehc
            }%
        }
    \else
      \noexpand\saved@newcommand
        \IfBooleanT{#1}{*}%
        {\noexpand#2}%
        \IfNoValueF{#3}{[#3]}%
        \IfNoValueF{#4}{[\unexpanded{#4}]}%
        {\unexpanded{#5}}
    \fi
  }%
  \x
}
\makeatother
\begin{document}
\newcommand{\cmd}{FIRST}   %
\cmd                       % outputs FIRST
\newcommand{\cmd}{SECOND}  % no problem yet
\cmd                       % error: expanding ambiguous command
\renewcommand{\cmd}{THIRD} %
\cmd                       % outputs THIRD
\end{document}

The approach here is to grab all of the arguments to the redefined \newcommand in one go, then work out whether they are to be 'recycled' or not.

Of course, you could set all of this up without xparse, but it would be a pain in the next: lots of \@ifstar and \@ifnextchar ( or slightly better \@testopt) stuff and several auxiliaries.

5
  • Nice! I can almost fully understand this one. And thanks for introducing me to xparse! Why doesn't everyone use it all the time? :-)
    – mhelvens
    Sep 20, 2012 at 9:52
  • @mhelvens xparse is quite new (at least in the usable form it's in now).
    – Joseph Wright
    Sep 20, 2012 at 10:19
  • I have one question about your code: Why do you need \begingroup and \endgroup there? Thanks!
    – mhelvens
    Sep 20, 2012 at 10:22
  • 1
    @mhelvens The group here means that \x is not affected outside of the use here to expand material. This is a standard 'trick' to avoid breaking anyone else's code.
    – Joseph Wright
    Sep 20, 2012 at 10:34
  • Ah, I see. That's even better than using @, which I thought was the standard approach for that. Thanks!
    – mhelvens
    Sep 20, 2012 at 11:16
9

Instead of \newcommand I would redefine \@ifdefinable. Then also some other things like \newcounter or \newsavebox are catched:

\documentclass{article}

\makeatletter
% Save old meaning of \@ifdefinable in \saved@ifdefinable
\newcommand*{\saved@ifdefinable}{}
\let\saved@ifdefinable\@ifdefinable
% Redefine \@ifdefinable
% #1: command token
% #2: code that defines the command in #1
\renewcommand{\@ifdefinable}[2]{%
  % Here the same test for checking #1 is used as in the
  % original definition.
  \edef\reserved@a{\expandafter\@gobble\string#1}%
  % \reserved@a contains the name without backslash
  \@ifundefined\reserved@a{%
    \saved@ifdefinable{#1}{#2}%
  }{%
    % Report the command with the name clash in the .log file
    \@latex@info{Ambigous command: \string#1}%
    % Redefine the command to generate an error message.
    % \@ehd is the standard help text that starts with "You're in trouble here."
    \def#1{\@latex@error{Expanding ambiguous command}\@ehd}%
  }%
}
\makeatother

\begin{document}

\newcommand*{\cmd}[1]{FIRST(#1)}
\cmd{argument}

\newcommand*{\cmd}[2]{SECOND(#1,#2)}
\cmd{param1}{param2}

\renewcommand*{\cmd}{THIRD}
\cmd

\end{document}

"Border cases":

  • LaTeX complains if someone tries to define a command \end... starting with end or in the case of \relax. These cases are not redefined by the above redefinitions.

  • Treatment of arguments is ambiguous. In the case with the ambiguous error the arguments remain untouchted in the input.

  • Commands are only detected, if they are defined via the LaTeX interface (\newcommand, \newcounter, \newsavebox, …). Definitions can also be done by TeX's primitive commands \def, \edef, \gdef, … or plain TeX commands (\newcount et. al.) These commands do not differentiate between new and old commands.


Answer for Further Motiviation:

Of course, \newcommand could be redefined to drop some definitions and keep \Lightning as only command of package marvosym, but

  • Other macros, defined without \newcommand, might be available with undesired effects.
  • The kept macro might rely on other macros that are defined by \newcommand and not kept.
4
  • Hehe, great! I love it when code 'just works'. ;-) Of course, I have no idea how you did it (so I don't know how to reproduce it), nor if there are any corner-cases that this code doesn't yet cover. It would be great if you could point me to a book or website that could teach me to understand your code. For now, I've upvoted your answer, but will hold off on accepting it for now, until I've looked at some other answers (and have slept on it). Cheers!
    – mhelvens
    Sep 19, 2012 at 22:18
  • As for the 'further motivation' bit, see my answer to egreg's comment on my original post. I haven't figured out yet exactly how to go about it, but I'd like to try.
    – mhelvens
    Sep 19, 2012 at 22:19
  • @mhelvens Further reading for plain TeX: TeX by Topic. LaTeX internals: source2e.pdf. Sep 19, 2012 at 22:36
  • Thanks a lot for the comments you put in your code! It's a lot more understandable now.
    – mhelvens
    Sep 20, 2012 at 11:31

You must log in to answer this question.

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