\appendix
我正在尝试使用新的钩子管理系统为命令设置钩子。但是,当cleveref
加载时,钩子失败,错误为“\appendix 定义中的参数编号非法。” begindocument
,因此推测钩子是由设置的ltcmdhooks
。该消息通常出现 46 次,在不同的文档中出现相同的次数,我认为这是##
定义中的数量cleveref
。
使用等效etoolbox
代码不会导致同样的问题。
下面的 MWE 并不是真正的“真实用例”,但足以重现该问题:
\documentclass{book}
\usepackage{cleveref}
\AddToHook{cmd/appendix/before}{\label{appendix}}
% 'etoolbox' works though
% \usepackage{etoolbox}
% \preto{\appendix}{\label{appendix}}{}{}
\begin{document}
Hello world!
\appendix
\end{document}
我有点不知该如何处理这个问题。但是,如果能更好地了解罪魁祸首是什么,特别是为什么ltcmdhooks
会出现故障etoolbox
,以便我更好地了解我能用前者做什么,那就太好了。
编辑:如果可以的话,我想在这里为问题添加一个维度。我的实际用例是,我试图在启动时\appendix
在用于发布的包中进行一些设置。现在,我无法确定\appendix
可能包含的内容,因为它在一个或另一个包或类中的很多地方被重新定义。我原本希望保留钩子,但如果在一个或另一个用例中出现问题,我可以指示用户手动移除钩子并重新建立工作状态。但添加和移除钩子仍然会中断\appendix
:
\documentclass{book}
\usepackage{cleveref}
\AddToHook{cmd/appendix/before}[package-1]{\label{appendix}}
\RemoveFromHook{cmd/appendix/before}[package-1]
\begin{document}
Hello world!
\appendix
\end{document}
是否有一种安全的方法来在应用钩子之前禁用它begindocument
?
答案1
修补命令大致有两种方法:通过\scantokens
,以及通过扩展+重新定义。本答案的末尾对这两种方法都有(不那么)简短的解释。当ltcmdhooks
可以检测到命令的类型时,它确切地知道<parameter text>
命令的 ,它通过扩展+重新定义进行修补,因此它对定义宏时生效的 catcode 设置没有任何限制。在 的情况下\appendix
,它不带参数,因此可以将其视为标记列表并进行扩展,然后使用添加的材料重新定义。
例如,这里有一个关于其工作原理的简单示意图:
\def\appendix{%
\typeout{This starts the appendix.}}
\def\append#1{%
\expandafter\appendaux\expandafter{#1}#1}
\def\appendaux#1#2#3{%
\def#2{#1#3}}
\append\appendix{\typeout{I added this.}}
\appendix
然而,当我编写该代码时我没有预料到的是,当的原始定义\appendix
包含时会出现这种情况##
(请在上面的代码中尝试这个定义):
\def\appendix{%
\typeout{This starts the appendix. ##BOOM!}}
当\appendix
如此定义时,TeX 的定义扫描器会看到,并在 的定义中用单个参数标记替换它,到目前为止一切正常。但是,当您展开命令时,TeX 还会返回单个,然后当您尝试重新定义命令时,您会得到:#6#6
#6
\appendix
#6
\def\appendix{%
\typeout{This starts the appendix. #BOOM!}%
\typeout{I added this.}}
其中包含非法参数(#B
),并且定义错误。
我有已更改ltcmdhooks
来处理这种情况(下面有一个简短的解释),但同时您可以使用\ActivateGenericHook
(或\ProvideHook
在 LaTeX 2021-06-01 中)来表示ltcmdhooks
您已经修补了该命令,因此它不会尝试修补,然后您可以使用以下命令手动进行修补etoolbox
:
\documentclass{book}
\usepackage{cleveref}
\usepackage{etoolbox}
\IfFormatAtLeastTF{2021-11-15}%
{\ActivateGenericHook}% LaTeX > 2021-11-15
{\ProvideHook}% LaTeX = 2021-06-01
{cmd/appendix/before}
\pretocmd\appendix
{\UseHook{cmd/appendix/before}}
{}{\FAILED}
\AddToHook{cmd/appendix/before}{\label{appendix}}
\begin{document}
Hello world!
\appendix
\end{document}
为什么上述方法有效
ltcmdhooks
in的接口\AddToHook
应该按如下方式工作:
如果最终用户写入\AddToHook{cmd/name/before}{code}
,并且钩子cmd/name/before
尚不存在(这意味着命令\name
没有“安装”该钩子),那么代码会尝试在命令中修补该钩子。
如果最终用户写入\AddToHook{cmd/name/before}{code}
,并且钩子cmd/name/before
已经存在,这(可能)意味着该命令\name
已经有该钩子,因此它只需将代码添加到钩子中,并保留命令。
这意味着软件包作者可能想要微调cmd/name/before
钩子的位置(例如\def\name{<some initialization>\UseHook{cmd/name/before}<definition>}
),然后我们不想ltcmdhooks
再次修补该命令(两次添加相同的钩子是错误的),因此我们ltcmdhooks
通过说来表示钩子已经存在\ActivateGenericHook{cmd/name/before}
,然后不再尝试修补。
这适用于您的情况,因为您可以手动将钩子添加到命令中,然后告知ltcmdhooks
不再需要 pathching。请参阅部分3 包作者界面的ltcmdhooks
文件。
因此从本质上讲,您作为软件包作者,是\appendix
通过自己添加钩子(确切地说ltcmdhooks
是在添加它的位置)来挪用该命令,然后ltcmdhooks
通过使用来告知不要对其进行修补\ActivateGenericHook
。
\appendix
如果您要将钩子添加到而不是\UniqueCommandFromMyPackage
,那么您可以使用\NewHook
而不是\ActivateGenericHook
(效果相同),因为不存在名称冲突的可能性。
LaTeX2ε 现在如何处理这种情况
问题:事实证明,在上述情况下,我们陷入了死胡同。当你写出如下定义时
\def\foo#1{#1##X}
TeX 将其存储<replacement text>
为一个标记列表,其中包含:
out_param 1, par_token #, letter X
(在宏展开时要替换为实际参数,out_param 1
是catcode 6 ,是 catcode 11 )。#1
par_token #
#
letter X
X
然后,当你\foo
用#1
( par_token #, character 1
) 展开时,TeX 会进行替换out_param 1
,你会得到:
par_token #, character 1, par_token #, letter X
这相当于输入#1#X
。如果你把它带回到 的新定义中,\foo
你将得到:
\def\foo#1{#1#X}
这显然是错误的(因此是Illegal parameter number
错误)。此时,您无法分辨定义宏时哪个是实际参数,哪个是单个参数标记。
半解决方案: 有一种非常简单的情况很容易检测和解决(碰巧是您的问题中的情况):没有参数的宏。在这种情况下,宏没有参数,因此##
其定义中的任何松散都不可能与参数混淆,因此我们可以将此类宏视为标记列表(在某种意义上expl3
),并执行类似操作\tl_put_right:Nn
并解决问题。
另一个相对简单的情况是宏定义中没有参数##
。在这种情况下,我们不必担心参数混淆,因此我们正常处理宏(这是最初实现的情况)。LaTeX 使用一个相当简单的循环来检查宏定义中是否有参数标记(\__hook_if_has_hash:nTF
):它查看定义中的每个标记,并将其与进行比较#
。
另一半: 当宏属于具有两个参数的一般情况时和参数标记在其定义中(如上所示\foo
),那么我们必须手动将定义中的每个参数标记重新加倍,以便可以重新制作。为此, LaTeX 不会使用\foo
来扩展它#1
,而是使用 来扩展它\c_@@_hash_tl
,因此\foo{\c_@@_hash_tl}
变成了这样的定义:
\foo#1{\c_@@_hash_tl 1#X}
然后我们循环遍历宏的替换文本(括号内)并将 every 翻倍##
,并将 every 替换\c_@@_hash_tl
为一个#
,然后得到:
\foo#1{#1##X}
然后我们就可以正常地进行定义(呼!
修补\scantokens
假设一个宏定义为
\long\def\mycmd[#1]#2{\typeout{#1//#2}}
要通过 向其中附加一些代码\scantokens
,首先要\meaning\mycmd
获取如下字符串:
\long macro:[#1]#2->\typeout {#1//#2}
(使用通常的\detokenize
catcode:除空格外全部 12 个,空格是 catcode 10),然后使用分隔宏来分隔<prefixes>
、<parameter text>
和<replacement text>
,大致如下所示:
\def\split#1{\expandafter\splitaux\meaning#1\relax}
\expanded{%
\noexpand\def\noexpand\splitaux#1\detokenize{macro:}#2->#3\relax}{%
\def\prefixes{#1}%
\def\parameter{#2}%
\def\replacement{#3}}
(我使用\def\prefixes{#1}
,等等,以便于理解,但实际上你会将所有东西都可扩展地注入;如果你够勇敢,请参阅,和\__kernel_prefix_arg_replacement:wN
的expl3-code.tex
定义)。\etb@patchcmd
etoolbox.sty
此时,您将定义的每一部分都单独作为一个字符串。现在,您可以向其附加或添加一些代码\replacement
(或替换其中的某些部分,如在中所做的那样\patchcmd
),或者在极少数情况下更改\prefixes
或\parameter
。此时,您有三个字符串,每个字符串都是定义的一部分。要重建定义,您需要:
<prefixes>\def\mycmd<parameter text>{<replacement text>}
但您拥有的三个部分仍然是 catcode 12 个标记,这没什么用。接下来是\scantokens
:您将这些字符串重新扫描回“正常”标记:
\expanded{%
\noexpand\scantokens{%
% <prefixes>\def \mycmd<parameter text>{<replacement text>}
\prefixes \def\noexpand\mycmd\parameter {\replacement <added material>}%
}%
}
\expanded
完成任务后,变为:
\scantokens{%
\long\def\mycmd[#1]#2{\typeout {#1//#2}<added material>}%
}%
然后\scantokens
执行其操作并使用当前 catcode 设置将所有内容转换为标记,然后正常进行定义。
这种方法的优点是您可以在定义的任何部分进行几乎任何操作。
缺点如下:
- 您需要知道在第一次定义时哪些 catcode 有效(修补时您通常需要验证简单的一轮
\meaning
–\scantokens
不会改变宏的含义)否则您无法安全地修补; - 如果该宏是使用和的某种组合创建的
\edef
,\detokenize
以强制创建某些 catcode 12 标记,则您可能无法修补该宏(例如,\splitaux
如上文所定义,无法用它修补,\patchcmd
因为它包含 catcodes 11 和 12 的字母(例如m
); - 如果
<parameter text>
宏的包含字符->
,则您将无法修补该宏。
通过扩展+重新定义进行修补
这种方法简单得多,但需要事先了解宏的定义方式。这可以在少数情况下完成,即当您确切知道<parameter text>
宏的是什么时。内核知道的情况是宏使用 定义\DeclareRobustCommand
,或使用ltcmd
(\NewDocumentCommand
或\NewExpandableDocumentCommand
),或\newcommand
使用可选参数,或宏不使用参数。
假设与之前相同的宏,但定义为:
\newcommand\mycmd[2][default]{\typeout{#1//#2}}
(它将有一个名为 的内部宏\\mycmd
,但为了简单起见,我们\mycmd
也将其称为 ),那么我们肯定知道它<parameter text>
是[#1]#2
。知道宏需要什么参数,我们可以将#1
、#2
、 ... 作为参数提供给它,因此对于\mycmd
我们可以这样做:
\mycmd[#1]{#2}
然后它将扩展为<replacement text>
宏的,其中第一个参数(#1
)被替换为(参数标记后跟字符)。修补方案如下:#6112
#
1
\expanded{%
\def\noexpand\mycmd[#1]#2{%
\unexpanded\expandafter{\mycmd[#1]{#2}<added material>}%
}%
}
完成后\expanded
你将得到:
\def\mycmd[#1]#2{\typeout{#1//#2}<added material>}}
这正是您使用的方法\scantokens
,只是您没有将标记转换为字符串,因此 catcodes 在这里根本不重要。
该方法的优点大致就是该\scantokens
方法的缺点:
- catcodes 根本不重要;
\splitaux
只要您确切知道它<parameter text>
是什么,您就可以使用此方法修补复杂的宏(包括之前的宏);- 宏的
<parameter text>
可以包含任何你想要的标记(只要你知道它是什么标记);并且 - 此方法不需要健全性检查来确保宏可以正确修补。
缺点是该方法要起作用有一定的要求:你需要确切地知道是什么<parameter text>
。