12

Here's a little challenge. The goal is to create a command that takes is two or three arguments without using square brackets for optional arguments. The first and second argument are separated by a comma and a space, while the second and the third (if present) are separated by a dash. This means that both of the following inputs should be acceptable

\mycommand{arg_1, arg_2}

and

\mycommand{arg_1, arg_2-arg_3}

The definition of what \mycommand does with the two/three arguments is irrelevant to the problem.

3
  • 2
    That's one argument from a TeX point of view ... I guess you want a parser for that one document-level argument.
    – Joseph Wright
    Feb 8, 2019 at 17:46
  • Try \def\mycommand#1,\space#2-#3{...}. Feb 8, 2019 at 21:41
  • Since I assume this is more or less a follow-up to tex.stackexchange.com/q/473949/35864, let me please re-iterate that I believe that the usual biblatex syntax is not really inferior to this syntax. In fact the syntax proposed here has the serious drawback that it removes the ability to cite several entries in one cite command \autocite{sugfridsson,worman,nussbaum} as that uses the comma-separated syntax in one argument already.
    – moewe
    Feb 9, 2019 at 8:00

5 Answers 5

12

You can do it with xparse:

\documentclass{article}
\usepackage{xparse}

% split at the comma
\NewDocumentCommand{\mycommand}{ >{\SplitArgument{1}{,}}m }{%
  \mycommandA#1%
}
% do something with #1 and split the second part at the hyphen
\NewDocumentCommand{\mycommandA}{ m >{\SplitArgument{1}{-}}m }{%
  Main is #1%
  \IfNoValueTF{#2}{.}{; \mycommandB#2}%
}
% do something with the second part
\NewDocumentCommand{\mycommandB}{mm}{%
  Secondary is #1\IfValueT{#2}{ plus #2}.%
}

\begin{document}

\mycommand{arg1}

\mycommand{arg1, arg2}

\mycommand{arg1, arg2-arg3}

\end{document}

enter image description here

An advantage: it's irrelevant if you forget the space after the comma.

18

enter image description here

\documentclass{article}

\def\mycommand#1{\xmycommand#1--\relax}
\def\xmycommand#1,#2-#3-#4\relax{[#1][#2][#3]}


\begin{document}

$\mycommand{arg_1, arg_2}$

$\mycommand{arg_1, arg_2-arg3}$

\end{document}
9
  • 6
    Even without electricity. I'm impressed!
    – user121799
    Feb 8, 2019 at 17:53
  • How about adding some mortar to the bricks!?
    – Werner
    Feb 8, 2019 at 18:06
  • @Werner where I come from we prefer this style: derbyshireuk.net/derbyshire_drystonewalls.html Feb 8, 2019 at 18:12
  • 6
    @noibe oh no a disaster! I don't mind not getting the tick (I have more than enough points) but seeing points go to egreg is so depressing:) Feb 8, 2019 at 18:49
  • 3
    No \zzz? :'-( Feb 9, 2019 at 6:02
6

Some crocheting/knitting by hand (or whatever)... ;-)

I suppose this question came into being while you thought about your other question: Custom \cite command. In that question you ask for a customized \cite-command with the same syntax.

Be that as it may.

The challenge is doing it in a way where desired space-removal but no undesired removal of argument-braces takes place.

E.g., with things like \mycommand{{arg_1}a, arg_2} the braces surrounding arg_1 should probably be preserved while with things like \mycommand{{arg_1} , arg_2} they should probably be removed.
Also probably only space-tokens surrounding the entire arguments/surrounding those commas and those dashes that are taken for argument-separators should get removed.

Below is an approach where comma takes precedence over the dash and where the leftmost unhidden comma delimits the first argument from the remaining arguments and where the first unhidden dash following that comma delimits the second from the third argument.
Exactly one pair of matching curly braces will be removed in case after removing surrounding spaces it surrounds an entire argument as in that case it is assumed that the braces serve for hiding commas/dashes/spaces/emptiness. In all other cases braces will be preserved.

E.g., with things like \mycommand{ {arg_1} , {arg_2-2} - arg_3} you should get:
First argument: arg_1
Second argument: arg_2-2
Third argument: arg_3.

E.g., with things like \mycommand{ {{arg 1,a} - arg 1b} , {arg 2-2} , 2 - arg 3a - arg 3b} you should get:
First argument (left from the first unhidden comma, surrounding spaces and outermost brace-level that surrounds the entire argument removed): {arg 1,a} - arg 1b
Second argument (right from the first unhidden comma, left from the first unhidden dash following the comma): {arg 2-2} , 2
Third argument (the remainder, right from the first unhidden dash following the comma): arg 3a - arg 3b.

E.g., with things like \mycommand{{arg 1}, {arg 2-2} , 2 - arg 3a - arg 3b} you should get:
First argument (left from the first unhidden comma): arg 1
Second argument (right from the first unhidden comma, left from the first unhidden dash following the comma): {arg 2-2} , 2
Third argument (the remainder, right from the first unhidden dash following the comma): arg 3a - arg 3b.

You may not be familiar to methods for removing space-tokens that surround arguments, thus a quote from the manual of my deprecated labelcas-package:

A note about removing leading and trailing spaces

The matter of removing trailing spaces from an (almost) arbitrary token-sequence is elaborated in detail by Michael Downes, 'Around the Bend #15, answers', a summary of internet-discussion which took place under his guidance primarily at the INFO-TEX list, but also at comp.text.tex (usenet) and via private email; December 1993. Online archived at http://www.tug.org/tex-archive/info/arobend/answer.015.

One basic approach suggested therein is using TeX's scanning of delimited parameters in order to detect and discard the ending space of an argument:

... scan for a pair of tokens: a space-token and some well-chosen bizarre token that can't possibly occur in the scanned text. If you put the bizarre token at the end of the text, and if the text has a trailing space, then TeX's delimiter matching will match at that point and not before, because the earlier occurrences of space don’t have the requisite other member of the pair.

Next consider the possibility that the trailing space is absent: TeX will keep on scanning ahead for the pair ⟨space⟩⟨bizarre⟩ until either it finds them or it decides to give up and signal a 'Runaway argument?' error. So you must add a stop pair to catch the runaway argument possibility: a second instance of the bizarre token, preceded by a space. If TeX doesn't find a match at the first bizarre token, it will at the second one.

(Look up the macros \KV@@sp@def, \KV@@sp@b, \KV@@sp@c and \KV@@sp@d in David Carlisle’s keyval-package for an interesting variation on this approach.)

When scanning for parameters
##1⟨space⟩⟨bizarre⟩##2⟨B1⟩
the sequence:
⟨stuff where to remove trail-space⟩⟨bizarre⟩⟨space⟩⟨bizarre⟩⟨B1⟩
, you can fork two cases:

  1. Trailing-space:
    ##1 = ⟨stuff where to remove trail-space⟩, but with removed space. (And possibly one removed brace-level!)
    ##2 = ⟨space⟩⟨bizarre⟩.

  2. No trailing-space:
    ##1 = ⟨stuff where to remove trail-space⟩⟨bizarre⟩.
    ##2 is empty.

So forking can be implemented depending on the emptiness of ##2.
You can easily prevent the brace-removal in the first case, e.g., by adding (and later removing) something (e.g., a space-token) in front of the ⟨stuff where to remove trail-space⟩.

You can choose ⟨B1⟩=⟨bizarre⟩⟨space⟩.

The link in the quote is out of date.
Nowadays you can find the entire "Around the bend" -collection at http://mirrors.ctan.org/info/challenges/AroBend/AroundTheBend.pdf.

As you see, the methods for space-removal exhibited in the quote above do rely on some sequence of tokens that must not occur within the argument.

In the example below it is relied on the token \UD@seldom not occurring within arguments.
In other words: You must not use the token \UD@seldom within your arguments.

If you don't like that restriction, then I can deliver space-removal-routines which do without such restrictions, but they are slower and they form another huge load of code.

\documentclass{article}

\makeatletter
%%-----------------------------------------------------------------------------
%% Paraphernalia ;-) :
%%.............................................................................
\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>
%%.............................................................................
\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 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}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%.............................................................................
%%   \UD@ExtractFirstArg{ABCDE} yields  {A}
%%
%%   \UD@ExtractFirstArg{{AB}CDE} yields  {AB}
%%
%% !!! The argument of \UD@ExtractFirstArg must not be empty. !!!
%% You can check for emptiness via \UD@CheckWhetherNull before applying
%% \UD@ExtractFirstArg.
%%.............................................................................
\newcommand\UD@RemoveTillUD@SelDOm{}%
\long\def\UD@RemoveTillUD@SelDOm#1#2\UD@SelDOm{{#1}}%
\newcommand\UD@ExtractFirstArg[1]{%
  \romannumeral0%
  \UD@ExtractFirstArgLoop{#1\UD@SelDOm}%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
  { #1}%
  {\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillUD@SelDOm#1}}%
}%
%%-----------------------------------------------------------------------------
%% \UD@RemoveSpacesAndOneLevelOfBraces{<argument>} removes leading and 
%% trailing spaces from <argument>.
%% If after that the <argument> is something that is entirely nested
%% between at least one pair of matching curly braces, the outermost
%% pair of these braces will be removed.
%%
%% !!!! <argument> must not contain the token \UD@seldom !!!!
%%.............................................................................
\begingroup
\newcommand\UD@RemoveSpacesAndOneLevelOfBraces[1]{%
  \endgroup
  \newcommand\UD@RemoveSpacesAndOneLevelOfBraces[1]{%
    \romannumeral0%
    \UD@trimtrailspaceloop#1##1\UD@seldom#1\UD@seldom\UD@seldom#1{##1}%
  }%
  \newcommand\UD@trimtrailspaceloop{}%
  \long\def\UD@trimtrailspaceloop##1#1\UD@seldom##2\UD@seldom#1##3{%
    \UD@CheckWhetherNull{##2}{%
      \UD@trimleadspaceloop{##3}%
    }{%
      \UD@trimtrailspaceloop##1\UD@seldom#1\UD@seldom\UD@seldom#1{##1}%
    }%
  }%
}%
\UD@RemoveSpacesAndOneLevelOfBraces{ }%
\newcommand\UD@trimleadspaceloop[1]{%
  \UD@CheckWhetherLeadingSpace{#1}{%
    \expandafter\UD@trimleadspaceloop\expandafter{\UD@removespace#1}%
  }{%
    \UD@CheckWhetherNull{#1}{ }{%
      \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}{%
        \UD@exchange{ }{\expandafter}\UD@secondoftwo{}#1%
      }{ #1}%
    }%
  }%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument contains no comma which is not nested 
%% in braces:
%%.............................................................................
%% \UD@CheckWhetherNoComma{<Argument which is to be checked>}%
%%                        {<Tokens to be delivered in case that argument
%%                          contains no comma>}%
%%                        {<Tokens to be delivered in case that argument
%%                          contains comma>}%
%%
\newcommand\UD@GobbleToComma{}\long\def\UD@GobbleToComma#1,{}%
\newcommand\UD@CheckWhetherNoComma[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@GobbleToComma#1,}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument contains no dash which is not nested 
%% in braces:
%%.............................................................................
%% \UD@CheckWhetherNoDash{<Argument which is to be checked>}%
%%                       {<Tokens to be delivered in case that argument
%%                         contains no dash>}%
%%                       {<Tokens to be delivered in case that argument
%%                         contains dash>}%
%%
\newcommand\UD@GobbleToDash{}\long\def\UD@GobbleToDash#1-{}%
\newcommand\UD@CheckWhetherNoDash[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@GobbleToDash#1-}%
}%
%%-----------------------------------------------------------------------------
%% Take a comma-delimited/dash-delimited argument where a space was
%% prepended for preventing brace-removal and wrap it in curly braces.
%%.............................................................................    
\newcommand\UD@SplitCommaArg{}%
\long\def\UD@SplitCommaArg#1,{%
  \romannumeral0\UD@exchange{ }{\expandafter}\expandafter{\UD@removespace#1}%
}%
\newcommand\UD@SplitDashArg{}%
\long\def\UD@SplitDashArg#1-{%
  \romannumeral0\UD@exchange{ }{\expandafter}\expandafter{\UD@removespace#1}%
}%
%%-----------------------------------------------------------------------------
%% Now we have the tools for creating the desired \mycommand-macro:
%%.............................................................................    
\newcommand\mycommand[1]{%
  \romannumeral0%
  \UD@CheckWhetherNoComma{#1}{%
    \expandafter\mycommand@oneargument\expandafter{%
      \romannumeral0\UD@exchange{ }{\expandafter\expandafter\expandafter}%
      \UD@RemoveSpacesAndOneLevelOfBraces{#1}%
    }%
  }{%
    \expandafter\UD@exchange\expandafter{\expandafter
      {\UD@GobbleToComma#1}%
    }{%
      \expandafter\@mycommand
      \romannumeral0%
      \UD@exchange{ }{%
        \expandafter\expandafter\expandafter\expandafter
        \expandafter\expandafter\expandafter
      }%
      \expandafter\UD@ExtractFirstArg\expandafter{%
      \romannumeral0%
      \UD@exchange{ }{%
        \expandafter\expandafter\expandafter\expandafter
        \expandafter\expandafter\expandafter
      }%
      \expandafter\UD@SplitCommaArg
      \UD@firstoftwo{ }{}#1}%
    }%
  }%
}%
\newcommand\@mycommand[2]{%
  \UD@CheckWhetherNoDash{#2}{%
    \expandafter\UD@exchange\expandafter{\expandafter
      {%
        \romannumeral0%
        \UD@exchange{ }{\expandafter\expandafter\expandafter}%
        \UD@RemoveSpacesAndOneLevelOfBraces{#2}%
      }%
    }{%
      \expandafter\mycommand@twoarguments\expandafter{%
        \romannumeral0%
        \UD@exchange{ }{\expandafter\expandafter\expandafter}%
        \UD@RemoveSpacesAndOneLevelOfBraces{#1}%
      }%
    }%
  }{%
    \expandafter\UD@exchange\expandafter{\expandafter%
      {\UD@GobbleToDash#2}%
    }{%
      \expandafter\@@mycommand
      \romannumeral0%
      \UD@exchange{ }{%
        \expandafter\expandafter\expandafter\expandafter
        \expandafter\expandafter\expandafter
      }%
      \expandafter\UD@ExtractFirstArg\expandafter{%
      \romannumeral0%
      \UD@exchange{ }{%
        \expandafter\expandafter\expandafter\expandafter
        \expandafter\expandafter\expandafter
      }%
      \expandafter\UD@SplitDashArg
      \UD@firstoftwo{ }{}#2}%
    }%
    {#1}%
  }%
}%
\newcommand\@@mycommand[3]{%
  \expandafter\UD@exchange\expandafter{%
    \expandafter{%
      \romannumeral0%
      \UD@exchange{ }{\expandafter\expandafter\expandafter}%
      \UD@RemoveSpacesAndOneLevelOfBraces{#2}%
    }%
  }{%
    \expandafter\UD@exchange\expandafter{%
       \expandafter{%
         \romannumeral0%
         \UD@exchange{ }{\expandafter\expandafter\expandafter}%
         \UD@RemoveSpacesAndOneLevelOfBraces{#1}%
      }%
    }{%
      \expandafter\mycommand@threearguments\expandafter{%
        \romannumeral0%
        \UD@exchange{ }{\expandafter\expandafter\expandafter}%
        \UD@RemoveSpacesAndOneLevelOfBraces{#3}%
      }%
    }%
  }%
}%
\newcommand\mycommand@oneargument[1]{%
  \UD@secondoftwo{%
    %We need a space to stop \romannumeral-expansion.
  }{ }%
  There is no comma or only empty args after the comma, thus:\\
  argument 1 = \printmeaning{#1}%
}%
\newcommand\mycommand@twoarguments[2]{%
  \UD@CheckWhetherNull{#2}{%
    \mycommand@oneargument{#1}%
  }{%
    \UD@secondoftwo{%
      %We need a space to stop \romannumeral-expansion.
    }{ }%
    There is a comma but either no dash or only one non-empty argument near
    the dash, thus:\\
    argument 1 = \printmeaning{#1}\\
    argument 2 = \printmeaning{#2}%
  }%
}%
\newcommand\mycommand@threearguments[3]{%
  \UD@CheckWhetherNull{#2}{%
    \UD@CheckWhetherNull{#3}{%
      \mycommand@oneargument{#1}%
    }{%x
      \mycommand@twoarguments{#1}{#3}%
    }%
  }{%
    \UD@CheckWhetherNull{#3}{%
      \mycommand@twoarguments{#1}{#2}%
    }{%
      \UD@secondoftwo{%
        %We need a space to stop \romannumeral-expansion.
      }{ }%
      There is a comma and a dash, thus:\\
      argument 1 = \printmeaning{#1}\\
      argument 2 = \printmeaning{#2}\\
      argument 3 = \printmeaning{#3}%
    }%
  }%
}%

\newcommand\printmeaning[1]{%
  \def\UD@tempa{#1}%
  \fbox{\texttt{\expandafter\strip@prefix\meaning\UD@tempa}}%
}%
\makeatother

\parindent=0ex
\parskip=\smallskipamount
\pagestyle{empty}%


\begin{document}
\vspace*{-3.5cm}%
\enlargethispage{7cm}%

\verb|\mycommand{  arg 1  }|:\\
\mycommand{  arg 1  }\\
\null\hrulefill\null

\verb|\mycommand{ arg 1 , }|:\\
\mycommand{ arg 1 , }\\
\null\hrulefill\null    

\verb|\mycommand{ arg 1 ,  - }|:\\
\mycommand{ arg 1 ,  - }\\
\null\hrulefill\null

\verb|\mycommand{ arg 1 ,  {{arg 2}} }|:\\
\mycommand{ arg 1 ,  {{arg 2}} }\\
(Be aware that (only) the outermost braces get removed from
an argument if surrounding the entire argument after removal
of surrounding spaces.)\\
\null\hrulefill\null

\verb|\mycommand{ arg 1 , arg 2 - }|:\\
\mycommand{ arg 1 , arg 2 - }\\
\null\hrulefill\null

\verb|\mycommand{ arg 1 , arg 2 - arg 3}|:\\
\mycommand{ arg 1 , arg 2 - arg 3}\\
\null\hrulefill\null

\verb|\mycommand{ arg 1 , - arg 3}|:\\
\mycommand{ arg 1 , - arg 3}\\
\null\hrulefill\null

\verb|\mycommand{  {arg 1a} - arg 1b ,  {arg 2-2} , 2 -  arg 3a - arg 3b}|:\\
\mycommand{  {arg 1a} - arg 1b ,  {arg 2-2} , 2 -  arg 3a - arg 3b}\\
\null\hrulefill\null

\verb|\mycommand{ {arg 1} ,  {arg 2-2} , 2 -  arg 3a - arg 3b}|:\\
\mycommand{ {arg 1} ,  {arg 2-2} , 2 -  arg 3a - arg 3b}

\end{document}

enter image description here

4

A listofitems approach.

By setting a nested parsing with \setsepchar{,/-} prior to reading the list into \myargs, everything to the left of the 1st comma is read into \myargs[1], everything to the right of the comma (and before the next comma) is read into \myargs[2].

Within \myargs[2], everything to the left of the first dash is assigned to \myargs[2,1], and everything after the dash (and before the next dash) is assigned to \myargs[2,2].

The number of sub-elements within \myargs[2] is available through \listlen\myargs[2].

All these elements and list length variables are fully expandable.

\documentclass{article}
\usepackage{listofitems}
\newcommand\mycommand[1]{\setsepchar{,/-}\readlist*\myargs{#1}%
  \noindent Arg 1 is \myargs[1]\\
  Arg 2 is \myargs[2,1]\\
  Arg 3 is \ifnum\listlen\myargs[2]=1\relax Absent\else\myargs[2,2]\fi%
}
\begin{document}

\mycommand{arg1, arg2}

and

\mycommand{arg1, arg2-arg3}

\end{document}

enter image description here

1

One more:

\documentclass{article}
\usepackage{xstring}
\def\mycommand#1,#2 {\IfSubStr{#2}{-}{\[[#1],
[\StrBefore{#2}{-}]-[\StrBehind{#2}{-}]\] }{\[[#1],[#2]\]}}
\begin{document}
\mycommand aaa,bbb-ccc  
\mycommand aaa,bbb 
\end{document}

mwe

You must log in to answer this question.

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