7

What does TeX do, when reading an argument?

I wanted to write a command that has a comma separated list of arguments (\fun[a@b,c@d,e@f,g,]). Each argument should be exploded in two parts at the separator @.

The following example shows the current code without extra packages (mypackage.sty):

\def\@explode#1@#2@#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}
\def\explode#1{\expandafter\@explode#1@@\@nil}

\newcommand{\fun}[1][a@b,c@d,e@f,g,]{
    \@for\elem:=#1\do{\explode{\elem}1:\@pOne-2:\@pTwo, }
}

Now calling the command with and without an argument.

 \fun
 \fun[a@b,c@d,e@f,g,]

The output is:

1:a-2:b, 1:c-2:d, 1:e-2:f, 1:g-2:, 1:-2:,
1:a@b-2:, 1:c@d-2:, 1:e@f-2:, 1:g-2:, 1:-2:,

In the first case the output is as expected, but in the second case the explosion does not work.

What is the difference? What das TeX do? How can I debug/solve this?

4
  • The @ character is a special LaTeX character. Within style files, it is interpreted as an alphabetical character. Within documents, it is interpreted as a symbol. If you place \makeatletter in your document (not recommended except for debugging and emulating style-file syntax), the second case works properly. Commented May 16, 2016 at 11:46
  • 1
    You are in a \makeatletter aware place and you just defined \@explode to separate the arguments in letter-@ and when using in the document you are using other-@.
    – Manuel
    Commented May 16, 2016 at 11:47
  • See tex.stackexchange.com/questions/8351/… Commented May 16, 2016 at 11:48
  • Thank all for the fast answers. To make thinks simple I will change the separator.
    – T. Jaschke
    Commented May 16, 2016 at 12:57

6 Answers 6

5

You can use another separator different than @ that doesn't change catcodes, or use

\begingroup\lccode`;=`@ \lowercase{\endgroup
  \def\@explode#1;#2;#3\@nil}{\edef\@pOne{#1}\edef\@pTwo{#2}}

instead of

\def\@explode#1@#2@#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}

but note that you won't be able to use it in a \makeatletter part of the code.


And the full game

\begingroup\lccode`;=`@ \lowercase{\endgroup
  \def\@explodeother#1;#2;#3\@nil}{\edef\@pOne{#1}\edef\@pTwo{#2}}
\def\@explodeletter#1@#2@#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}

\def\explode{\ifnum\catcode`@=11 \expandafter\explodeletter\else\expandafter\explodeother\fi}
\begingroup\lccode`;=`@ \lowercase{\endgroup
  \def\explodeother#1{\expandafter\@explodeother#1;;\@nil}}
\def\explodeletter#1{\expandafter\@explodeletter#1@@\@nil}

\newcommand{\fun}[1][a@b,c@d,e@f,g,]{%
  \@for\elem:=#1\do{\explode{\elem}1:\@pOne-2:\@pTwo, }%
}

This last one checks the catcode of @ and uses the letter-@ division or other-@ division accordingly.

4

It is a category code problem of @. The definition used category code "letter", whereas the category code of @ is usually "other" in the main document, example:

\documentclass{article}

% \FunLetter using @ with category code "letter"

\makeatletter
\def\@explode@letter#1@#2@#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}
\newcommand*{\ExplodeLetter}[1]{\expandafter\@explode@letter#1@@\@nil}

\newcommand{\FunLetter}[1][a@b,c@d,e@f,g,]{%
    \@for\elem:=#1\do{\ExplodeLetter{\elem}1:\@pOne-2:\@pTwo, }%
}
\makeatother

% \FunOther using @ with category code "other"

\makeatletter
\begingroup
  \lccode`9=`@
\lowercase{%
  \endgroup
  \def\@explode@other#19#29#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}
  \newcommand*{\ExplodeOther}[1]{\expandafter\@explode@other#199\@nil}

  \newcommand{\FunOther}[1][a9b,c9d,e9f,g,]{%
      \@for\elem:=#1\do{\ExplodeOther{\elem}1:\@pOne-2:\@pTwo, }%
  }
}
\makeatother

\begin{document}
\setlength{\parindent}{0pt}

\verb|\makeatother|\makeatother\\
\begin{tabular}{@{}l@{ }l@{}}
\verb|\FunLetter|:& \FunLetter\\
\verb|\FunLetter[a@b,c@d,e@f,g,]|:& \FunLetter[a@b,c@d,e@f,g,]\\
\verb|\FunOther|:& \FunOther\\
\verb|\FunOther[a@b,c@d,e@f,g,]|:& \FunOther[a@b,c@d,e@f,g,]
\end{tabular}

\medskip
\verb|\makeatletter|\makeatletter\\
\begin{tabular}{@{}l@{ }l@{}}
\verb|\FunLetter|:& \FunLetter\\
\verb|\FunLetter[a@b,c@d,e@f,g,]|:& \FunLetter[a@b,c@d,e@f,g,]\\
\verb|\FunOther|:& \FunOther\\
\verb|\FunOther[a@b,c@d,e@f,g,]|:& \FunOther[a@b,c@d,e@f,g,]
\end{tabular}
\end{document}

Result

Workarounds/solutions:

  • Checking for both @ tokens with category code "letter" and "other".
  • Using a different separator character. For example, there are lots of key value parser, if the equals sign = is uses as separating character, e.g. package kvsetkeys.
3

As already explained, the problem is that @ has category code 11 at definition time, but category code 12 at usage time.

Here's an implementation with xparse; the \SplitArgument processor pushes -NoValue- when the argument hasn't the indicated number of tokens to split at, so it's necessary to use \IfValueT in order to print the second part when existing.

\documentclass{article}
\usepackage{xparse}

\NewDocumentCommand{\fun}{>{\SplitList{,}}O{a@b,c@d,e@f,g,}}{%
  \ProcessList{#1}{\explode}%
}

\NewDocumentCommand{\explode}{>{\SplitArgument{1}{@}}m}{%
  \doexplosion#1, %
}

\NewDocumentCommand{\doexplosion}{mm}{%
  1:#1-\IfValueT{#2}{#2}% No @ pushes -NoValue-
}

\begin{document}

\fun

\fun[a@b,c@d,e@f,g,]

\end{document}

enter image description here

2

I suppose @ is of catcode 11 (letter) due to \makeatletter at the time of defining \@explode, \explode and \fun. Thus within argument-delimiters at definition-time and within the default-value of the optional argument at definition-time, @ was tokenized as acharacter-token of catcode 11 (letter). Therefore those of your macros processing delimited arguments expect @ to be a character-token of catcode 11 (letter).

Probably you call \fun[a@b,c@d,e@f,g,] at a time, when the catcode of @ is not 11 (letter) any more but 12 (other) due to \makeatother. If this is the case, then @ within the optional argument gets tokenized as a character-token of catcode 12 (other) which is not suitable for matching an argument-delimiter that is expected to be a character-token of catcode 11 (letter).

2

This is a suggestion on implementing checking both for letter@ and other-@.

\documentclass{minimal}
\makeatletter
%%----------------------------------------------------------------------
%% Check whether argument is empty:
%%......................................................................
%% \CheckWhetherNull{<Argument which is to be checked>}%
%%                  {<Tokens to be delivered in case that
%%                    argument which is to be checked is empty>}%
%%                  {<Tokens to be delivered in case that
%%                    argument which is to be checked is not empty>}%
\newcommand\@CheckWhetherNull[1]{%
  \expandafter\@secondoftwo\string{\expandafter\@secondoftwo
  \expandafter{\expandafter{\string#1}\expandafter\@secondoftwo
  \string}\expandafter\@firstoftwo\expandafter{\expandafter
  \@firstoftwo\expandafter\@secondoftwo\expandafter}\string
  }\@firstoftwo
}%
%%----------------------------------------------------------------------
%% Check whether arg contains no @ of catcode 11 (letter) on top brace
%% level
%%......................................................................
%% \CheckWhetherNoLetterAt{<Argument which is to be checked>}%
%%               {<Tokens to be delivered in case that argument which is
%%                to be checked does not contain @ of catcode 11>}%
%%               {<Tokens to be delivered in case that argument which is
%%                to be checked does contain @ of catcode 11>}%
\newcommand\@RemoveToLetterAt{}%
\long\def\@RemoveToLetterAt#1@{}%
\newcommand\@CheckWhetherNoLetterAt[1]{%
  \expandafter\@CheckWhetherNull
  \expandafter{\@RemoveToLetterAt#1@}%
}%
%%----------------------------------------------------------------------
%% \fun, \exolode etc:
%%......................................................................
\newcommand{\fun}[1][a@b,c@d,e@f,g,]{%%%
    \@for\elem:=#1\do{\explode{\elem}1:\@pOne-2:\@pTwo, }%%%
}%%
\begingroup
\lccode`\!=`\@% Catcode of ! is 12 (other). 
              % Lowercasing ! now yields @ of catcode 12 (other).
\lowercase{%
  \endgroup
  \newcommand\@AtLetterExplode{}%
  \def\@AtLetterExplode#1@#2@#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}%
  \newcommand\@AtOtherExplode{}%
  \def\@AtOtherExplode#1!#2!#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}%
  \newcommand\explode[1]{%
    \expandafter\@CheckWhetherNoLetterAt\expandafter{#1}%
    {\expandafter\@AtOtherExplode#1!!}%
    {\expandafter\@AtLetterExplode#1@@}%
    \@nil
  }%
}%

\makeatother


\begin{document}

\fun

\makeatletter

\fun[a@b,c@d,e@f,g,]

\makeatother

\fun[a@b,c@d,e@f,g,]

\end{document}
1

The optional argument of \fun is to hold a comma-separated list of elements that are to be "exploded".

With your code, there is always trailing empty element. That trailing element is also processed by the \@for-loop.

Therefore the output is always

1:a-2:b, 1:c-2:d, 1:e-2:f, 1:g-2:, 1:-2:,

ìnstead of:

1:a-2:b, 1:c-2:d, 1:e-2:f, 1:g-2:,

Below is a suggestion where both blank elements and empty elements within the optional argument's list are cranked out.

Ulrich

\documentclass{minimal}
\makeatletter
%%----------------------------------------------------------------------
%% Check whether argument is empty:
%%......................................................................
%% \CheckWhetherNull{<Argument which is to be checked>}%
%%                  {<Tokens to be delivered in case that
%%                    argument which is to be checked is empty>}%
%%                  {<Tokens to be delivered in case that
%%                    argument which is to be checked is not empty>}%
\newcommand\@CheckWhetherNull[1]{%
  \expandafter\@secondoftwo\string{\expandafter\@secondoftwo
  \expandafter{\expandafter{\string#1}\expandafter\@secondoftwo
  \string}\expandafter\@firstoftwo\expandafter{\expandafter
  \@firstoftwo\expandafter\@secondoftwo\expandafter}\string
  }\@firstoftwo
}%
%%----------------------------------------------------------------------
%% Check whether arg contains no @ of catcode 11 (letter) on top brace
%% level
%%......................................................................
%% \CheckWhetherNoLetterAt{<Argument which is to be checked>}%
%%               {<Tokens to be delivered in case that argument which is
%%                to be checked does not contain @ of catcode 11>}%
%%               {<Tokens to be delivered in case that argument which is
%%                to be checked does contain @ of catcode 11>}%
\newcommand\@RemoveToLetterAt{}%
\long\def\@RemoveToLetterAt#1@{}%
\newcommand\@CheckWhetherNoLetterAt[1]{%
  \expandafter\@CheckWhetherNull
  \expandafter{\@RemoveToLetterAt#1@}%
}%
%%----------------------------------------------------------------------
%% \fun, \explode etc:
%%......................................................................
\newcommand{\fun}[1][a@b,c@d,e@f,g,]{%%%
    \@for\elem:=#1\do{%
      \expandafter\expandafter\expandafter\@CheckWhetherNull
      \expandafter\expandafter\expandafter{%
      \expandafter\@firstoftwo\elem{}.}{}{%
        \explode{\elem}1:\@pOne-2:\@pTwo, %
      }%
    }%
}
\begingroup
\lccode`\!=`\@% Catcode of ! is 12 (other).
              % Lowercasing ! now yields @ of catcode 12 (other).
\lowercase{%
  \endgroup
  \newcommand\@AtLetterExplode{}%
  \def\@AtLetterExplode#1@#2@#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}%
  \newcommand\@AtOtherExplode{}%
  \def\@AtOtherExplode#1!#2!#3\@nil{\edef\@pOne{#1}\edef\@pTwo{#2}}%
  \newcommand\explode[1]{%
    \expandafter\@CheckWhetherNoLetterAt\expandafter{#1}%
    {\expandafter\@AtOtherExplode#1!!}%
    {\expandafter\@AtLetterExplode#1@@}%
    \@nil
  }%
}%

\makeatother


\begin{document}

\fun

\makeatletter

\fun[a@b,, ,c@d,e@f,g, ]

\fun[a@b,, ,c@d,e@f,g,]

\fun[a@b,c@d,e@f,g, ]

\fun[a@b,c@d,e@f,g]

\makeatother

\fun[a@b,, ,c@d,e@f,g, ]

\fun[a@b,, ,c@d,e@f,g,]    

\fun[a@b,c@d,e@f,g, ]

\fun[a@b,c@d,e@f,g]

\end{document}

You must log in to answer this question.

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