Take the 2-minute tour ×
TeX - LaTeX Stack Exchange is a question and answer site for users of TeX, LaTeX, ConTeXt, and related typesetting systems. It's 100% free, no registration required.

This question led to a new package:
xpatch

This is likely a simple question, but I'm getting stuck on the small details. I have an internal command, defined by a package as

\newcommand\foo@[3][]{...stuff...}

and somewhere in there I'd like to patch the command to include a call to \label{#1}. I'm already using etoolbox, so I planned to use

\patchcmd{\foo@}{hook}{\label{#1}hook}{success}{failure}

Unfortunately this doesn't work, because \foo@ is defined as

> \foo@=macro:
->\@protected@testopt \foo@ \\foo@ {}.
l.359 \show\foo@

Which has no parameters and therefore the patching code fails. So I tried to figure out how to get \\foo@  (note the doubled slash and the trailing space) as a csname, and I got snarled up.

So then I tried using the old-style approach of \let\my@foo@=\foo@\renewcommand\foo@[3][]{...\my@foo@[#1]{#2}{#3} -- I know the original idiom is to use \let and \def, but since \foo@ was defined with \newcommand I tried using \renewcommand. But this doesn't work, and fails with a TeX capacity exceeded error, and the log file appears to show an endless cycle of macro expansion.

So. What is the correct name for the internal macro defined by \newcommand when optional arguments are present, and how do I build the csname for that so I can patch it with \patchcmd?

share|improve this question
add comment

3 Answers

up vote 17 down vote accepted

You can get \\foo@ using \csname\string\foo@\endcsname. The space shown behind it with \show is not part of the macro name (but could be added using a \space before \endcsname). So you can use:

\expandafter\patchcmd\csname\string\foo@\endcsname{hook}{\label{#1}hook}{success}{failure}

Full example:

\documentclass{article}

\usepackage{etoolbox}

\begin{document}

\makeatletter
\newcommand\foo@[3][]{stuff (#1) more stuff :#2: hook even more stuff = *#3*}

\expandafter\patchcmd\csname\string\foo@\endcsname{hook}{\label{#1}hook}{success}{failure}

\texttt{\string\foo@ = \meaning\foo@}

\texttt{\expandafter\string\csname\string\foo@\endcsname = \expandafter\meaning\csname\string\foo@\endcsname}

\makeatother


\section{Test}
\makeatletter
\foo@[test]{One}{Two}
\makeatother

\section{Test2}
See section \ref{test}!

\end{document}

Result

share|improve this answer
    
Thanks, that works. (Well, with the slight tweak that I need a trailing space, so \csname\string\foo@ \endcsname, but the \string command was what I was missing) –  Ben Lerner Jul 27 '11 at 17:16
    
I don't see why you need the space. \show places one space after every control sequence (aka macro), so there would be two after \\foo@ if it would include one space. I know that robust commands use a trailing space in the internal macro name, but this isn't used here. –  Martin Scharrer Jul 27 '11 at 17:42
    
@Ben Lerne: besides, the space before \endcsname in your comment is ignored by TeX, since it follows a control word. –  Bruno Le Floch Jul 28 '11 at 13:34
add comment

NOTE: There are some minor weaknesses in the code shown below, so it's best to use the corresponding package.


The following macros define the commands \xpatchcmd, \xpretocmd and \xapptocmd that recognize robust commands (defined by \DeclareRobustCommand or \newrobustcmd) and also if they have an optional argument.

\usepackage{etoolbox,xparse}

\ExplSyntaxOn
\makeatletter
% generate a variant of \tl_if_in:NnT
\cs_generate_variant:Nn \tl_if_in:NnT { Nx }
% a boolean for testing robust commands
\bool_new:N \l_xpatch_protect_bool
% a "bizarre" token list that's quite improbable to find in a replacement text
\tl_set:Nx \c_xpatch_bizarre_tl { \tl_to_str:n { **)-(**/**[-]** } }
% the main command; it takes as first argument one of \patchcmd, \pretocmd or
% \apptocmd; the second argument is the command we want to patch
\cs_new:Npn \xpatchcmd_main:NN #1 #2
  {
% first of all we gather the command-to-patch name
   \tl_set:Nx \l_xpatch_name_tl { \exp_after:wN \use_none:n \token_to_str:N #2  }
% now we get the replacement text of the command-to-patch, but adding the bizarre
% token list in front of it which consists of all category 12 characters
   \tl_set:Nx \l_xpatch_tmpa_tl { \c_xpatch_bizarre_tl \token_get_replacement_spec:N #2 }
% now we look whether the token list contains the bizarre list followed by \protect
% which happens if it's a control sequence defined by \DeclareRobustCommand, so
% we add a space to the command name
   \tl_if_in:NxT \l_xpatch_tmpa_tl { \c_xpatch_bizarre_tl \token_to_str:N \protect} 
     {
      \bool_set_true:N \l_xpatch_protect_bool 
      \xpatch_add_space:
     }
% now we look whether the token list contains the bizarre list followed by \x@protect
% which happens if it's a control symbol defined by \DeclareRobustCommand, so
% we add a space to the command name
   \tl_if_in:NxT \l_xpatch_tmpa_tl { \c_xpatch_bizarre_tl \token_to_str:N \x@protect} 
     {
      \bool_set_true:N \l_xpatch_protect_bool
      \xpatch_add_space:
     }
% now we look whether the token list contains the bizarre list followed by \@protected@testopt
% which happens if it's a command with an optional argument (from \newcommand)
   \tl_if_in:NxT \l_xpatch_tmpa_tl { \c_xpatch_bizarre_tl \token_to_str:N \@protected@testopt} 
     { \xpatch_add_backslash: }
% now we look whether the token list contains the bizarre list followed by \@testopt
% which happens if it's a command with an optional argument (from \newrobustcmd)
   \tl_if_in:NxT \l_xpatch_tmpa_tl { \c_xpatch_bizarre_tl \token_to_str:N \@testopt} 
     { \xpatch_add_backslash: }
% if the command-to-patch was defined by \DeclareRobustCommand we look whether the
% command name with backslash in front and space at the end exists and we assume that
% it has an optional argument
   \bool_if:NT \l_xpatch_protect_bool 
     {
      \cs_if_exist:cT { \c_backslash_tl \l_xpatch_name_tl }
        { \xpatch_add_backslash: }
     }
% now we pass the real command-to-patch name to the patching macro
   \exp_after:wN #1 \cs:w \l_xpatch_name_tl \cs_end:
  }
% how we add a space after the name
\cs_new:Npn \xpatch_add_space: 
  { \tl_set:Nx \l_xpatch_name_tl { \l_xpatch_name_tl \c_space_tl } }
% how we add a backslash in front of the name
\cs_new:Npn \xpatch_add_backslash:
  { \tl_set:Nx \l_xpatch_name_tl { \c_backslash_tl \l_xpatch_name_tl } }
% a constant containing a backslash (category 12)
\tl_set:Nx \c_backslash_tl { \exp_after:wN \use_none:n \token_to_str:N \\ }
% the user level commands
\NewDocumentCommand{\xpatchcmd}{}{ \xpatchcmd_main:NN \patchcmd }
\NewDocumentCommand{\xpretocmd}{}{ \xpatchcmd_main:NN \pretocmd }
\NewDocumentCommand{\xapptocmd}{}{ \xpatchcmd_main:NN \apptocmd }
\NewDocumentCommand{\xshowcmd}{}{ \xpatchcmd_main:NN \show }


\NewDocumentCommand{\xpatchbibmacro} { m }
  { \exp_after:wN \xpatchcmd_main:NN \exp_after:wN \patchcmd \cs:w abx@macro@ \tl_to_str:n {#1} \cs_end: }
\NewDocumentCommand{\xpretobibmacro} { m }
  { \exp_after:wN \xpatchcmd_main:NN \exp_after:wN \pretocmd \cs:w abx@macro@ \tl_to_str:n {#1} \cs_end: }
\NewDocumentCommand{\xapptobibmacro} { m }
  { \exp_after:wN \xpatchcmd_main:NN \exp_after:wN \apptocmd \cs:w abx@macro@ \tl_to_str:n {#1} \cs_end: }
\NewDocumentCommand{\xshowbibmacro} { m }
  { \exp_after:wN \xpatchcmd_main:NN \exp_after:wN \show \cs:w abx@macro@ \tl_to_str:n {#1} \cs_end: }

\makeatother
\ExplSyntaxOff

Of course commands defined in a devious way can break these macros, but this is true of almost any set of macros.

Patching commands defined by \NewDocumentCommand or the others provided by xparse is not supported.

Usage

\newcommand\foo[3][]{...hook...}
\xpatchcmd{\foo}{hook}{\label{#1}hook}{success}{failure}

\xshowcmd\foo

The last one shows the "real" meaning of \foo. The commands

\xpatchbibmacro \xpretobibmacro \xapptobibmacro 

can be used to patch commands defined via \newbibmacro of biblatex; with \xshowbibmacro one can see the "real" meaning of the macro.

share|improve this answer
add comment

To patch a command (with or without an optional argument) defined by means of \newcommand or \DeclareRobustCommand I use a wrapper \xpatchcmd around \patchcmd. \xpatchcmd has an additional optional argument in front of the first argument of \patchcmd that indicates the nature of the command to be patched:

  • n --> \newcommand
  • o --> \newcommand + optional argument
  • N --> \DeclareRobustCommand
  • O --> \DeclareRobustCommand + optional argument

Definition and application of \xpatchcmd:

\documentclass{article}

\usepackage{etoolbox}

\usepackage{xifthen}

\makeatletter

\newcommand*{\xpatchcmd}[1][]{%
  \bgroup%
  \let\@gobble@\@gobble\let\@space@\empty%
  \ifthenelse{\isempty{#1}\OR\equal{\unexpanded{#1}}{n}}{}{%
    \ifthenelse{\equal{\unexpanded{#1}}{o}}{\let\@gobble@\empty}{%
      \ifthenelse{\equal{\unexpanded{#1}}{N}}{\let\@space@\space}{%
        \ifthenelse{\equal{\unexpanded{#1}}{O}}{\let\@gobble@\empty\let\@space@\space}{%
          \typeout{%
            Command \protect\xpatchcmd\space Warning: Invalid command specifier "\unexpanded{#1}".%
          }%
        }%
      }%
    }%
  }%
  \catcode`\#=12%
  \@xpatchcmd%
}

\newcommand{\@xpatchcmd}[6][\undefined]{%
  \catcode`\#=6%
  \edef\undefined{\noexpand\undefined}%
  \def\@prefix{#1}%
  \letcs{\@command}{\expandafter\@gobble@\string#2\@space@}%
  \ifx\@prefix\undefined%
    \patchcmd{\@command}{#3}{#4}{#5}{#6}%
  \else%
    \patchcmd[\@prefix]{\@command}{#3}{#4}{#5}{#6}%
  \fi%
  \global\cslet{\expandafter\@gobble@\string#2\@space@}{\@command}%
  \egroup%
}

\makeatother

\begin{document}

\newcommand*{\test}[2]{#1me#2}
\show\test
\xpatchcmd[n][\protected]{\test}{me}{you}{}{}
\show\test

\renewcommand{\test}[2][]{#1me#2}
\expandafter\show\csname\string\test\endcsname
\xpatchcmd[o][]{\test}{me#2}{you#1}{}{}
\expandafter\show\csname\string\test\endcsname

\DeclareRobustCommand*{\test}[2]{#1me#2}
\expandafter\show\csname test \endcsname
\xpatchcmd[N][\long]{\test}{#1me}{#2you}{}{}
\expandafter\show\csname test \endcsname

\DeclareRobustCommand*{\test}[2][only]{#1me#2}
\expandafter\show\csname\string\test\space\endcsname
\xpatchcmd[O]{\test}{#2}{}{}{}
\expandafter\show\csname\string\test\space\endcsname

\end{document}
share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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