6

I would like to "wrap" an existing command with another command.

For example, suppose that there is one command

\cmda

I would like that each time this is called we end up executing:

\wrapper{some text}{\cmda}

If this was only one command, I know how to do this:

\let\oldcmda{\cmda}
\renewcommand{\cmda}{\wrapper{some text}{\oldcmda}}

However, what I would like is that this procedure is automated.

For example:

\wrapcmd{cmda}{Some text for cmda}
\wrapcmd{cmdb}{Some other text for cmdb}
\wrapcmd{cmdc}{No text for cmdc}

Looking at this SO for many hours, I managed to realized that the various "patch" commands either append something at the beginning or at the end, but wouldn't work for this case.

Here is my "almost working solution":

\def\oldcmd{}
\newcommand{\wrapcmd}[2]{%
    \expandafter\let\expandafter\oldcmd\csname #1\endcsname%
    \expandafter\renewcommand\csname #1\endcsname{\wrapper{#2}{\oldcmd}}%
}

This works for one command. However, for the case of multiple commands, it doesn't work, because \oldcmd is reused.

I'm trying to cobble together a solution that instead of using \oldcmd would expand to \old#1, with another \csname. However I am stuck.

Here's a minimal example:

\documentclass{article}
\newcommand{\cmda}{CommandA}
\newcommand{\cmdb}{CommandB}

\newcommand{\wrapper}[2]{about (#1): #2}

\def\oldcmd{}
\newcommand{\wrapcmd}[2]{%
    \expandafter\let\expandafter\oldcmd\csname #1\endcsname%
    \expandafter\renewcommand\csname #1\endcsname{\wrapper{#2}{\oldcmd}}%
}

\begin{document}

Before:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}

\wrapcmd{cmda}{This is comment for cmda}
\wrapcmd{cmdb}{This is comment for cmdb}


After:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}


\end{document}

As you can see, now \cmda has the original content of cmdb.

result

0

4 Answers 4

6

Well, every call of \wrapcmd uses always the same macro \oldcmd as backup, so you keep overwriting it.

\documentclass{article}

\newcommand{\cmda}{CommandA}
\newcommand{\cmdb}{CommandB}

\newcommand{\wrapper}[2]{about (#1): #2}

\newcommand{\wrapcmd}[2]{%
   \expandafter\let\csname old@\string#1\endcsname=#1%
   \renewcommand{#1}{\wrapper{#2}{\csname old@\string#1\endcsname}}%
}

\begin{document}

Before:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}

\wrapcmd{\cmda}{This is comment for cmda}
\wrapcmd{\cmdb}{This is comment for cmdb}

After:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}

\end{document}

enter image description here

I've implemented a slightly different (but safer) syntax, such that you should use \wrapcmd{\cmda}{...} instead of your \wrapcmd{cmda}{...}.

1
  • Thanks for the quick response! Happy to leave behind all those expandafter! Commented Mar 27, 2022 at 20:46
4

You want that \cmda becomes \wrapper{some text}{\cmda}, but of course you need an alias for \cmda or you'd get infinite recursion.

Your attempt is almost good, but \let\oldcmda{\cmda} is wrong: no braces should be used. And it's risky because \cmda might be a robust command. The safer way is to do

\NewCommandCopy\oldcmda\cmda
\renewcommand{\cmda}{\wrapper{some text}{\oldcmda}}

Now we want to abstract this. You might decide to use the names, instead of the macros, which is indeed easier:

\newcommand{\wrapcmd}[2]{%
  % alias the command
  \expandafter\NewCommandCopy\csname old#1\expandafter\endcsname\csname #1\endcsname
  \expandafter\renewcommand\csname #1\endcsname{%
    \wrapper{#2}{\csname old#1\endcsname}%
  }%
}

Now a call such as

\wrapcmd{cmda}{Some text}

will do the same as the initial attempt. But this has the drawback that the new definition of \cmda will contain \csname oldcmda\endcsname, not \oldcmda.

Can we do better and get that \cmda will become \wrapper{Some text}{\oldcmda}? Yes, of course we can.

\newcommand{\wrapcmd}[2]{%
  % alias the command
  \expandafter\NewCommandCopy\csname old#1\expandafter\endcsname\csname #1\endcsname
  \expandafter\edef\csname #1\endcsname{%
    \unexpanded{\wrapper{#2}}{\expandafter\noexpand\csname old#1\endcsname}%
  }%
}

Let's try your example.

\documentclass{article}

\newcommand{\cmda}{CommandA}
\newcommand{\cmdb}{CommandB}

\newcommand{\wrapper}[2]{about (#1): #2}

\newcommand{\wrapcmd}[2]{%
  % alias the command
  \expandafter\NewCommandCopy\csname old#1\expandafter\endcsname\csname #1\endcsname
  \expandafter\edef\csname #1\endcsname{%
    \unexpanded{\wrapper{#2}}{\expandafter\noexpand\csname old#1\endcsname}%
  }%
}

\begin{document}

Before:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}

\wrapcmd{cmda}{This is comment for cmda}
\wrapcmd{cmdb}{This is comment for cmdb}

After:
\begin{itemize}
    \item \cmda \\ \texttt{\meaning\cmda}
    \item \cmdb \\ \texttt{\meaning\cmdb}
\end{itemize}

\end{document}

Of course you want to use something different from old as a prefix.

enter image description here

You can avoid all those \expandafter tokens, though, and use the command instead of the name.

\documentclass{article}
\usepackage{geometry} % wider textwidth

\newcommand{\cmda}{CommandA}
\newcommand{\cmdb}{CommandB}

\newcommand{\wrapper}[2]{about (#1): #2}

\ExplSyntaxOn

\NewDocumentCommand{\wrapcmd}{mm}
 {
  \andreacensi_wrapcmd:Ncn #1 {andreacensi@\cs_to_str:N #1} { #2 }
 }
\cs_new_protected:Nn \andreacensi_wrapcmd:NNn
 {
  \NewCommandCopy #2 #1
  \renewcommand #1 { \wrapper{#3}{#2} }
 }
\cs_generate_variant:Nn \andreacensi_wrapcmd:NNn { Nc }

\ExplSyntaxOff

\begin{document}

Before:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}

\wrapcmd{\cmda}{This is comment for cmda}
\wrapcmd{\cmdb}{This is comment for cmdb}

After:
\begin{itemize}
    \item \cmda \\ \texttt{\meaning\cmda}
    \item \cmdb \\ \texttt{\meaning\cmdb}
\end{itemize}

\end{document}

The trick is to use a further argument, which will be the alias name for the old macro, to \andreacensi_wrapcmd:NNn (the c variant will build the command).

enter image description here

3
  • I tried this code - something very weird happens. For context, the commands wrapped are used in math mode. If I wrap them with this code, then \mathsf{...}, \mathrm{...}, \mathcal{...} produce empty strings. I have difficulty providing a minimal example because it's a very large project. Does this ring a bell? Commented Mar 28, 2022 at 17:13
  • @AndreaCensi I suspected your real application was different. Sorry, but the information is really insufficient. Please open a new question with the details of what you're really after.
    – egreg
    Commented Mar 28, 2022 at 17:17
  • Coming back to this to say that the solution worked perfectly. The error I had been seeing was a mistake on my part in implementing the solution. Commented Mar 30, 2022 at 12:56
2

Some facts:

  • Non-delimited macro arguments consisting of a single token don't need to be nested in curly braces.
  • \csname..\endcsname is used for creating a control-sequence-token.
  • The \expandafter\endcsname\csname-trick can be used for creating several control sequence-tokens in a row:
    \csname foo\expandafter\endcsname\csname bar\expandafter\endcsname\csname baz\endcsname
    yields
    \foo\bar\baz
    by triggering a single expansion-step on the first \csname.

A common trick is to have a helper-macro do the actual work which gets as arguments all the control-sequence-tokens needed, while within the definition of the main macro/user-level-macro using the \expandafter\endcsname\csname-trick for creating and passing as arguments to the helper-macro all the control-sequence-tokens needed:

%Format: LaTeX2e
%
\newcommand\wrapcmd[2]{%
  % \wrapcmd{foo}{argument} yields: \wrapcmdhelper\foo\oldfoo{argument}
  \expandafter\wrapcmdhelper\csname#1\expandafter\endcsname\csname old#1\endcsname{#2}%
}%
\newcommand\wrapcmdhelper[3]{%
  % Probably \protected\def instead of \def?
  \csname @ifdefinable\endcsname#2{\let#2=#1\def#1{\wrapper{#3}{#2}}}%
}%
%%===============================================================================
%% Testing:
%%===============================================================================
\wrapcmd{cmda}{Some text for cmda}
\message{^^J\string\cmda=\meaning\cmda^^J}
%%-------------------------------------------------------------------------------
\wrapcmd{cmdb}{Some other text for cmdb}
\message{^^J\string\cmdb=\meaning\cmdb^^J}
%%-------------------------------------------------------------------------------
\wrapcmd{cmdc}{No text for cmdc}
\message{^^J\string\cmdc=\meaning\cmdc^^J}
%%-------------------------------------------------------------------------------
\stop

Console Output:

This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex-dev)
 restricted \write18 enabled.
entering extended mode
(./test.tex
LaTeX2e <2021-05-01> pre-release-1 (develop 2021-2-27 branch)
L3 programming layer <2021-02-18>

\cmda=macro:->\wrapper {Some text for cmda}{\oldcmda }

\cmdb=macro:->\wrapper {Some other text for cmdb}{\oldcmdb }

\cmdc=macro:->\wrapper {No text for cmdc}{\oldcmdc }
 )
No pages of output.
Transcript written on test.log.

Probably a routine \CsNameToCsToken⟨stuff not in braces⟩{NameOfCs}⟨stuff not in braces⟩\NameOfCs where ⟨stuff not in braces⟩ may be empty also does the trick:

%Format: LaTeX2e
%
\makeatletter
%%===============================================================================
%% End \romannumeral-driven expansion safely:
%%===============================================================================
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
%%===============================================================================
%% Obtain control sequence token from name of control sequence token:
%%===============================================================================
%% \CsNameToCsToken<stuff not in braces>{NameOfCs}
%% ->  <stuff not in braces>\NameOfCs
%% (<stuff not in braces> may be empty.)
\@ifdefinable\CsNameToCsToken{%
  \long\def\CsNameToCsToken#1#{\romannumeral\InnerCsNameToCsToken{#1}}%
}%
\newcommand\InnerCsNameToCsToken[2]{%
  \expandafter\UD@Exchange\expandafter{\csname#2\endcsname}{\UD@stopromannumeral#1}%
}%
\newcommand\UD@Exchange[2]{#2#1}%
%%===============================================================================
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
%%===============================================================================
%% \wrapcmd{<csname>}{<arg>}
%%===============================================================================
\newcommand\wrapcmd[2]{%
  \CsNameToCsToken\@ifdefinable{old#1}{%
     \CsNameToCsToken\CsNameToCsToken\let{old#1}={#1}%
     %\CsNameToCsToken\protected\expandafter\def\expandafter{#1}\expandafter{%
     \CsNameToCsToken\expandafter\def\expandafter{#1}\expandafter{%
       \romannumeral
       \CsNameToCsToken\UD@PassFirstToSecond{old#1}{\UD@stopromannumeral\wrapper{#2}}%
     }%
  }%  
}%
\makeatother
%%===============================================================================
%% Testing:
%%===============================================================================
\wrapcmd{cmda}{Some text for cmda}
\message{^^J\string\cmda=\meaning\cmda^^J}
%%-------------------------------------------------------------------------------
\wrapcmd{cmdb}{Some other text for cmdb}
\message{^^J\string\cmdb=\meaning\cmdb^^J}
%%-------------------------------------------------------------------------------
\wrapcmd{cmdc}{No text for cmdc}
\message{^^J\string\cmdc=\meaning\cmdc^^J}
%%-------------------------------------------------------------------------------
\stop

Console-output:

This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex-dev)
 restricted \write18 enabled.
entering extended mode
(./test.tex
LaTeX2e <2021-05-01> pre-release-1 (develop 2021-2-27 branch)
L3 programming layer <2021-02-18>

\cmda=macro:->\wrapper {Some text for cmda}{\oldcmda }

\cmdb=macro:->\wrapper {Some other text for cmdb}{\oldcmdb }

\cmdc=macro:->\wrapper {No text for cmdc}{\oldcmdc }
 )
No pages of output.
Transcript written on test.log.

If the patched variants of commands are to be used only within code written by you so that patches to commands shall not affect code written by others/shall not affect macros when being used in package-code and the like provided by others, then you might consider not patching commands at all but just defining a macro \wrapcmd{NameOfCs}{arg} which does \wrapper{arg}{\NameOfCs} and within your code using that instead of \NameOfCs:

%Format: LaTeX2e
%
\newcommand\PassFirstToSecond[2]{#2{#1}}%
\newcommand\wrapcmd[2]{%
  \expandafter\PassFirstToSecond\expandafter{\csname#1\endcsname}{\wrapper{#2}}%
}%
% Dummy-definition for \wrapper
\newcommand\wrapper[2]{\message{^^J\detokenize{\wrapper{#1}{#2}}^^J}}%

\wrapcmd{cmda}{Some text for cmda}
\wrapcmd{cmdb}{Some other text for cmdb}
\wrapcmd{cmdc}{No text for cmdc}

\stop

Console-output:

This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex-dev)
 restricted \write18 enabled.
entering extended mode
(./test.tex
LaTeX2e <2021-05-01> pre-release-1 (develop 2021-2-27 branch)
L3 programming layer <2021-02-18> 
\wrapper {Some text for cmda}{\cmda }

\wrapper {Some other text for cmdb}{\cmdb }

\wrapper {No text for cmdc}{\cmdc }
 )
No pages of output.
Transcript written on test.log.

In order to ensure that the result is delivered by triggering two expansion-steps you can wrap this into \romannumeral-expansion:

%Format: LaTeX2e
%
\makeatletter
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\wrapcmd[2]{%
  \romannumeral
  \expandafter\UD@PassFirstToSecond
  \expandafter{\csname#1\endcsname}%
              {\UD@stopromannumeral\wrapper{#2}}%
}%
\makeatother

\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{%
  \wrapcmd{cmda}{Some text for cmda}%
}%
\message{^^J\string\temp=\meaning\temp^^J}

\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{%
  \wrapcmd{cmdb}{Some other text for cmdb}%
}%
\message{^^J\string\temp=\meaning\temp^^J}


\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\temp
\expandafter\expandafter\expandafter{%
  \wrapcmd{cmdc}{No text for cmdc}%
}%
\message{^^J\string\temp=\meaning\temp^^J}

\stop

Console-output:

This is pdfTeX, Version 3.14159265-2.6-1.40.21 (TeX Live 2020) (preloaded format=pdflatex-dev)
 restricted \write18 enabled.
entering extended mode
(./test.tex
LaTeX2e <2021-05-01> pre-release-1 (develop 2021-2-27 branch)
L3 programming layer <2021-02-18>

\temp=macro:->\wrapper {Some text for cmda}{\cmda }

\temp=macro:->\wrapper {Some other text for cmdb}{\cmdb }

\temp=macro:->\wrapper {No text for cmdc}{\cmdc }
 )
No pages of output.
Transcript written on test.log.
1

If you are able to expand the command immediately, you can define \oldcmd local to a group. If you have to wait, then you will need a global macro with a different name for each usage.

\documentclass{article}
\newcommand{\cmda}{CommandA}
\newcommand{\cmdb}{CommandB}

\newcommand{\wrapper}[2]{about (#1): #2}

\newcommand{\wrapcmd}[2]{\bgroup
    \expandafter\let\expandafter\oldcmd\csname #1\endcsname%
    \expandafter\xdef\csname #1\endcsname{\wrapper{#2}{\oldcmd}}%
\egroup}

\begin{document}

Before:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}

\wrapcmd{cmda}{This is comment for cmda}
\wrapcmd{cmdb}{This is comment for cmdb}


After:
\begin{itemize}
    \item \cmda
    \item \cmdb
\end{itemize}

\end{document}

You must log in to answer this question.

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