10

I'm working with some generated LaTeX code, which contains code like this:

\command{a, b}

I supply the definition of \command. It's internals are not really relevant to the question, but most importantly it's passing the argument to another command \backend (which cannot be changed by me):

\newcommand{\command}[1]{\backend{#1}}

Now my issue is that \backend fails with an argument such as a, b because it cannot handle whitespace after that comma.

How can I remove that (single) whitespace after each comma in an argument passed to a macro? Is it possible to do so with another TeX macro?

3
  • 1
    This trick may help, as long as the argument of \command does not include macros.
    – campa
    Jun 2, 2017 at 9:15
  • @campa Indeed, that works! Jun 2, 2017 at 9:23
  • 1
    If any of the answers solved your problem consider accepting one of those to mark that question as solved.
    – TeXnician
    Jul 10, 2017 at 10:37

4 Answers 4

7

Just another solution that works (apart from the one linked by campa):

\documentclass{article}
\usepackage{expl3,xparse}
\NewDocumentCommand{\backend}{m}{#1}
\ExplSyntaxOn
\NewDocumentCommand{\command}{m}{
    \tl_set:Nn \l_tmpa_tl {#1}
    \tl_replace_all:Nnn \l_tmpa_tl { ~ } { }
    \backend{\tl_use:N\l_tmpa_tl}
}
\ExplSyntaxOff
\begin{document}
\command{My space is useless!}
\end{document}

Update: As suggested in the comments now a version with expansion (and with correction of the typo [forgotten comma]).

\documentclass{article}
\usepackage{expl3,xparse}
\NewDocumentCommand{\backend}{m}{#1}
\ExplSyntaxOn
\NewDocumentCommand{\command}{m}{
    \tl_set:Nn \l_tmpa_tl {#1}
    \tl_replace_all:Nnn \l_tmpa_tl { ,~ } { , }
    \exp_args:No \backend { \l_tmpa_tl }
}
\ExplSyntaxOff

\def\foo{quack}

\begin{document}
\command{My space, is useless!, \foo}
\end{document}
4
  • 2
    \tl_replace_all:Nnn \l_tmpa_tl { ,~ } { , } \exp_args:No \backend { \l_tmpa_tl } would be safer IMO.
    – Manuel
    Jun 2, 2017 at 9:45
  • I would note that the argument to \backend needs a number of expansions to reach the final result. That may or may not be compatible with the way \backend works. Jun 2, 2017 at 10:48
  • To see this, make \backend perform a \detokenize{#1}. Jun 2, 2017 at 10:56
  • 1
    Good recovery! +1 Jun 2, 2017 at 12:42
5

Here's a version that can handle macros in the argument. For demonstration purposes, I make \backend perform a \detokenize on its argument, so that we can see exactly what tokens it receives. Note that, in the presented output, the space after \mymacro is a function of \detokenize and not of the \command macro.

As to the logic, the \readlist macro reads the comma separated list into an array \myarg. The * invocation of \readlist* discards empty space around each item in the list. Then, the \foreachitem loop goes through each item in sequence and I use \g@addto@maco to append the tokens of the item to \tmp, along with commas at the proper places. A once-expanded \tmp is finally passed to \backend.

The key point I will mention is that the original tokens are passed directly to \backend with this method. No further expansion of the argument is required by \backend to obtain the desired tokens. It is as if one typed the space-free argument list directly to \backend.

\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{listofitems}
\makeatletter
\newcommand\command[1]{%
  \setsepchar{,}%
  \readlist*\myarg{#1}%
  \def\tmp{}%
    \foreachitem\x\in\myarg{%
      \ifnum\xcnt=1\relax\else\g@addto@macro\tmp{,}\fi%
      \expandafter\g@addto@macro\expandafter\tmp\expandafter{\x}%
    }%
  \expandafter\backend\expandafter{\tmp}%
}
\makeatother
\newcommand\backend[1]{[\detokenize{#1}]}
\begin{document}
\command{a, b}

\command{a, b,c,  \mymacro, dfdg}
\end{document}

enter image description here

Note that, with the above code, only spaces around commas are discarded...whereas spaces within an argument are preserved, so that \command{a a , bb b, c} is processed as \backend{a a,bb b,c}. That may be a non-issue for the OP, if all arguments contain no spaces by their nature. Or it may even be a desired feature.

On the other hand, if it is desired to remove all spaces, including intra-argument spaces, then the following code will suffice. Here, it uses the space, rather than the comma, as the item-separator in digesting the list. Then, when it regurgitates the list item-by-item, the separator (space) is not part of the regurgitated list item.

\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{listofitems}
\makeatletter
\newcommand\command[1]{%
  \setsepchar{ }%
  \readlist\myarg{#1}%
  \def\tmp{}%
    \foreachitem\x\in\myarg{%
      \expandafter\g@addto@macro\expandafter\tmp\expandafter{\x}%
    }%
  \expandafter\backend\expandafter{\tmp}%
}
\makeatother
\newcommand\backend[1]{[\detokenize{#1}]}
\begin{document}
\command{a, b}

\command{a, b,c,  \mymacro, df dg}
\end{document}
3

This would remove all spaces around commas:

\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\command}{m}
 {
  \seq_set_split:Nnn \l_jour_command_input_seq {,} { #1 }
  \exp_args:Nf \backend { \seq_use:Nn \l_jour_command_input_seq {,} }
 }
\seq_new:N \l_jour_command_input_seq
\ExplSyntaxOff

A test document

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\command}{m}
 {
  \seq_set_split:Nnn \l_jour_command_input_seq {,} { #1 }
  \exp_args:Nf \backend { \seq_use:Nn \l_jour_command_input_seq {,} }
 }
\seq_new:N \l_jour_command_input_seq
\ExplSyntaxOff

\newcommand{\backend}[1]{#1} % just for testing

\begin{document}

\command{a, b}

\command{a , b}

\command{a, ,b}

\end{document}

enter image description here

If you also want to remove empty items, use a clist:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\command}{m}
 {
  \clist_set:Nn \l_jour_command_input_clist { #1 }
  \exp_args:Nf \backend { \clist_use:Nn \l_jour_command_input_clist {,} }
 }
\clist_new:N \l_jour_command_input_clist
\ExplSyntaxOff

\newcommand{\backend}[1]{#1} % just for testing

\begin{document}

\command{a, b}

\command{a , b}

\command{a, ,b}

\end{document}

enter image description here

Explanation

We pass \backend the items of either the sequence or the clist, with , in between them. Both \seq_set_split:Nnn remove leading and trailing spaces from the items, storing the list in an “optimized form”. With \seq_use:Nn or \clist_use:Nn, the items are delivered “all at once” with the stated separator, when f-expanded. Hence the use of \exp_args:Nf, which sets aside the first token after it (\backend, in this case) and performs f-expansion to the contents of the braced group after it (the braces are left in place).

The main difference between \seq_set_split:Nnn and \clist_set:Nn is that the former honors empty items between two separator (that is, zero or one space), whereas the latter removes them.

1
  • @TeXnician Bruno powered.
    – egreg
    Jun 2, 2017 at 20:11
1

You can do this

\documentclass{article}

\usepackage{xinttools}

\newcommand{\command}[1]{%
   \expandafter\backend\expandafter
    {\romannumeral0\xintlistwithsep,{\xintCSVtoListNoExpand{#1}}}%
}

% for demonstration:

\newcommand\backend[1]{\detokenize{#1}}

\begin{document}

\ttfamily

+++\command{ \foo BAR1 BAR2 , \bar FOO1 FOO2 }+++

\end{document}

enter image description here

This removes spaces around commas, at start of first argument and at end of last argument. In-between spaces are left untouched. Even in case of successive space tokens, which requires special input as TeX collapses consecutive spaces into one.

Nota Bene: detokenize always prints with a space after each CS, hence I added BAR1 and BAR2 for additional demonstration.


If you want to filter out blank items you can use this. But, if really \backend chokes on space after a comma, then it might not like either varying number of items in its input, which this will produce on eliminating empty or blank items.

\documentclass{article}

\usepackage{xinttools}

\catcode`_ 11
\newcommand\EmptinessFilter[1]{%
% originally blank items were converted into empty brace pairs
% by \xintCSVtoList, hence will get treated here as the true empty ones
   \if\relax\detokenize{#1}\relax\expandafter\xint_gobble_thenstop\fi{#1}%
}
\catcode`_ 8

\newcommand{\command}[1]{%
   \expandafter\backend\expandafter
    {\romannumeral0\xintlistwithsep{,}
         {\xintApplyUnbraced\EmptinessFilter{\xintCSVtoListNoExpand{#1}}}}%
}%


% for demonstration:

\newcommand\backend[1]{\detokenize{#1}}

\begin{document}

\ttfamily

+++\command{ \foo BAR1 BAR2 , \bar FOO1 FOO2 , , \third item (previous was
  empty or blank) , \fourth item, ,, , fifth (previous three were empty or
  blank) ,

             , seventh because empty line gives \par token (the space
             after \par or after \backend at the end of this sentence is put
             in its output by \detokenize there is no actual space token in
             the argument list passed to \backend }+++

\end{document}

This needs the extra \EmptinessFilter as xinttools has no variant of \xintCSVtoList which eliminates head front blank items, that would need a dedicated one.

enter image description here

You must log in to answer this question.

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