考虑以下示例。
默认情况下\g@addto@macro
不会扩展其参数,如果宏值稍后发生变化,则在将宏作为参数传递时可能会产生意外结果。
为了扩展宏参数,我使用了下面显示的解决方法。这种方法有什么缺点吗?有没有更好的方法?
在此示例代码中,\append
不扩展其参数。\newappend
扩展。 它们分别将参数#1
(后跟逗号)附加到宏\slist
和\newslist
。
\documentclass[12pt]{article}
\def\slist{}
\def\newslist{}
\makeatletter
\def\append#1
{
\g@addto@macro\slist{#1,}%
}
\def\newappend#1
{
\def\tmpmacroval{#1}
\edef\next{\noexpand\g@addto@macro\noexpand\newslist{\tmpmacroval,}}%
\next
}
\makeatother
\def\foo{fooval}
\begin{document}
\append{\foo}
\newappend{\foo}
\def\foo{someotherval}
\verb|\slist| is \slist
\verb|\newslist| is \newslist
\end{document}
\append
附录:为了简单起见,我省略了这个问题的上下文,但/的参数\newappend
基本上是一个标签。这通常只是一串字符,但我开始使用一个变体,它是一个扩展为字符串的单个宏,并附加一个字符串。例如\theletternum:2019.09.30
,其中letternum
是一个计数器,由 定义\stepcounter{letternum}
。在这种情况下,参数需要先扩展,然后才能附加到\slist
/ \newslist
。我目前认为输入不会变得比这更复杂,但当然很难预见未来。
答案1
首先:正如我多次写过的,TeX 是不是自由形式。两者之间有很大区别
\def\append#1
{
\g@addto@macro\slist{#1,}%
}
以及你可能想要的代码,即
\def\append#1{%
\g@addto@macro\slist{#1,}%
}
在您的代码中,参数\append
被确定为它后面直到第一个空格标记的任何内容。因此,如果您写
\append{a}Some words
结果是{a}Some
将其附加到列表中。
现在来看看你的问题。你期望的输入类型并不十分清楚。如果输入是任何一个文本或者一个扩展为文本的宏,你可以简单地做
\expandafter\append\expandafter{\foo}
如果您想要传递可能包含要扩展的宏的任意文本,那么您需要非常小心。如果此文本包含类似这样的内容,\ref
并且引用的对象仍然未知,那么即使您使用,您也注定会失败\protected@edef
。代码
\documentclass{article}
\makeatletter
\def\slist{}
\def\append#1{%
\protected@xdef\slist{\unexpanded\expandafter{\slist}#1,}%
}
\makeatother
\begin{document}
\append{\ref{what}}
\show\slist
\end{document}
将在终端上打印
> \slist=macro:
->\protect \G@refundefinedtrue {\protect \mbox {\protect \normalfont \protect
\bfseries ??}}\protect \GenericWarning { }{LaTeX Warning: Refe
rence `what' on page 1 undefined},.
这或许是你不太愿意看到的。
因此我假设参数\append
是文本或单个宏。
\documentclass{article}
\makeatletter
\def\slist{}
\def\append{\@ifstar{\append@expand}{\append@text}}
\def\append@text#1{\g@addto@macro\slist{#1,}}
\def\append@expand#1{\expandafter\append@text\expandafter{#1}}%
\makeatother
\newcommand{\foo}{something}
\begin{document}
\append{a}\append*{\foo}
\show\slist
\end{document}
这将打印预期
> \slist=macro:
->a,something,.
我会谨慎地建议更简单的
\documentclass{article}
\makeatletter
\def\slist{}
\def\append#1{%
\expandafter\g@addto@macro\expandafter\slist\expandafter{#1,}%
}
\makeatother
\newcommand{\foo}{something}
\begin{document}
\append{a}\append*{\foo}
\show\slist
\end{document}
效果相同。如果您的输入始终是一串字符或扩展为一串字符的单个宏(非技术意义上的“字符串”,即“序列”),则可以使用这种更简单的方法。
答案2
正如 @UlrikeFischer 和 @DavidCarlisle 指出的那样,使用\edef
任意材料都可能导致错误。例如,包含的材料就是这种情况\textbf
,还有很多其他情况。
LaTeX2e 样式的两种方法
我介绍了两种使用 LaTeX2e 样式的技术,\protected@edef
以便\DeclareRobustCommand
不会影响用 定义的宏。这样,添加的材料可能包含\textbf
。
这些技术也不会覆盖\next
宏,这总是件好事(好吧,它们会覆盖宏,但完成后会自动恢复它)。还请注意,您的\tmpmacroval
宏实际上没什么用。
请注意,尽管我添加了分组,但您的列表仍会得到适当修改,因为\g@addto@macro
使用了\xdef
执行全局分配的。
第一种技术
\documentclass{article}
\newcommand{\newslist}{}
\makeatletter
\newcommand{\newappend}[1]{%
\begingroup
\protected@edef\next{\endgroup\noexpand\g@addto@macro\noexpand\newslist{#1,}}%
\next
}
\makeatother
\newcommand{\foo}{\textbf{foo}val}
\begin{document}
\newappend{\foo}
\renewcommand{\foo}{someotherval}
\verb|\newslist| is \newslist.
\end{document}
解释
假设 TeX 刚刚扩展了\newappend{〈argument〉}
。这会将以下内容留在输入流的最前面:
\begingroup
\protected@edef\next{\endgroup\noexpand\g@addto@macro\noexpand\newslist{〈argument〉,}}%
\next
执行 后\begingroup
,TeX 打开一个新组。然后他看到以下行:
\protected@edef\next{\endgroup\noexpand\g@addto@macro\noexpand\newslist{〈argument〉,}}
\protected@edef
展开后,所有生成的标记都已处理完毕,宏\next
将在当前组中定义。该定义仅限于此组,并且\next
此组结束后,TeX 将自动恢复先前的定义(可能:无)——这是 TeX 组的主要目的。现在,输入流中的下一个标记是\next
控制序列标记。由于它是一个宏,TeX 会将其展开,这会导致此\next
标记被替换为:
\endgroup\g@addto@macro\newslist{〈expanded argument〉,}
(在 的实现中,这些\noexpand
标记已作为 执行的一部分被删除)。因此,输入流中的下一个标记现在是。TeX 执行它(它是不可扩展的,因此它在 TeX 的胃中处理,与宏相反),这将关闭当前组。如前所述,这会取消我们所做的(重新)定义。这是干净的。输入流前面剩下的现在是\edef
\protected@edef
\endgroup
\next
\g@addto@macro\newslist{〈expanded argument〉,}
这正是您想要的。
第二种技术
\documentclass{article}
\newcommand{\newslist}{}
\makeatletter
\newcommand{\newappend}[1]{%
\begingroup
\protected@edef\next{\noexpand\g@addto@macro\noexpand\newslist{#1,}}%
\expandafter
\endgroup
\next
}
\makeatother
\newcommand{\foo}{\textbf{foo}val}
\begin{document}
\newappend{\foo}
\renewcommand{\foo}{someotherval}
\verb|\newslist| is \newslist.
\end{document}
与前面的示例输出相同。
示例expl3
expl3
有许多工具可以处理列表类材料。我将展示一些示例clist
,即逗号分隔的列表。这允许从用户输入轻松直接设置,因为可以在文本中输入逗号。对于更一般的列表,您可以使用seq
变量。对于(键,值)对的列表,有prop
模块(“属性列表”)。所有这些模块都是用语言构建的expl3
,因此在\usepackage{expl3}
或之后可用\usepackage{xparse}
,expl3.
无需加载任何其他内容。
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\clist_new:N \g_faheem_clist
\cs_new_protected:Npn \faheem_append:Nn #1#2
{
\clist_gput_right:Nn #1 {#2} % 'g' for “global”
}
% Variant that performs one expansion step on the second argument
\cs_generate_variant:Nn \faheem_append:Nn { No }
% Variant that performs \edef-like expansion on the second argument
\cs_generate_variant:Nn \faheem_append:Nn { Nx } % \edef-like expansion
% There are also 'e' to perform \expanded-like expansion, 'f' for
% \romannumeral-like expansion, etc.
\NewDocumentCommand \newList { m }
{
\clist_new:N #1
}
% In fact, for serious work, better do:
%
% \cs_new_protected:Npn \faheem_new_list:N #1
% {
% \clist_new:N #1
% }
%
% and make \newList call \faheem_new_list:N or \faheem_new_list:c as I do for
% the \append* functions below (the 'c' variant would allow you to choose
% arbitrary names for the clist variable: you would pass whatever name you
% want to the 'c' variant, normally without a leading slash, otherwise that
% would create a strange beast: \\the_passed_name).
\NewDocumentCommand \clearList { O{\g_faheem_clist} }
{
\clist_gclear:N #1 % 'g' for “global”
}
\NewDocumentCommand \appendNoExp { O{\g_faheem_clist} m }
{
\faheem_append:Nn #1 {#2}
}
\NewDocumentCommand \appendOneExp { O{\g_faheem_clist} m }
{
\faheem_append:No #1 {#2}
}
\NewDocumentCommand \appendFullExp { O{\g_faheem_clist} m }
{
\faheem_append:Nx #1 {#2}
}
% This requires a recent LaTeX release (2020 or later?)
\NewDocumentCommand \appendTextExpand { O{\g_faheem_clist} m }
{
% \text_expand:n works inside an 'x'-type argument because it is fully
% expandable, as indicated by the star in the margin in interface3.pdf.
\faheem_append:Nx #1 { \text_expand:n {#2} }
}
\NewDocumentCommand \useList { O{\g_faheem_clist} m }
{
\clist_use:Nn #1 {#2}
}
\ExplSyntaxOff
\newcommand*{\showres}[1]{\makebox[0pt][r]{#1.~}\ignorespaces}
\begin{document}
\newList{\somelist}
\appendNoExp{first in default list}
\appendNoExp[\somelist]{first in somelist}
\def\foobar{element with \textbf{non-expandable}\_material}
\def\baz{quux}
\appendOneExp{\foobar}
\appendOneExp[\somelist]{\baz}
\showres{1}
Default list: \useList{, }\par
\verb|\somelist|: \useList[\somelist]{, }
\def\fooleveli{\foolevelii}
\def\foolevelii{\fooleveliii}
\def\fooleveliii{other element}
\appendFullExp{\fooleveli}
\showres{2}
Default list: \useList{, }
\renewcommand{\fooleveliii}{\textbf{non-expandable} \emph{stuff}}
% Expand again \fooleveli, \foolevelii and (the new) \fooleveliii, this time
% with a method that won't break with LaTeX non-expandable commands defined
% with \DeclareRobustCommand.
\appendTextExpand{\fooleveli}
\showres{3}
Default list: \useList{, }.
\def\foobar{overwrite}
\def\fooleveli{overwrite}
\showres{4}
Default list: \useList{, }.\par
\verb|\somelist|: \useList[\somelist]{, }.
\showres{5}
With X as separator: \useList{X}.
\clearList % clear the default list
\showres{6}
Y\useList{X}Y
\end{document}
注意:为了扩展文本材料,避免由于不可扩展、不受引擎保护的命令(如)而导致错误,\textbf
最近的expl3
命令是(见\text_expand:n
\protected@edef
Joseph Wright 的博客和界面3.pdf)。
答案3
您的代码会产生大量不需要的空格标记:
因为\endlinechar
在每行的右端添加了一个回车符(类别代码为 5(行尾))。在生成诸如and和(如= )之类的字符标记后,(La)TeX 的读取装置处于状态 m(行中),因此后面的类别代码 5 回车符和后面的空格字符都将被标记为空格标记。空格标记反过来会在(受限)水平模式下产生可见的水平粘连。{1(begin group)
}2(end group)
112(other)
#1
#6(parameter)112(other)
\edef
而不是\protected@edef
可能会扩大不应进一步扩大的事物。如果的参数
\newappend
包含哈希值( ),则展开#
时两个连续的哈希值将合并为一个哈希值。展开时也会发生同样的情况。反过来会使哈希值翻倍。换句话说:将连续哈希值的数量除以 2。\tmpmacroval
\next
\g@addto@macro
\newappend
纠正 1 和 2 可能会将您的代码变成如下形式:
\documentclass[12pt]{article}
\def\slist{}
\def\newslist{}
\makeatletter
\def\append#1{\g@addto@macro\slist{#1,}}
\def\newappend#1{%%%%
\def\tmpmacroval{#1}%%%%
\protected@edef\next{\noexpand\g@addto@macro\noexpand\newslist{\tmpmacroval,}}%
\next
}%%%
\makeatother
\def\foo{fooval}
\begin{document}
\append{\foo}
\newappend{\foo}
\def\foo{someotherval}
\verb|\slist| is \texttt{\meaning\slist}
\verb|\newslist| is \texttt{\meaning\newslist}
% Here are sequences with four consecutive hashes:
\def\foo{\def\noexpand\bar####1{Arg of \string\bar\space is ####1}}
\append{\foo}
\newappend{\foo}
\verb|\slist| is \texttt{\meaning\slist}
\verb|\newslist| is \texttt{\meaning\newslist}
\end{document}
当你查看结果输出的最后一行时,你会注意到四源中的连续哈希值####
减半,因此\newslist
仅包含两个哈希值的序列。(扩展时\newslist
,哈希值的数量再次减半,因此扩展\newslist
会产生标记:
fooval,\def\bar#1{Arg of \bar is #1},
;第二个\bar
被字符串化。)
\expanded
使用-primitive 和-primitive都可用的 TeX 引擎\unexpanded
,您还可以纠正问题 3(扩展期间两个连续哈希值折叠为单个哈希值)。我可能会做这样的事情:
\documentclass{article}
\newcommand\exchange[2]{#2#1}
\makeatletter
\newcommand\MyExpandedG@addto@macro[2]{%
% The following three lines form the \protected@edef-mechanism:
\let\@@protect\protect
\let\protect\@unexpandable@protect
\afterassignment\restore@protect
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\xdef#1{%
% \unexpanded inside \edef yields doubling of amounts of hashes.
\unexpanded\expandafter{%
\romannumeral0%
% \romannumeral0-\exchange-trickery to get the things that are to be appended
% expanded fully via \expanded, and to get the macro where things are to be
% appended expanded once; during expansion - no matter if expansion
% is only once or fully - amounts of hashes get halved:
\expandafter\exchange\expandafter{\expanded{#2}}%
{\exchange{ }\expandafter#1}%
}%
}%
}%
\newcommand\newappend[1]{\MyExpandedG@addto@macro\newslist{#1,}}%
\newcommand\append[1]{\g@addto@macro\slist{#1,}}%
\makeatother
\newcommand\slist{}
\newcommand\newslist{}
\newcommand\foo{fooval}
\begin{document}
\append{\foo}
\newappend{\foo}
\def\foo{someotherval}
\verb|\slist| is \texttt{\meaning\slist}
\verb|\newslist| is \texttt{\meaning\newslist}
% Here are sequences with two consecutive hashes:
\def\foo{\def\noexpand\bar##1{Arg of \string\bar\space is ##1}}
\append{\foo}
\newappend{\foo}
\verb|\slist| is \texttt{\meaning\slist}
\verb|\newslist| is \texttt{\meaning\newslist}
\end{document}
当你查看结果输出的最后一行时,你会注意到 summa summarum 的数量 二源中的连续哈希值##
不会减半,因此\newslist
包含具有相同数量哈希值的序列。