15

I have a situation where I want to define a command that takes a variable number of arguments, where the number of arguments is known programmatically via a \count, and process the parameters in some way (say as if they are a list).

As an example, say I'd like to output the parameters as a comma-separated list.

\newcommand{\makecsv}[N]{#1, #2, ..., #N}

The code that I've come up with to do this kind of operation (in a generic-ish way) essentially takes a command, \csv and expands it recursively N times. \csv needs to know how to continue the recursion, and has some state that I'd like to thread through the recursion (rather than using \global).

\documentclass{report}
\usepackage{etoolbox}

\makeatletter
\newcommand{\ifzero}[3]{%
  % #1: count
  % #2: state for #3
  % #3: macro to expand to
  % - should take at least 2 parameters
  % - ##1: count threaded through
  % - ##2: macro state threaded through
  \ifnum\c > 0
    \def\tmp@f##1##2##3{##1{##2}{##3}}%
    \advance#1 -1%
  \else
    \def\tmp@f##1##2##3{)}% note closeparen here (could be param)
  \fi
  \tmp@f{#3}{#1}{#2}%
}
\makeatother

\newcommand{\csv}[3]{
  % #1: count
  % #2: separator state
  % #3: string to concat
  % 
  #2#3\ifzero{#1}{, }{\csv}%
}

\newcommand{\makecsv}[1]{%
  \ifzero{#1}{}{\csv}%
}

\makeatletter
\newcommand{\decl}[3]{%
  % #1: decl id
  % #2: decl symbol
  % #3: # params
  \csgdef{decl@#1}{#2}%
  \global\expandafter\newcount\csname decl@#1@nparams\endcsname%
  \csuse{decl@#1@nparams} #3\relax%
}

\newcommand{\usedecl}[1]{%
  \newcount\c
  \c \the\csuse{decl@#1@nparams}
  \csuse{decl@#1}(\makecsv{\c}%
}
\makeatother

% declare some interface routines
\decl{foo}{FOO}{3}
\decl{bar}{BAR}{4}

\begin{document}
\usedecl{foo}{p1}{p2}{p3}\par
\usedecl{bar}{p1}{p2}{p3}{p4}\par
\end{document}

Is this a reasonable thing to do in 2e, or is there some sort of standard approach to this that is normally used?

Edit 1

It seems like my original MWE wasn't adequate to describe why someone might want this. I've updated the MWE with a use case. \decl allows authors to declaratively define a C-style function, and \usedecl allows the author to generate a use of the function, with its parameters bound to specific arguments.

This is similar enough to what I'm doing that it should help motivate the example.

  • You are asking for a list. I think you can search TeX.SX to find a lot of examples. Related packages are etoolbox or l3clist. – Marco Daniel Jun 7 '13 at 16:42
  • @Marco, I don't think that I'm asking for a list directly. I'm more interested in the technique for writing a command that can process a variable number of arguments. I can't control the source directly, so I can't change {foo}{bar}{foobar} to {foo|bar|foobar}. I do see how I could solve this problem somewhat generically with a list though, with a generic \makelistfromargs{\listname}{\counter} that reads a bunch of arguments into an internal list. Is this the more common solution that you see people doing? – Luke Jun 7 '13 at 16:54
  • Perhaps it's easier for us if you provide an aim. – Marco Daniel Jun 7 '13 at 17:39
  • I can't understand the role of the counter here; what if your counter is set to 2 and you find \makecsv{\c}{abc}{def}{ghi}? – egreg Jun 7 '13 at 18:06
  • Is there a reason you're not considering using keys? – A.Ellett Jun 7 '13 at 18:31
8

As commented, here a solution that uses \@ifnextchar. I also implemented checks against too many or too few arguments (or why are they provided by the user?).

The \@ifnextchar(or its “very internal” big brother \kernel@ifnextchar) skips spaces which results in removed spaces in the third and fourth example.

Code

\documentclass{report}
\usepackage{etoolbox}
\makeatletter
\newcommand*{\decl}[3]{%
  % #1: decl id
  % #2: decl symbol
  % #3: # params
  \csdef{decl@symbol@#1}{#2}%
  \expandafter\newcount\csname c@decl@params@#1\endcsname
  \csuse{c@decl@params@#1}=#3\relax
}
\newcount\decl@params@check
\newcommand*{\usedecl}[1]{%
  \def\decl@name{#1}%
  \edef\decl@params{\the\csuse{c@decl@params@#1}}%
  \def\decl@symbol{\csuse{decl@symbol@#1}}%
  \decl@params@check=\z@
  \let\decl@list\@gobble % the \@gobble removes the first , (expandable)
  \def\decl@next{\kernel@ifnextchar\bgroup\use@decl\use@decl@finish}%
  \decl@next
}
\newcommand*{\use@decl}[1]{%
  \advance\decl@params@check\@ne
  \expandafter\ifnum\the\decl@params@check>\decl@params\relax % too many!
    \PackageWarning{decl}{You have used more params than the \decl@name\space function expected!
                                                    I ignore this (and any following) param, ok?}% but insert the extra argument anyway?!
     \def\decl@next{\use@decl@finish{#1}}% the extra pair of braces {} keeps '#1' local as it is in the input stream
  \else
    \expandafter\def\expandafter\decl@list\expandafter{\decl@list\decl@list@sep#1}%
  \fi
  \decl@next
}
\newif\ifuse@decl@message
\newcommand*{\use@decl@finish}{%
  \ifnum\decl@params@check<\decl@params\relax % too few!
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi
  {%
    \ifuse@decl@message\else
      \PackageWarning{decl}{You have used fewer params than the \decl@name\space function expected! I'm filling up with '??'!}%
      \use@decl@messagetrue
    \fi
    \use@decl{??}}
  {%
    \decl@symbol\decl@list@start\decl@list\decl@list@end
    \use@decl@messagefalse
  }%
}
\newcommand*{\setdeclstart}[1]{\def\decl@list@start{#1}}
\newcommand*{\setdeclend}[1]{\def\decl@list@end{#1}}
\newcommand*{\setdeclsep}[1]{\def\decl@list@sep{#1}}
\makeatother

\setdeclstart{(}
\setdeclend{)}
\setdeclsep{, }

% declare some interface routines
\decl{foo}{FOO}{3}
\decl{bar}{BAR}{4}

\begin{document}
given $P$, $Q$ and $R$ such \dots\ that $\usedecl{foo}{P}{Q}{R}$ results in \dots\par
given P, Q and R such \dots\ that \usedecl{foo}{P}{Q}{R}\ results in \dots\par
\usedecl{foo}{p1}{p2}{p3}\par
\usedecl{bar}{p1}{p2}{p3}{p4}\par
\usedecl{bar}{p1} foo\par
\usedecl{foo}{p1}{p2}{p3} {p4}\par
\end{document}

Output

enter image description here

9

If you're willing to peek ahead, you can check whether there's "another argument" and keep gobbling them on the fly:

enter image description here

\documentclass{article}
\usepackage{etoolbox}% http://ctan.org/pkg/etoolbox

\makeatletter
\newcommand{\newdecl}[2]{\csgdef{decl@#1}{#2}}% Creates a declaration
\newcommand{\csvdel}{}% Delimiter used in CSV representation
\newcommand{\newusedecl}[2][,]{% Use a declaration
  \renewcommand{\csvdel}{\renewcommand{\csvdel}{#1\,}}% Delay \csvdel one cycle.
  \csname decl@#2\endcsname(\checknextarg}
\newcommand{\checknextarg}{\@ifnextchar\bgroup{\gobblenext}{}}% Check if another "argument" exists
\newcommand{\gobblenext}[1]{\csvdel#1\@ifnextchar\bgroup{\gobblenext}{)}}% Gobble next "argument"
\makeatother

% declare some interface routines
\newdecl{foo}{FOO}
\newdecl{bar}{BAR}

\begin{document}

\newusedecl{foo}{p1}{p2}{p3}\par
\newusedecl{bar}{p1}{p2}{p3}{p4}{p1}{p2}{p3}{p4}{p1}{p2}{p3}{p4}{p1}{p2}{p3}{p4}
  {p1}{p2}{p3}{p4}\par
\newusedecl[;]{foo}{p1}{p2}{p3}{p4}{p1}{p2}{p3}{p4}{p1}{p2}{p3}{p4}{p1}{p2}{p3}{p4}
  {p1}{p2}{p3}{p4}

\end{document}

The peeking is done using \@ifnextchar. For some explanation around this, see Understanding \@ifnextchar. The delayed use of \csvdel (the CSV delimiter) stems from Cunning (La)TeX tricks).

The optional argument to \newusedecl adapts \csvdel.

  • Ah, that's nice. So the #1 in \gobblenext is the next parameter contents, which I can process as necessary. I think that this is a nice alternative to the way that I'm doing it, but I'm not sure that it's more maintainable than what I've got. I'm going to accept it as an answer and just conclude that this isn't done often enough for there to be a "commonly accepted" pattern. – Luke Jun 7 '13 at 19:42
  • @Luke: You can adapt the solution to your liking. I'm not entirely sure of your use-case. I've added an optional argument to \newusedecl that allows you to update/specify the CSV delimiter. Default is ,. – Werner Jun 7 '13 at 20:02
  • There is an understandable exampke usage of this idea: davidyat.es/2016/07/27/…. I understood Werner's comments much better after reading that. – pauljohn32 Mar 29 '18 at 9:15
3

You've not given us much to go off of, but here's something that seems to implement what you want while being fed a comma separated list (in lieu of passing a variable number of arguments).

\documentclass{article}
\usepackage{xparse}
\newcounter{myargcounter}
\ExplSyntaxOn
\clist_new:N \l_myvararg_parameters_clist
\tl_new:N    \l_myvararg_current_item_tl
\NewDocumentCommand{\makecsv}{ m }
    {
        \clist_set:Nn \l_myvararg_parameters_clist { #1 }
        \int_while_do:nNnn { \clist_count:N \l_myvararg_parameters_clist } > { 1 }
            {
                \clist_pop:NN \l_myvararg_parameters_clist \l_myvararg_current_item_tl
                \tl_use:N \l_myvararg_current_item_tl,
            }   
         \clist_pop:NN \l_myvararg_parameters_clist
            \l_myvararg_current_item_tl
         {} ~ and ~ \tl_use:N \l_myvararg_current_item_tl
    }
\ExplSyntaxOff
\pagestyle{empty}
\begin{document}
\makecsv{a,b,c,d}

\makecsv{a,b,c,d,e,f,g}

\makecsv{a,b}

\end{document}
  • Thanks, but as previously stated, I don't have the flexibility to adjust the use-site of \makecsv, nor do I want to rely on any non-2e stuff. – Luke Jun 7 '13 at 19:09
  • I'm not sure I understand what you mean by use-site. – A.Ellett Jun 7 '13 at 19:26
  • I mean as opposed to definition site. That is, I control the .sty where \makecsv it is defined, but I am stuck with the way that .tex authors are already using it. I can't get them to change from \makecsv{\c}{p1}{p2}{p3} to \makecsv{p1, p2, p3} without a meeting that I don't want to have, since I can actually provide what they want without the API change. – Luke Jun 7 '13 at 19:33
  • This is a valuable answer! Even though OP cannot use it, readers should not overlook. Similar solution tex.stackexchange.com/questions/417064/…. Only caution: require newer LaTeX distribution – pauljohn32 Mar 29 '18 at 9:22
1

Here is an expansion-based implementation where the loop for gathering the parameters is based on \romannumeral both for triggering expansion and for keeping track of the amount of parameters that still is to be collected.

The \usedecl-mechanism does without using any count-registers and without whatsoever temporary assignments for carrying out the loop. e-TeX-extensions and whatsoever fancy packages are not needed. ;-)

Due to \romannumeral-expansion \usedecl delivers the result after two expansion-steps/after being hit by \expandafter twice.

\documentclass{report}

\makeatletter
%%----------------------------------------------------------------------
%% Form control sequence token from sequence of characters denoting its
%% name:
%% \UD@name foo{bar} -> foo\bar
%%   , e.g.,
%% \UD@name\newcommand*{foo} -> \newcommand*\foo
%% \UD@name\string{foo} -> \string\foo
%% \UD@name\UD@name\let{foo}={bar} -> \UD@name\let\foo={bar} -> \let\foo=\bar
%% \UD@name{foo} -> \foo
%%......................................................................
\@ifdefinable\UD@name{%
  \long\def\UD@name#1#{\romannumeral0\UD@innername{#1}}%
}%
\newcommand\UD@innername[2]{%
  \expandafter\UD@exchange\expandafter{\csname#2\endcsname}{ #1}%
}%
\newcommand\UD@exchange[2]{#2#1}%
%%----------------------------------------------------------------------
%% 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>
\newcommand\UD@CheckWhetherNull[1]{%
  \romannumeral0\expandafter\@secondoftwo\string{\expandafter
  \@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \@secondoftwo\string}\expandafter\@firstoftwo\expandafter{\expandafter
  \@secondoftwo\string}\expandafter\expandafter\@firstoftwo{ }{}%
  \@secondoftwo}{\expandafter\expandafter\@firstoftwo{ }{}\@firstoftwo}%
}%
%%----------------------------------------------------------------------
%% Associate IDs with amounts of parameters and symbols:
%% No count-registers get allocated/get wasted. Just macros get defined.
%%......................................................................
\newcommand\decl[3]{%
  % #1: decl id; #2: decl symbol; #3: # params
  \UD@name\newcommand*{decl@#1}{#2}%
  \UD@name\newcommand*{decl@#1@nparams}{#3}%
}%
%%----------------------------------------------------------------------
%% \usedecl{<decl id>}<list of parameters of length \decl@<decl id>@nparams>
%%
%%   creates comma list from parameters, nested in parentheses and
%%   lead by the tokens that come from expanding decl@<decl id>
%%
%% Due to \romannumeral-expansion the result is delivered after two
%% expansion-steps/after "hitting" \usedecl by \expandafter twice.
%%......................................................................
\newcommand\usedecl[1]{%
  \romannumeral0%
  \expandafter\UD@exchange
  \expandafter{%
    \expandafter{%
      \romannumeral0%
      \UD@exchange{ }{\UD@name\expandafter}{decl@#1}%
    }{}{}%
  }{%
    \expandafter\usedeclloop
    \expandafter{%
      \romannumeral\UD@name\number\number{decl@#1@nparams} 000 %
    }%
  }%
}%
\newcommand\usedeclloop[4]{%
  %#1 amount of m = amount of parameters to collect
  %#2 decl symbol
  %#3 prepend-tokens
  %#4 parameters collected so far
  \UD@CheckWhetherNull{#1}{ #2(#4)}{%
    \expandafter\usedeclloopatfetch\expandafter{\@gobble#1}{#2}{, }{#4#3}%
  }%
}%
\newcommand\usedeclloopatfetch[5]{%
  %#1 amount of m = amount of parameters to collect
  %#2 decl symbol
  %#3 prepend tokens
  %#4 parameters collected so far
  %#5 next parameter fetched.
  \usedeclloop{#1}{#2}{#3}{#4#5}%
}%
\newcommand\withoutdecl[2]{%
  \romannumeral0%
  \expandafter\usedeclloop\expandafter{\romannumeral\number\number#2 000 }{#1}%
  {}{}%
}%

\makeatother

% declare the symbols and the amounts of parameters:
\decl{foo}{FOO}{3}
\decl{bar}{BAR}{4}

\begin{document}

\usedecl{foo}{p1}{p2}{p3}\par
\usedecl{bar}{p1}{p2}{p3}{p4}\par
\withoutdecl{BAZ}{5}{p1}{p2}{p3}{p4}{p5}\par

\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\foolist
\expandafter\expandafter\expandafter{%
  \usedecl{foo}{p1}{p2}{p3}%
}%

\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\barlist
\expandafter\expandafter\expandafter{%
  \usedecl{bar}{p1}{p2}{p3}{p4}%
}%

\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\bazlist
\expandafter\expandafter\expandafter{%
  \withoutdecl{BAZ}{5}{p1}{p2}{p3}{p4}{p5}%
}%

\texttt{|\frenchspacing\string\foolist=\meaning\foolist|}

\texttt{|\frenchspacing\string\barlist=\meaning\barlist|}

\texttt{|\frenchspacing\string\bazlist=\meaning\bazlist|}

\end{document}

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.