3

TeX FAQ's "Patching existing commands" gives a method to add to existing commands that have an optional argument:

\documentclass{article}
\usepackage{letltxmacro}

\newcommand{\rough}[2][\default]{...}

\LetLtxMacro{\OldRough}{\rough}

\renewcommand{\rough}[2][\newdef]{\mumble\OldRough[{#1}]{#2}}

However, it assumes that you want a new default argument "\newdef". If the default argument is to be kept, you may change "\newdef" to "\default", but that's only possible if you have internal knowledge of the previously defined command. Furthermore, if the that value is modified, you have to change it in your patching as well.

Is it possible to add to an existing command with optional arguments in a way that the default argument is kept, even if you don't know what that argument is?

Note: The argument has to still be usable inside the new definition.


Edit: After trying the suggested \xapptocmd and \xapptocmd I was still unable to reproduce the desired result in the command I'm working with, clevelref's \label.

I would expect the code below to write "aaa", and then apply the label, which is not the case, even when replaced by things such as math, the contents are still not present in the final document:

\documentclass[]{article}

\usepackage{cleveref,xpatch}

\xpretocmd\label{aaa}{}{}

\begin{document}
\label{example}
\end{document}

This gives me the impression that it's not being correctly appended.

Also, when trying to access the second argument of \label, TeX throws the error Illegal parameter number in definition of \etb@resrvda. \xpretocmd\label{#1#2}:

\documentclass[]{article}

\usepackage{cleveref,xpatch}

\xpretocmd\label{#1#2}{}{}

\begin{document}
\end{document}

I presume it is because it is detecting the standard definition of \label, which only has a single parameter, instead of cleverf's one.

How may these be corrected?

1
  • In the special case where the redefined macro does nothing with the arguments but pass them to the macro \OldRough which is let equal to the old definition you can do: \newcommand{\rough}[2][\default]{...} \LetLtxMacro{\OldRough}{\rough} \renewcommand{\rough}{\mumble\OldRough}. Jan 22, 2021 at 18:34

3 Answers 3

5

What does LaTeX do when processing the definition of a command with an optional argument? Say we have

\newcommand{\test}[2][default]{Optional: #1; mandatory: #2.}

With a rather long sequence of steps that's not relevant for the discussion, the basic working is to do

\def\test{\@protected@testopt\test\\test{default}}
\expandafter\def\csname\string\test\endcsname[#1]#2{Optional: #1; mandatory: #2.}

In the first line, \\test represents a single token with a backslash in its name, which can be obtained by \csname\string\test\endcsname.

The macro \@protected@testopt branches according to whether \protect is \relax or not and either calls \@testopt (removing the \else...\fi part) or \@x@protect\test (which removes everything up to \fi and only leaves \protect\test). The macro \@testopt checks for a following [ and does the right thing.

So, if you want to patch the real replacement text of such a command you need to do

\expandafter\patchcmd\csname\string\test\endcsname
  {<search>}
  {<replace>}
  {<success>}{<fail>}

It would be worse if you have

\DeclareRobustCommand{\test}[2][default]{Optional: #1; mandatory: #2.}

because in this case you'd need to do

\expandafter\patchcmd\csname\string\test\space\endcsname
  {<search>}
  {<replace>}
  {<success>}{<fail>}

For no optional argument, in the case of \DeclareRobustCommand{\test}[<n>]{...} the call should be

\expandafter\patchcmd\csname test \endcsname
  {<search>}
  {<replace>}
  {<success>}{<fail>}

This is the reason why I wrote the xpatch package, which is able to test how the command was defined to begin with and calls \patchcmd with the right incantation. It also supports \newrobustcmd from etoolbox and has built-in tools for patching internals of biblatex.

So you can simply do

\usepackage{xpatch}

\xpatchcmd{\test}
  {<search>}
  {<replace>}
  {<success>}{<fail>}

and it will work in all cases.

What if you want to patch the default value for the optional argument? Even without knowing what the default value is?

\documentclass{article}
\usepackage{etoolbox}

\makeatletter
\def\replaceoptionalargument#1#2#3#4{%
  \ifcsname\string#1\endcsname
    \@replaceoptionalargument#1{#2}{#3}%
  \else
    #4%
  \fi
}
\def\@replaceoptionalargument#1#2#3{%
  \edef#1{%
    \unexpanded\expandafter\expandafter\expandafter{%
      \expandafter\@@replaceoptionalargument#1{#2}%
    }%
  }%
  #3%
}
\def\@@replaceoptionalargument#1#2#3#4#5{#1#2#3{#5}}
\makeatother

\newcommand{\test}[2][default]{}

\replaceoptionalargument\test{new}{\message{YES}}{\message{NO}}

Addition about patching \label

When you're trying to patch a command, you have to know in detail how it's defined. For instance, if you do \show\label in the preamble of your test document, you'll be presented the following information

> \label=macro:
#1->\@bsphack \protected@write \@auxout {}{\string \newlabel {#1}{{\@currentlabel }{\thepage }}}\@esphack .

which is the standard LaTeX definition. Indeed, cleveref delays the redefinition at begin document. Good, let's issue \show\label after \begin{document}, getting

> \label=macro:
->\@ifnextchar [\label@optarg \label@noarg .

which shows that \label is not redefined like

\renewcommand{\label}[2][<default>]{...}

so your attempt will fail because \label has no argument at all. Why does the developer do that way? Because there is no default value for the optional argument and different code has to be executed with or without the optional argument.

3
  • Thank you for the detailed explanation! I've looked into a solution with the xpatch package, however some unexpected behaviors arose. I've added them at the end of the question. Jan 16, 2021 at 20:07
  • @LuizMartins I added some comments about patching \label. I'm not sure what your aim is. Maybe a new question with a more detailed explanation of your objective is needed.
    – egreg
    Jan 16, 2021 at 21:54
  • "\label has no argument at all" that... certainly makes what I intended to do unfeasible. Thank you for pointing that out. My objective was to "hack" my way to an answer for this question by redefining \label to create two labels (e.g. one for theorem and theoremAbbr). However, to do that I would need access to the original counter name, which I incorrectly assumed was passed as an optional argument. Jan 16, 2021 at 22:20
4

The xpatch package can patch commands containing optional arguments. The following changes the definition without changing the optional argument's value (and can access the parameters using #1 or #2):

\documentclass[]{article}

\usepackage{xpatch}

\newcommand\foo[2][default]
  {%
    Normal definition. Optional argument: #1. Mandatory argument: #2.%
  }

\begin{document}
\foo{abc}

\xpatchcmd\foo{Normal}{Patched}{}{}
\foo{abc}

\xpretocmd\foo{Optional=#1. }{}{}
\foo{abc}

\xapptocmd\foo{ Mandatory=#2.}{}{}
\foo{abc}
\end{document}
2
  • I've just noticed that I used the word "adding to" in my phrasing, but "patching" in my question. What I actually want to do is to add to the start/end of the command, so I've changed my phrasing to better reflect my intention. After looking at the xpatch documentation, \xpretocmd and \xapptocmd seem to be made for that, but I still have a question about them. Can you use the arguments #1 and #2 inside of them normally? Jan 16, 2021 at 18:31
  • @LuizMartins see the edit. Yes, this is possible.
    – Skillmon
    Jan 16, 2021 at 18:48
0

\show\rough reveals:

> \rough=macro:
->\@protected@testopt \rough \\rough {\newdef }.
l.12 \show\rough

\rough is just a wrapper for launching the LaTeX 2e-kernel's mechanism for detecting the presence of an optional argument while taking the \protection-mechanism into account.

Internally the macro \\rough is called for processing the arguments, thus that's the macro that needs to be patched:

\documentclass{article}

\newcommand{\rough}[2][\default]{... s.th. with #1 and #2 ...}

\expandafter\show\csname\string\rough\endcsname

% > \\rough=\long macro:
% [#1]#2->... s.th. with #1 and #2 ....
% <recently read> \\rough 

%------------------------------------------------------------

\newcommand\PassFirstToSecond[2]{#2{#1}}%
\newcommand\Exchange[2]{#2#1}%

\expandafter\PassFirstToSecond\expandafter{%
  \romannumeral
  \expandafter\expandafter\expandafter\Exchange
  \expandafter\expandafter\expandafter{%
    \csname\string\rough\endcsname[{#1}]{#2}%
  }{0 \mumbleatbegin}%
  \mumbleatend%
}{%
  \long\expandafter\def\csname\string\rough\endcsname[#1]#2%
}%

\expandafter\show\csname\string\rough\endcsname

% > \\rough=\long macro:
% [#1]#2->\mumbleatbegin ... s.th. with #1 and #2 ...\mumbleatend .
% <recently read> \\rough 

\stop

You must log in to answer this question.

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