Steuersequenz mit variabler Anzahl unbegrenzter Parameter in Tokenliste umwandeln

Steuersequenz mit variabler Anzahl unbegrenzter Parameter in Tokenliste umwandeln

In expl3 kann ich eine Tokenliste \l_greet_tlin eine Steuersequenz übersetzen \greet, die zwei Parameter wie folgt annimmt:

\tl_new:N
  \l_greet_tl
\tl_set:Nn
  \l_greet_tl
  { Hello,~#1!~How~is~#2~doing? }
\cs_generate_variant:Nn
  \cs_generate_from_arg_count:NNnn
  { NNnV }
\cs_generate_from_arg_count:NNnV
  \greet
  \cs_set:Npn
  { 2 }
  \l_greet_tl

Dann kann ich \greet { world } { favorite~species~(humans,~presumably) }die folgende Erweiterung schreiben und erhalten:

Hallo Welt! Wie geht es deiner Lieblingsspezies (vermutlich dem Menschen)?

Ich möchte nun allerdings den umgekehrten Weg gehen, also den Ersetzungstext der Kontrollsequenz \greetwieder in eine Tokenliste umwandeln, um ihn dann vor der Rückverwandlung in eine Kontrollsequenz noch anhängen zu können. Manuell kann ich das für zwei Parameter folgendermaßen machen:

\tl_set:No
  \l_greet_tl
  { \greet { #1 } { #2 } }
\tl_put_right:Nn
  \l_greet_tl
  { ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
  \greet
  \cs_set:Npn
  { 3 }
  \l_greet_tl

Jetzt kann ich \greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }die folgende Erweiterung schreiben und empfangen:

Hallo Welt! Wie geht es deiner Lieblingsspezies (vermutlich dem Menschen)? Mach einen großen Evolutionssprung!

Ich möchte dies jedoch auf eine Weise tun können, die für eine beliebige Anzahl von Parametern funktioniert, nicht nur für zwei, wie zum Beispiel:

\tl_set_from_cs:NNn
  \l_greet_tl  % token list (output)
  \greet       % control sequence (input)
  { 2 }        % parameter count
\tl_put_right:Nn
  \l_greet_tl
  { ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
  \greet
  \cs_set:Npn
  { 3 }
  \l_greet_tl

Im Gegensatz zur vorhandenen Funktion würde \cs_replacement_spec:Ndie neue Funktion \tl_set_from_cs:NNndie Kategoriencodes im Ersetzungstext beibehalten.

EDIT: Weitere Klarstellungen (26.04.2024)

Anders als in der Antwort von Max Chernoff sollte die Antwort für alle von expl3 unterstützten TeX-Engines gelten, nicht nur für LuaTeX.

Anders als in den Antworten von mir und @egreg #sollten doppelte s im Ersetzungstext erhalten bleiben. Nehmen wir beispielsweise die folgende Neudefinition der Steuersequenz an \greet:

\cs_new_protected:Npn
  \greet
  #1#2
  {
    Hello,~#1!
    \group_begin:
    \cs_set:Npn
      \greet
      ##1
      { ~How~is~##1~doing? }
    \greet { #2 }
    \group_end:
  }

Nun kann ich schreiben \greet { world } { favorite~species~(humans,~presumably) }und erhalte trotzdem folgende Erweiterung:

Hallo Welt! Wie geht es deiner Lieblingsspezies (vermutlich dem Menschen)?

Das Konvertieren der Steuersequenz in eine Token-Liste wie folgt verhält sich jedoch nicht mehr wie erwartet:

\tl_set:No
  \l_greet_tl
  { \greet { #1 } { #2 } }
\tl_put_right:Nn
  \l_greet_tl
  { ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
  \greet
  \cs_set:Npn
  { 3 }
  \l_greet_tl

Dies liegt daran, dass \greetjetzt der folgende Ersatztext enthalten ist:

Hello,~#1!
\group_begin:
\cs_set:Npn
  \greet
  #1
  { ~How~is~#1~doing? }
\greet { #2 }
\group_end:
~Have~a~great~#3!

Wie Sie sehen, ist die Unterscheidung zwischen #1und ##1verloren gegangen. Wenn ich jetzt schreibe \greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }, erzeugt TeX den folgenden Fehler:

Die Verwendung von \greet entspricht nicht der Definition.

Das Beibehalten des doppelten #s ist Teil der Herausforderung.

Antwort1

Für Makros mit nicht begrenzten Argumenten:

\documentclass{article}

\newcommand{\test}[2]{-#1-#2-}

\ExplSyntaxOn

\seq_new:N \l__witiko_getreplacement_seq
\tl_new:N  \l__witiko_getreplacement_tmp_tl

\seq_set_from_clist:Nn \l__witiko_getreplacement_seq
 {
  {},{#1},{},{#1}{#2},{},{#1}{#2}{#3},{},{#1}{#2}{#3}{#4},
  {},{#1}{#2}{#3}{#4}{#5},{},{#1}{#2}{#3}{#4}{#5}{#6},
  {},{#1}{#2}{#3}{#4}{#5}{#6}{#7},
  {}.{#1}{#2}{#3}{#4}{#5}{#6}{#7}{#8},
  {},{#1}{#2}{#3}{#4}{#5}{#6}{#7}{#8}{#9}
 }

\cs_new_protected:Nn \witiko_getreplacement:NN
 {
  \tl_set:Ne \l__witiko_getreplacement_tmp_tl
   {
    \seq_item:Nn \l__witiko_getreplacement_seq { \tl_count:e { \cs_parameter_spec:N #1 } }
   }
  \__witiko_getreplacement:NNV #2 #1 \l__witiko_getreplacement_tmp_tl
 }

\cs_new_protected:Nn \__witiko_getreplacement:NNn
 {
  \tl_set:No #1 { #2 #3 }
 }
\cs_generate_variant:Nn \__witiko_getreplacement:NNn { NNV }

\witiko_getreplacement:NN \test \l_tmpa_tl

\tl_show:N \l_tmpa_tl

\tl_set:Nn \l_tmpb_tl { -#1-#2- }

\tl_if_eq:NNTF \l_tmpa_tl \l_tmpb_tl { \typeout{EQUAL} } { \typeout{DIFFERENT} }

\stop

Die Konsole druckt

> \l_tmpa_tl=-##1-##2-.
<recently read> }

l.36 \tl_show:N \l_tmpa_tl

?
EQUAL

Die möglichen Parametertexte speichere ich in einer Sequenz, aus der ich bei der \tl_set:NoAusführung jeweils das passende Item extrahiere um es zu übergeben.

Antwort2

Dies ist auch für Makros mit abgegrenzten Argumenten möglich, wenn Sie LuaTeX verwenden können. Die Catcodes einer Makrodefinition zu erhalten ist jedoch viel schwieriger als erwartet, daher müssen wir viele schmutzige Tricks anwenden, damit dies funktioniert.

\documentclass{article}

\usepackage{luacode}
\begin{luacode*}
    -----------------
    --- Constants ---
    -----------------

    local assign_toks_cmdcode = token.command_id("assign_toks")
    local car_ret_cmdcode = token.command_id("car_ret")
    local cs_token_flag = 0x1FFFFFFF
    local first_digit_chrcode = string.byte("0")
    local first_letter_chrcode = string.byte("a")
    local hash_chrcode = string.byte("#")
    local last_letter_chrcode = string.byte("z")
    local let_token = token.create("let")
    local mac_param_cmdcode = token.command_id("mac_param")
    local other_char_cmdcode = token.command_id("other_char")
    local par_end_cmdcode = token.command_id("par_end")
    local random_csname_length = 8
    local slice = table.sub
    local stop_cmdcode = token.command_id("stop")
    local tokspre_token = token.create("tokspre")


    ----------------------------
    --- Function Definitions ---
    ----------------------------

    -- Gets a table representing tokens of a macro definition
    function get_toktab_from_macro(value_csname)
        local value_token = token.create(value_csname)

        -- By default, LuaTeX only gives us the contents of a macro as a string.
        -- However, it will give us the contents of a mark node as a table of
        -- tokens, so we create a fake mark node that points to the macro's
        -- definition.
        local tmp_nd = node.direct.new("mark")
        node.direct.setprev(tmp_nd + 1, value_token.mode)
        return node.direct.getfield(tmp_nd, "mark")
    end

    -- Splits a macro definition token table into its parameters and its
    -- replacement text.
    local function split_macro_toktab(meaning_toktab)
        local stop_index
        local args_count = 0

        for i, t in ipairs(meaning_toktab) do
            -- Separator between parameters and replacement text (represented by
            -- "->" inside of \meaning).
            if t[1] == stop_cmdcode then
                stop_index = i
            -- Convert a macro parameter token in the body back into a "#"
            -- token.
            elseif t[1] == mac_param_cmdcode and t[3] == 0 then
                table.insert(
                    meaning_toktab,
                    i + 1,
                    { mac_param_cmdcode, hash_chrcode, 1 }
                )
            elseif t[1] == mac_param_cmdcode and t[3] == 1 then
                t[3] = 0
            -- Convert a macro parameter token in the body back into a <digit>
            -- token.
            elseif t[1] == car_ret_cmdcode then
                table.insert(
                    meaning_toktab,
                    i + 1,
                    { other_char_cmdcode, first_digit_chrcode + t[2], 0 }
                )
                t[2] = hash_chrcode
                t[1] = mac_param_cmdcode
            -- Convert a macro parameter token in the parameters back into a
            -- pair of tokens {"#", <digit>}.
            elseif t[1] == par_end_cmdcode then
                args_count = args_count + 1
                t[1] = mac_param_cmdcode
                t[2] = hash_chrcode
                table.insert(
                    meaning_toktab,
                    i + 1,
                    { other_char_cmdcode, first_digit_chrcode + args_count, 0 }
                )
            end
        end

        -- Split the token table
        return slice(meaning_toktab, 2,              stop_index - 1),
               slice(meaning_toktab, stop_index + 1, nil           )
    end

    -- Generates a random control sequence name.
    local function random_csname()
        local random_table = {}

        for i = 1, random_csname_length do
            local random_letter = string.char(
                math.random(first_letter_chrcode, last_letter_chrcode)
            )
            table.insert(random_table, random_letter)
        end

        return table.concat(random_table)
    end

    -- Converts a token table into a \toks token (without giving it a name).
    local function toktab_to_token(value_toktab)
        local tmp_csname = random_csname()

        -- First, we create a mark node to store the raw token in.
        local tmp_nd = node.direct.new("mark")
        node.direct.setfield(tmp_nd, "mark", value_toktab)

        -- TeX expects two levels of indirection for a \toks token, so we first
        -- point a \chardef token to the token created by the mark node.
        token.set_char(tmp_csname, node.direct.getprev(tmp_nd + 1), "global")

        -- Then, we create a \toks token that points to the \chardef token.
        return token.create(
            token.create(tmp_csname).tok - cs_token_flag,
            assign_toks_cmdcode
        )
    end

    -- \let's a token to a control sequence name.
    local function let_csname_token(name_csname, value_token)
        -- We need to create a token with the name first, otherwise we get an
        -- "undefined_cs" token which is useless.
        token.set_char(name_csname, 0)
        local name_token = token.create(name_csname)

        -- There's no way to do this directly from Lua, so we start a new TeX
        -- run and use \let to assign the token.
        tex.runtoks(function()
            token.put_next(let_token, name_token, value_token)
        end)

        return token.create(name_csname)
    end

    -- Copies a fake \toks token into a real \toks register.
    --
    -- The token created by "let_csname_token" is a semi-valid \toks register:
    -- it behaves like a \toks register with \the and similar, but it gives a
    -- (mostly harmless) error with \show and \meaning. To fix this, we copy
    -- the token's contents into a real \toks register.
    local function token_to_toksreg(toksreg_csname, value_token)
        -- Clear the register
        tex.toks[toksreg_csname] = ""

        local toksreg_token = token.create(toksreg_csname)
        local value_toksreg = let_csname_token(random_csname(), value_token)

        -- Prepend the fake \toks register onto the empty real one, giving
        -- us a real \toks register with the correct value.
        tex.runtoks(function()
            token.put_next(tokspre_token, toksreg_token, value_toksreg)
        end)
    end

    -- Registers a TeX command that calls the given Lua function.
    local function register_tex_cmd(target_csname, func, args)
        local scanners = {}
        for _, arg in ipairs(args) do
            scanners[#scanners+1] = token["scan_" .. arg]
        end

        local function scanning_func()
            local values = {}
            for _, scanner in ipairs(scanners) do
                values[#values+1] = scanner()
            end

            func(table.unpack(values))
        end

        local index = luatexbase.new_luafunction(target_csname)
        lua.get_functions_table()[index] = scanning_func
        token.set_lua(target_csname, index, "global")
    end


    --------------------
    --- TeX Commands ---
    --------------------

    register_tex_cmd("__example_macro_to_toks:N", function(value_csname)
        -- Get a table representing the macro's definition tokens
        local meaning_toktab = get_toktab_from_macro(value_csname)

        -- Split the macro into its parameters and replacement text
        local params_toktab, replacement_toktab = split_macro_toktab(meaning_toktab)

        -- Save the parameters in a \toks register
        local params_token = toktab_to_token(params_toktab)
        token_to_toksreg("l__example_parameters_toks", params_token)

        -- Save the replacement text in a \toks register
        local replacement_token = toktab_to_token(replacement_toktab)
        token_to_toksreg("l__example_replacement_toks", replacement_token)
    end, {"csname"})
\end{luacode*}

\ExplSyntaxOn
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    %%% Variable Declarations %%%
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%

    \exp_args_generate:n { NNNV }
    \newtoks \l__example_parameters_toks
    \newtoks \l__example_replacement_toks
    \tl_new:N \l_example_parameters_tl
    \tl_new:N \l_example_replacement_tl
    \scan_new:N \s_example


    %%%%%%%%%%%%%%%%%%%%%%%%%
    %%% Macro Definitions %%%
    %%%%%%%%%%%%%%%%%%%%%%%%%

    % Sets "\l_example_parameters_tl" with the parameters of the provided macro
    % and "\l_example_replacement_tl" with the replacement text of the same
    % macro.
    \cs_new:Nn \example_macro_to_tl:N {
        \__example_macro_to_toks:N #1
        \tl_set:NV \l_example_parameters_tl  \l__example_parameters_toks
        \tl_set:NV \l_example_replacement_tl \l__example_replacement_toks
    }

    % Defines the provided macro with parameters "\l_example_parameters_tl" and
    % replacement text "\l_example_replacement_tl".
    \cs_new:Nn \example_tl_to_macro:N {
        \exp_args:NNNNV \exp_last_unbraced:NNV \cs_set:Npn #1 \l_example_parameters_tl \l_example_replacement_tl
    }
\ExplSyntaxOff


%%%%%%%%%%%%%%%%%%%%%
%%% Demonstration %%%
%%%%%%%%%%%%%%%%%%%%%

\pagestyle{empty}

\begin{document}
    \ExplSyntaxOn
        % Define a test macro with weird parameters and body
        \cs_new:Npn \example_test:w #1 #2 \s_example {
            A B C~
            <#1>

            { #2 }
            \par $a^2 + b^\bgroup \pi^2\egroup$
        }

        % Get the parameters and replacement text of the test macro
        \example_macro_to_tl:N \example_test:w

        % Inspect the extracted token lists
        \tl_analysis_show:N \l_example_parameters_tl
        \tl_analysis_show:N \l_example_replacement_tl

        % Modify the extracted token lists
        \tl_put_right:Nn \l_example_parameters_tl { #3 }
        \tl_put_left:Nn \l_example_replacement_tl { \textbf{#3} }

        % Assemble a new macro from the modified token lists
        \example_tl_to_macro:N \test

        % Test the new macro
        \test X {\itshape y}\s_example Z

        % Compare the meanings of the original and new macros
        \par \meaning\example_test:w
        \par \meaning\test
    \ExplSyntaxOff
\end{document}

Ausgabe

Damit dies funktioniert, sind zwei wesentliche „Tricks“ nötig.

  1. Damit dies im Allgemeinen funktioniert, benötigen wir die Definition des Makros als Token-Liste (mit den richtigen Catcodes), nicht nur als Lua-String. Um die Definition eines Makros zu erhalten, erstellen wir also zuerst einen leeren \markKnoten. Als Nächstes verwenden wir einen Trick, um seinen Inhalt als Ganzzahl festzulegen, die die Makrodefinition darstellt. Wenn wir dann nach dem Wert des \markKnotens fragen, erhalten wir tatsächlich den Inhalt des Makros, dargestellt als Lua-Tabelle.

  2. Um dann ein Register mit den Token aus einer Lua-Tabelle zu setzen \toks, verwenden wir denselben \markTrick, nur umgekehrt. Dann erstellen wir ein \chardefToken, das auf dieselbe Tokenliste zeigt wie das \mark. Als nächstes überlaufen wir vorsichtig den Index eines \toksRegisters, sodass er am Ende auf das \chardefToken zeigt. Da wir die Offsets sorgfältig ausgewählt haben, verhält sich dieses „falsche“ \toksRegister jetzt so, als hätte es denselben Wert wie das \mark.

Wir benötigen außerdem etwas Code, um die internen TeX-Token-Codes wieder in die vom Frontend erwartete Form zu konvertieren und etwas Code, um das gefälschte \toksRegister in ein echtes zu verwandeln, aber das ist relativ unkompliziert.

Antwort3

Hier ist eine Funktion \tl_set_from_cs:NNn, die eine Tokenliste, eine Steuersequenz und eine Anzahl von Parametern aufnimmt und den Ersetzungstext der Steuersequenz der Tokenliste zuweist:

\cs_new_protected:Nn
  \tl_set_from_cs:NNn
  {
    \tl_set:Nn
      \l_tmpa_tl
      { #2 }
    \int_step_inline:nn
      { #3 }
      {
        \tl_put_right:Nn
          \l_tmpa_tl
          { { #### ##1 } }
      }
    \exp_args:NNV
      \tl_set:No
      #1
      \l_tmpa_tl
  }

Anders als in der Antwort von egreg ist keine zusätzliche Datenstruktur erforderlich. Dies geht auf Kosten zusätzlicher Erweiterungsschritte, daher ist der Ansatz von egreg möglicherweise vorzuziehen, wenn die Funktion häufig aufgerufen werden soll.

Hier ist eine Demonstration der Verwendung der Funktion \tl_set_from_cs:NNn:

\cs_new:Npn
  \greet
  #1#2
  { Hello,~#1!~How~is~#2~doing? }
\tl_set_from_cs:NNn
  \l_greet_tl
  \greet
  { 2 }
\tl_put_right:Nn
  \l_greet_tl
  { ~Have~a~great~#3! }
\cs_generate_variant:Nn
  \cs_generate_from_arg_count:NNnn
  { NNnV }
\cs_generate_from_arg_count:NNnV
  \greet
  \cs_set:Npn
  { 3 }
  \l_greet_tl

Jetzt kann ich \greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }die folgende Erweiterung schreiben und empfangen:

Hallo Welt! Wie geht es deiner Lieblingsspezies (vermutlich dem Menschen)? Mach einen großen Evolutionssprung!

Wie in der Antwort von egreg gezeigt, können wir die Funktionssignatur auch vereinfachen, indem wir \tl_set_from_cs:NNdie Anzahl der Parameter aus der Parameterspezifikation ableiten:

\cs_new_protected:Nn
  \tl_set_from_cs:NN
  {
    \tl_set:Ne
      \l_tmpa_tl
      { \cs_parameter_spec:N #2 }
    \int_set:Nn
      \l_tmpa_int
      { \tl_count:N \l_tmpa_tl / 2 }
    \tl_set_from_cs:NNV
      #1
      #2
      \l_tmpa_int
  }
\cs_generate_variant:Nn
  \tl_set_from_cs:NNn
  { NNV }

Hier ist eine Demonstration der Verwendung der Funktion \tl_set_from_cs:NN:

\cs_new:Npn
  \greet
  #1#2
  { Hello,~#1!~How~is~#2~doing? }
\tl_set_from_cs:NN
  \l_greet_tl
  \greet
\tl_put_right:Nn
  \l_greet_tl
  { ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
  \greet
  \cs_set:Npn
  { 3 }
  \l_greet_tl

Jetzt kann ich \greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }wieder folgende Erweiterung schreiben und empfangen:

Hallo Welt! Wie geht es deiner Lieblingsspezies (vermutlich dem Menschen)? Mach einen großen Evolutionssprung!

Wenn die Funktion \tl_set_from_cs:NNzum l3tl-Modul von expl3 hinzugefügt werden sollte, müssten auch andere Varianten hinzugefügt werden, wie z. B. \tl_gset_from_cs:NN.

Darüber hinaus sollte klar dokumentiert werden, dass nur Steuersequenzen mit nicht begrenzten Parametern auf diese Weise in Tokenlisten umgewandelt werden können.

EDIT: Weitere Klarstellungen (27.04.2024)

Wir können doppelte #s im Ersetzungstext wie folgt beibehalten:

  1. Erweitern Sie die Steuersequenz, indem Sie als Parameter Steuersequenzen wie \witiko_parameter_1, \witiko_parameter_2, \witiko_parameter_3, usw. verwenden, die im Ersetzungstext wahrscheinlich nicht vorkommen.
  2. Verdoppeln Sie das #s im Ergebnis der Erweiterung.
  3. Ersetzen Sie \witiko_parameter_1, \witiko_parameter_2, \witiko_parameter_3, usw. im Ergebnis der Erweiterung durch #1, #2, #3, usw.

Hier ist eine konkrete Implementierung:

\cs_new_protected:Nn
  \tl_set_from_cs:NNn
  {
    \tl_set:Nn
      \l_tmpa_tl
      { #2 }
    \int_step_inline:nn
      { #3 }
      {
        \exp_args:NNc
          \tl_put_right:Nn
          \l_tmpa_tl
          { witiko_parameter_ ##1 }
      }
    \exp_args:NNV
      \tl_set:No
      \l_tmpb_tl
      \l_tmpa_tl
    \regex_replace_all:nnN
      { \cP. }
      { \0\0 }
      \l_tmpb_tl
    \int_step_inline:nn
      { #3 }
      {
        \regex_replace_all:nnN
          { \c { witiko_parameter_ ##1 } }
          { \cP\# ##1 }
          \l_tmpb_tl
      }
    \tl_set:NV
      #1
      \l_tmpb_tl
  }

Hier ist eine Demonstration der Implementierung:

\cs_new_protected:Npn
  \greet
  #1#2
  {
    Hello,~#1!
    \group_begin:
    \cs_set:Npn
      \greet
      ##1
      { ~How~is~##1~doing? }
    \greet { #2 }
    \group_end:
  }
\tl_set_from_cs:NN
  \l_greet_tl
  \greet
\tl_put_right:Nn
  \l_greet_tl
  { ~Have~a~great~#3! }
\cs_generate_from_arg_count:NNnV
  \greet
  \cs_set:Npn
  { 3 }
  \l_greet_tl

Jetzt kann ich \greet { world } { favorite~species~(humans,~presumably) } { evolutionary~leap }die folgende Erweiterung schreiben und erhalten:

Hallo Welt! Wie geht es deiner Lieblingsspezies (vermutlich dem Menschen)? Mach einen großen Evolutionssprung!

Das ist wie erwartet.

Antwort4

Dies gelingt uns mit doppelten Parameterzeichen ( #), mit abgegrenzten Argumenten und mit jeder Engine, allerdings mit zwei Einschränkungen:

  1. Das Zielmakrokann nicht"FFals Makroparameter Zeichen verwenden .

  2. Alle Trennzeichenmusshaben „normale“ Catcodes.

Ich würde erwarten, dass >99 % der Makros diese Anforderungen erfüllen, daher sollte dies in der Praxis nicht zu restriktiv sein.

\documentclass{article}

\pagestyle{empty}
\parindent=0pt

\ExplSyntaxOn

%%%%%%%%%%%%%%%%%%%
%%% Definitions %%%
%%%%%%%%%%%%%%%%%%%

%% Copies the replacement text of a macro (#1) into a token list (#2).
\cs_new_protected:Nn \example_cs_to_tl:NN {
    %% Get the parameters used by the macro
    \tl_set:Ne #2 { \cs_parameter_spec:N #1 }

    %% Convert the parameters into normal catcodes to handle any delimited
    %% arguments.
    \tl_set_rescan:NnV #2 {} #2

    %% Use <"FF>_6 as the macro parameter character instead of the usual <#>_6.
    %% We do this so that we can distinguish between any inner macro parameters
    %% and the new ones that we're passing in here.
    \regex_replace_all:nnN { \# ( \d ) } { { \cP\xFF \1 } } #2

    %% Expand the macro to get at its replacement text.
    \tl_set:Nf #2 {
        \exp_last_unbraced:NNNo \exp_after:wN \exp_stop_f: #1 #2
    }

    %% Double all the original parameter characters, ignoring our new ones.
    \regex_replace_all:nnN { \cP [^ \xFF ] } { \0 \0 } #2
}

%% I've used inline regexes here to make the code easier to follow, but you
%% should use `\regex_const:Nn` in the real code since it's considerably faster.

\cs_generate_variant:Nn \cs_generate_from_arg_count:NNnn { NNnV }


%%%%%%%%%%%%%%%%%%%%%
%%% Demonstration %%%
%%%%%%%%%%%%%%%%%%%%%

%% A test macro with two normal arguments and some inner doubled #'s.
\cs_new:Npn \original #1 #2 {
    Hello,~#1!~
    \group_begin:
        \cs_set:Npn \original ##1 {
            ~How~is~##1~doing?

            \cs_gset:Npn \inner ####1 {
                #1~##1~####1.
            }
        }
        \original { #2 }
    \group_end:
}

%% Extract the replacement text and define a new macro with the same body.
\example_cs_to_tl:NN \original \l_tmpa_tl
\cs_generate_from_arg_count:NNnV \new \cs_set:Npn { 2 } \l_tmpa_tl

%% A test macro with some weird delimited arguments.
\cs_new:Npn \weirddelims #1, #2 #3 ABC \relax {
    <#2>
    \def \inner ##1 #3 {
        <#1>
    }
}

%% Extract the replacement text and define a new macro with the same body.
\example_cs_to_tl:NN \weirddelims \l_tmpa_tl
\cs_generate_from_arg_count:NNnV \newweirddelims \cs_set:Npn { 3 } \l_tmpa_tl

\ExplSyntaxOff

%% Show the comparison between the original and new macros.
\begin{document}
    \texttt{\small \meaning\original}%
    \par
    \original{AAA}{BBB} \inner{CCC}
    \bigskip

    \texttt{\small \meaning\original}%
    \par
    \new{DDD}{EEE} \inner{FFF}
    \bigskip

    \texttt{\small \meaning\weirddelims}%
    \par
    \weirddelims{GGG},{HHH}{III}ABC\relax \inner{JJJ}III
    \bigskip

    \texttt{\small \meaning\newweirddelims}%
    \par
    \newweirddelims{KKK}{LLL}{MMM} \inner{NNN}MMM
\end{document}

Ausgabe

Der Trick besteht hier darin, beim Erweitern des Zielmakros ein anderes Makroparameterzeichen zu verwenden, sodass wir zwischen den „inneren“ verschachtelten Parametern und den äußersten „echten“ Parametern unterscheiden können.

verwandte Informationen