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:
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

\expandafterin the definition of your\witerationdoesn't do anything (it tries to expand{which isn't expandable), the same is true for your\@withworddefinition. You'll get issues with lists with many elements, and the last list element has an additional{}added to it. – Skillmon Aug 19 '22 at 21:28\fiaccumulated which might cause memory-trouble. – Ulrich Diez Aug 19 '22 at 21:57\witeration{\foobar}{Item1 {}Item2 Item3}{Do this with \foobar}terminates after having\@withwordprocess Item1 because then\@withword's fourth argument is empty... – Ulrich Diez Aug 19 '22 at 23:34&and the like this might cause trouble in case of using the macros inside a\halignor atabular-environment or the like. – Ulrich Diez Aug 20 '22 at 00:10\expandafterwas a relic of previous experiments. Removed. – madmurphy Aug 20 '22 at 11:30Item1 {}Item2 Item3. I think it is an edge case I am willing to accept, since it requires a{}at the beginning (and only at the beginning) of a word. – madmurphy Aug 20 '22 at 11:40\fiaccumulation. – madmurphy Aug 20 '22 at 15:52\if\relax\detokenize{<set of tokens that forms the argument>}\relax...\else..\fifor testing whether the set of tokens that forms the argument is empty. If you like to be paranoid you can instead do\ifcat$\detokenize{<set of tokens that forms the argument>}$...\else..\fibecause this way you don't need to rely on the user not redefining\relaxin ways where expanding\relaxdistorts things. ;-) Currently I am writing an answer, but that might take some time. (E.g., I saw code where within local scopes you find\let\relax=\empty.) – Ulrich Diez Aug 20 '22 at 16:05\whilelist-delimited argument of\@withwordprovides unbalanced\elseor\fior the like, this might confuse the\if..\else..\fi-expressions of the definition of\@withword. Are you interested in "paranoia"-style-programming where such weirdnesses are handled to some degree? The code might turn out a bit more complicated. Are you interested in keeping things simple and leaving edge cases to those weird people who produce them? ;-) – Ulrich Diez Aug 20 '22 at 16:13\ifwith\ifcat. As for the hypothetical unbalanced\elseor\fiin the\whilelistargument, you will have to provide a convincing malevolent example :-) – madmurphy Aug 20 '22 at 17:02\ifcat, put a percent right behind the second$at the line's end .$yields an explicit character-token when reading/tokenizing TeX-input; usually a line-end after an explicit character-token yields an explicit space token (character-code 32, category 10(space)), which in (restricted) horizontal mode yields horizontal glue or - in not restricted horizontal mode - a place for a linebreak. (After a control-word-token - a control sequence whose name consists of several characters/only of characters of category 11(letter) - TeX skips spaces that occur in the input.) – Ulrich Diez Aug 20 '22 at 17:32{foo}bar, this will be turned intofoobar. – Ulrich Diez Aug 21 '22 at 02:27\@withwordappeding\long\gdef\@stop@withword\@withword#1\do#2\whilelist {}{foo}barbecomingfoobar. I should think of a solution… – madmurphy Aug 22 '22 at 19:15\@witerationmacro invokes\@withword, terminating the list with{} {} {}. The\@witerationmacro must capture exactly three arguments. But if I replace its{} {} {}with{} {}or{}, and then write in my document\@witeration{\myword}{one two three}{Item is ``\myword''\par}{This text should not be captured}, the argument{This text should not be captured}will be captured. – madmurphy Aug 22 '22 at 19:26\long\gdef\@stop@withword\@withword#1\do#2\whilelist {}, so that not 4 but 2 arguments are processed/removed, and append\@withwordis processing the last element of the list in the last but one iteration. Otherwise it is removed/discarded in the last iteration while grabbing#3because spaces are discarded when grabbing undelimited arguments. The brace group is#3. The 2nd space delimits empty space-delimited#4.\@witeration's#3which is considered empty. Subsequent space is considered empty#4's delimiter. So what is left and needs to be removed by\@stop@withwordis{} {}. Thereof\@stop@withwordgrabs the first{}as its undelimited#3and takes the following space as delimiter for an empty#4. So in any case a spurious{}is left in the token-stream. – Ulrich Diez Aug 23 '22 at 00:19