3

I am trying to write a macro that works in math-mode like this: \mymacro^3 str produces the same result as \pi^3 str, whereas \mymacro{str1}{str2}{str3} produces the same result as \pi str1 \pi str2 \pi str3. In other words, if the character that immediately follows is ^, it only prints π; otherwise it absorbs an argument (a string), printing π and the argument, and keeps absorbing arguments until the next character is not {.

Here is my code:

\def\mmymacro#1{
  \pi #1
  \@ifnextchar\bgroup{\mmymacro}{\relax}
}
\def\mymacro{
  \@ifnextchar^{\pi}\mmymacro
}

It works as long as it is in display-mode followed by other characters or in inline-mode, but it produces a warning when used as

$$
\mymacro
$$

The warning is

! Display math should end with $$.<to be read again> $
! Missing $ inserted.<inserted text>$

I have already added \relax in the definition, but it does not fix the problem. So I tried another way:

\def\mmymacro#1{
  \pi #1
  \@ifnextchar\bgroup{\mmymacro}{}
}
\def\mymacro{
  \@ifnextchar^{\pi}{\mmymacro}\relax
}

This no longer produces warnings, but when used like \mymacro{x}{y}, i.e. no leading ^, it produces the same result as \pi \pi x \pi y, no matter in display-mode or in inline-mode. I saw Werner's answer, but following his clue I find it seem to have an extra { inserted. Any explanation please? How can fix it?

  • The error isn't related to ifnextchar, you need to test before absorbing the argument $$\mymacro $$ is $$\mymacro{$}$ so $$\pi$ $ with the space coming as you have no % at the ends of lines in the definition. Of course $$ is not supported syntax in latex anyway, so you could not worry if that is broken.... – David Carlisle Dec 07 '16 at 13:51
  • As for your last question, in \@ifnextchar^{\pi}{\mmymacro}\relax there's no extra brace inserted, the problem just is that the \relax becomes the token checked against ^ which is always false and so \mmymacro will always be called. – siracusa Dec 07 '16 at 15:33

2 Answers2

4

Really this breaks all the latex syntax guidelines (no standard latex command ever takes a variable number of {} arguments) but

enter image description here

\documentclass{article}
\makeatletter
\def\mmmymacro#1{%%%
\pi#1\mymacro
}

\def\mmymacro{%%%
  \@ifnextchar\bgroup{\mmmymacro}{\pi}%%%
}
\def\mymacro{%%%
  \@ifnextchar^{\pi}\mmymacro
}

\begin{document}

$\mymacro aaa$

$\mymacro^3 aaa$

$\mymacro{111}{222}{333} aaa$


$$
\mymacro
$$

\end{document}
David Carlisle
  • 757,742
1

The following approach works with \mymacro neither performing any assignment (→ \@ifnextchar is not used) nor doing any branching in terms of \if....\else..\fi-constructs.
No extensions like eTeX are needed for \mymacro.
(But the \testsuite-macro used for testing/exhibiting the behavior of \mymacro uses eTeX' \scantokens.)

\mymacro itself will in any case process exactly one undelimited macro argument.

If the content of that argument does have a leading opening brace or does have leading space tokens trailed by an opening brace, then it is assumed that the argument contains a list of undelimited arguments, each argument encapsulated in braces. The token \pi will be placed in front of each of these arguments. Space tokens between brace-encapsulated arguments will be ignored.

If the content of that argument neither does have a leading opening brace nor does have leading spaces trailed by an opening brace, then the token \pi will be placed in front of that argument.

\documentclass{article}
\makeatletter
%%=========================================================================
%% Paraphernalia:
%%    \UD@firstoftwo, \UD@secondoftwo, \UD@Exchange, \UD@removespace
%%    \UD@CheckWhetherNull, \UD@CheckWhetherBrace, \UD@Loopcall
%%=========================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@Exchange[2]{#2#1}%
\newcommand\UD@removespace{}\UD@firstoftwo{\def\UD@removespace}{} {}%
%%----------------------------------------------------------------------
%% Check whether argument is empty:
%%......................................................................
%% \UD@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>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
%%
%% (\romannumeral expansion was introduced in order to overcome the
%% concerns and worries about improperly balanced \if..\else..\fi constructs.)
%%
\newcommand\UD@CheckWhetherNull[1]{%
  \romannumeral0\expandafter\UD@secondoftwo\string{\expandafter
  \UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
  \UD@secondoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@firstoftwo}%
}%
%%----------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%......................................................................
%% \UD@CheckWhetherBrace{<Argument which is to be checked>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked has leading
%%                        catcode-1-token>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked has no leading
%%                        catcode-1-token>}%
\newcommand\UD@CheckWhetherBrace[1]{%
  \romannumeral0\expandafter\UD@secondoftwo\expandafter{\expandafter{%
  \string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
  \UD@firstoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingSpace{<Argument which is to be checked>}%
%%                             {<Tokens to be delivered in case <argument
%%                               which is to be checked>'s 1st token is a
%%                               space-token>}%
%%                             {<Tokens to be delivered in case <argument
%%                               which is to be checked>'s 1st token is not
%%                               a space-token>}%
\newcommand\UD@CheckWhetherLeadingSpace[1]{%
  \romannumeral0\UD@CheckWhetherNull{#1}%
  {\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
  {\expandafter\UD@secondoftwo\string{\UD@CheckWhetherLeadingSpaceB.#1 }{}}%
}%
\newcommand\UD@CheckWhetherLeadingSpaceB{}%
\long\def\UD@CheckWhetherLeadingSpaceB#1 {%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@secondoftwo#1{}}%
  {\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
  {\UD@Exchange{ }{\expandafter\expandafter\expandafter\expandafter
   \expandafter\expandafter\expandafter}\expandafter\expandafter
   \expandafter}\expandafter\UD@secondoftwo\expandafter{\string}%
}%
%%-------------------------------------------------------------------------
%% Expandable Loop:
%% \UD@Loopcall{<action>}%
%%          {<action if list empty>}%
%%          {<preset>}%
%%          {{<e_k>}{<e_(k+1)>}..{e_n}}% <- this is the list
%%
%% If list is empty: <action if list empty>
%% Else:
%% <action>{<e_k>}<preset> \UD@Loopcall{<action>}%
%%                                  {<action if list empty>}%
%%                                  {<preset>}{{<e_(k+1)>}..{e_n}}
%%
%% <action> can be defined to mesh into the iteration-process, e.g.,
%% (ex)changing arguments like the <action if list empty>-argument
%% for the next \UD@Loopcall-iteration, e.g., terminating iteration
%% prematurely under some circumstances.
%%
%% Space tokens between elements will be ignored.
%%.........................................................................
\newcommand\UD@RemoveTillUD@nil{}%
\long\def\UD@RemoveTillUD@nil#1#2\UD@nil{{#1}}%
\newcommand\UD@Extractfirstloop[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
  {\UD@Exchange{#1}}%
  {\expandafter\UD@Extractfirstloop\expandafter{\UD@RemoveTillUD@nil#1}}%
}%
\newcommand\UD@Loopcall[4]{%
  \UD@CheckWhetherNull{#4}{#2}{%
    \UD@CheckWhetherLeadingSpace{#4}{%
      \expandafter\UD@Exchange
      \expandafter{\expandafter{\UD@removespace#4}}%
      {\UD@Loopcall{#1}{#2}{#3}}%
    }{%
      \expandafter\UD@Exchange
      \expandafter{\expandafter{\UD@firstoftwo{}#4}}%
      {\UD@Extractfirstloop{#4\UD@nil}{#1}#3\UD@Loopcall{#1}{#2}{#3}}%
    }%
  }%
}%
%%=========================================================================
%% Macros for the user, defined by means of the paraphernalia:
%%
\newcommand\mymacro[1]{\mmymacro{#1}{#1}}%
\newcommand\mmymacro[2]{%
  \UD@CheckWhetherLeadingSpace{#1}{%
    \expandafter\mmymacro\expandafter{\UD@removespace#1}{#2}%
  }{%
    \UD@CheckWhetherBrace{#1}{%
      \UD@Loopcall{\expandafter\pi\UD@secondoftwo{}}{}{}{#1}%
    }{\pi#2}%
  }%
}%
\makeatother

%%=========================================================================
%% Macros for the testsuite that is used for testing the behavior of the 
%% macros for the user:
%%

\begingroup
\catcode`\X=13\relax
\gdef\Activex{%
  \catcode`\X=13\relax
  \defX{\,}%
}%
\endgroup

\newcommand\testsuite[1]{%
  \begingroup
  \def\pi{\string\piX}%
  \let\olddospecials\dospecials
  \def\dospecials{\olddospecials\Activex}%
  \edef\test{$\string\mymacro{#1}$}%
  \scantokens\expandafter\expandafter\expandafter{%
                      \expandafter\string
                      \expandafter\verb
                      \expandafter*%
                      \expandafter|\test|}yields:\\%
  \edef\test{$\mymacro{#1}$}%
  \scantokens\expandafter\expandafter\expandafter{%
             \expandafter\string
             \expandafter\verb
             \expandafter*%
             \expandafter|\test|}yields:\\%
  \endgroup
  $\mymacro{#1}$
  \medskip  
}%

\parindent=0ex %

\begin{document}

\testsuite{^3 str}

\testsuite{str}

\testsuite{ str}

\testsuite{{str1}{str2}{str3}}

\testsuite{{^2 str1}{^5 str2}{^{11} str3}}

\testsuite{ {^2 str1} {^5 str2} {^{11} str3} }

\testsuite{}

\hrulefill\null

\verb*|$\mymacro{^3 str}$|: 
$\mymacro{^3 str}$

\verb*|$\mymacro{str}$|: 
$\mymacro{str}$

\verb*|$\mymacro{ str}$|: 
$\mymacro{ str}$

\verb*|$\mymacro{{str1}{str2}{str3}}$|: 
$\mymacro{{str1}{str2}{str3}}$

\verb*|$\mymacro{{^2 str1}{^5 str2}{^{11} str3}}$|: 
$\mymacro{{^2 str1}{^5 str2}{^{11} str3}}$

\verb*|$\mymacro{   {^2 str1}  {^5 str2}  {^{11} str3}  }$|: 
$\mymacro{   {^2 str1}  {^5 str2}  {^{11} str3}  }$

\verb*|$\mymacro{}$|: 
$\mymacro{}$

\end{document}

Screenshot of pdf file produced by compiling the example above.

Ulrich Diez
  • 28,770