15

I am trying to define a macro with variable number of arguments. For example I want to define a macro with two arguments that can work with one argument as well. So the same macro works in two ways.

\textcolor{red}{sample text}

This would limit the red color to the sample text and whereas just calling \textcolor{red} would turn on the red color until the end of the document. So the second way of using the macro would be

\textcolor{red}

I was trying to implement the macro using \@ifnextchar. I am not having much success. Could you please give me some hints?

Based on @Joseph's suggestion below I modified my toy code and it works. I still have problem understanding the actual execution flow in the code. Here is my code.

\documentclass[12pt]{article}
\makeatletter
\def\oneortwoargs#1{%
\@ifnextchar\bgroup%
   {\twoargs@aux{#1}}
   {\onearg@only{#1}}
}

\def\onearg@only#1{%
   Only argument provided: #1 %
}

\def\twoargs@aux#1#2{%
   Two arguments are provided. \\%
   First argument: #1 \\ %
   Second argument: #2%
}
\makeatother

\begin{document}
\oneortwoargs{gg}{fff}
\oneortwoargs{qq}
\end{document}

As @Joseph explained below one token is left in the input stream. What is confusing me is that \onearg@only and \twoargs@aux are invoked the same way with only one argument.

egreg
  • 1,121,712
mahes25
  • 345
  • 3
    Since you have some responses below that seem to answer your question, please mark one of them as ‘Accepted’ by clicking on the tickmark below their vote count. This moves the question off the ‘unanswered’ list and assigns them (and you!) reputation points. – Will Robertson Feb 07 '11 at 07:23

2 Answers2

23

I'd use the xparse package to do this, as everything is then 'pre-packaged':

\documentclass{article}
\usepackage{color,xparse}
\NewDocumentCommand\MyTextColor{m+g}{%
  \IfNoValueTF{#2}
    {\color{#1}}
    {\textcolor{#1}{#2}}%
}
\begin{document}
\MyTextColor{green}{stuff}

\MyTextColor{red} Some text
\end{document} 

The same can of course be done using \@ifnextchar

\documentclass{article}
\usepackage{color}
\makeatletter
\newcommand*\MyTextColor[1]{%
  \@ifnextchar\bgroup
    {\textcolor{#1}}
    {\color{#1}}%
}
\makeatother
\begin{document}
\MyTextColor{green}{stuff}

\MyTextColor{red} Some text
\end{document} 

The key point is that you need to look for { using \bgroup rather than trying to use it directly (as it is the begin-group character).


To explain how the \@ifnextchar part deals with the need for either 1 or 2 arguments, what happens in my second approach is that

\newcommand*\MyTextColor[1]{%

defines a macro which absorbs one argument. So:

\MyTextColor{red}{stuff}

absorbs red as #1 and leaves {stuff} in the input stream, while

\MyTextColor{red} other stuff

also absorbs red as #1 and leaves other stuff in the input stream. We now apply \@ifnextchar, which 'peeks' at the next token without absorbing it. The test reads

  \@ifnextchar\bgroup
    {\textcolor{#1}}
    {\color{#1}}%

and so depending on the result, either \textcolor{red} or \color{red} is inserted into the input stream. Thus we end up with either

\textcolor{red}{stuff}

or

\color{red} other stuff

which means that \textcolor will get the required two arguments, while \color only gets one. The key is that {red} is reinserted into the input after either \textcolor or \color.


Although it is not directly related to the question, a few notes on the xparse approach may be useful. When using the xparse package, arguments are described by letter, with m representing a mandatory argument and g representing an optional argument in braces (i.e. an optional TeX group). A + before the letter allows that argument to accept paragraph tokens, so in my definition the first argument cannot (it's supposed to be the name of a colour), while the second one can (it is arbitrary text).

The g argument returns a special token (\NoValue) if the optional argument was not present at all. If a default value was wanted, I'd have used G{<default>}. The same approach applies to standard LaTeX optional arguments, which are represented as o or O, depending on whether a default value is required. I have tested for \NoValue with the test \IfNoValueTF; \IfNoValueT and \IfNoValueT are also available.

James
  • 103
  • 4
Joseph Wright
  • 259,911
  • 34
  • 706
  • 1,036
  • Thank you very much for the quick response. I should have used the \bgroup! – mahes25 Feb 06 '11 at 17:07
  • 1
    @mahes25. Like many things, it is obvious once you know :-) – Joseph Wright Feb 06 '11 at 17:09
  • 1
    Note, however, that \MyTextColor{blue} \bgroup ... is likely to lead to unexpected behaviour in the second solution. Also, remember that \@ifnextchar gobbles spaces, so if you want a left brace after a single argument invocation of \MyTextColor then you'd better insert a \relax in between. This sort of requirement is likely to confuse users of your macro, so you'd better keep it to yourself. 8-) – Harald Hanche-Olsen Feb 06 '11 at 17:16
  • @Harald: Very true, and worth remembering. In my first solution, xparse does not skip spaces, and so \MyTextColor{blue} \bgroup is fine. – Joseph Wright Feb 06 '11 at 17:19
  • @Joseph: In your second solution, \textcolor has one argument. However, \textcolor needs two arguments. How does the second argument get passed to textcolor? – mahes25 Feb 06 '11 at 17:31
  • 1
    @mahes25: I've only absorbed one argument in \MyTextColor, so if \@ifnextchar finds a second one it is still in the input. – Joseph Wright Feb 06 '11 at 17:35
  • 3
    @Joseph: Great answer. Could you please expand a bit on the m+g? – Hendrik Vogt Feb 07 '11 at 13:15
  • @Hendrik: done (I hope). – Joseph Wright Feb 07 '11 at 13:49
  • @Joseph: Great, thanks. m was clear to me, but both + and g were completely new for me. And what you explain is directly related to your first answer :-) – Hendrik Vogt Feb 07 '11 at 14:05
8

In ConTeXt, you can define such macros using \dodoublegroupempty. For example:

\def\TwoArgMacro
  {\dodoublegroupempty\doTwoArgMacro}

\def\doTwoArgMacro#1#2%
  {\ifsecondargument
      Two arguments were passed: #1 and #2
    \else
      One argument was passed: #1
    \fi}

This works by checking if the next character is equal to \bgroup.

Aditya
  • 62,301