5

I wanted to iterate through a space-separated list of words, so I created the following macro:

%
%
% \withword\variable\do{run for each member}\inlist space-separated list\endlist
% *****************************************************************************
%
% Iterate through a space-separated list (at least two members are required)
%
% Requires: Nothing
%
\long\gdef\withword#1\do#2\inlist#3 #4#5\endlist{%
    \if\relax\detokenize{#3}\relax\else\def#1{#3}#2\fi%
    \if\relax\detokenize{#4}\relax\else\withword{#1}\do{#2}\inlist#4#5
        {}\endlist\fi%
}

With it, I could do

\withword\myword\do{%
    List member is ``\myword''\par%
}\inlist one two three four five six\endlist

and get

Result of \withword macro

However there were two problems: The first problem was that the macro required a list with at least two members, and the second problem was that I could not replace one two three four five six with \mylist.

So I decided to turn the macro above into a helper macro (\@withword), and after thinking a bit I came out with the following redirections:

\makeatletter
%
%
% \@withword\variable\do{run for each member}\inlist space-separated list\endlist
% *****************************************************************************
%
% Iterate through a space-separated list (at least two members are required)
%
% Requires: Nothing
%
\long\gdef\@withword#1\do#2\inlist#3 #4#5\endlist{%
    \if\relax\detokenize{#3}\relax\else\def#1{#3}#2\fi%
    \if\relax\detokenize{#4}\relax\else\@withword{#1}\do{#2}\inlist#4#5
        {}\endlist\fi%
}
%
%
% \witeration{\variable}{space-separated list}{run for each member}
% *****************************************************************************
%
% Iterate through a space-separated list
%
% Requires: macro `\@withword`
%
\long\gdef\witeration#1#2#3{%
    \@withword{#1}\do{#3}\inlist#2{} {}\endlist%
}
\makeatother
%
%
% \forwordx\variable\in{ space-separated list }{ run for each member }
% *****************************************************************************
%
% Iterate through a space-separated list -- like `\forwordx`, but using a more
% compact syntax
%
% Requires: macros `\@withword` and `\witeration`
%
\long\gdef\forwordx#1\in#2{%%
    \edef\tmp{\noexpand\witeration{\noexpand#1}{#2}}\tmp%
}
%
%
% \forword \variable in { space-separated list } { run for each member }
% *****************************************************************************
%
% Iterate through a space-separated list
%
% Requires: macros `\@withword`, `\witeration` and `forwordx`
%
\long\gdef\forword#1in{\forwordx{#1}\in}

We can test these macros by doing:

\def\mylist{one two three four five six}

\forword \myword in { one two three four five six } {%
    List member is ``\myword''\par%
}\bigskip

\forword \myword in \mylist {%
    List member is ``\myword''\par%
}\bigskip

\forwordx\myword\in{one two three four five six}{%
    List member is ``\myword''\par%
}\bigskip

\forwordx\myword\in\mylist{%
    List member is ``\myword''\par%
}\bigskip

\witeration{\myword}{one two three four five six}{%
    List member is ``\myword''\par%
}\bigskip

I would like to know if you think that my approach is the correct one or if instead you have suggestions on how to improve it.

EDIT

After the comments have made me aware that my macros keept nesting \if...\fi statements, I rewrote the code.

Now there are only two user macros. One macro (\withword) is for iterating through literal lists:

\withword\myvariable\do{%
    run for each member%
}\throughlist word1 word2 ... wordN\endlist

The other macro (\forword) is for iterating through both literal and non-literal lists:

\forword\myvariable\in{space-separated list}{%
    run for each member%
}

So, here is the code:

\makeatletter
%
%
% @withword\variable\do{ run for each member }\whilelist word1 word2 ... wordN {} {} {}
% *****************************************************************************
%
% Iterate through a literal space-separated list (private helper macro)
%
% This macro is identical to `\withword`, except that the list, instead of
% being confined within `\throughlist` and `\endlist` is confined within
% `\whilelist` and `{} {} {}` (please notice the whitespaces in `{} {} {}`).
%
% Like `\withword`, this macro supports only literal lists.
%
% Requires: nothing
%
\long\gdef\@withword#1\do#2\whilelist #3#4 {%
    \ifcat$\detokenize{#3}$%
        \expandafter\@stop@withword%
    \else%
        \def#1{#3#4}%
        #2%
    \fi%
    \@withword{#1}\do{#2}\whilelist%
}
%
%
% \@stop@withword\@withword ...
% *****************************************************************************
%
% Prevent a following invocation of `\@withword` (private helper macro)
%
% Requires: nothing
%
\long\gdef\@stop@withword\@withword#1\do#2\whilelist #3#4 {}
%
%
% \@witeration{\variable}{space-separated list}{run for each member}
% *****************************************************************************
%
% Iterate through a space-separated list (private helper macro)
%
% This macro is identical to `\@withword`, but has a different syntax.
%
% Requires: macro `\@withword`
%
\long\gdef\@witeration#1#2#3{\@withword{#1}\do{#3}\whilelist #2 {} {} {}}
%
% \forword\variable\in{ space-separated list }{ run for each member }
% *****************************************************************************
%
% Iterate through a space-separated list, with the expansion of the  list
% argument
%
% Requires: macros `\@withword` and `\@witeration`
%
\long\gdef\forword#1\in#2{\edef\tmp{\noexpand\@witeration{\noexpand#1}{#2}}\tmp}
%
%
% \withword\variable\do{ run for each member }\throughlist word1 word2 ... wordN\endlist
% *****************************************************************************
%
% Iterate through a literal space-separated list (private helper macro)
%
% Requires: the `\@withword` macro
%
\long\gdef\withword#1\do#2\throughlist#3\endlist{%
    \@withword{#1}\do{#2}\whilelist #3 {} {} {}%
}%
%
%
\makeatother

Tests:

\def\mylist{one two three four five six}

\forword\myword\in{one two three four five six}{%
    Item is ``\myword''\par%
}\bigskip

\forword\myword\in\mylist{%
    Item is ``\myword''\par%
}\bigskip

\withword\myword\do{%
    Item is ``\myword''\par%
}\throughlist one two three four five six\endlist

Result:

Result of \forword and \withword macros

I have tested these updated macros with thousands of words and they do not break.

21
  • 1
    The \expandafter in the definition of your \witeration doesn't do anything (it tries to expand { which isn't expandable), the same is true for your \@withword definition. You'll get issues with lists with many elements, and the last list element has an additional {} added to it.
    – Skillmon
    Aug 19, 2022 at 21:28
  • What behavior do you wish in case your list has a leading/trailing space? A first/last empty list-item? Aug 19, 2022 at 21:53
  • With long lists you get some \fi accumulated which might cause memory-trouble. Aug 19, 2022 at 21:57
  • In case an element of your space-delimited list is wrapped in curly braces: Shall these curly braces be preserved or shall these curly braces be stripped off? Aug 19, 2022 at 22:55
  • \witeration{\foobar}{Item1 {}Item2 Item3}{Do this with \foobar} terminates after having \@withword process Item1 because then \@withword's fourth argument is empty... Aug 19, 2022 at 23:34

4 Answers 4

2

I'd use expl3. The command \forword takes two arguments; in case you use \forword* the first argument should be a macro that contains your list. The second argument is the template to use, where the current item is denoted by #1.

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\forword}{s m +m}
 {
  \IfBooleanTF{#1}
   {
    \madmurphy_forword:on { #2 } { #3 }
   }
   {
    \madmurphy_forword:nn { #2 } { #3 }
   }
 }

\seq_new:N \l__madmurphy_forword_seq
\cs_generate_variant:Nn \seq_set_split:Nnn { Nnx }

\cs_new_protected:Nn \madmurphy_forword:nn
 {
  \seq_set_split:Nnx \l__madmurphy_forword_seq { ~ } { \tl_trim_spaces:n { #1 } }
  \cs_set:Nn \__madmurphy_forword_item:n { #2 }
  \seq_map_function:NN \l__madmurphy_forword_seq \__madmurphy_forword_item:n
 }

\cs_generate_variant:Nn \madmurphy_forword:nn { o }

\ExplSyntaxOff

\begin{document}

\forword{ one two three four five six }{List member is ``#1''\par}

\newcommand{\mylist}{AAA BBB CCC DDD EEE}

\forword*\mylist{Item is #1\par}

\end{document}

enter image description here

It would be possible to avoid * by examining the first argument and deciding if it is a single macro name.

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\forword}{m +m}
 {
  \bool_lazy_and:nnTF { \tl_if_single_p:n { #1 } } { \token_if_cs_p:N #1 }
   {
    \madmurphy_forword:on { #1 } { #2 }
   }
   {
    \madmurphy_forword:nn { #1 } { #2 }
   }
 }

\seq_new:N \l__madmurphy_forword_seq
\cs_generate_variant:Nn \seq_set_split:Nnn { Nnx }

\cs_new_protected:Nn \madmurphy_forword:nn
 {
  \seq_set_split:Nnx \l__madmurphy_forword_seq { ~ } { \tl_trim_spaces:n { #1 } }
  \cs_set:Nn \__madmurphy_forword_item:n { #2 }
  \seq_map_function:NN \l__madmurphy_forword_seq \__madmurphy_forword_item:n
 }

\cs_generate_variant:Nn \madmurphy_forword:nn { o }

\ExplSyntaxOff

\begin{document}

\forword{ one two three four five six }{List member is ``#1''\par}

\newcommand{\mylist}{AAA BBB CCC DDD EEE}

\forword\mylist{Item is #1\par}

\end{document}
2

The following defines a fully expandable loop \slistloop to iterate over the elements of a space separated list. The macro expands in exactly two steps of expansion and leaves its result inside \unexpanded, so doesn't expand further in an \edef or \expanded context.

To expand the first token of the list once (e.g., to loop over the contents of a variable) you can use \slistloopO. To expand the list fully you can use \slistloopE (expands inside \expanded, not in an \edef, this is mostly the same, except that you don't have to double # tokens).

In your list the token \slistloop@stop should not be used.

Additionally an unexpandable loop \slistinline is provided in which you can define the code that should be used for each element as the first argument (refer to the list element using #1). For this the O and E variants are added as well.

\documentclass{article}

\makeatletter
\newcommand\slistloop[1]
  {%
    \long\def\slistloop##1##2%
      {\unexpanded\expanded{{\slistloop@{##1}##2#1\slistloop@stop#1}}}%
    \newcommand\slistloopO[2]
      {%
        \unexpanded\expanded
          {{%
            \expanded{\unexpanded{\slistloop@{##1}}\expandafter}##2%
              #1\slistloop@stop#1%
          }}%
      }%
    \newcommand\slistloopE[2]
      {%
        \unexpanded\expanded
          {{\expanded{\unexpanded{\slistloop@{##1}}##2}#1\slistloop@stop#1}}%
      }%
  }
\slistloop{ }
\long\def\slistloop@#1#2 % <- leave this space
  {%
    \slistloop@tostop#2\slistloop@end\slistloop@stop
    % use the following `\if...\fi` block if empty elements should be ignored
    %\if\relax\detokenize{#2}\relax
      %\expandafter\@gobbletwo
    %\fi
    \unexpanded{#1{#2}}%
    \slistloop@{#1}%
  }
\long\def\slistloop@tostop#1\slistloop@stop{}
\long\def\slistloop@end\slistloop@stop#1\slistloop@#2{}

\newcount\slistinline@depth
\protected\long\def\slistinline#1#2%
  {%
    \global\advance\slistinline@depth by\@ne
    \long\expandafter\def\csname slistinline@\the\slistinline@depth\endcsname
      ##1{#1}%
    \expandafter\slistloop\csname slistinline@\the\slistinline@depth\endcsname
      {#2}%
    \expandafter\let
      \csname slistinline@\the\slistinline@depth\endcsname\@undefined
    \global\advance\slistinline@depth by\m@ne
  }
\protected\long\def\slistinlineO#1#2%
  {\expanded{\unexpanded{\slistinline{#1}}\expandafter}\expandafter{#2}}
\protected\long\def\slistinlineE#1#2%
  {\expanded{\unexpanded{\slistinline{#1}}{#2}}}
\makeatother

\usepackage{csquotes}

\newcommand*\mylist{one two three four five six}

\begin{document}
\slistinline{List member is ``#1''\par}{one two three four five six}

\edef\foo{\slistloopO\enquote\mylist}\texttt{\meaning\foo}
\end{document}

enter image description here

1

This is the second part of my answer.
The first part of my answer is at https://tex.stackexchange.com/a/654681


If the focus is on obtaining a set of tokens within two expansion-steps, via expansion only, without the need of doubling hashes, without whatsoever (delimiting) token sequences (except \outer-tokens) being forbidden to occur in the arguments, without the mechanism itself using eTeX-extensions or the like, even within tabular-environments, I can offer a slow and cumbersome routine

\UD@iterateSpaceList{⟨prepend-tokens⟩}%
                    {⟨append-tokens⟩}%
                    {⟨separate-tokens⟩}%
                    {⟨space-separated list⟩}

which delivers

⟨prepend-tokens⟩{⟨list item 1⟩}⟨append-tokens⟩%
⟨separate-tokens⟩%
⟨prepend-tokens⟩{⟨list item 2⟩}⟨append-tokens⟩%
⟨separate-tokens⟩%
...
⟨prepend-tokens⟩{⟨last list item⟩}⟨append-tokens⟩%

More information is in the commenting of the code.

\makeatletter
%%=============================================================================
%% Paraphernalia:
%%    \UD@firstoftwo, \UD@secondoftwo, \UD@Exchange, \UD@PassFirstToSecond,
%%    \UD@stopromannumeral, \UD@CheckWhetherNull, \UD@CheckWhetherBlank, 
%%    \UD@ExtractFirstSpaceArg,  \UD@removespace, 
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@gobbletwo[2]{}%
\newcommand\UD@Exchange[2]{#2#1}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\@ifdefinable\UD@removespace{\UD@Exchange{ }{\def\UD@removespace}{}}%
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
%%-----------------------------------------------------------------------------
%% 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]{%
  \romannumeral\expandafter\UD@secondoftwo\string{\expandafter
  \UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@secondoftwo}{%
  \expandafter\UD@stopromannumeral\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument is blank (empty or only spaces):
%%-----------------------------------------------------------------------------
%% -- Take advantage of the fact that TeX discards space tokens when
%%    "fetching" _un_delimited arguments: --
%% \UD@CheckWhetherBlank{<Argument which is to be checked>}%
%%                      {<Tokens to be delivered in case that
%%                        argument which is to be checked is blank>}%
%%                      {<Tokens to be delivered in case that argument
%%                        which is to be checked is not blank>}%
\newcommand\UD@CheckWhetherBlank[1]{%
  \romannumeral\expandafter\expandafter\expandafter\UD@secondoftwo
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo#1{}{}}%
}%
%% 
%% In the following "space" denotes an explicit space token, i.e., 
%% an explicit character token of category 10(space) and character code 32.
%%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingExplicitSpace{<Argument which is to be checked>}%
%%                                     {<Tokens to be delivered in case <argument
%%                                       which is to be checked> does have a
%%                                       leading explicit space-token>}%
%%                                     {<Tokens to be delivered in case <argument
%%                                       which is to be checked> does not have a
%%                                       a leading explicit space-token>}%
\newcommand\UD@CheckWhetherLeadingExplicitSpace[1]{%
  \romannumeral\UD@CheckWhetherNull{#1}%
  {\expandafter\UD@stopromannumeral\UD@secondoftwo}%
  {%
    % Let's nest things into \UD@firstoftwo{...}{} to make sure they are nested in braces
    % and thus do not disturb when the test is carried out within \halign/\valign:
    \expandafter\UD@firstoftwo\expandafter{%
      \expandafter\expandafter\expandafter\UD@stopromannumeral
      \romannumeral\expandafter\UD@secondoftwo
      \string{\UD@CheckWhetherLeadingExplicitSpaceB.#1 }{}%
    }{}%
  }%
}%
\@ifdefinable\UD@CheckWhetherLeadingExplicitSpaceB{%
  \long\def\UD@CheckWhetherLeadingExplicitSpaceB#1 {%
    \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
    {\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
    {\expandafter\expandafter\expandafter\UD@stopromannumeral
     \expandafter\expandafter\expandafter}%
     \expandafter\UD@secondoftwo\expandafter{\string}%
  }%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner space-delimited argument:
%%
%%   \UD@ExtractFirstSpaceArg{A B C D E} yields  {A}
%%
%%   \UD@ExtractFirstSpaceArg{{AB} C D E} yields  {{AB}}
%%
%%   \UD@ExtractFirstSpaceArg{ A B C D E} yields  {}
%%
%%   \UD@ExtractFirstSpaceArg{AB C D E} yields  {AB}
%%
%%   \UD@ExtractFirstSpaceArg{{AB}} yields  {{AB}}
%%
%%   \UD@ExtractFirstSpaceArg{} yields  {}
%%
%% Due to \romannumeral-expansion the result is delivered after two 
%% expansion-steps/after "hitting" \UD@ExtractFirstSpaceArg with \expandafter
%% twice.
%%
%% Use frozen-\relax as delimiter for speeding things up.
%% I chose frozen-\relax because David Carlisle pointed out in
%% <https://tex.stackexchange.com/a/578877>
%% that frozen-\relax cannot be (re)defined in terms of \outer and cannot be
%% affected by \uppercase/\lowercase.
%%
%% \UD@ExtractFirstSpaceArg's argument may contain frozen-\relax:
%% The only effect is that internally more iterations are needed for
%% obtaining the result.
%%
%%.............................................................................
\@ifdefinable\UD@gobbletoSpace{\long\def\UD@gobbletoSpace#1 {}}%
\@ifdefinable\UD@keeptoSpace{\long\def\UD@keeptoSpace#1 {#1}}%
%%
%% \long\def\UD@RemoveFromSpaceTillFrozenrelax#1 #2<frozen relax>{{#1} } 
%%
\@ifdefinable\UD@RemoveFromSpaceTillFrozenrelax{%
  \expandafter\expandafter\expandafter\UD@Exchange
  \expandafter\expandafter\expandafter{%
  \expandafter\expandafter\ifnum0=0\fi}%
  {\long\def\UD@RemoveFromSpaceTillFrozenrelax#1 #2}{{#1} }%
}%
%%
%% \newcommand\UD@ExtractFirstSpaceArg[1]{%
%%   \romannumeral\UD@ExtractFirstSpaceArgLoop{{{}}#1 <frozen relax>}%
%% }
%% 
\expandafter\UD@PassFirstToSecond\expandafter{%
  \romannumeral\expandafter
  \UD@PassFirstToSecond\expandafter{\romannumeral
    \expandafter\expandafter\expandafter\UD@Exchange
    \expandafter\expandafter\expandafter{%
    \expandafter\expandafter\ifnum0=0\fi}{\UD@stopromannumeral{{}}#1 }%
  }{%
    \UD@stopromannumeral\romannumeral\UD@ExtractFirstSpaceArgLoop
  }%
}{%
  \newcommand\UD@ExtractFirstSpaceArg[1]%
}%
\newcommand\UD@ExtractFirstSpaceArgLoop[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@gobbletoSpace#1}%
  {%
    \expandafter\UD@stopromannumeral
    \expandafter{%
      \romannumeral
      \expandafter\UD@firstoftwo\expandafter\UD@stopromannumeral
      \UD@keeptoSpace#1%
    }%
  }%
  {\expandafter\UD@ExtractFirstSpaceArgLoop\expandafter{\UD@RemoveFromSpaceTillFrozenrelax#1}}%
}%
%%-----------------------------------------------------------------------------
%% Remove all leading spaces from argument:
%
\newcommand\UD@TrimAllLeadingspaces[1]{%
  \romannumeral\UD@TrimAllLeadingspacesLoop{#1}%
}%
\newcommand\UD@TrimAllLeadingspacesLoop[1]{%
   \UD@CheckWhetherLeadingExplicitSpace{#1}{%
     \expandafter\UD@TrimAllLeadingspacesLoop\expandafter{\UD@removespace#1}%
   }{\UD@stopromannumeral{#1}}%
}%
%%-----------------------------------------------------------------------------
%% Remove all trailing spaces from argument:
%
\newcommand\UD@TrimAllTrailingspaces[1]{%
  \romannumeral\UD@TrimAllTrailingspacesLoop{#1 }{}{}%
}%
\newcommand\UD@TrimAllTrailingspacesLoop[3]{%
   % #1 remaining list of space-delimited arguments
   % #2 Separator
   % #3 result gathered so far
   \UD@CheckWhetherBlank{#1}{%
      \UD@stopromannumeral{#3}%
   }{%
     \expandafter\UD@PassFirstToSecond\expandafter{%
       \romannumeral\expandafter\expandafter\expandafter\UD@Exchange
       \UD@ExtractFirstSpaceArg{#1}{\UD@stopromannumeral#3#2}%
     }{%
       \expandafter\UD@TrimAllTrailingspacesLoop\expandafter{\UD@gobbletoSpace#1}{ }%
     }%
   }%
}%
%%-----------------------------------------------------------------------------
%%  \UD@iterateSpaceList{<prepend-tokens>}%
%%                      {<append-tokens>}%
%%                      {<separate-tokens>}%
%%                      {<space-separated list>}%
%%
%%  Each item of the <space-separated list> is nested in curly braces before prepending
%%  <prepend-tokens> and appending <append-tokens><separate-tokens> to it.
%%  With the last item <separate-tokens> is not appended.
%%  The resulting set of tokens is delivered by triggering two expansion-steps on
%%  \UD@iterateSpaceList.
%%  
%%  Empty items are ignored.
%%  If the <space-separated list> is blank, i.e., is empty or consists of explicit
%%  space-tokens only, \UD@iterateSpaceList delivers emptiness.
%%  If the <space-separated list> contains leading and/or trailing spaces, these are
%%  discarded and not taken for indicators for empty items.
%%
%%  You can nest items in curly braces - an outermost level of curly surrounding
%%  braces - if present - is stripped off, thus an outermost level of surrounding
%%  curly braces can serve the purpose of "hiding" spaces in case a list-item
%%  itself is to contain space tokens. An empty brace-group can be used for 
%%  having a list item which is empty.
%%
%%  The mechanism delivers the result by triggering two expansion-steps on \UD@iterateSpaceList.
%%
%%  Actually the mechanism is more cumbersome than needed:
%%  First all leading spaces are removed from the <space-separated list> via \UD@TrimAllLeadingspaces.
%%  Then all trailing spaces are removed from the <space-separated list> via \UD@TrimAllTrailingspaces.
%%  Then the routine \UD@iterateSpacelistloop is started for iterating on a list of space-separated
%%  arguments, whose sub-routine  \UD@iterateSpacelistloopEmptyItemFork handles the case of a list
%%  item being empty, being wrapped in curly braces entirely, being several tokens not all wrapped
%%  in curly brace. By modifying \UD@iterateSpacelistloopEmptyItemFork you can change, e.g., the
%%  behavior with empty items in the middle of the list, but be aware that you can get empty 
%%  list-items only via consecutive explicit space-tokens while getting consecutive explicit
%%  space-tokens is tricky.
%%  
%%-----------------------------------------------------------------------------
\newcommand\UD@iterateSpaceList[4]{%
  \romannumeral
  \expandafter\UD@iterateSpacelistloop
  \romannumeral\expandafter\expandafter\expandafter\expandafter
               \expandafter\expandafter\expandafter\UD@stopromannumeral
  \expandafter\UD@TrimAllTrailingspaces
  \romannumeral\expandafter\expandafter\expandafter\UD@stopromannumeral
  \UD@TrimAllLeadingspaces
  {#4}{#1}{#2}{#3}{}{}%
}%
\newcommand\UD@iterateSpacelistloop[6]{%
  % #1 - Remaining space-separated list
  % #2 - prepend-tokens
  % #3 - append-tokens
  % #4 - separate-tokens
  % #5 - separator to prepend in this iteration
  % #6 - resulting tokens gathered so far
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@gobbletoSpace#1 }{%
    % The remaining space-separated list does not have space-tokens, so iterating is done 
    % and, if not empty, the remaining space-separated list itself forms an item
    \UD@iterateSpacelistloopEmptyItemFork{#1}{#6}{#5}{#2}{#3}%
  }{%
    % The remaining space-separated list does have space-tokens, so continue iterating
    % after extracting the first item/the first space-delimited argument and appending
    % that to the resulting tokens gathered so far.
    \expandafter\UD@PassFirstToSecond\expandafter{%
      \romannumeral
      \expandafter\expandafter\expandafter\UD@iterateSpacelistloopEmptyItemFork
      \UD@ExtractFirstSpaceArg{#1}{#6}{#5}{#2}{#3}%
    }{%
      \expandafter\UD@iterateSpacelistloop\expandafter{\UD@gobbletoSpace#1}{#2}{#3}{#4}{#4}%
    }%
  }%
}%
\newcommand\UD@iterateSpacelistloopEmptyItemFork[5]{%
  % #1 - list item in this iteration
  % #2 - resulting tokens gathered so far
  % #3 - separator to prepend in this iteration
  % #4 - prepend-tokens
  % #5 - append-tokens
  \UD@CheckWhetherNull{#1}{% <- The list item is empty
    \UD@stopromannumeral#2%
  }{%
    \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}{%
      % The list item either is a single token or is wrapped in a everything-surrounding brace-group
      \expandafter\UD@PassFirstToSecond\expandafter{\UD@secondoftwo{}#1}%
    }{%
      % The list item is several tokens not all in the same brace-group
      \UD@PassFirstToSecond{#1}%
    }%
    {\UD@stopromannumeral#2#3#4}#5%
  }%
}%

\documentclass{article}

% \makeatletter is still in effect

\begin{document}

Let's define a macro from the result of iteration:

\bigskip

\expandafter\def\expandafter\test\expandafter{%
    % Let's abuse \romannumeral for triggering 2 expansion steps on \UD@iterateSpaceList
    % before being terminated via \UD@stopromannumeral in a way where \romannumeral does nothing:
  \romannumeral\expandafter\expandafter\expandafter\UD@stopromannumeral
  \UD@iterateSpaceList{\Foo{Arg1}}{{Arg3}}{Between}{  one two { t h r e e } four five six }%
}%

\noindent
\texttt{\string\test: \meaning\test}

\bigskip

Let's use iteration for creating a table:

\bigskip

\noindent
\begin{tabular}{|c|c|c|}
\hline
\UD@iterateSpaceList{\textbf}{}{&}{  one two { t h r e e } }\\
\hline
\UD@iterateSpaceList{\textbf}{}{&}{  four five six }\\
\hline
\UD@iterateSpaceList{%
  \UD@iterateSpaceList{\textsc}{}{&}%
}{}{\\\hline}{  {seven eight nine} {ten eleven twelve} {thirteen fourteen fifteen } }%
\\\hline
\end{tabular}

\bigskip

Let's use iteration for stringifying some tokens and displaying their meanings:

\bigskip

% Can't use \texttt here as that due to its defining of scratch-macros would break the test with {#}
\newcommand\printmeaning[1]{% 
  ``{\tt \string#1}'' $\rightarrow$ {\tt \meaning#1}%
}%
\UD@iterateSpaceList{\par\noindent\printmeaning}{}{}{ {\fi} {\else} {#} {\if}  {\def}  { }  }

\end{document}

enter image description here


This is the second part of my answer.
The first part of my answer is at https://tex.stackexchange.com/a/654681

1
  • Thank you for your insightful answer, Ulrich. It focuses on my first version, and I have now adopted a second version after my edits. Just to answer one of your questions, what the desired behavior in front of {{one1}{one2}} {two} should be, my answer is one1one2//two.
    – madmurphy
    Aug 22, 2022 at 19:03
1

This is the first part of my answer.
The second part of my answer is at https://tex.stackexchange.com/a/654692.


You have presented code for a mechanism for processing a comma-separated list of items that is very well thought out. It already reveals a lot about the desired functionality. I like the top-down approach of first implementing a generic solution \@withword and then using it in different "applications".

However, as far as the desired functionality is concerned, not every subtle aspect has been mentioned explicitly. Therefore I would like to address a few rather subtle things:

  • The code works on the basis of macros that process arguments. If an entire macro argument altogether is surrounded by curly braces, then TeX normally strips the outermost surrounding pair of curly braces when fetching that argument from the token stream. With delimited arguments there are tricks for preventing the brace-stripping if brace-stripping is undesired. Curly braces themselves usually are not elements that get typeset, so regarding the look of the text of the pdf-file it usually does not make a difference whether things were still nested in curly braces when typesetting. But in math-mode-typesetting surrounding curly braces can make a difference. And when passing arguments on as arguments of other macros, the presence/absence of surrounding curly braces can also make a difference.

    So a question is: What behavior do you wish in case an item of your space-separated list itself is entirely surrounded by braces?

    The mechanism for grabbing a single item from the list exhibited in your second example strips off surrounding braces. Thus, e.g., with

    \def\gobble#1{}%
    \forword\myword in {{{one1}{one2}} {two}}{After gobbling the first component you have: \expandafter\gobble\myword\\}`  
    

    at some stage you get

    \def\myword{{one1}{one2}}%
    ...
    After gobbling the first component you have: \expandafter\gobble\myword\\
    

    , which in turn yields:

    After gobbling the first component you have: \gobble{one1}{one2}\\
    

    , which in turn yields:

    After gobbling the first component you have: {one2}\\
    

    If surrounding braces were not stripped, you would get:

    \def\myword{{{one1}{one2}}}%
    ...
    After gobbling the first component you have: \expandafter\gobble\myword\\
    

    , which in turn yields:

    After gobbling the first component you have: \gobble{{one1}{one2}}\\
    

    , which in turn yields:

    After gobbling the first component you have: \\
    

    , which is a subtly different result.

    When it comes to providing generic mechanisms for recursively getting hold of single items of a list of delimited arguments, I tend to avoid the stripping-off of curly braces at the level of gathering an item and to leave that to those mechanisms by which a single item, after having grabbed it, is to be processed. (When processing lists of undelimited arguments, you cannot avoid the stripping-off of the outermost level of surrounding curly braces if present.) If you wish to detect whether an entire argument is probably surrounded by curly braces, you can apply a macro which gobbles an argument and see if that yields emptiness. If that is the case you can safely apply a macro which just spits out its argument for safely removing one level of surrounding curly braces if present. (Actually this just cranks out whether an argument either is a single token/a collection of tokens nested between the same pair of matching curly braces or is a collection of several tokens whereof not all are nested in between the same pair of matching curly braces.) This way you can use surrounding curly braces for "hiding" things that otherwise might erroneously be recognized as argument-delimiters, e.g., in case a space-delimited argument itself shall contain spaces.

    But probably, in case an item itself is surrounded by curly braces, you wish to apply the entire routine with that item as its argument... In such scenarios you need to distinguish the case of the argument being a single token from the case of the argument being a collection of tokens that is entirely nested in curly braces. If you already know that one of the two is the case, it is sufficient to additionally detect whether the argument's first token is a curly brace; s.th. like the following can be used for testing this:

    \@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
    \newcommand\UD@firstoftwo[2]{#1}%
    \newcommand\UD@secondoftwo[2]{#2}%
    %%-----------------------------------------------------------------------------
    %% Check whether argument's first token is an explicit character of category 1:
    %% .............................................................................
    %% \UD@CheckWhetherBrace{<Argument which is to be checked>}%
    %%                      {<Tokens to be delivered in case that argument
    %%                        which is to be checked has a leading
    %%                        explicit catcode-1-character-token>}%
    %%                      {<Tokens to be delivered in case that argument
    %%                        which is to be checked does not have a
    %%                        leading explicit catcode-1-character-token>}%
    \newcommand\UD@CheckWhetherBrace[1]{%
      \romannumeral\expandafter\UD@secondoftwo\expandafter{\expandafter{%
      \string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
      \UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@firstoftwo}{%
      \expandafter\UD@stopromannumeral\UD@secondoftwo}%
    }%
    
  • With your second example's variant of \@withword the \do-delimited argument is to be a control-sequence-token that throughout the iteration serves as a scratch-macro which in each iteration is redefined whereby the definition-text comes from the list-item which currently is processed. This needs to be taken into account in case that item itself shall deliver hashes, because in this case hashes need to be doubled. E.g., for producing the text "#1:Element1. #2:Element2. " you'd need to do:

    \forword\myword in {\string##1:Element1 \string##2:Element2}{\myword. }%
    

    So a question is: Is the need of doubling hashes with some of the macro-arguments acceptable?

  • Due to (re)defining the scratch-macro (\variable in your commenting of \@withword), the mechanism does not work out in situations where only macro-expansion takes place but assignments are not carried out. Such situations colloquially are called "pure expansion contexts". E.g., with tokens between \csname..\endcsname you have such a pure-expansion-context. Here expansion of tokens between \csname and the matching \endcsname yielding unexpandable tokens like \def, which cannot be a component of the name of a control-sequence-token, leads to some error-messages. E.g., the definition-text of an \edef-definition forms such a context. Here you probably won't get error-messages immediately but unexpandable \def-tokens occurring while expanding the definition-text of the \edef-definition might just end up as tokens of the macro's definition-text rather than being carried out. E.g., \write and \immediate\write form such a context: Expandable tokens to be written get expanded, but if expansion yields unexpandable tokens like \def, those won't be carried out but will be written to file/screen according to TeX's rules for writing tokens.

    So a question is: What about the requirement of expandability/about the requirement of the mechanism working out in pure expansion contexts as well?

  • Iteration/recursion often is a problem when it is to be used with \halign or within tabular-environments: Each table-cell forms its own local scope. Thus if in each iteration a scratch-macro is defined and the code of the \inlist-delimited-argument (which forms the code to be executed in each iteration and where that scratch-macro can be used for denoting the current list-item) also delivers & for creating another table-cell, then the definition of the scratch-macro is gone after the &. This means within the code provided as \inlist-delimited-argument the scratch-macro can be used for denoting the current list-item only before the first &.

    So a question is: Shall the mechanism work out within things like tabular-environments as well?

  • Another issue is the treatment of emptiness:

    • What to do if the list itself, which is nested between \inlist...\endlist, is empty? Shall nothing be done? Or shall one iteration take place, whereby the to-be-processed word is empty?
    • What to do if the list itself has a leading space token? Shall nothing be done? Shall this be taken for a first word which is empty so that one iteration takes place whereby the to-be-processed word is empty?
    • What to do if the list itself has a trailing space token? Shall nothing be done? Shall this be taken for a last word which is empty so that one iteration takes place whereby the to-be-processed word is empty?
    • What to do if the list itself consists of a space token only? Shall nothing be done? Shall this be taken for two empty words being separated by one space?
    • You can trick consecutive space tokens into the middle of the list. E.g., with
      \def\spaceinsert#1{%
         \forword \myword in {#1one#1#1two#1three#1#1four#1five#1six#1}%
      }%
      \spaceinsert{ }{List member is `\myword''\par}
      
      
      you have an empty item at the beginning of the list, an empty item after \one#1 and you have emptiness after six.
      What to do in case an empty item occurs somewhere in the middle of the list?

    In cases of emptiness the code presented by you does nothing. I don't know if you prefer a programming style where things are kept simple and readable and easily maintainable at the cost of probably not handling every edge situation? Are you interested in making the code user-proof, so that rather strange user-input is handled as well at the cost of making it more complicated. I prefer the further when writing stuff for me privately, because I know about the limitations

  • The code presented by you makes use of delimited arguments. Thus usually arguments themselves should not contain the respective sequences of delimiting tokens (unless "hidden" by nesting between a pair of matching curly braces whose stripping-off in the right moment needs to be taken care of) as otherwise these sequences might erroneously match up delimiters.

    So a question is: Is it acceptable to have sequences of tokens which the user is forbidden to use within arguments?

  • Another question is: How sloppy can you be with the termination condition of your loop/recursion/iteration?
    E.g., with the second of your examples the definition of \@withword is:

    \long\gdef\@withword#1\do#2\inlist#3 #4#5\endlist{%
         \if\relax\detokenize{#3}\relax\else\def#1{#3}#2\fi%
         \if\relax\detokenize{#4}\relax\else\@withword{#1}\do{#2}\inlist#4#5
             {}\endlist\fi%
    }
    

    The condition for stopping the recursion is the emptiness of \@withword's 4th argument.
    Let's look at:

    \@withword\ThisListItem\do{In this iteration the item is: \ThisListItem}\inlist Item1 {}Item2 Item3 Item4 {}\endlist
    

    The loop terminates when processing {}Item2.

    By the way: With each iteration another empty brace-group is appended to \@withword's fifth \endlist-delimited argument.

  • When you have mixtures of delimited and undelimited arguments you may need to be picky about brace-stripping - let's look at:

    \@withword\ThisListItem\do{In this iteration the item is: \ThisListItem}\inlist Item1 {Item}2 Item3 Item4 {}\endlist
    

    In the first iteration you loose the braces that surround the phrase Item of {Item}2.

  • A pitfall with macro definitions that has almost cult status is the situation of putting placeholders for macro arguments (#1, #2, ... #9) inside \if..\else..\fi expressions. If in a macro call the arguments contain unbalanced \else or \fi, then you get unexpected behavior because the \if-tokens from the macro definitions are matched by the \else or \fi-tokens from the macro arguments instead of being matched by those \else or \fi that come from the macro definition. The situation can then turn very intricate, also because \if..\else..\fi-matching is independent of group nesting. Often you can easily circumvent the like pitfalls by doing

    \long\def\firstoftwo#1#2{#1}%
    \long\def\secondoftwo#1#2{#2}%
    \if.. \expandafter\firstoftwo\else\expandafter\secondoftwo\fi
    {<tokens in case condition is true>}%
    {<tokens in case condition is false>}%
    

    instead of

    \if.. %
      <tokens in case condition is true>%
    \else
      <tokens in case condition is false>%
    \fi
    

    The further approach also resolves another issue: The entire \if..\else..\fi-expression is processed and the tokens of the branch that is not selected are removed before it comes to processing the tokens of the selected branch. Accumulation of \fi, which might lead to memory-issues, is prevented in case of repeatedly calling another iteration from within a forking-branch.

    Assume you wish to deliver the stringification of some tokens and with the \@withword-variant of your second example do:

    \@withword\ThisListItem\do{This is the token {\tt\expandafter\string\ThisListItem}}\inlist{\LaTeX} {\fi} {}\endlist
    

    In the first iteration #4 is \fi. That is inserted right behind \inlist and erroneously matches up things - you get:

      \if\relax\detokenize{\LaTeX}\relax\else\def\ThisListItem{\LaTeX}This is the token {\tt\expandafter\string\ThisListItem}\fi%
      \if\relax\detokenize{\fi}\relax\else\@withword{\ThisListItem}\do{This is the token {\tt\expandafter\string\ThisListItem}}\inlist\fi<space token>{}%<-this \fi is a problem now.
          {}\endlist\fi%
    
  • In my humble opinion a very important aspect is:
    Where is the focus of the iterative routines which you are implementing?
    Is the focus on the result of typesetting things, i.e., on what you can see when viewing the pdf file?
    Is the focus on obtaining a set of tokens which can be passed on to some other macro for further processing?
    Assume you have a list of names "Joe William Jack Averell".
    Applying means of recursion for having TeX place the phrases "Hello, Joe!", "Hello, William!", "Hello, Jack!", "Hello, Averell!" into the pdf-file is a different task than obtaining a set of tokens Hello, Joe! Hello, William! Hello, Jack! Hello, Averell! . The latter usually involves having a macro-argument where the result of running the recursion-based routine is accumulated.

    If the focus is on typesetting outside tabular-environments and the like, and if I mind neither the token \endlist not being allowed to be used in arguments unless nested in curly braces, nor the need of hash-doubling with list-items, I might probably do something like the following which does strip one level of curly braces that surround entire items of the list of space separated items so that you can have an empty item by explicitly specifying an empty brace-group and so that you can have list-items with space-tokens and/or tokens \endlist by nesting the entire item inside curly braces:

    \catcode`\@=11
    % Paraphernalia I often use:%%%%%%%%%%%%
    \long\def\gobble#1{}%
    \long\def\firstofone#1{#1}%
    \long\def\firstoftwo#1#2{#1}%
    \long\def\secondoftwo#1#2{#2}%
    \long\def\gobbletwo#1#2{}%
    \long\def\PassFirstToSecond#1#2{#2{#1}}%
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    \long\def\@withword#1\do#2\inlist{\@@withword{#1}{#2}{{}}}% In the following {{}} is prepended before grabbing delimited argument.
                                                              % This prevents brace-removal.
                                                              % But the prepended thing needs to be removed after grabbing the delimited argment.
                                                              % Two levels of braces because one might be stripped off in case the item is empty,
                                                              % and \gobble shall work anyway.
    \long\def\@@withword#1#2#3\endlist{\@withwordloop{#1}{#2}#3 \endlist}%
    \long\def\@withwordloop#1#2#3 #4\endlist{%
      % Check if the {{}}-prepended item is empty:
      \ifcat$\detokenize\expandafter{\gobble#3}$\expandafter\gobble\else\expandafter\firstofone\fi
        {%
          % Check if the {{}}-prepended item might be surrounded by curly braces:
          \ifcat$\detokenize\expandafter{\gobbletwo#3}$\expandafter\firstoftwo\else\expandafter\secondoftwo\fi
          {\expandafter\PassFirstToSecond\expandafter{\secondoftwo#3}}%
          {\expandafter\PassFirstToSecond\expandafter{\gobble#3}}%
          {\def#1}#2%
        }%
      \ifcat$\detokenize{#4}$\expandafter\gobble\else\expandafter\firstofone\fi
        {\@withwordloop{#1}{#2}{{}}#4\endlist}%
    }%
    \@withword\ThisItem\do{\par\noindent In this iteration the item is: ``{\tt\detokenize\expandafter{\ThisItem}}''}\inlist One {Two} {Three Three} {} {four \endlist} five\fi five \endlist
    \bye
    

    enter image description here


This is the first part of my answer.
The second part of my answer is at https://tex.stackexchange.com/a/654692.

You must log in to answer this question.

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