Add material to a macro with parameter

Here's a shameless copy minimal implementation of etoolbox's \apptocmd. I left out all sanity checking of the input macro to keep the code to a reasonable amount. This assumes that: the macro (that is, the hook being added to) is defined, is a macro, has parameters (otherwise the patching can be simply done with \edef and \unexpanded), can be used in \scantokens without change in meaning (that is, all its tokens have the same catcodes as the ones in force when the patching is done), and any parameter token in the text-to-be-patched is not catcode 6. In short, eveything in \etb@hooktocmd (line 1357 of etoolbox.sty) passes.

That said, the actual appending process begins. First it defines a temporary \etb@resrvda which splits the macro (the one being patched) in three parts: its prefix, its parameter text, and its replacement text. When you do \meaning\mymacro TeX expands to the (catcode 10 and 12) tokens:

⟨prefixes⟩macro:⟨parameter text⟩->⟨replacement text⟩

where ⟨prefixes⟩ is a combination of \long, \protected and David's favourite, \outer, or empty. ⟨parameter text⟩ and ⟨replacement text⟩ have their usual meaning. The macro above could be re-defined with ⟨prefixes⟩\def\mymacro⟨parameter text⟩{⟨replacement text⟩}. As you can guess, this will be used to redefine it with the new text appended to it. The temporary macro looks like this:

%                                                       V --- catcode 12 -- V
\def\etb@resrvda#1macro:#2->#3&{#1\def\​etb@resrvda #2{#3⟨text-to-be-inserted⟩}}%
%                                     ^ not a macro

where everything marked under catcode 12 is the expansion of \detokenize{⟨text-to-be-inserted⟩}. Note also that the \​etb@resrvda inside the definition of \etb@resrvda (the one marked not a macro) is not a macro, but the character tokens shown (i.e, \string\etb@resrvda). Then it uses that macro in:

\edef\etb@resrvda{\etb@resrvda\meaning⟨macro-to-patch⟩&}

which will split the ⟨macro-to-patch⟩ as discussed above leaving you with:

⟨prefixes⟩\def\​etb@resrvda⟨parameter text⟩{⟨replacement text⟩⟨text-to-be-inserted⟩}

everything(ish) in catcode 12. After that a properly set \scantokens is used to retokenize that and perform the definition.

It's not an absurdely complicated process (even though I made it look so), but it's not trivial either, and it has many details here and there which make it to a handful of code, and that is without all the verification if the macro can be patched “cleanly” and so on.

The process for prepending tokens is the same, except the order of the tokens is changed. Patching is also similar, but somewhere in between you have a delimited macro that will split the macro-to-be-patched in two.


Now, specific to your case: etoolbox's \(patch|appto|preto)cmd try to ensure that the # are all read in with catcode 12 to avoid the usual #-duplication issue. However you put the patching inside a macro, so you froze the catcode of # and etoolbox complains. As I removed all of that, the patching silently fails. To avoid that you need to define the \addtohook under a different catcode setting, in which # (or whatever parameter character you are using when you use \addtohook) is catcode 12. I defined the macro to have / as a parameter character.


Here's your code:

\documentclass{article}

%%% Code stolen from etoolbox.sty
\makeatletter
\protected\def\apptocmd{%
  \begingroup
    \@makeother\#%
    \etb@hooktocmd}
\long\def\etb@hooktocmd#1#2{%
  \endgroup
  \begingroup
    \edef\etb@resrvda{%
      \def\noexpand\etb@resrvda####1\detokenize{macro}:####2->####3&{%
        ####1\def\string\etb@resrvda\space####2{####3\detokenize{#2}}}%
      \edef\noexpand\etb@resrvda{%
        \noexpand\etb@resrvda\meaning#1&}}%
    \etb@resrvda
  \etb@patchcmd@scantoks\etb@resrvda
  \let#1\etb@resrvda
  \let\etb@resrvda\etb@undefined}
\def\etb@patchcmd@scantoks#1{%
  \edef\etb@resrvda{\endgroup
    \endlinechar\m@ne
    \unexpanded{\makeatletter\scantokens}{#1}%
    \endlinechar\the\endlinechar\relax
    \catcode\number`\@=\the\catcode`\@\relax}%
  \etb@resrvda}
\makeatother
%%%

\def\hook#1{hello}
\def\dosomething#1#2{.(#1).[#2].}
\begingroup
  \catcode`/=6
  \catcode`#=12
  \gdef\addtohook/1{%
    \apptocmd\hook
      {\dosomething{#1}{/1}}%
    \show\hook
  }
\endgroup

\addtohook{foo}
\addtohook{bar}
\addtohook{baz}

\begin{document}

\texttt{\meaning\hook}
\hook{hey}

\end{document}

and the output is:

enter image description here


All in all, I'd recommend loading etoolbox instead ;-)


After looking at your MWE for testing I assume that you are happy with LaTeX.

The doubling and halving of the amount of consecutive hashes might be a source for problems:

When during expansion of a macro delivering the ⟨balanced text⟩ of a definition, (La)TeX will collapse two consecutive hashes into one, i.e., the amount of consecutive hases will be halved.

E.g., with \def\temp{######}, expanding \temp yields: ###.

The hashes inside the ⟨balanced text⟩ of \unexpanded will be doubled when \unexpanded takes place during an \edef or \xdef.

The hashes inside the ⟨balanced text⟩ of the content of a token-register will be doubled in case the content of that token-register is delivered via \the-expansion during an \edef or \xdef.

You tried:

\def\addtohook#1{%
    \edef\hook##1{%
        \unexpanded\expandafter{\hook{#1}}%
        \noexpand\dosomething{##1}{#1}%
    }%
}

This will in the set of tokens that formerly formed the ⟨replacement text⟩ of \hook replace the macro-parameter #1, e.g. by foo.
And you might get undesired expansion of \addtohook's argument.

You might try:

\def\addtohook#1{%
    \edef\hook##1{%
        \unexpanded\expandafter{%
          \hook{##1}\dosomething{##1}{#1}%
        }%
    }%
}

But this way you get undesired hash-doubling: With that above definition, e.g., try

\def\hook#1{\dosomething{#1}{start}}%
\addtohook{\def\bal#1{#1}}
\show\hook
\addtohook{foo}
\show\hook
\addtohook{bar}
\show\hook
\addtohook{baz}
\show\hook
\addtohook{\def\bat#1{#1}}
\show\hook
\csname stop\endcsname % stop a LaTeX run
\bye % stop a plain TeX run

and see what you get.

You cannot easily get out of this hash-doubling-pitfall because e(La)TeX's \unexpanded/ (La)TeX's \the⟨token register⟩ inside \edef or \xdef cannot know whether a hash comes from \addtohook's argument and thus forms a token of another \dosomething-instance's second argument and therefore should be doubled or whether that hash was provided as argument to \hook in order to obtain that set of tokens that forms the former definition-text of \hook and therefore should not be doubled.

The gist of the pitfall is:

\newtoks\mytoks
%
\def\test#1{#1##1####1}%
\show\test
%
\mytoks\expandafter{\test{#1}}%
\edef\test#1{\the\mytoks}%
\show\test
%
\def\test#1{#1##1####1}%
\edef\test#1{\unexpanded\expandafter{\test{#1}}}%
\show\test
%
\csname stop\endcsname % stop a LaTeX run
\bye % stop a plain TeX run

The first \show yields something that looks okay:

> \test=macro:
#1->#1##1####1.

The second and the third \shows yield something that does not look okay as the very first hash after -> is doubled:

> \test=macro:
#1->##1##1####1.

The reason is:

With the \test-assignments before the second and third \show the amounts of consecutive hashes inside the definition-text get halved at the time of expanding \test and the one hash that belongs to #1 will be replaced by the token sequence #, 1:

After \def\test#1{#1##1####1}, \mytoks\expandafter{\test{#1}}% yields: \mytoks{#1#1##1}% because the second and third hash-sequence get halved while the first hash-sequence forms the parameter on this level of expansion and therefore gets replaced by the token-sequence inside \test's argument, which is #1. During the following \edef-assignment all hashes that stem from the token-register's content will be doubled.

After \def\test#1{#1##1####1}, \unexpanded\expandafter{\test{#1}}% yields: \unexpanded{#1#1##1}% because the second and third hash-sequence get halved while the first hash-sequence forms the parameter on this level of expansion and therefore gets replaced by the token-sequence inside \test's argument, which is #1. As \unexpanded gets carried out during \edef, all hashes that stem from carrying out \unexpanded will be doubled.

Therefore I suggest a different route:

Do something like this (sort of pseudocode):

\def\addtohook#1{%
  \def\hook##1{%
     Within the sequence 
        ( Expansion of \hook{<reserved token>1} + \dosomething{<reserved token>1}{#1} )
     have every hash doubled and every instance of <reserved token> replaced by a single hash.
   }%
}%

Of course you also need to check whether \hook is already defined.

This is what I implemented in the example below. With the example below eTeX-extensions are a requirement for implementing a reliable check for finding out whether a single token is an explicit character token of category code 6 (parameter) / for finding out whether a single token is an explicit hash character token. The gist of that test is: Apply \string to a hash and you get a single explicit character token of category code 12(other) . Apply eTeX's \detokenize to a hash and you get two such tokens because \detokenize doubles hashes.

The example below uses \romannumeral-expansion a lot: The gist of \romannumeral-expansion is that \romannumeral itself triggers a lot of expansion work but does silently not deliver any token in case after all that expansion work it finds a number which is not positive. This \romannumeral-feature is handy because it implies that in many situations a single \expandafter-chain "hitting" \romannumeral is sufficient for triggering several expansion-steps. You only need to ensure that the expansion work results in a token sequence whose leading tokens are, e.g., 0 and [space]. For \romannumeral that sequence will form the number 0 which is not positive and therefore that sequence will silently be discarded while anything behind it in the token-stream will be left in place.

I elaborated on that in my answer to the question How can I know the number of expandafters when appending to a csname macro?

\documentclass{article}

\makeatletter
%%=============================================================================
%% Paraphernalia:
%%    \UD@firstoftwo, \UD@secondoftwo,
%%    \UD@PassFirstToSecond, \UD@Exchange, \UD@removespace
%%    \UD@CheckWhetherNull, \UD@CheckWhetherBrace,
%%    \UD@CheckWhetherLeadingSpace, \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\newcommand\UD@removespace{}\UD@firstoftwo{\def\UD@removespace}{} {}%
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%%                     {<Tokens to be delivered in case that argument
%%                       which is to be checked is empty>}%
%%                     {<Tokens to be delivered in case that argument
%%                       which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
  \romannumeral0\expandafter\UD@secondoftwo\string{\expandafter
  \UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
  \UD@secondoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \UD@CheckWhetherBrace{<Argument which is to be checked>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked has leading
%%                        catcode-1-token>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked has no leading
%%                        catcode-1-token>}%
\newcommand\UD@CheckWhetherBrace[1]{%
  \romannumeral0\expandafter\UD@secondoftwo\expandafter{\expandafter{%
  \string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
  \UD@firstoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingSpace{<Argument which is to be checked>}%
%%                             {<Tokens to be delivered in case <argument
%%                               which is to be checked>'s 1st token is a
%%                               space-token>}%
%%                             {<Tokens to be delivered in case <argument
%%                               which is to be checked>'s 1st token is not
%%                               a space-token>}%
\newcommand\UD@CheckWhetherLeadingSpace[1]{%
  \romannumeral0\UD@CheckWhetherNull{#1}%
  {\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
  {\expandafter\UD@secondoftwo\string{\UD@CheckWhetherLeadingSpaceB.#1 }{}}%
}%
\newcommand\UD@CheckWhetherLeadingSpaceB{}%
\long\def\UD@CheckWhetherLeadingSpaceB#1 {%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@secondoftwo#1{}}%
  {\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
  {\UD@Exchange{ }{\expandafter\expandafter\expandafter\expandafter
   \expandafter\expandafter\expandafter}\expandafter\expandafter
   \expandafter}\expandafter\UD@secondoftwo\expandafter{\string}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument contains no exclamation mark which is not nested 
%% in braces:
%%.............................................................................
%% \UD@CheckWhetherNoExclam{<Argument which is to be checked>}%
%%                         {<Tokens to be delivered in case that argument
%%                           contains no exclamation mark>}%
%%                         {<Tokens to be delivered in case that argument
%%                           contains exclamation mark>}%
%%
\newcommand\UD@GobbleToExclam{}\long\def\UD@GobbleToExclam#1!{}%
\newcommand\UD@CheckWhetherNoExclam[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@GobbleToExclam#1!}%
}%
%%-----------------------------------------------------------------------------
%%  \addtohook@reservedFork grabs the first thing behind a
%%  a token-sequence of pattern  !!\addtohook@reserved!
%%.............................................................................
\newcommand\addtohook@reservedFork{}
\long\def\addtohook@reservedFork#1!!\addtohook@reserved!#2#3!!!!{#2}%
%%-----------------------------------------------------------------------------
%% Check whether argument consists only of the token \addtohook@reserved
%%.............................................................................
\newcommand\UD@CheckWhetherAddtohook@reserved[1]{%
  \romannumeral0%
  \UD@CheckWhetherNoExclam{#1}{%
    \addtohook@reservedFork
    %Case #1 is empty/has no tokens:
      !#1!\addtohook@reserved!{\UD@Exchange{ }{\expandafter}\UD@secondoftwo}%
    %Case #1 = \addtohook@reserved:
      !!#1!{\UD@Exchange{ }{\expandafter}\UD@firstoftwo}%
    %Case #1 = something else without exclamation-mark:
      !!\addtohook@reserved!{\UD@Exchange{ }{\expandafter}\UD@secondoftwo}%
      !!!!%
  }{%
    %Case #1 = something else with exclamation-mark:
    \UD@Exchange{ }{\expandafter}\UD@secondoftwo
  }%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%
%%   \UD@ExtractFirstArg{ABCDE} yields  {A}
%%
%%   \UD@ExtractFirstArg{{AB}CDE} yields  {AB}
%%.............................................................................
\newcommand\UD@RemoveTillUD@SelDOm{}%
\long\def\UD@RemoveTillUD@SelDOm#1#2\UD@SelDOm{{#1}}%
\newcommand\UD@ExtractFirstArg[1]{%
  \romannumeral0%
  \UD@ExtractFirstArgLoop{#1\UD@SelDOm}%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
  { #1}%
  {\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillUD@SelDOm#1}}%
}%
%%=============================================================================
%% \DoubleEveryHashAndReplaceAddtohook@reserved{<argument>}%
%%
%%   Each explicit catcode-6(parameter)-character-token of the <argument> 
%%   will be doubled. Each instance of \addtohook@reserved will be replaced
%%   by a single hash.
%%
%%   You obtain the result after two expansion-steps, i.e., 
%%   in expansion-contexts you get the result after "hitting" 
%%   \DoubleEveryHashAndReplaceAddtohook@reserved by two \expandafter.
%%   
%%   As a side-effect, the routine does replace matching pairs of explicit
%%   character tokens of catcode 1 and 2 by matching pairs of curly braces
%%   of catcode 1 and 2.
%%   I suppose this won't be a problem in most situations as usually the
%%   curly braces are the only characters of category code 1 / 2...
%%
%%   This routine needs \detokenize from the eTeX extensions.
%%-----------------------------------------------------------------------------
\newcommand\DoubleEveryHashAndReplaceAddtohook@reserved[1]{%
   \romannumeral0\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop{#1}{}%
}%
\newcommand\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop[2]{%
  \UD@CheckWhetherNull{#1}{ #2}{%
    \UD@CheckWhetherLeadingSpace{#1}{%
       \expandafter\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop
       \expandafter{\UD@removespace#1}{#2 }%
    }{%
      \UD@CheckWhetherBrace{#1}{%
        \expandafter\expandafter\expandafter\UD@PassFirstToSecond
        \expandafter\expandafter\expandafter{%
        \expandafter\UD@PassFirstToSecond\expandafter{%
            \romannumeral0%
            \expandafter\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop
            \romannumeral0%
            \UD@ExtractFirstArgLoop{#1\UD@SelDOm}{}%
        }{#2}}%
        {\expandafter\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop
         \expandafter{\UD@firstoftwo{}#1}}%
      }{%
        \expandafter\UD@CheckWhetherHash
        \romannumeral0\UD@ExtractFirstArgLoop{#1\UD@SelDOm}{#1}{#2}%
      }%
    }%
  }%
}%
\newcommand\UD@CheckWhetherHash[3]{%
  \expandafter\UD@CheckWhetherLeadingSpace\expandafter{\string#1}{%
    \expandafter\expandafter\expandafter\UD@CheckWhetherNull
    \expandafter\expandafter\expandafter{%
    \expandafter\UD@removespace\string#1}{%
      \expandafter\expandafter\expandafter\UD@CheckWhetherNull
      \expandafter\expandafter\expandafter{%
      \expandafter\UD@removespace\detokenize{#1}}{%
        % something whose stringification yields a single space
        \UD@secondoftwo
      }{% explicit space of catcode 6
        \UD@firstoftwo
      }%
    }{% something whose stringification has a leading space
      \UD@secondoftwo
    }%
  }{%
    \expandafter\expandafter\expandafter\UD@CheckWhetherNull
    \expandafter\expandafter\expandafter{%
    \expandafter\UD@firstoftwo
    \expandafter{\expandafter}\string#1}{%
      \expandafter\expandafter\expandafter\UD@CheckWhetherNull
      \expandafter\expandafter\expandafter{%
      \expandafter\UD@firstoftwo
      \expandafter{\expandafter}\detokenize{#1}}{%
        % no hash
        \UD@secondoftwo
      }{% hash
        \UD@firstoftwo
      }%
    }{% no hash
      \UD@secondoftwo
    }%
  }%
  {% hash
    \expandafter\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop
    \expandafter{\UD@firstoftwo{}#2}{#3#1#1}%
  }{% no hash
    \UD@CheckWhetherAddtohook@reserved{#1}{%
      \expandafter\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop
      \expandafter{\UD@firstoftwo{}#2}{#3##}%
    }{%
      \expandafter\UD@DoubleEveryHashAndReplaceAddtohook@reservedLoop
      \expandafter{\UD@firstoftwo{}#2}{#3#1}%
    }%
  }%
}%
%%=============================================================================
% \addtohook{<name of hook-macro which processes one argument>}{%
%   <tokens to add to hook>%  
% }%
% 
% adds the sequence `\dosomething{#1}{<tokens to add to hook>}` to the
% definition-text of the macro whose name is  
% <name of hook-macro which processes one argument>.
%
% That nacro must be defined to process one non-optional argument.
%------------------------------------------------------------------------------
\newcommand\addtohook[2]{%
  \expandafter\long
  \expandafter\def
  \csname #1\expandafter\endcsname
  \expandafter##%
  \expandafter1%
  \expandafter{%
    \romannumeral0%
    \UD@Exchange{ }{%
      \expandafter\expandafter
      \expandafter            \expandafter
      \expandafter\expandafter
      \expandafter
    }%
    \expandafter\DoubleEveryHashAndReplaceAddtohook@reserved
    \expandafter{%
      \romannumeral0%
      \expandafter\ifx\csname #1\endcsname\relax
         \expandafter\UD@firstoftwo\else\expandafter\UD@secondoftwo
      \fi
      { }%
      {%
        \UD@Exchange{ }{\expandafter\expandafter\expandafter}%
        \csname#1\endcsname{\addtohook@reserved1}%
      }%
      \dosomething{\addtohook@reserved1}{#2}%
    }%
  }%
}%

\makeatother

\addtohook{hook}{\def\bal#1{#1}}
\show\hook
\addtohook{hook}{foo}
\show\hook
\addtohook{hook}{bar}
\show\hook
\addtohook{hook}{baz}
\show\hook
\addtohook{hook}{\def\bat#1{#1}}
\show\hook

\stop  % stop the LaTeX-run without a document-environment

enter image description here

Tags:

Macros

Hooks