5

Am having difficulty determining the correct syntax for using \csuse from the etoolbox package with a \foreach.

I have a list, and for each element in this list I need to create a new list. To simplify the MWE, I used \IfStrEq to define the new list using \csgdef.

The problem in in the second \foreach where I have an additional curly brace:

{\csuse{ContentFor\x}}

which should not be needed as per TikZ \foreach loop with macro-defined list.


If I leave out the {} in the second \foreach I get:

Argument of \csuse has an extra }

With the {} it compiles and I get:

enter image description here

However, the desired output is:

enter image description here

Code:

\documentclass{article}
\usepackage{xstring}
\usepackage{etoolbox}
\usepackage{pgffor}

\newcommand*{\MyList}{ABC,XYZ}%

\begin{document}

%% For each element of \MyList I need to create a new list.
\foreach \x in \MyList {%
    % In my actual use case the list that gets defined for each element of \MyList
    % is based on timestamps of files, but as that is not relevant to this 
    % particular issue I left out that complexity, to make this test case easier to read.
    \IfStrEq{\x}{ABC}{%
        \csgdef{ContentFor\x}{FileA,FileB,FileC}%
    }{%
        \csgdef{ContentFor\x}{FileX,FileY,FileZ}%
    }%
}%

% Now, only after I have ALL the lists created, I want to print EACH list
% for each element of \MyList.
\foreach \x in \MyList {%
    \par\x=
    \foreach \y in {\csuse{ContentFor\x}} {% <--- The {} around \csuse{} should NOT be necessary.
        \par\hspace*{1.0cm}\y
    }%
}%

%%% This is the expected output:
%   \par ABC=
%   \foreach \y in {FileA,FileB,FileC} {%
%       \par\hspace*{1.0cm}\y
%   }%
%   \par XYZ=
%   \foreach \y in {FileX,FileY,FileZ} {%
%       \par\hspace*{1.0cm}\y
%   }%

\end{document}
Peter Grill
  • 223,288

2 Answers2

4

It's the usual expansion issue. If \foreach finds a brace, it doesn't do expansion and expects a list, which in this case is just one element, that is \csuse{ContentFor\x}. Otherwise, it expands once the token that follows and the expansion should be a list, putting it in braces. However, the first level expansion of \csuse{ContentFor\x} is

\ifcsname ContentFor\x\endcsname\csname\ContentFor\x\expandafter\endcsname\fi

which is definitely not a list.

You have to fully expand the \csuse. Depending on what's expected to be in the list, you need one of the following two methods.

First method

The usual full expansion method

\foreach \x in \MyList {%
\par\x=
\begingroup\edef\x{\endgroup\noexpand\foreach\noexpand\y in {\csuse{ContentFor\x}}}\x {%
    \par\hspace*{1.0cm}\y
    }%
}

will do if the list is fully expandable, that is, it contains only ASCII characters. If the list may contain “dangerous” items, such as \textbf.

Second method

If the list contains “dangerous” items, you have to limit its expandability:

\foreach \x in \MyList {%
    % In my actual use case the list that gets defined for each element of \MyList
    % is based on timestamps of files, but as that is not relevant to this 
    % particular issue I left out that complexity, to make this test case easier to read.
    \IfStrEq{\x}{ABC}{%
        \protected\csgdef{ContentFor\x}{FileA,FileB,FileC}%
    }{%
        \protected\csgdef{ContentFor\x}{FileX,FileY,FileZ}%
    }%
}

\foreach \x in \MyList {%
\par\x=
\begingroup\edef\x{\endgroup\noexpand\foreach\noexpand\y in {\csuse{ContentFor\x}}}\x {%
    \par\hspace*{1.0cm}\y
    }%
}

Note that \csuse requires two steps of expansion to arrive at, say, \ContentForABC.


Just for fun, an implementation with xparse and expl3:

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn
\NewDocumentCommand{\createlist}{ m m }
 {
  \clist_new:c { g_grill_list_#1_clist }
  \clist_gset:cn { g_grill_list_#1_clist } { #2 }
 }

\NewDocumentCommand{\createcontents}{ m m }
 {
  \clist_new:c { g_grill_list_content_#1_clist }
  \clist_set:cn { g_grill_list_content_#1_clist } { #2 }
 }

\NewDocumentCommand{\processlist}{ m +m }
 {
  \clist_map_inline:cn { g_grill_list_#1_clist } { #2 }
 }

\NewDocumentCommand{\processcontents}{ m +m }
 {
  \clist_map_inline:cn { g_grill_list_content_#1_clist } { #2 }
 }
\ExplSyntaxOff

\createlist{MyList}{ABC,XYZ}
\createcontents{ABC}{FileA, FileB, FileC}
\createcontents{XYZ}{FileX, FileY, FileZ}

\begin{document}

\processlist{MyList}{%
  \par#1=
  \processcontents{#1}{\par\hspace*{1cm}##1}%
}
\end{document}

In the second argument to \processlist and \processcontents you can refer to the current item (\x and \y in your \foreach statements) as #1; in the inner \processcontents you have to double the #.

enter image description here

egreg
  • 1,121,712
  • Based on your earlier comment I found that \edef\ContentForThisX{\csuse{ContentFor\x}} followed by \foreach \y in \ContentForThisX works just fine. Is there an inherent problem in this for the case of no "dangerous" content? – Peter Grill May 05 '14 at 00:48
  • @PeterGrill No, it's just the same as the first method, just slightly less efficient. – egreg May 05 '14 at 06:43
  • Correction ■ the 2-level expansion of \csuse is \ContentForABC\fi, not \ContentForABC. ■ (it's not recommended to rely on "undocumented behavior" (i.e. the macro requires exactly 2 expansion step) anyway. Could use csname or expl3's :c argument type) ■ (see the comment above for the edef, it's much cleaner, although I don't know if it's really less efficient or not -- I suspect not) ■ The problem is not just that the single expansion does not give the desired result, but that the initial content is not a single token. For example \def\f #1{#1,2}\foreach \x in \f{1}{\x} won't work – user202729 Jun 18 '22 at 00:51
0

This is the "modern" solution using TikZ expand list -- just in case someone come across this question in the future:

%! TEX program = lualatex

\documentclass{article} \usepackage{xstring} \usepackage{etoolbox} \usepackage{pgffor}

\newcommand*{\MyList}{ABC,XYZ}%

\begin{document}

%% For each element of \MyList I need to create a new list. \foreach \x in \MyList {% % In my actual use case the list that gets defined for each element of \MyList % is based on timestamps of files, but as that is not relevant to this % particular issue I left out that complexity, to make this test case easier to read. \IfStrEq{\x}{ABC}{% \csgdef{ContentFor\x}{FileA,FileB,FileC}% }{% \csgdef{ContentFor\x}{FileX,FileY,FileZ}% }% }%

% Now, only after I have ALL the lists created, I want to print EACH list % for each element of \MyList. \foreach \x in \MyList {% \par\x= \foreach[expand list] \y in {\csuse{ContentFor\x}} { \par\hspace*{1.0cm}\y }% }%

\end{document}

Credit to this answer.

user202729
  • 7,143