Is there a way to retrieve the “source code” and the argument pattern from a macro for reuse (i.e. what \show shows)?

It is possible to recover the parameter text, provided no fancy change of category codes is involved.

The two macros are supposed to have the same nonempty parameter text.

\documentclass{article}
\usepackage{xparse}

\def\first#1#2thing{first(#1)(#2)}
\def\second#1#2thing{\mbox{second(#1)(#2)}}

\ExplSyntaxOn

\tl_new:N \l__siemer_para_tl
\tl_new:N \l__siemer_repl_first_tl
\tl_new:N \l__siemer_repl_second_tl
\exp_args_generate:n { NNNV }

\NewDocumentCommand{\makeunion}{mmm}
 {% #1 = new command name, #2 = first macro, #3 = second macro
  % #2 and #3 are assumed to have the same parameter text
  \tl_set_rescan:Nnx \l__siemer_para_tl { } { \cs_argument_spec:N #2 }
  \tl_set_rescan:Nnx \l__siemer_repl_first_tl { } { \cs_replacement_spec:N #2 }
  \tl_set_rescan:Nnx \l__siemer_repl_second_tl { } { \cs_replacement_spec:N #3 }
  \tl_put_right:NV \l__siemer_repl_first_tl \l__siemer_repl_second_tl
  \exp_args:NNNNV \exp_last_unbraced:NNo \cs_new:Npn #1 \l__siemer_para_tl \l__siemer_repl_first_tl
 }

\ExplSyntaxOff

\makeunion\union\first\second

\begin{document}

\first XYthing

\second XYthing

\union XYthing

\texttt{\meaning\union}

\end{document}

enter image description here

Some explanations: with \tl_set_rescan:Nnx we store the replacement text with the correct category codes; note that \cs_argument_spec:N only returns a string (all characters of category code 12, except spaces that keep category code 10). The same for the replacement texts.


As David noted in a comment, TeX cannot figure out the argument signature, you have to provide it. But assuming you can, the concatenation is straightforward. I introduce \concat with 4 arguments:

  1. the name of the concatenated macro,

  2. the argument signature of both macros to be concatenated,

  3. The first macro of the concatenation,

  4. the second macro of the concatenation.

The MWE. Here, \third is created as the concatenation of \first and \second, both of which share the argument signature of #1#2thing.

\documentclass{article}
\usepackage[T1]{fontenc}
\def\first#1#2thing{one\{#1\}two\{#2\}}
\def\second#1#2thing{three\{#1\}four\{#2\}}

\def\concat#1#2#3#4{\def#1#2{#3#2#4#2}}
\begin{document}
\concat\third{#1#2thing}\first\second

\meaning\third

\first abthing

\second abthing

\third abthing

\third\today\textit{t}thing
\end{document}

enter image description here


As long as the ⟨parameter text⟩ which the macros have in common is known to you, you can probably do:

\long\def\exchange#1#2{#2#1}%
\long\def\exchangeafterexpand#1#2{%
  \romannumeral0%
  \expandafter\exchange\expandafter{#1}{\exchange{ }{\expandafter}#2}%
}%

\def\first#1#2thing{first-one: #1 first-two: #2}
\def\second#1#2thing{second-one: #1 second-two: #2}

\expandafter\expandafter\expandafter\exchange
\expandafter\expandafter\expandafter{%
  \expandafter\expandafter\expandafter{%
      \exchangeafterexpand{\second{#1}{#2}thing}{\first{#1}{#2}thing}%
  }%
}{\def\union#1#2thing}%

\show\first

\show\second

\show\union

\bye

enter image description here

Be aware that this does probably not work out with macros defined in terms of LaTeX's \DeclareRobustCommand and also probably does not work out with macros that process optional arguments defined in terms of LaTeX's \newcommand. Reason: Such macros actually are mechanisms where the user-level command performs some test (with \DeclareRobustCommand the definition of the token \protect is tested; with \newcommand the presence of an optional argument is tested) before calling an internal command whose name is derived from the name of the user-level-command.

Also be aware that this approach of expanding with arguments passed in parameter-syntax {#1}/{#2} does only work out as long as the definition texts of \first and/or \second do themselves not not contain definitions of other macros where arguments are denoted by ##1, ##2, i.e., by pairs of hashes. This is because pairs of hashes will be reduced to a single hash during expansion which we don't want.

Therefore if eTeX extensions are available I suggest another route:

\def\union#1#2thing{%
     Within the sequence 
        ⟨ Expansion of \first{⟨reserved token⟩1}{⟨reserved token⟩2}thing +
          Expansion of \second{⟨reserved token⟩1}{⟨reserved token⟩2}thing
        ⟩
     have every hash doubled and every instance of ⟨reserved token⟩ replaced by a single hash.
   }%
}%

This is what I implemented in the example below. This time I used LaTeX. 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!}%
}%
%%-----------------------------------------------------------------------------
%%  \Parameterchar@reservedFork grabs the first thing behind a
%%  a token-sequence of pattern  !!\Parameterchar@reserved!
%%.............................................................................
\newcommand\Parameterchar@reservedFork{}
\long\def\Parameterchar@reservedFork#1!!\Parameterchar@reserved!#2#3!!!!{#2}%
%%-----------------------------------------------------------------------------
%% Check whether argument consists only of the token \Parameterchar@reserved
%%.............................................................................
\newcommand\UD@CheckWhetherParameterchar@reserved[1]{%
  \romannumeral0%
  \UD@CheckWhetherNoExclam{#1}{%
    \Parameterchar@reservedFork
    %Case #1 is empty/has no tokens:
      !#1!\Parameterchar@reserved!{\UD@Exchange{ }{\expandafter}\UD@secondoftwo}%
    %Case #1 = \Parameterchar@reserved:
      !!#1!{\UD@Exchange{ }{\expandafter}\UD@firstoftwo}%
    %Case #1 = something else without exclamation-mark:
      !!\Parameterchar@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}}%
}%
%%=============================================================================
%% \DoubleEveryHashAndReplaceParameterchar@reserved{<argument>}%
%%
%%   Each explicit catcode-6(parameter)-character-token of the <argument> 
%%   will be doubled. Each instance of \Parameterchar@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" 
%%   \DoubleEveryHashAndReplaceParameterchar@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\DoubleEveryHashAndReplaceParameterchar@reserved[1]{%
   \romannumeral0\UD@DoubleEveryHashAndReplaceParameterchar@reservedLoop{#1}{}%
}%
\newcommand\UD@DoubleEveryHashAndReplaceParameterchar@reservedLoop[2]{%
  \UD@CheckWhetherNull{#1}{ #2}{%
    \UD@CheckWhetherLeadingSpace{#1}{%
       \expandafter\UD@DoubleEveryHashAndReplaceParameterchar@reservedLoop
       \expandafter{\UD@removespace#1}{#2 }%
    }{%
      \UD@CheckWhetherBrace{#1}{%
        \expandafter\expandafter\expandafter\UD@PassFirstToSecond
        \expandafter\expandafter\expandafter{%
        \expandafter\UD@PassFirstToSecond\expandafter{%
            \romannumeral0%
            \expandafter\UD@DoubleEveryHashAndReplaceParameterchar@reservedLoop
            \romannumeral0%
            \UD@ExtractFirstArgLoop{#1\UD@SelDOm}{}%
        }{#2}}%
        {\expandafter\UD@DoubleEveryHashAndReplaceParameterchar@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@DoubleEveryHashAndReplaceParameterchar@reservedLoop
    \expandafter{\UD@firstoftwo{}#2}{#3#1#1}%
  }{% no hash
    \UD@CheckWhetherParameterchar@reserved{#1}{%
      \expandafter\UD@DoubleEveryHashAndReplaceParameterchar@reservedLoop
      \expandafter{\UD@firstoftwo{}#2}{#3##}%
    }{%
      \expandafter\UD@DoubleEveryHashAndReplaceParameterchar@reservedLoop
      \expandafter{\UD@firstoftwo{}#2}{#3#1}%
    }%
  }%
}%
%%=============================================================================
\newcommand\UD@TripleExpandAndConcatSecondAndThird[3]{%
   \romannumeral0\expandafter\UD@PassFirstToSecond\expandafter{%
     \romannumeral0%
     \expandafter\UD@Exchange\expandafter{%
       \romannumeral0%
       \UD@Exchange{ }{\expandafter\expandafter\expandafter
                       \expandafter\expandafter\expandafter\expandafter}%
       #3%
     }{%
       \UD@Exchange{ }{\expandafter\expandafter\expandafter
                       \expandafter\expandafter\expandafter\expandafter}#2%
     }%
   }{ #1}%
}%
%%=============================================================================

\def\first#1#2thing{first-one: #1 first-two: #2 \def\firsttest##1{##1}}
\def\second#1#2thing{second-one: #1 second-two: #2 \def\secondtest##1{##1}}

\UD@TripleExpandAndConcatSecondAndThird{%
  \def\union#1#2thing%
}{%
  \expandafter\DoubleEveryHashAndReplaceParameterchar@reserved
  \expandafter{\first{\Parameterchar@reserved1}{\Parameterchar@reserved2}thing}%
}{%
  \expandafter\DoubleEveryHashAndReplaceParameterchar@reserved
  \expandafter{\second{\Parameterchar@reserved1}{\Parameterchar@reserved2}thing}%
}%

\show\first

\show\second

\show\union

\stop

enter image description here

Of course this does work out only with macros whose ⟨definition text⟩ does not contain the token \Parameterchar@reserved and where replacing arbitrary explicit character tokens of category code 1 (begin group) and matching arbitrary explicit character tokens of category code 2 (end group) by explicit left-curly-brace-character tokens of category code 1 (begin group) and matching explicit right-curly-brace-character tokens of category code 2 (end group) does not matter.