17

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.

Luke
  • 367
  • 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 07 '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 07 '13 at 16:54
  • Perhaps it's easier for us if you provide an aim. – Marco Daniel Jun 07 '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 07 '13 at 18:06
  • Is there a reason you're not considering using keys? – A.Ellett Jun 07 '13 at 18:31
  • @egreg If the counter is not set correctly, then it would be an error. A counter exists because I am going to use this command as part of a higher-level interface where the number of parameters absorbed actually matters. At the same time, the counter also exists because I have no idea how to write a command that can "process arguments to this command until there are no more arguments," i.e., in my working example, I don't know how to distinguish that the \par is not a command argument. – Luke Jun 07 '13 at 18:31
  • @MarcoDaniel I guess I'm trying to emulate a form of a varargs command that takes it's args through latex command parameters. So similar situations expressed as C, it might look something like void makecsv(int N, ...) or int main(int argc, char** argv). It may be that you've answered my question by establishing that this isn't normally done and any custom solution that works is good enough (as opposed to there being an established technique). – Luke Jun 07 '13 at 18:37
  • @A.Ellett Yes. I don't entirely control the form of the use of the macro. Well, I do, but this the form that has been requested. In fact, my solution works fine. I just wanted to make sure that a custom solution was needed, as opposed to a common solution. – Luke Jun 07 '13 at 18:40
  • @Qrrbrbirlbel I'm not sure what you mean. \decl records a declaration for an interface function (presumably the .tex author is documenting an API), and \usedecl refers back to the declaration by id. There are two example declarations in the text. Note that these are restricted forms of what our documentation effort actually provides. Real text use looks like given P and Q such that [...] \usedecl{foo}{P}{Q} results in [...]. Thanks for \@ifnextchar\bgroup, that may be a more reasonable approach. – Luke Jun 07 '13 at 19:29
  • @Luke (I should have run your example prior to commenting.) Is the number of parameters variable? Then of course, Werner’s use of \@ifnextchar is much simpler. – Qrrbrbirlbel Jun 07 '13 at 20:14

5 Answers5

10

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.

Werner
  • 603,163
  • 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 07 '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 07 '13 at 20:02
  • There is an understandable exampke usage of this idea: https://davidyat.es/2016/07/27/writing-a-latex-macro-that-takes-a-variable-number-of-arguments/. I understood Werner's comments much better after reading that. – pauljohn32 Mar 29 '18 at 09:15
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

Qrrbrbirlbel
  • 119,821
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}
A.Ellett
  • 50,533
  • 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 07 '13 at 19:09
  • I'm not sure I understand what you mean by use-site. – A.Ellett Jun 07 '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 07 '13 at 19:33
  • This is a valuable answer! Even though OP cannot use it, readers should not overlook. Similar solution https://tex.stackexchange.com/questions/417064/expl3-syntax-graceful-handing-of-conditional-undefined-arguments. Only caution: require newer LaTeX distribution – pauljohn32 Mar 29 '18 at 09:22
2

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

Ulrich Diez
  • 28,770
0

Using my new Python library (currently on CTAN) to execute Python code from TeX...

%! TEX program = lualatex
\documentclass{report}
\usepackage{pythonimmediate}

\begin{pycode}

import pythonimmediate

declared_commands = {}  # global variable

@pythonimmediate.newcommand
def decl():
    name = pythonimmediate.get_arg_str()
    symbol = pythonimmediate.get_arg_str()
    num_param = int(pythonimmediate.get_arg_str())
    declared_commands[name] = symbol, num_param

@pythonimmediate.newcommand
def usedecl():
    name = pythonimmediate.get_arg_str()
    symbol, num_param = declared_commands[name]
    args = [pythonimmediate.get_arg_str() for _ in range(num_param)]
    return symbol + "(" + ", ".join(args) + ")"

@pythonimmediate.newcommand
def withoutdecl():
    symbol = pythonimmediate.get_arg_str()
    num_param = int(pythonimmediate.get_arg_str())
    args = [pythonimmediate.get_arg_str() for _ in range(num_param)]
    return symbol + "(" + ", ".join(args) + ")"
\end{pycode}


% 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

\end{document}

The template is copied from Ulrich's answer, and the output is the same:

output

I believe this should be much more understandable than any solution using any existing Python-binding library for TeX, as far as I can tell.

It's not expandable. (in LuaTeX that would be fixable without too much trouble.) And requires --shell-escape. But otherwise usable in any engine (as well as on Overleaf) in one compilation pass.

user202729
  • 7,143