28

I'd like a command that works like this:

\dothings{Takes several words as argument}

and have this expand to:

\@dothings{Takes} \@dothings{several} \@dothings{words}...

I know how to define a simple macro that takes everything up to the next space, but I don't know how to use that to apply it to each word in a sentence. Is there a simple way to do this?

I'm also curious about taking the next letter as an argument, but I guess that's maybe a separate question...

Seamus
  • 73,242
  • I just found this related question: http://tex.stackexchange.com/questions/11595/iterate-over-space-separated-list – Seamus Aug 29 '11 at 10:40

4 Answers4

26
\documentclass{article}
\usepackage{xcolor}
\makeatletter
\def\dothings#1{\color{blue}\expandafter\dothings@i#1 \@nil}
\def\dothings@i#1 #2\@nil{%
  \color{.!80}#1
  \ifx\relax#2\relax\else\dothings@i#2\@nil\fi}
\makeatother

\begin{document}
\dothings{Takes several words as argument}

\end{document}

enter image description here

15

LaTeX3 now provides

\seq_set_split:Nnn<seq var>{<delimiter>} {<argument>}

which splits the <argument> at each occurrence of <delimiter> (and strips spaces from both ends of each argument), and puts the result in the <seq var> (the list data structure in LaTeX3). Once you have that sequence, you can map a given function on all its items.

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\seq_new:N \l_foo_seq
\NewDocumentCommand{\dothings}{m}
  {
    \seq_set_split:Nnn \l_foo_seq {~} {#1}
    \seq_map_inline:Nn \l_foo_seq { \fbox{##1} }
  }
\ExplSyntaxOff
\begin{document}
\dothings{Takes several words as argument}
\end{document}

EDIT: Actually you can do it faster with xparse's argument processors (here \SplitList).

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand {\dothings}
  { > { \SplitList { ~ } } m }
  { \tl_map_inline:nn {#1} { \fbox{##1} } }
\ExplSyntaxOff
\begin{document}
\dothings{Takes several words as argument}
\end{document}
9

Herbert has shown how to seperate words by spaces. Here is a very similar solution which prevent deep \ifx ... \fi nesting (Deep nesting may produce errors when many words is used):

\documentclass{minimal}
\begin{document}

\def\wordsloop#1{\wordsloopiter#1 \nil}
\def\wordsloopiter#1 #2\nil{%
  \do{#1} %
  \ifx&#2&% #2 is empty, then & equeals &
    \let\next\relax
  \else
    \def\next{\wordsloopiter#2\nil}% iterate
  \fi
  \next}

\def\do#1{(#1)}
\wordsloop{This is a sentence.}
% we have: (This) (is) (a) (sentence.)

\end{document}

However, it is still very slow if you use many words in the argument.


Loop on tokens can be obtained by similar approch, if you remove the spaces after #1 in the definitions above. But it can be easier:

\documentclass{minimal}
\begin{document}

\def\tokensloop#1{\tokensloopiter#1\nil}
\def\tokensloopiter#1{%
  \ifx\nil#1\else\do{#1}\expandafter\tokensloopiter\fi}

\def\do#1{(#1)}
\tokensloop{aa  bb}
% we have (a)(a)(b)(b)

\end{document}

You can also use \toksloop command from etextools package directly.

Note that spaces are ignored.


For spaces, catcode or other tricks may be used to read the characters verbatimly. That is more complex.

For example, we can use \obeyspaces to change the catcode of spaces:

\documentclass{minimal}
\begin{document}

\def\charsloop{\begingroup\obeyspaces\charsloophelper}
\def\charsloophelper#1{\charsloopiter#1\nil\endgroup}
\def\charsloopiter#1{%
  \ifx\nil#1\else\do{#1}\expandafter\charsloopiter\fi}

\def\do#1{(#1)}
\charsloop{aa  bb}
% we have (a)(a)( )( )(b)(b)

\end{document}

(Newlines are still ignored with \obeyspaces only.)

Leo Liu
  • 77,365
3

ConTeXt provides a macro \processisolatedwords that does exactly that. For example, to draw a frame around each word, you can use

\processisolatedwords {\input ward \relax} \inframed

In MkIV, this macro is implemented in lua using the following funcction:

local function applytowords(list,what,nested)
    local doaction = context[what or "ruledhbox"]
    local noaction = context
    local current  = checkedlist(list)
    local start
    while current do
        local id = current.id
        if id == glue_code then
            if start then
                doaction(copynodelist(start,current))
                start = nil
            end
            noaction(copynode(current))
        elseif nested and (id == hlist_code or id == vlist_code) then
            context.hbox()
            context.bgroup()
            applytowords(current.list,what,nested)
            context.egroup()
        elseif not start then
            start = current
        end
        current = current.next
    end
    if start then
        doaction(copynodelist(start))
    end
end

See also:

  • \applytowords
  • \processwords and \applytosplitstringword
Dave Jarvis
  • 11,809
Aditya
  • 62,301