使用宏评估一组 xkeyval 参数作为另一个命令的参数

使用宏评估一组 xkeyval 参数作为另一个命令的参数

我正在尝试编写一个宏,将宏中包含的 xkeyval 参数添加到默认 xkeyval 参数列表中。

以下最小的例子更好地解释了我想要做的事情。

\documentclass{article}

\usepackage{xkeyval}

\makeatletter

\define@cmdkey{f}[c@]{a}[a]{}
\define@cmdkey{f}[c@]{b}[b]{}
\define@cmdkey{f}[c@]{c}[c]{}
\newcommand{\dispKeys}[2][toMakeItMoreComplex]{
  \setkeys{f}{a,b,c,#2}
  c@a = \c@a{} \\
  c@b = \c@b{} \\
  c@c = \c@c{} \\
}

\makeatother

\newcommand{\prefilled}[2][toMakeItMoreComplex]{
 \dispKeys[#1]{a=prefilled, #2}
}

\begin{document}

\dispKeys{a=test}

\def\dummy{dummy}
\prefilled[\dummy]{}

\prefilled[\dummy]{b=other}

\def\otherArgs{c}
\prefilled[\dummy]{b=other, \otherArgs}

\def\otherArgs{c=another}
\prefilled[\dummy]{b=other, \otherArgs}

\end{document}

应该生成输出:

c@a = test
c@b = b
c@c = c
c@a = prefilled
c@b = b
c@c = c
c@a = prefilled
c@b = other
c@c = c
c@a = prefilled
c@b = other
c@c = c
c@a = prefilled
c@b = other
c@c = another

然而,最后一次调用\prefilled[\dummy]{b=other, \otherArgs}会产生错误,因为它将整个内容解释\otherArgs为键名。

我尝试\expandafter在不同的地方使用(我真的不知道如何使用)但\prefilled[\dummy]{b=other, \otherArgs}没有成功。{b=other, \otherArgs}不作为参数,我得到:

c@a = prefilled
c@b = b
c@c = c
b=other, c=another

我该如何修改调用\prefilled[\dummy]{b=other, \otherArgs}或定义\prefilled才能使其工作?实际情况比这个最小示例更复杂(它涉及到包括图形和长度),但希望我能够适应一个潜在的解决方案。

答案1

你必须展开隐藏在看到该宏\otherArgs之前的选项列表\setkeys:记住\setkeys 没有进行扩展。

\newcommand{\prefilled}[2][toMakeItMoreComplex]{%
 \begingroup\edef\x{\endgroup\noexpand\dispKeys[#1]{a=prefilled,#2}}\x
}

正如 Martin 所说,这会带来一些风险,因为扩展已经完成,所以赋予键的值不应包含无法在扩展中生存的内容(典型示例是\textbf和 等)。或者,使用\protected@edef而不是\edef

\begingroup确保\endgroup所赋予的含义在扩展\x时会立即被遗忘。\x

答案2

如果\otherArgs实际上是由某些内部代码添加的,而不是由用户直接添加的,则可以先将其展开,然后使用辅助宏将展开的代码添加到正确的位置,如下图所示\withotherargs\@withotherargs如果是\otherArgs来自用户,则可以提供第二个参数来传递这样的宏。

\documentclass{article}

\usepackage{xkeyval}

\makeatletter

\define@cmdkey{f}[c@]{a}[a]{}
\define@cmdkey{f}[c@]{b}[b]{}
\define@cmdkey{f}[c@]{c}[c]{}
\newcommand{\dispKeys}[2][toMakeItMoreComplex]{%
  \setkeys{f}{a,b,c,#2}%
  c@a = \c@a{} \\
  c@b = \c@b{} \\
  c@c = \c@c{} \\
}

\makeatother

\newcommand{\prefilled}[2][toMakeItMoreComplex]{%
 \dispKeys[#1]{a=prefilled, #2}%
}

\makeatletter
\newcommand\withotherargs[2][]{%
    \expandafter\@withotherargs\expandafter{\otherArgs}{#1}{#2}%
}
\def\@withotherargs#1#2#3{%
    \prefilled[#2]{#3,#1}%
}
\makeatother

\begin{document}

\dispKeys{a=test}

\def\dummy{dummy}
\prefilled[\dummy]{}

\prefilled[\dummy]{b=other}

\def\otherArgs{c}
\prefilled[\dummy]{b=other, \otherArgs}

\def\otherArgs{c=another}
%\prefilled[\dummy]{b=other, \otherArgs}
\withotherargs[\dummy]{b=other}

\end{document}

答案3

事实证明,一个更通用、更强大的解决方案比我想象的更便宜。我选择规范化键值集,因为如果不先规范化列表,预扩展每个键名的方案并不总是有效。这是因为可能\expandafter只是作用于空格。规范化还处理活动逗号和等号。键名的顶级扩展是安全的,因为活动字符不太可能引导任何键名。

您可以使用或expandkeyname中的选项全局启用“扩展键名”。此外,如果有人担心全局分配,可以使用命令和本地启用和禁用。您还可以使用命令和类似命令一次或两次输入、移除或删除名称应扩展的键。\documentclass\usepackage\ExpandKeynameInSetkeys\NoExpandKeynameInSetkeysexpandkeyname\expandnamekeys\setkeys

提取文件内容后keyvalx.sty,将其移动到 TeX 存储库中的正确位置。

另外:我经常听到有人说键值包的一个缺点是它不能在之前加载\documentclass。以下键值软件包补丁键值以便键值可以提前加载\documentclass。它推迟了键值喜欢与...一起做\@classoptionslist直到\documentclass被看到/调用。

\begin{filecontents}{keyvalx.sty}
\NeedsTeXFormat{LaTeX2e}[2011/06/27]
\ProvidesPackage{keyvalx}[2012/05/13 v0.0.1 Patches for xkeyval package (AM)]
\RequirePackage{catoptions}
\ifdefTF\XKeyValLoaded{}{\input xkeyval }
\edef\XKV@restorecatcodes{%
  \catcode`\noexpand\@\the\catcode`\@\relax
  \catcode`\noexpand\=\the\catcode`\=\relax
  \catcode`\noexpand\,\the\catcode`\,\relax
  \catcode`\noexpand\:\the\catcode`\:\relax
  \let\noexpand\XKV@restorecatcodes\relax
}
\catcode`\@11\relax
\catcode`\=12\relax
\catcode`\,12\relax
\catcode`\:12\relax
\let\XKV@doxs\relax
\def\XKV@warn#1{\PackageWarning{xkeyval}{#1}}
\def\XKV@err#1{\PackageError{xkeyval}{#1}\@ehd}
\cptnewvariables{if}[XKV@]{expandkeyname}[false]
\cptnewvariables{if}[XKV]{forbreak}[false]
\new@def*\XKVifcond#1\fi{%
  #1\relax\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
}
\newletcs\XKVforbreak\XKVforbreaktrue
% \XKVfor[<parser>]<list>\do{<1-parameter-callback>}
% Star (*) implies <listcmd> in place of <list>
\robust@def*\XKVfor{\cpt@teststopt\XKV@for,}
\robust@def*\XKV@for[#1]#2\do#3{%
  \global\advance\XKV@depth by1
  \csn@def{XKV@fordo@\romannumeral\XKV@depth}##1#1{%
    \XKV@for@a#1\XKVfor{XKV@fordo@\romannumeral\XKV@depth}{##1}{#3}%
  }%
  \XKVforbreakfalse
  \begingroup % for \XKV@tempa
  \cpt@stchoose{cpt@st}{#2}\XKV@tempa\XKVfor
  \cpt@sttrue\cpt@csvnormalize[#1]\XKV@tempa
  \expandafter\endgroup
  \csname XKV@fordo@\romannumeral\XKV@depth\expandafter\endcsname
  \XKV@tempa#1\XKVfor#1%
  \global\advance\XKV@depth by-1
}
% \XKVtfor<list>\do{<callback>}
\robust@def*\XKVtfor{\cpt@testst\XKV@tfor}
\robust@def*\XKV@tfor#1\do#2{%
  \global\advance\XKV@depth by1
  \csn@def{XKV@tfordo@\romannumeral\XKV@depth}##1{%
    \XKV@for@a{}\XKVtfor{XKV@tfordo@\romannumeral\XKV@depth}{##1}{#2}%
  }%
  \XKVforbreakfalse
  \begingroup
  \cpt@stchoose{cpt@st}{#1}\XKV@tempa\XKVtfor
  \expandafter\endgroup
  \csname XKV@tfordo@\romannumeral\XKV@depth\expandafter\endcsname
  \XKV@tempa\XKVtfor
  \global\advance\XKV@depth by-1
}
\robust@def*\XKV@for@a#1#2#3#4#5{%
  \csname i\ifx#2#4\else i\fi ofii\endcsname{%
    % In case it was set true by the last item:
    \XKVforbreakfalse
  }{%
    \csname i\ifXKVforbreak\else i\fi ofii\endcsname{%
      \XKVforbreakfalse
      \begingroup
      \defpass\reserved@a##1#2#1{\endgroup
        \edef\XKV@remainder{\unexpanded{##1}}%
      }%
    }{%
      #5\relax\csname#3\endcsname
    }%
  }%
}
% \XKVforwhile[<parser>]<list><holder-cmd><if-cond>\fi{<callback>}
% Star (*) -> <listcmd> in place of <list>
\robust@def*\XKVforwhile{\cpt@teststopt\XKV@forwhile,}
\robust@def\XKV@forwhile[#1]#2#3#4\fi#5{%
  \XKVifcond#4\fi{%
    \edef#3{\ifboolTF{cpt@st}\expandcsonce\unexpanded{#2}}%
    \XKVfor*[#1]#3\do{%
      \edef#3{\unexpanded{##1}}%
      \XKVifcond#4\fi{#5}\XKVforbreak
    }%
  }{}%
}
\robust@redef*\XKV@whilist{\XKVforwhile*}
\robust@def*\XKV@normalizelist#1#2{%
  \ifcsnullTF#2{}{%
    \usename{\ifcase#1csv\else kv\fi @@normalize}*#2%
  }%
}
\robust@redef\XKV@sp@deflist#1#2{%
  \edef#1{\unexpanded{#2}}%
  \XKV@normalizelist{0}#1%
}
\robust@redef*\XKV@checksanitizea#1#2{%
  \XKV@ch@cksanitize{#1}#2=%
  \edef#2{\unexpanded{#1}}%
  \ifboolTF{in@}{}{\XKV@ch@cksanitize{#1}#2,}%
  \ifboolTF{in@}{\XKV@normalizelist{1}#2}{}%
}
\robust@redef*\XKV@checksanitizeb#1#2{%
  \XKV@ch@cksanitize{#1}#2,%
  \edef#2{\unexpanded{#1}}%
  \ifboolTF{in@}{\XKV@normalizelist{0}#2}%
}
% Save/add to 'expand name keys':
\robust@def*\expandnamekeys{\XKV@stfalse\XKV@testoptb\XKV@expandnamekeys}
\robust@def*\gexpandnamekeys{\XKV@sttrue\XKV@testoptb\XKV@expandnamekeys}
\robust@def*\XKV@expandnamekeys#1{%
  \ifdefFT{XKV@\XKV@header expandname}{%
    \XKV@checksanitizeb{#1}\XKV@tempa
    \ifXKV@st\expandafter\global\fi
    \csn@edef{XKV@\XKV@header expandname}{\expandcsonce\XKV@tempa}%
  }{%
    \aftercsname\XKV@merge{XKV@\XKV@header expandname}{#1}\XKV@getsg
  }%
}
% Remove from 'expand name keys' list:
\robust@def*\remexpandnamekeys{\XKV@stfalse\XKV@testoptb\XKV@remexpandnamekeys}
\robust@def*\gremexpandnamekeys{\XKV@sttrue\XKV@testoptb\XKV@remexpandnamekeys}
\robust@def*\XKV@remexpandnamekeys#1{%
  \ifdefFT{XKV@\XKV@header expandname}{%
    \XKV@err{No 'expandname keys' defined for family `\XKV@header'}%
  }{%
    \aftercsname\XKV@delete{XKV@\XKV@header expandname}{#1}\XKV@getsg
  }%
}
% Delete entire 'expand name keys' list:
\robust@def*\delexpandnamekeys{\XKV@stfalse\XKV@testoptb\XKV@delexpandnamekeys}
\robust@def*\gdelexpandnamekeys{\XKV@sttrue\XKV@testoptb\XKV@delexpandnamekeys}
\robust@def*\XKV@delexpandnamekeys{%
  \ifdefFT{XKV@\XKV@header expandname}{%
    \XKV@err{No 'expandname keys' defined for family `\XKV@header'}%
  }{%
    \ifXKV@st\expandafter\global\fi
    \undefcsn{XKV@\XKV@header expandname}%
  }%
}
\robust@def*\XKV@checkexpandname#1{%
  \ifboolTF{XKV@expandkeyname}{%
    \expandafter
  }{%
    \XKV@ch@ckexpandname{#1}%
  }%
}
\robust@def*\XKV@ch@ckexpandname#1{%
  \begingroup
  \def\XKV@exponce{01}%
  \def\XKV@exptwice{01}%
  \def\XKV@tempc##1{%
    \ifcsndefFT{XKV@\XKV@prefix ##1@expandname}{}{%
      \expandcsnonce{XKV@\XKV@prefix ##1@expandname}%
    }%
  }%
  \XKVfor*\XKV@fams\do{%
    \expandafter\XKV@g@tkeyname#1=\@nil\XKV@tempa
    \xifinsetTF{,\expandcsonce\XKV@tempa,}{,\XKV@tempc{##1},}{%
      \def\XKV@exponce{00}%
      \XKVforbreak
    }{%
      \expandafter\expandafter\expandafter
      \XKV@g@tkeyname#1=\@nil\XKV@tempa
      \xifinsetTF{,\expandcstwice\XKV@tempa,}{,\XKV@tempc{##1},}{%
        \def\XKV@exptwice{00}%
        \XKVforbreak
      }{}%
    }%
  }%
  \ifswitchTF{XKV@exponce}{%
    \endgroup\expandafter
  }{%
    \ifswitchTF{XKV@exptwice}{%
      \endgroup\expandafter\expandafter\expandafter
    }{%
      \endgroup
    }%
  }%
}
\robust@redef\XKV@setkeys[#1]#2{%
  \edef\XKV@userkeys{\unexpanded{#2}}%
  \XKV@normalizelist{1}\XKV@userkeys
  \let\XKV@naa\@empty
  \XKVfor*\XKV@userkeys\do{%
    \XKV@checkexpandname{##1}%
    \XKV@g@tkeyname##1=\@nil\XKV@tempa
    \XKV@addtolist@x\XKV@naa\XKV@tempa
  }%
  \ifnum\XKV@depth=\z@\let\XKV@rm\@empty\fi
  \XKV@usepresetkeys{#1}{preseth}%
  \expandafter\XKV@s@tkeys\expandafter{\XKV@userkeys}{#1}%
  \XKV@usepresetkeys{#1}{presett}%
  \let\CurrentOption\@empty
}
\robust@redef\XKV@s@tkeys#1#2{%
  \edef\XKV@na{\unexpanded{#2}}%
  \XKV@normalizelist{0}\XKV@na
  \XKVfor#1\do{%
    \edef\CurrentOption{\unexpanded{##1}}%
    \XKV@checkexpandname{##1}%
    \XKV@s@tk@ys##1==\@nil
  }%
}

\XKVforwhile*\@filelist\XKV@tempa\ifx\XKV@documentclass\@undefined\fi{%
  \filename@parse\XKV@tempa
  \ifxTF\filename@ext\@clsextension{%
    \ifcsndefFT{opt@\filename@area\filename@base.\filename@ext}{}{%
      \edef\XKV@documentclass{\filename@area\filename@base.\filename@ext}%
    }%
  }{}%
}
\robust@def*\XKV@filterclassoptions{%
  \XKV@sgfalse
  \ifx\@classoptionslist\@undefined\else
    \ifx\@classoptionslist\relax\else\XKV@sgtrue\fi
  \fi
  \ifXKV@sg
    \let\XKV@filterclassoptions\relax
    \let\XKV@classoptionslist\@classoptionslist
    \def\@classoptionslist{}%
    \XKVfor*\XKV@classoptionslist\do{%
      \ifinsetTF={##1}{}{%
        \edef\@classoptionslist{%
          \@classoptionslist\ifx\@classoptionslist\@empty\else,\fi
          \unexpanded{##1}%
        }%
      }%
    }%
  \fi
}
\ifdefTF\XKV@documentclass{%
  \XKV@filterclassoptions
}{%
  \let\XKV@documentclass\@empty
  \let\XKV@classoptionslist\@empty
  \edef\@popfilename{\expandcsonce\@popfilename\XKV@filterclassoptions}%
}
\robust@redef*\XKV@testopte#1{%
  \XKV@ifstar{\XKV@sttrue\XKV@t@stopte#1}{\XKV@stfalse\XKV@t@stopte#1}%
}
\robust@redef*\XKV@t@stopte#1{\@testopt{\XKV@t@st@pte#1}{KV}}
\robust@redef*\XKV@t@st@pte#1[#2]{%
  \XKV@makepf{#2}%
  \@ifnextchar<{\XKV@@t@st@pte#1}%
    {\XKV@@t@st@pte#1<\@currname.\@currext>}%
}
\robust@redef*\XKV@@t@st@pte#1<#2>{%
  \XKV@sp@deflist\XKV@fams{#2}%
  \@testopt#1{}%
}
\robust@redef*\DeclareOptionX{%
  \let\@fileswith@pti@ns\@badrequireerror
  \XKV@ifstar\XKV@dox\XKV@d@x
}
\robust@redef\XKV@dox#1{\XKV@toks{#1}\edef\XKV@doxs{\the\XKV@toks}}
\robust@redef*\XKV@d@x{\@testopt\XKV@@d@x{KV}}
\robust@redef*\XKV@@d@x[#1]{%
  \@ifnextchar<{\XKV@@@d@x[#1]}{\XKV@@@d@x[#1]<\@currname.\@currext>}%
}
\robust@redef*\XKV@@@d@x[#1]<#2>#3{\@testopt{\define@key[#1]{#2}{#3}}{}}
\robust@redef*\ExecuteOptionsX{\XKV@stfalse\XKV@plfalse\XKV@t@stopte\XKV@setkeys}
\robust@redef*\ProcessOptionsX{\XKV@plfalse\XKV@testopte\XKV@pox}
\robust@redef*\XKV@pox[#1]{%
  \XKV@normalizelist{1}\XKV@classoptionslist
  \let\XKV@tempa\@empty
  \XKV@inpoxtrue
  \let\@fileswith@pti@ns\@badrequireerror
  \edef\XKV@testclass{\@currname.\@currext}%
  \ifxTF\XKV@testclass\XKV@documentclass{%
    \let\@unusedoptionlist\XKV@classoptionslist
    \ifdefFT{[email protected]}{}{%
      \@onelevel@sanitize\@unusedoptionlist
    }%
  }{%
    \ifboolTF{XKV@st}{%
      \ifcsnullTF\XKV@classoptionslist{}{%
        \XKVfor*\XKV@classoptionslist\do{%
          % When 'xkvltxp' or 'catoptions' is loaded, options aren't expanded:
          \XKV@checkexpandname{##1}%
          \XKV@g@tkeyname##1=\@nil\CurrentOption
          \XKV@key@if@ndefined{\CurrentOption}{}{%
            \expandafter\XKV@useoption\expandafter{\CurrentOption}%
            \XKV@addtolist@n\XKV@tempa{##1}%
          }%
        }%
      }%
    }{}%
  }%
  \aftercsname{\XKV@addtolist@o\XKV@tempa}{opt@\@currname.\@currext}%
  \cptexpandargonce{\XKV@setkeys[#1]}\XKV@tempa
  \let\XKV@doxs\relax\let\XKV@rm\@empty
  \XKV@inpoxfalse
  \let\@fileswith@pti@ns\@@fileswith@pti@ns
  \AtEndOfPackage{\let\@unprocessedoptions\relax}%
}
\robust@redef*\XKV@useoption#1{%
  \def\XKV@resa{#1}%
  \ifdefFT{[email protected]}{}{%
    \@onelevel@sanitize\XKV@resa
  }%
  \@expandtwoargs\@removeelement{\XKV@resa}%
    {\@unusedoptionlist}\@unusedoptionlist
}
\define@boolkey[KV]{keyvalx}[XKV@]{expandkeyname}[true]{}
\robust@def*\ExpandKeynameInSetkeys{%
  \setkeys[KV]{keyvalx}{expandkeyname=true}%
}
\robust@def*\NoExpandKeynameInSetkeys{%
  \setkeys[KV]{keyvalx}{expandkeyname=false}%
}
\DeclareOptionX*{%
  \PackageWarning{keyvalx}{Unknown option '\CurrentOption' ignored}%
}
\ExecuteOptionsX[KV]<keyvalx>{expandkeyname=false}
\ProcessOptionsX*[KV]<keyvalx>\relax
\XKV@restorecatcodes
\endinput 
\end{filecontents}

% Your example document:
\documentclass%[expandkeyname]
  {article}
\usepackage%[expandkeyname]
  {keyvalx}
\makeatletter
\define@cmdkey{gus}[c@]{a}[a]{}
\define@cmdkey{gus}[c@]{b}[b]{}
\define@cmdkey{gus}[c@]{c}[c]{}
\newcommand{\dispKeys}[2][toMakeItMoreComplex]{%
  \setkeys{gus}{a,b,c,#2}%
  c@a = \c@a{} \\
  c@b = \c@b{} \\
  c@c = \c@c{} \\
}
\makeatother

\newcommand{\prefilled}[2][toMakeItMoreComplex]{%
  \dispKeys[#1]{a=prefilled,#2}%
}

\begin{document}
\setkeys{gus}{a,b}

\def\dummy{dummy}
\prefilled[\dummy]{}
\prefilled[\dummy]{b=other}

\def\otherArgs{c}
%\ExpandKeynameInSetkeys
% Enter key 'c' as 'expand name key':
\expandnamekeys{gus}{c}
\prefilled[\dummy]{b=other,\otherArgs}

\def\otherArgs{c=another}
\prefilled[\dummy]{b=other, \otherArgs}
%\NoExpandKeynameInSetkeys
\end{document}

相关内容