2

I am writing a package and I would ideally like to save long stretches of code using the key-value mechanism of xkeyval. That is, I want to be able to say something like

\setkeys{foo}{mytext={This is some long text, 
                      including multiple paragraphs.

                      Here is the second paragraph.}}

and have the mytext key do some processing or manipulation. I have no problems when the value includes a \par token, and I have no problems when it includes a comma, but when it includes both I get an error message. Here's a minimal (non-)working example:

\documentclass{minimal}
\usepackage{xkeyval}

\makeatletter
\define@key{ABC}{mykey}{\long\def\mymacro{#1}}

\begin{document}

%\setkeys{ABC}{mykey={DE,F}}     % This line works.
%\setkeys{ABC}{mykey={D\par EF}} % This line works.
\setkeys{ABC}{mykey={D\par E,F}} % This line doesn't work.

\mymacro

\end{document}

It produces the error message

Runaway argument?
{mykey={D
! Paragraph ended before \@s@l@ctive@sanitize was complete.
<to be read again> 
                   \par 
l.11 \setkeys{ABC}{mykey={D\par E,F}}

?

I'm guessing that when xkeyval sees a comma in a key's value, it invokes sanitizing macros which aren't defined as long; a quick glance at xkvutils.tex seems to confirm my suspicion.

Is there a way for an xkeyval key-value pair to include both a \par and a comma?

2 Answers 2

2

Since xkeyval doesn't expand the keyvalues, it's possible to store the 'offending' part with D\par E,F to a macro, say, \myarg. This will work then.

\documentclass{minimal}
\usepackage{xkeyval}

\makeatletter
\define@key{ABC}{mykey}{\long\def\mymacro{#1}}

\newcommand{\myarg}{D\par E,F}
\begin{document}

%\setkeys{ABC}{mykey={DE,F}}     % This line works.
%\setkeys{ABC}{mykey={D\par EF}} % This line works.
%\setkeys{ABC}{mykey={{D\protect\par E},F}} % This line doesn't work.

\setkeys{ABC}{mykey={\myarg}}
\mymacro



\end{document}
3
  • That works in a way, I suppose, but it's not quite satisfying. I was hoping to keep my package code clean by being able to take user input from a macro I'm writing and pass it to xkeyval; for example, \long\def\usermacro#1#2#3{\setkeys{ABC}{mykey={#1},myotherkey={#2}}\dosomethingelse{#3}}. If I have to add the "wrapper" \myarg as you suggest, I might as well simply write \long\def\usermacro#1#2#3{\long\def\mymacro{#1}\setkeys{ABC}{myotherkey={#2}}\dosomethingelse{#3}}. It works just fine, but since I'm using xkeyval for other arguments, I was hoping to keep a parallel structure. Commented Sep 1, 2015 at 19:09
  • @AndersHendrickson: There might be other (and better) solutions, but your question isn't really precise.
    – user31729
    Commented Sep 1, 2015 at 20:22
  • That's fair. I guess what I'm really hoping for is a patch for xkeyval that would let it accept both the \par and the comma without any intermediate steps. I think I've got one worked up, and I'll try to put it below. Commented Sep 11, 2015 at 20:52
0

Christian Hupfer's comment above helped me clarify for myself that I really wanted to just make xkeyval work the way I wanted, accepting both a \par and a comma, without any extra intervention required on the part of the end user or a package author. After spending a week trying to understand the code in xkvutils.tex in idle minutes, I came up with the following code to redefine a few macros of xkeyval:

\documentclass{minimal}
\usepackage{xkeyval}
\makeatletter

\long\def\@s@lective@sanitize[#1]#2#3{%
  \begingroup
    \count@#1\relax\advance\count@\@ne
    \XKV@toks\expandafter{#3}%
    \def#3{#2}\@onelevel@sanitize#3%
    \edef#3{{#3}{\the\XKV@toks}}%
    \expandafter\@s@l@ctive@sanitize\expandafter#3#3%
    \expandafter\XKV@tempa@toks\expandafter{#3}%
  \expandafter\endgroup\expandafter\toks@\expandafter{\the\XKV@tempa@toks}%
  \edef#3{\the\toks@}%
}
\long\def\@s@l@ctive@sanitize#1#2#3{%
  \def\@i{\futurelet\@@tok\@ii}%
  \def\@ii{%
    \ifx\@@tok\@s@l@ctive@sanitize
      \let\@@cmd\@gobble
    \else
      \expandafter\@iii\meaning\@@tok\relax
      \ifx\@@tok\@sptoken
        \XKV@toks\expandafter{#1}\edef#1{\the\XKV@toks\space}%
        \def\@@cmd{\afterassignment\@i\let\@@tok= }%
      \else
        \let\@@cmd\@iv
      \fi
    \fi
    \@@cmd
  }%
  \def\@iii##1##2\relax{\if##1\@backslashchar\let\@@tok\relax\fi}%
  \long\def\@iv##1{%
    \toks@\expandafter{#1}\XKV@toks{##1}%
    \ifx\@@tok\bgroup
      \advance\count@\m@ne
      \ifnum\count@>\z@
        \begingroup
          \def#1{\expandafter\@s@l@ctive@sanitize
            \csname\string#1\endcsname{#2}}%
          \expandafter#1\expandafter{\the\XKV@toks}%
          \XKV@toks\expandafter\expandafter\expandafter
            {\csname\string#1\endcsname}%
          \edef#1{\noexpand\XKV@toks{\the\XKV@toks}}%
        \expandafter\endgroup#1%
      \fi
      \edef#1{\the\toks@{\the\XKV@toks}}%
      \advance\count@\@ne
      \let\@@cmd\@i
    \else
      \edef#1{\expandafter\string\the\XKV@toks}%
      \expandafter\in@\expandafter{#1}{#2}%
      \edef#1{\the\toks@\ifin@#1\else
              \ifx\@@tok\@sptoken\space\else\the\XKV@toks\fi\fi}%
      \edef\@@cmd{\noexpand\@i\ifx\@@tok\@sptoken\the\XKV@toks\fi}%
    \fi
    \@@cmd
  }%
  \let#1\@empty\@i#3\@s@l@ctive@sanitize
}

\define@key{ABC}{mykey}{\long\def\mymacro{#1}}

\begin{document}

%\setkeys{ABC}{mykey={DE,F}}     % This line works.
%\setkeys{ABC}{mykey={D\par EF}} % This line works.
\setkeys{ABC}{mykey={D\par E,F}} % This line now works.
\mymacro

%It can still handle conditionals.
\setkeys{ABC}{mykey={\ifnum1<2\relax True \else False\fi}}
{\tt\meaning\mymacro}

\end{document}

The only change to \@s@lective@sanitize is making it a \long macro. The interior macro \@s@l@ctive@sanitize required a little more work. In case anyone's interested in the details, here is the original code for comparison:

\def\@s@l@ctive@sanitize#1#2#3{%
  \def\@i{\futurelet\@@tok\@ii}%
  \def\@ii{%
    \expandafter\@iii\meaning\@@tok\relax
    \ifx\@@tok\@s@l@ctive@sanitize
      \let\@@cmd\@gobble
    \else
      \ifx\@@tok\@sptoken
        \XKV@toks\expandafter{#1}\edef#1{\the\XKV@toks\space}%
        \def\@@cmd{\afterassignment\@i\let\@@tok= }%
      \else
        \let\@@cmd\@iv
      \fi
    \fi
    \@@cmd
  }%
  \def\@iii##1##2\relax{\if##1\@backslashchar\let\@@tok\relax\fi}%
  \def\@iv##1{%
    \toks@\expandafter{#1}\XKV@toks{##1}%
    \ifx\@@tok\bgroup
      \tracingmacros=1\relax
      \advance\count@\m@ne
      \ifnum\count@>\z@
        \begingroup
          \def#1{\expandafter\@s@l@ctive@sanitize
            \csname\string#1\endcsname{#2}}%
          \expandafter#1\expandafter{\the\XKV@toks}%
          \XKV@toks\expandafter\expandafter\expandafter
            {\csname\string#1\endcsname}%
          \edef#1{\noexpand\XKV@toks{\the\XKV@toks}}%
        \expandafter\endgroup#1%
      \fi
      \edef#1{\the\toks@{\the\XKV@toks}}%
      \advance\count@\@ne
      \let\@@cmd\@i
    \else
      \edef#1{\expandafter\string\the\XKV@toks}%
      \expandafter\in@\expandafter{#1}{#2}%
      \edef#1{\the\toks@\ifin@#1\else
              \ifx\@@tok\@sptoken\space\else\the\XKV@toks\fi\fi}%
      \edef\@@cmd{\noexpand\@i\ifx\@@tok\@sptoken\the\XKV@toks\fi}%
    \fi
    \@@cmd
  }%
  \let#1\@empty\@i#3\@s@l@ctive@sanitize
}

Of course \@s@l@ctive@sanitize needed to be made \long itself, but it defines another macro \@iv which also reads tokens to be sanitized, so \@iv also needed to be made \long.

These changes made sense, but now there was a new problem: the recursion never stopped. The macro uses itself, \@s@l@ctive@sanitize, as the delimiter to let it know when to stop. First \@i uses \futurelet to save the next token in \@@tok, before \@ii takes over. Then \@ii has \@iii check whether that next token is a TeX primitive, using the mechanism of asking whether its \meaning begins with a backslash; if so, it sets \@@tok equal to \relax. (The goal here seems to be to keep conditionals like \if, \else, and \fi from causing trouble by being expanded inside \@iv.) When \@iii is done, \@ii takes over again, checking whether (a) \@@tok is the \@s@l@ctive@sanitize at the end of the tokens to be sanitized, in which case we stop the recursion by simply \@gobble-ing that \@s@l@ctive@sanitize token; (b) \@@tok is a space token, which requires special handling; or (c) \@@tok is anything else, in which case it gets added to the sanitized list by \@iv.

So the new problem is that when we made \@s@l@ctive@sanitize long, its \meaning has changed from

macro: #1#2#3->\def \@i {\futurelet ...

to

\long macro: #1#2#3->\def \@i {\futurelet ...

Now when we reach the end of the sequence of tokens to be sanitized, when \@@tok has been \let equal to \@s@l@ctive@sanitize, \@iii tests the meaning of \@@tok and sees the backslash of \long, believes it has found a TeX primitive to be guarded against, and sets \@@tok equal to \relax. Thus when \@ii runs \ifx\@@tok\@s@l@ctive@sanitize, the conditional evaluates as false, and the recursion never ends.

I fixed this in my patch above by having \@ii test for the end-of-recursion marker first, before having \@iii check for the backslash.

I was little worried about whether this would break anything \@iii was supposed to guard against, but so far my new code seems to work as long as the conditionals in the key are balanced, which was needed for xkeyval before the patch anyway. I'll test it a little more, but if anyone sees a problem with my patch or a better solution, please let me know!

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .