如何通过 pgfkeys 稳健地测试宏是否尚未设置?

如何通过 pgfkeys 稳健地测试宏是否尚未设置?

我正在尝试定义一个\ifempty带有三个参数的新命令:

  • /.store in通过OR绑定到 pgfkey 的宏/.estore in
  • 如果这个宏还没有设置(为空)该怎么办
  • 如果这个宏已经设置(不为空)该怎么办

我当前的代码如下。这是我的课程的代码:

% CLASS
\NeedsTeXFormat{LaTeX2e}[1994/06/01]
\ProvidesClass{myclass}[2022/10/11]
\LoadClass[varwidth]{standalone}
\makeatletter
\RequirePackage{pgfopts}
\pgfkeys{
    /myclass/.cd,
    optionA/.store in = \myclass@optionA,
    optionA = ,
    optionB/.estore in = \myclass@optionB,
    optionB = ,
}
\ProcessPgfOptions{/myclass}

% If empty macro
\newcommand{\ifempty}[3]{%
    \ifx#1\empty% <= I'm really not sure about this line, even if it seems to work
        #2%
    \else%
        #3%
    \fi%
}

% Using the macro
\newcommand{\checkA}{\ifempty{\myclass@optionA}{A = IS EMPTY}{A = IS NOT EMPTY}}
\newcommand{\checkB}{\ifempty{\myclass@optionB}{B = IS EMPTY}{B = IS NOT EMPTY}}
\makeatother

使用方法如下:

\documentclass{myclass}
\begin{document}
\fbox{\texttt{\checkA}} % A = IS EMPTY
\fbox{\texttt{\checkB}} % B = IS EMPTY
\end{document}

\documentclass[optionA = , optionB = ]{myclass}
\begin{document}
\fbox{\texttt{\checkA}} % A = IS EMPTY
\fbox{\texttt{\checkB}} % B = IS EMPTY
\end{document}

\documentclass[optionA = Hello, optionB = Hey]{myclass}
\begin{document}
\fbox{\texttt{\checkA}} % A = IS NOT EMPTY
\fbox{\texttt{\checkB}} % B = IS NOT EMPTY
\end{document}

问题:看起来可行,但是:

  • 我想知道\ifempty在某些情况下是否会失败,如果会,哪些情况
  • 我想知道这个\ifempty命令是否可以变得更强大

答案1

一种方法是维护仅存储值并初始化为的选项键,\pgfkeysnovalue并使用宏\MYSTUFF@pgfkeysifnovalue来检查是否\pgfkeysvalueof传递了令牌\pgfkeysnovalue
请注意,\pgfkeysvalueof传递了一个\csname..\endcsname-thingie。因此,需要触发三个扩展步骤\pgfkeysvalueof来获取形成值的令牌:第一个扩展步骤传递-thingie \csname..\endcsname。第二个扩展步骤导致\csname其工作并传递相应的控制序列令牌。第三个扩展步骤传递该控制序列令牌的顶层扩展。
三个扩展步骤需要 ((1)*2+1)*2+1 = 7 \expandafter

在下面的例子中,宏为您\MYSTUFF@pgfkeysifnovalue执行了 -orgy 操作\expandafter,以便您获得标记。反过来检查其参数是否包含未嵌套在花括号之间的感叹号。 如果是,则参数不由单个​​标记 组成。 该检查很简单:在前面添加一个感叹号,然后让 TeX 吞噬所有内容,直到第一个感叹号。 如果结果为空,则参数本身不包含未嵌套在花括号之间的感叹号。 否则会包含。 对空性的检查本身是: 的类别为 11(字母),而如果其参数为空,则不提供任何标记,因此在这种情况下,将比较两个类别 11 的类别,这导致测试采用真分支,否则只提供类别 12(其他)和/或 10(空格)的标记,否则将第一个类别 11与不属于类别 11 的东西进行比较,这导致测试采用假分支。 如果该测试得出的结果是,不包含未嵌套在花括号之间的感叹号,则可以安全地应用另一个基于用分隔符分隔的参数的测试来抓取第一次出现的后面的参数。
\MYSTUFF@CheckWhetherpgfnovalue{⟨value associated to key⟩}
\MYSTUFF@CheckWhetherpgfnovalue\pgfkeysnovalue\ifcat A\detokenize{...}AA\detokenizeAA
\MYSTUFF@CheckWhetherpgfnovalue\MYSTUFF@pgfkeysifnovaluefork!\pgfkeysnovalue!!\pgfkeysnovalue!

\documentclass{article}
\usepackage{pgfkeys}

\makeatletter
%----This could as well go into a class file which uses pgfopts---------------------------------
\newcommand\MYSTUFF@pgfkeysifnovalue[1]{%
  \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\MYSTUFF@CheckWhetherpgfnovalue
  \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter{\pgfkeysvalueof{#1}}%
}%
\newcommand\MYSTUFF@CheckWhetherpgfnovalue[1]{%
  \ifcat A\detokenize\expandafter{\MYSTUFF@gobbletoexclam#1!}A%
  \expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
  {\MYSTUFF@pgfkeysifnovaluefork!#1!{\@firstoftwo}!\pgfkeysnovalue!{\@secondoftwo}!!!}%
  {\@secondoftwo}%
}%
\@ifdefinable\MYSTUFF@gobbletoexclam{\long\def\MYSTUFF@gobbletoexclam#1!{}}%
\@ifdefinable\MYSTUFF@pgfkeysifnovaluefork{%
  \long\def\MYSTUFF@pgfkeysifnovaluefork#1!\pgfkeysnovalue!#2#3!!!{#2}%
}%

\pgfkeys{
    /myclass/.cd,
    optionA/.initial=\pgfkeysnovalue,
    % optionA/.default=\pgfkeysnovalue,
    optionA/.value required,
    optionB/.initial=\pgfkeysnovalue,
    optionB/.code=\pgfkeyssetevalue{/myclass/optionB}{#1},
    % optionB/.default=\noexpand\pgfkeysnovalue,
    optionB/.value required,
}
%-----------------------------------------------------------------------------------------------
%\makeatother


% Some dirty scratch-macro which carries out `\pgfkeysvalueof` to obtain the tokens that form the key's value
% and to define a scratch-macro to deliver these tokens and to spit out the meaning of that scratch-macro, with
% \meaning's-prefix removed so that you can see what tokens made the value -- this is dirty and only used in
% this example so that you can see how much expansion took place before the tokens forming the value of the
% key were stored.
% \romannumeral is abused as expansion-trigger: If the number is not positive, \romannumeral doesn't deliver
% any token. \romannumeral keeps expanding tokens while gathering the tokens that form the number to convert
% to lowercase roman notation.
% After the digit "0" you have the tokens  "\firstofone{<\expandafter-chain>}<space>%".
% \firstofone is used so that the space behind its argument's closing curly brace does not get discarded
% during tokenozation but gets tokenized as a <space-token>.
% Expansion of \firstofone makes the token \firstofone and the braces go away while keeping the tokens
% forming the <\expandafter-chain> % so that you have s.th. like
% \romannumeral0<\expandafter-chain><space-token>.
% \romannumeral found the digit 0 and is still seeking more digits, hereby expanding expandable tokens.
% Thus the <\expandafter-chain> is expanded/used for "hopping" over the <space-token> and triggering three
% expansion-steps on \pgfkeysvalueof. Then the \expandafter-chain is gone and \romannumeral finds the
% <space token> and discards it and takes it for a terminator of the digit-sequence to gather and stops
% expanding/gathering more digits. So \romannumeral gathered only the digit 0 which forms a non-positive
% number and therefore does not deliver any token in return for that.

\newcommand\pgfkeysvalueoftokens[1]{%
  \begingroup
  \expandafter\def\expandafter\tempa\expandafter{%
  \romannumeral0\@firstofone{%
    \expandafter\expandafter\expandafter\expandafter\expandafter\expandafter\expandafter
  } %<-this space must be!
  \pgfkeysvalueof{#1}}%
  \texttt{\expandafter\strip@prefix\meaning\tempa}%
  \endgroup
}

\newcommand\BAR{BAR}

\begin{document}

\begingroup

\verb|\pgfkeys{ /myclass/.cd, }|:

\pgfkeys{ /myclass/.cd, }

\MYSTUFF@pgfkeysifnovalue{/myclass/optionA}{The key optionA was not used}%
                                           {The key optionA was used: \pgfkeysvalueoftokens{/myclass/optionA}}

\MYSTUFF@pgfkeysifnovalue{/myclass/optionB}{The key optionB was not used}%
                                           {The key optionB was used: \pgfkeysvalueoftokens{/myclass/optionB}}

\endgroup

\bigskip

%-----------------------------------------------------------------------------------------------

\begingroup

\verb|\pgfkeys{ /myclass/.cd, optionA=\FOO}|:

\pgfkeys{ /myclass/.cd, optionA=\FOO}

\MYSTUFF@pgfkeysifnovalue{/myclass/optionA}{The key optionA was not used}%
                                           {The key optionA was used: \pgfkeysvalueoftokens{/myclass/optionA}}

\MYSTUFF@pgfkeysifnovalue{/myclass/optionB}{The key optionB was not used}%
                                           {The key optionB was used: \pgfkeysvalueoftokens{/myclass/optionB}}

\endgroup

\bigskip

%-----------------------------------------------------------------------------------------------

\begingroup

\verb|\pgfkeys{ /myclass/.cd, optionB=\BAR}|:

\pgfkeys{ /myclass/.cd, optionB=\BAR}

\MYSTUFF@pgfkeysifnovalue{/myclass/optionA}{The key optionA was not used}%
                                           {The key optionA was used: \pgfkeysvalueoftokens{/myclass/optionA}}

\MYSTUFF@pgfkeysifnovalue{/myclass/optionB}{The key optionB was not used}%
                                           {The key optionB was used: \pgfkeysvalueoftokens{/myclass/optionB}}

\endgroup

\bigskip

%-----------------------------------------------------------------------------------------------

\begingroup

\verb|\pgfkeys{ /myclass/.cd, optionA=\FOO, optionB=\BAR}|:

\pgfkeys{ /myclass/.cd, optionA=\FOO, optionB=\BAR}

\MYSTUFF@pgfkeysifnovalue{/myclass/optionA}{The key optionA was not used}%
                                           {The key optionA was used: \pgfkeysvalueoftokens{/myclass/optionA}}

\MYSTUFF@pgfkeysifnovalue{/myclass/optionB}{The key optionB was not used}%
                                           {The key optionB was used: \pgfkeysvalueoftokens{/myclass/optionB}}

\endgroup

\end{document}

在此处输入图片描述



如果你很挑剔:

\pgfkeysnovalue通过为键提供值 ,可以欺骗系统检查选项键的值是否等于默认值,以推断是否提供了相应的键\pgfkeysnovalue。在这种情况下,提供了键,但系统将假定未提供键。因此,另一种方法可以是键,它实际上是执行两个键=值对的样式:一个键=值对用于根据值定义宏。另一个键=值对用于将\@firstoftwo/\@secondoftwo标志(初始化为\@firstoftwo)更改为\@secondoftwo,以便可以使用该标志来分叉是否使用了样式键:

\documentclass{article}
\usepackage{pgfkeys}

\makeatletter
%----This could as well go into a class file which uses pgfopts---------------------------------
\pgfkeys{
    /myclass/.cd,
    optionA/Value/.store in = \myclass@optionA,
    optionA/KeyUsedFlag/.initial = \@firstoftwo,
    optionA/KeyUsedFlag/.default = \@secondoftwo,
    optionA/.style={/myclass/optionA/KeyUsedFlag=\@secondoftwo, /myclass/optionA/Value=#1},
    optionA/.value required,
    % optionA/.default=default for optionA if key is specified without value,
    optionB/Value/.estore in = \myclass@optionB,
    optionB/KeyUsedFlag/.initial = \@firstoftwo,
    optionB/KeyUsedFlag/.default = \@secondoftwo,
    optionB/.style={/myclass/optionB/KeyUsedFlag=\@secondoftwo, /myclass/optionB/Value=#1},
    optionB/.value required,
    % optionB/.default=default for optionB if key is specified without value,
}
%-----------------------------------------------------------------------------------------------
%\makeatother

\newcommand\BAR{BAR}

\begin{document}

\begingroup

\verb|\pgfkeys{ /myclass/.cd, }|:

\pgfkeys{ /myclass/.cd, }

\pgfkeysvalueof{/myclass/optionA/KeyUsedFlag}{The key optionA was not used}%
                                             {The key optionA was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionA}}

\pgfkeysvalueof{/myclass/optionB/KeyUsedFlag}{The key optionB was not used}%
                                             {The key optionB was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionB}}

\endgroup
\bigskip

%-----------------------------------------------------------------------------------------------

\begingroup

\verb|\pgfkeys{ /myclass/.cd, optionA=\FOO}|:

\pgfkeys{ /myclass/.cd, optionA=\FOO}

\pgfkeysvalueof{/myclass/optionA/KeyUsedFlag}{The key optionA was not used}%
                                             {The key optionA was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionA}}

\pgfkeysvalueof{/myclass/optionB/KeyUsedFlag}{The key optionB was not used}%
                                             {The key optionB was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionB}}

\endgroup
\bigskip

%-----------------------------------------------------------------------------------------------

\begingroup

\verb|\pgfkeys{ /myclass/.cd, optionB=\BAR}|:

\pgfkeys{ /myclass/.cd, optionB=\BAR}

\pgfkeysvalueof{/myclass/optionA/KeyUsedFlag}{The key optionA was not used}%
                                             {The key optionA was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionA}}

\pgfkeysvalueof{/myclass/optionB/KeyUsedFlag}{The key optionB was not used}%
                                             {The key optionB was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionB}}

\endgroup
\bigskip

%-----------------------------------------------------------------------------------------------

\begingroup

\verb|\pgfkeys{ /myclass/.cd, optionA=\FOO, optionB=\BAR}|:

\pgfkeys{ /myclass/.cd, optionA=\FOO, optionB=\BAR}

\pgfkeysvalueof{/myclass/optionA/KeyUsedFlag}{The key optionA was not used}%
                                             {The key optionA was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionA}}

\pgfkeysvalueof{/myclass/optionB/KeyUsedFlag}{The key optionB was not used}%
                                             {The key optionB was used: \texttt{\expandafter\strip@prefix\meaning\myclass@optionB}}

\endgroup

\end{document}

在此处输入图片描述

答案2

如果以非预期的方式使用宏,则宏可能会失败。

例如,如果您将多个标记作为第一个参数传入,您的测试将测试完全不同的东西(即,它将测试参数的前两个标记是否匹配)。如果您嵌套 TeX 结构的各部分,它也可能以奇怪的方式失败。第二种情况很容易解决。第一种情况需要进行大量测试,但通常只需更改和\if的顺序即可,测试第一个标记是否有意义。#1\empty\empty

我会按照以下方式编写代码:

\makeatletter
\providecommand\@secondofthree[3]{#2}
\newcommand\ifempty[1] % #2 and #3 are curried
  {%
    \ifx\@empty#1%
      \expandafter\@secondofthree
    \fi
    \@secondoftwo
  }
\makeatother

这样,两个参数<true><false>不会出现在\if-test 内部,而且如果它们不包含平衡的 TeX-if,也不会以奇怪的方式中断。

剩下的唯一不稳定性是,如果你提供多个标记作为第一个参数,并且该参数的第一个标记确实具有\meaning\@empty与相同\empty),这会将其余所有标记保留为参数,但这是用户错误。


另一种方法可能是简单地使用一些非常不可能的值作为初始值,例如ltcmd/xparse使用包含具有不同类别代码的相同字符的标记列表(并打印为-NoValue-)。这必须以非常不可能的方式为任何自然输入创建,因此被认为是足够安全的™。您可以为此执行类似以下操作(请注意,这类似于 UlrichDiez 的解决方案测试,\pgfnovalue但即使有人optionA在没有值的情况下使用也会起作用):

\pgfkeys{
    /myclass/.cd,
    optionA/.store in = \myclass@optionA,
    optionA/.expand twice = \csname c_novalue_tl\endcsname,
    optionB/.estore in = \myclass@optionB,
    optionB = \csname c_novalue_tl \endcsname,
}

\ExplSyntaxOn
\newcommand\ifunused[1]{\ifx\c_novalue_tl#1\exp_after:wN\use_ii:nnn\fi\use_ii:nn}
\ExplSyntaxOff

\ifunused\myclass@optionA{<true>}{<false>}

相关内容