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 \show
s 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

etoolbox
'\apptocmd
macro seems to do more or less what you want, maybe its implementation can give you some inspiration.