在将宏定义写入文件的宏中出现“非法参数编号”错误

在将宏定义写入文件的宏中出现“非法参数编号”错误

问题。

我正在尝试编写一个类似于的命令\newcommand,但遇到了问题非法参数个数:由于我无法解决的原因,它要求我使用类型的参数,##1而不是#1。我熟悉在嵌套定义的情况下使用前者,例如在诸如 这样的代码片段中\def\foo#1{\def\bar##1{#1-##1}},但我不明白为什么在我尝试执行的操作中会出现错误。我希望有人可以向我展示如何使样式的参数#1足够,如下面我描述的示例中所示。

细节。

我正在尝试编写一个名为 的宏\newmacro,它模拟 的语法\newcommand,并使每个宏在第一次使用时将其自己的定义写入输出文件。尝试下面的最小示例以准确了解我的意思:

\documentclass{article}
\makeatletter

\newwrite\macro@outfile
\immediate\openout\macro@outfile=\jobname.macros%
\typeout{Writing extra macros to file \jobname.macros}%

\def\macro@write#1#2{%
    \immediate\write\macro@outfile{\string\renewcommand\string#1#2}%
    \renewcommand#1#2%
    #1%
}

\def\newmacro#1[#2]#3{%
    \def#1{\macro@write{#1}{[#2]{#3}}}}

\makeatother

这些宏定义不能正常工作:例如,使用上面的前言,\newmacro下面的第二次调用会引发错误。

\begin{document}

\newmacro\testmacro[1]{foo(##1)}
\testmacro{1}
\testmacro{2}

\newmacro\testmacro[1]{foo --- #1 ---}
\testmacro{3}
\testmacro{4}

\end{document}

如果本文档按照上述序言进行编译test.tex,则除了通常的输出外,test.macros还会创建一个新文件,其内容如下:

\renewcommand\testmacro[1]{foo(##1)}
\renewcommand\testmacro[1]{foo --- ##1 ---}

这几乎是正确的,这就是我所说的宏将“自己的定义”写入输出文件的意思。但是,我希望存储的定义使用参数#1而不是,并且使用参数##1调用不会抛出错误。我希望有人能告诉我如何做到这一点。\newmacro#1

答案1

当你说:

\newmacro\testmacro[1]{foo --- #1 ---}

你最终会得到:

\def\testmacro{\macro@write{\testmacro}{[1]{foo --- #1 ---}}}}

因此,您尝试定义一个没有任何参数的宏,但仍在#1其中使用。这会导致Illegal parameter number错误消息。

为了允许#1#2等,您需要使用正确数量的参数定义宏。您可以先使用九个参数定义宏以避免任何Illegal parameter number错误。然后使用##1等作为参数来扩展它,因此您基本上#1##1、 等替换,并将其定义为相同的宏但没有参数。因为所有这些都发生在另一个宏中,所以必须将\newmacro所有这些#都加倍:

\def\newmacro#1[#2]#3{%
    \def#1##1##2##3##4##5##6##7##8##9{\macro@write{#1}{[#2]{#3}}}% Define macro in a safe way
    \expandafter\def\expandafter#1\expandafter{#1{####1}{####2}{####3}{####4}{####5}{####6}{####7}{####8}{####9}}% Replace `#1` with `##1` etc.
}

但是,输出文件中会出现 的问题##。最好通过再次定义具有九个参数的宏并打印去除前缀的 来避免这种情况\meaning,然后再将其定义为其真实定义:

\def\macro@write#1#2{%
    \def#1##1##2##3##4##5##6##7##8##9{#2}%
    \immediate\write\macro@outfile{\string\renewcommand\string#1\expandafter\strip@prefix\meaning#1}%
    \renewcommand#1#2%
    #1%
}

\jobname.macros文件包含:

\renewcommand\testmacro[1]{foo(#1)}
\renewcommand\testmacro[1]{foo --- #1 ---}

但是,当您现在##1在宏定义中使用时,这种情况就会发生,例如:

\newmacro\testmacro[1]{foo --- #1 ---\def\A##1{(##1)}}

因此,您建议采用以下方法:将宏定义为不同的名称,然后将该定义复制到 内的真实名称\macro@write。这样,您就可以避免宏参数转义的所有问题。您可以使用\csnamewith\string#1构建第二个宏名称,该名称与官方宏名称不同。

\documentclass{article}
\makeatletter

\newwrite\macro@outfile
\immediate\openout\macro@outfile=\jobname.macros%
\typeout{Writing extra macros to file \jobname.macros}%

\def\newmacro#1[#2]#3{%
    \expandafter\newcommand\csname @\string#1\endcsname[#2]{#3}%
    \newcommand#1{\macro@write{#1}{#2}}%
}

\def\macro@write#1#2{%
    \expandafter\let\expandafter#1\csname @\string#1\endcsname
    \expandafter\let\csname @\string#1\endcsname\@undefined
    \immediate\write\macro@outfile{\string\newcommand\string#1[#2]{\expandafter\strip@prefix\meaning#1}}%
    #1%
}

\makeatother
\begin{document}

\newmacro\testmacroa[1]{foo(#1)}
\testmacroa{1}
\testmacroa{2}

\newmacro\testmacrob[1]{foo --- #1 ---}
\testmacrob{3}
\testmacrob{4}

\newmacro\testmacroc[1]{foo --- #1 ---\def\A##1{(##1)}}
\testmacroc{3}
\testmacroc{4}

\end{document}

我擅自用 替换了 ,\renewcommand因为\newcommand对于宏来说,这似乎更合乎逻辑\newmacro。如果您确实想要\renewcommand,即允许\newmacro重新定义其宏,则定义\newmacro如下:

\def\newmacro#1[#2]#3{%
    \expandafter\let\csname @\string#1\endcsname\empty
    \expandafter\renewcommand\csname @\string#1\endcsname[#2]{#3}%
    \def#1{\macro@write{#1}{#2}}%
}

\renewcommand第一行确保宏在使用之前无论如何都存在。

答案2

结语。

正如我在对 Martin 的回答的评论中指出的那样,当我尝试扩展我的/他的代码以接受 样式的可选参数时,我发现了其他障碍\newcommand。我接受了 Martin 的回答,认为它解决了我发布的问题:此答案旨在展示如何成功处理使用 定义的宏的两种情况\newcommand,包括带有可选参数和不带有可选参数的情况。

恢复并写入宏的预期含义。

基本问题是,对于用于\cseq接受可选参数的宏,\newcommand定义了两个宏:\cseq它本身,它查找可选参数,\\cseq它实际上对参数进行操作。因此,我们需要付出额外的努力来提取“含义”,\cseq以便将其写入文件\jobname.macros

我们定义一个单独的宏来“激活”一个宏\cseq,该宏的定义是\newmacro——赋予\cseq其预期含义,将其临时存储在宏中\macro@cseq,同时还将宏的“名称”提取到\@tempa

\def\macro@activate#1{%
    \edef\@tempa{\expandafter\@gobble\string#1}%
    \expandafter\let\expandafter#1\csname macro@\@tempa\endcsname
    \expandafter\let\csname macro@\@tempa\endcsname\@undefined
}

我们假设宏的名称(例如 cseq) 存储在\@tempa我们想要将 的预期含义写入\cseq输出文件时。然后,以下命令构造命令序列\macro@cseq,并将其存储为 的新含义\@tempa。然后,使用这个命令序列,它将 的含义\\macro@cseq(实际作用于 的参数的宏\macro@cseq)写入输出:最后一行上的宏\\macro@cseq本质上构造为 \csname\string\macro@cseq\endcsname

\def\macro@write@opts#1[#2][#3]{%
    \edef\@tempa{\expandafter\noexpand\csname macro@\@tempa\endcsname}%
    \def\@tempb##1{%
        \immediate\write\macro@outfile{\string\newcommand\string#1[#2][#3]{%
        \expandafter\strip@prefix\meaning##1}}}%
    \expandafter\expandafter\expandafter\@tempb%
        \expandafter\csname\expandafter\string\@tempa\endcsname%
}

相应的编写不带参数或不带可选参数的宏的命令更简单。在下面的宏中,#2等于空字符串或[numargs]

\def\macro@write@noopts#1#2{%
    \immediate\write\macro@outfile{%
        \string\newcommand\string#1#2{\expandafter\strip@prefix\meaning#1}}%
}

自激活宏的初始定义。

代码的其余部分是一个或多或少简单的案例分析,它确定所\cseq定义的宏是否有任何参数,是否有任何可选参数,然后重新定义\cseq以激活(并写入文件)其预期行为,然后再调用自身。在激活其预期行为的过程中,\@tempa用于存储宏的名称(例如 cseq),正如我们在宏采用可选参数的情况下所要求的那样。

\def\newmacro#1{%
    \edef\@tempa{\expandafter\@gobble\string#1}%
    \kernel@ifnextchar [{\newmacro@args#1}{\newmacro@noopts#1{}}}

\def\newmacro@args#1[#2]{%
    \kernel@ifnextchar [{\newmacro@opts#1{[#2]}}{\newmacro@noopts#1{[#2]}}}

\def\newmacro@noopts#1#2#3{%
    \expandafter\newcommand\csname macro@\@tempa\endcsname#2{#3}%
    \def#1{%
        \macro@activate#1%
        \macro@write@noopts#1{#2}%
        #1}}

\def\newmacro@opts#1#2[#3]#4{%
    \expandafter\newcommand\csname macro@\@tempa\endcsname#2[#3]{#4}%
    \def#1{%
        \macro@activate#1%
        \macro@write@opts#1#2[#3]%
        #1}}

再次感谢马丁的帮助。

相关内容