# Double hashes inside macro definition?

Assume a macro which is not defined in terms of \long and which is not defined in terms of \outer and which is not defined in terms of \protected and which does not process arguments and whose toplevel-expansion only yields tokens which are not \outer whereof some are explicit character tokens of category code 6(parameter):

\def\macro{This is some hashes: ##}

Is there some \edef-\unexpanded-\expanded-trickery for redefining \macro to deliver exactly the same set of tokens but explicit character tokens of category code 6(parameter) doubled as if you had defined

\def\macro{This is some hashes: ####}

?

Assume that the tokens that form the replacement-text of \macro are obtainable only by expanding \macro.

My own approach on the matter is a \romannumeral-expansion-driven recursive loop for iterating on the argument, quadrupling each explicit character-token of category code 6 (parameter).

(Quadrupling instead of doubling because expanding \macro implies halving the amount of consecutive hashes...)

I don't take my routine \QuadrupleEveryHash for very elegant:

• 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...)

• The routine needs \detokenize from the e-TeX extensions for checking whether a given token is a hash. (\string# yields a single #12-token while \detokenize{#} yields two consecutive #12-tokens...)

• Each time an opening brace is found, another \romannumeral-expansion-driven instance of the routine is started which takes its toll on the semantic nest in case the replacement text of \macro contains a lot of brace-nesting.

• the routine only works as long as \macro's replacement text does not contain \outer-tokens as could be the case with \def\macro{\foo and hash ##.}\outer\def\foo{Now foo is outer.}.

\catcode\@=11
%%=============================================================================
%% Paraphernalia:
%%    \UD@firstoftwo, \UD@secondoftwo,
%%    \UD@PassFirstToSecond, \UD@Exchange, \UD@removespace
%%    \UD@CheckWhetherNull, \UD@CheckWhetherBrace,
%%=============================================================================
\long\def\UD@firstoftwo#1#2{#1}%
\long\def\UD@secondoftwo#1#2{#2}%
\long\def\UD@PassFirstToSecond#1#2{#2{#1}}%
\long\def\UD@Exchange#1#2{#2#1}%
\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:
\long\def\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}\UD@firstoftwo\expandafter{} \UD@secondoftwo}%
{\UD@firstoftwo\expandafter{} \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>}%
\long\def\UD@CheckWhetherBrace#1{%
\romannumeral0\expandafter\UD@secondoftwo\expandafter{\expandafter{%
\string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\UD@firstoftwo\expandafter{} \UD@firstoftwo}%
{\UD@firstoftwo\expandafter{} \UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument's first token is an explicit
%% 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>}%
\romannumeral0\UD@CheckWhetherNull{#1}%
{\UD@firstoftwo\expandafter{} \UD@secondoftwo}%
}%
\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}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%
%%   \UD@ExtractFirstArg{ABCDE} yields  {A}
%%
%%   \UD@ExtractFirstArg{{AB}CDE} yields  {AB}
%%.............................................................................
\long\def\UD@RemoveTillUD@SelDOm#1#2\UD@SelDOm{{#1}}%
\long\def\UD@ExtractFirstArg#1{%
\romannumeral0%
\UD@ExtractFirstArgLoop{#1\UD@SelDOm}%
}%
\long\def\UD@ExtractFirstArgLoop#1{%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{ #1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillUD@SelDOm#1}}%
}%
%%=============================================================================
%%
%%   Each explicit catcode-6(parameter)-character-token of the <argument>
%%
%%   You obtain the result after two expansion-steps, i.e.,
%%   in expansion-contexts you get the result after "hitting"
%%
%%   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.
%%-----------------------------------------------------------------------------
}%
\UD@CheckWhetherNull{#1}{ #2}{%
\expandafter{\UD@removespace#1}{#2 }%
}{%
\UD@CheckWhetherBrace{#1}{%
\expandafter\expandafter\expandafter\UD@PassFirstToSecond
\expandafter\expandafter\expandafter{%
\expandafter\UD@PassFirstToSecond\expandafter{%
\romannumeral0%
\romannumeral0%
\UD@ExtractFirstArgLoop{#1\UD@SelDOm}{}%
}{#2}}%
\expandafter{\UD@firstoftwo{}#1}}%
}{%
\expandafter\UD@CheckWhetherHash
\romannumeral0\UD@ExtractFirstArgLoop{#1\UD@SelDOm}{#1}{#2}%
}%
}%
}%
}%
\long\def\UD@CheckWhetherHash#1#2#3{%
% The very edge case of probably having a space of catcode 6:
\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}}{%
% no hash
\UD@secondoftwo
}{% hash
\UD@firstoftwo
}%
}%
}{%
% The  case of probably having a non-space of catcode 6:
\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@firstoftwo{}#2}{#3#1#1#1#1}%
}{% no hash
\expandafter{\UD@firstoftwo{}#2}{#3#1}%
}%
}%
%%\catcode\@=12
%%=============================================================================

\tt\frenchspacing

1234567890123456789012345678901234567890123456789012345678901234567890

\def\macro{This is some hashes: ##}

\expandafter\def
\expandafter\Macro
\expandafter{%
\romannumeral0\UD@Exchange{ }{%
\expandafter\expandafter\expandafter
\expandafter\expandafter\expandafter\expandafter
}%
}%

\string\macro: \meaning\macro

\string\Macro: \meaning\Macro

\bigskip

Now the edge case of spaces of category code 6(parameter) being part of \string\macro's\break
replacement-text:

\bigskip

1234567890123456789012345678901234567890123456789012345678901234567890

\begingroup
\catcode\ =6\relax%
\gdef\macro{This  Is  Some  Hashes:  ##}%
\endgroup%

\expandafter\def
\expandafter\Macro
\expandafter{%
\romannumeral0\UD@Exchange{ }{%
\expandafter\expandafter\expandafter
\expandafter\expandafter\expandafter\expandafter
}%
}%

\string\macro: X\meaning\macro X

\string\Macro: X\meaning\Macro X

\bye


Each \def...{body} reduces the number of hash characters to half in its internal representation in the body. Each \toks<num>={body} keeps the number of hash characters. Each \write or \scantokens doubles the number of hash characters.

\def\macro{This is some hashes: ##} % we have one # in internal representation

\scantokens\expandafter{\expandafter\toks\expandafter0\expandafter{\macro}}
% \scantotkens does \write, we have two internal #, \toks0 keeps two #.

\edef\newmacro{\the\toks0}  % this keeps two # in internal representation

\message{\meaning\macro, \meaning\newmacro} % \meaning doubles the #, so we see:
%             ##              ####
% because there is:
%             #               ##     in internal representation

\bye

• +1/Upvote from me. The unexpanded-writing-part of \scantokens doubles hashes and inserts space-characters behind characters that come from unexpanded-writing control-word-tokens. In case the catcode-régime changed since defining \macro, the reading-back-and retokenizing-part of \scantokens might probably tokenize things under different catcode-régime, probably yielding a different set of tokens. But that's an edge case and unlikely to happen in real-life scenarios. ;-) – Ulrich Diez Jul 10 '20 at 12:41

You can use \edef with \unexpanded:

\documentclass{article}
\de\documentclass{article}
\def\safedef#1#2{\edef#1{\unexpanded{#2}}}
\begin{document}
\def\macro{This is some hashes: ##}
\safedef\Macro{This is some hashes: ##}
\typeout{\macro -\Macro}
\end{document}


shows

This is some hashes: ##-This is some hashes: ####

• +1/upvote from me, but strictly spoken the definition of \Macro is not derived from the definition of \macro. If you did something like \expandafter\safedef\expandafter\Macro\expandafter{\macro}, the amount of hashes would not be doubled in \Macro but would be the same as in \macro. – Ulrich Diez Jul 10 '20 at 12:32

With expl3:

\documentclass{article}

\ExplSyntaxOn
\cs_new_protected:Npn \doublehashes #1 #2
{
\tl_set_eq:NN \l_tmpa_tl #2
\regex_replace_all:nnN { \cP. } { \cP\#\cP\# } \l_tmpa_tl
\tl_set_eq:NN #1 \l_tmpa_tl
}
\ExplSyntaxOff

\def\macro{This is some hashes: ####}

\doublehashes\Macro\macro

\frenchspacing

\begin{document}

\texttt{\string\macro=\meaning\macro}

\texttt{\string\Macro=\meaning\Macro}

\end{document}


Of course you know that \meaning prints two hash symbols for each one it finds in the replacement text. So \macro has two of them and \Macro has four.

An l3regex approach:

\regex_replace_all:nnN { (\cP\#)+ } { \1\1 } \macro


A full example:

\documentclass{article}

\begin{document}
\ExplSyntaxOn

\tl_new:N \macro
\tl_set:Nn \macro {\def\y{\def\z##1{<##1>}}}

% before doubling the hash mark
\cs_generate_variant:Nn \cs_new:Npn {Npo}
\macro \y \z{abc} % output "<abc>"
\par

% after doubling the hash mark
\regex_replace_all:nnN { (\cP#)+ } { \1\1 } \macro
\cs_new:Npo \x {\l_test_tl}
\x\y\z{abc} % output "<abc>"

\ExplSyntaxOff
\end{document}

`