我想遍历以空格分隔的单词列表,因此我创建了以下宏:
%
%
% \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%
}
有了它,我可以做到
\withword\myword\do{%
List member is ``\myword''\par%
}\inlist one two three four five six\endlist
并得到
但是有两个问题:第一个问题是宏需要一个至少有两个成员的列表,第二个问题是我无法用one two three four five six
替换\mylist
。
因此我决定将上面的宏转变为辅助宏(\@withword
),经过一番思考后,我得出了以下重定向:
\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}
我们可以通过以下方式测试这些宏:
\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
我想知道您是否认为我的方法是正确的,或者您是否对如何改进它有什么建议。
编辑
在评论让我意识到我的宏不断嵌套\if...\fi
语句后,我重写了代码。
现在只有两个用户宏。一个宏(\withword
)用于迭代文字列表:
\withword\myvariable\do{%
run for each member%
}\throughlist word1 word2 ... wordN\endlist
另一个宏(\forword
)用于迭代文字和非文字列表:
\forword\myvariable\in{space-separated list}{%
run for each member%
}
以下是代码:
\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
测试:
\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
结果:
我已经用数千个单词测试了这些更新的宏,它们并没有损坏。
答案1
我会使用expl3
。该命令\forword
接受两个参数;如果您使用,\forword*
第一个参数应该是包含列表的宏。第二个参数是要使用的模板,其中当前项目用 表示#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}
可以*
通过检查第一个参数并确定它是否是单个宏名来避免这种情况。
\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
下面定义了一个完全可扩展的循环,\slistloop
用于迭代空格分隔列表的元素。该宏恰好以两步扩展并将其结果留在 内\unexpanded
,因此不会在或\edef
上下文中进一步扩展\expanded
。
要扩展列表的第一个标记一次(例如,循环遍历变量的内容),您可以使用\slistloopO
。要完全扩展列表,您可以使用\slistloopE
(在 内部扩展\expanded
,而不是在 中扩展\edef
,这大致相同,只是您不必将#
标记加倍)。
\slistloop@stop
在您的列表中,不应使用该令牌。
此外,还提供了一个不可扩展的循环\slistinline
,您可以在其中定义应该用于每个元素的代码作为第一个参数(请参阅使用 的列表元素#1
)。为此,还添加了O
和变体。E
\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}
答案3
这是我的回答的第二部分。
我的答案的第一部分在https://tex.stackexchange.com/a/654681
如果重点是在两个扩展步骤内仅通过扩展来获取一组标记,而无需加倍哈希值,没有任何(分隔)标记序列(除\outer
-tokens 外)被禁止出现在参数中,即使在表格环境中,不使用 eTeX 扩展或类似机制本身,我也可以提供一个缓慢而繁琐的例程
\UD@iterateSpaceList{⟨prepend-tokens⟩}%
{⟨append-tokens⟩}%
{⟨separate-tokens⟩}%
{⟨space-separated list⟩}
交付
⟨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⟩%
更多信息请参阅代码注释。
\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}
这是我的回答的第二部分。
我的答案的第一部分在https://tex.stackexchange.com/a/654681
答案4
这是我的回答的第一部分。
我的答案的第二部分在https://tex.stackexchange.com/a/654692。
您已经给出了一个处理逗号分隔的项目列表的机制的代码,这个机制经过了深思熟虑。它已经揭示了很多关于所需功能的信息。我喜欢自上而下的方法,即首先实现一个通用解决方案\@withword
,然后在不同的“应用程序中”使用它。
然而,就所需的功能而言,并不是每一个细微方面都明确提到了。因此,我想谈一些相当细微的事情:
代码基于处理参数的宏来工作。如果整个宏参数都被花括号包围,那么 TeX 通常会在从标记流中获取该参数时去掉最外面的一对花括号。对于分隔参数,如果不希望去掉花括号,可以使用一些技巧来防止去掉花括号。花括号本身通常不是排版元素,因此对于 pdf 文件文本的外观,排版时内容是否仍嵌套在花括号中通常没有区别。但在数学模式排版中,花括号的包围可能会有所不同。当将参数作为其他宏的参数传递时,是否存在包围花括号也会产生影响。
所以问题是:如果您的空格分隔列表本身的某个项目完全被括号包围,您希望采取什么行为?
第二个示例中展示的从列表中抓取单个项目的机制会剥去周围的括号。因此,例如,
\def\gobble#1{}% \forword\myword in {{{one1}{one2}} {two}}{After gobbling the first component you have: \expandafter\gobble\myword\\}`
在某个阶段你得到
\def\myword{{one1}{one2}}% ... After gobbling the first component you have: \expandafter\gobble\myword\\
,从而得出:
After gobbling the first component you have: \gobble{one1}{one2}\\
,从而得出:
After gobbling the first component you have: {one2}\\
如果周围的牙套没有被剥去,你会看到:
\def\myword{{{one1}{one2}}}% ... After gobbling the first component you have: \expandafter\gobble\myword\\
,从而得出:
After gobbling the first component you have: \gobble{{one1}{one2}}\\
,从而得出:
After gobbling the first component you have: \\
,这是一个略有不同的结果。
当要提供通用机制以递归方式获取分隔参数列表中的单个项时,我倾向于避免在收集项的级别剥离花括号,而将其留给那些在抓取单个项之后要对其进行处理的机制。(处理未分隔的参数列表时,如果存在最外层的花括号,则无法避免剥离该花括号。)如果您希望检测整个参数是否可能被花括号包围,则可以应用一个宏来吞噬参数并查看是否会产生空值。如果是这种情况,您可以安全地应用一个宏,该宏只会吐出其参数,以安全地删除一层周围的花括号(如果存在)。 (实际上,这只是确定一个参数是单个标记/嵌套在同一对匹配的花括号之间的标记集合,还是多个标记的集合,其中并非所有标记都嵌套在同一对匹配的花括号之间。)这样,您可以使用周围的花括号来“隐藏”可能被错误地识别为参数分隔符的东西,例如,以空格分隔的参数本身包含空格。
但可能,如果某个项本身被花括号包围,您希望以该项目作为其参数来应用整个例程……在这种情况下,您需要区分参数是单个标记的情况和参数是完全嵌套在花括号中的标记集合的情况。如果您已经知道其中一种情况,那么额外检测参数的第一个标记是否是花括号就足够了;可以使用类似下面的方法进行测试:
\@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}% }%
第二个示例的 -delimited 参数变体
\@withword
是\do
控制序列标记,在整个迭代过程中充当临时宏,每次迭代都会重新定义,其中定义文本来自当前正在处理的列表项。如果该项目本身将提供哈希值,则需要考虑这一点,因为在这种情况下哈希值需要加倍。例如,要生成文本“#1:Element1. #2:Element2.”您需要执行以下操作:\forword\myword in {\string##1:Element1 \string##2:Element2}{\myword. }%
所以问题是:使用一些宏参数加倍哈希值的需要是否可以接受?
由于(重新)定义了临时宏(
\variable
在 的注释中\@withword
),该机制在仅发生宏扩展但不执行赋值的情况下不起作用。这种情况通俗地称为“纯扩展上下文”。例如,在 之间有标记,您\csname..\endcsname
就有这样的纯扩展上下文。在这里,在 和\csname
之间的标记的扩展\endcsname
会产生不可扩展的标记,例如\def
,这些标记不能成为控制序列标记名称的组成部分,从而导致一些错误消息。例如,-definition 的定义文本\edef
形成了这样的上下文。在这里,您可能不会立即收到错误消息,但\def
在扩展 -definition 的定义文本时出现的不可扩展的 -tokens\edef
可能最终会成为宏的定义文本的标记,而不是被执行。例如,\write
和\immediate\write
形成这样的上下文:要写入的可扩展标记会被扩展,但是如果扩展产生了诸如之类的不可扩展标记\def
,则这些标记将不会被执行,而是会根据 TeX 的写入标记规则将其写入文件/屏幕。那么问题是:在纯扩展环境中,可扩展性的要求/机制发挥作用的要求又如何呢?
\halign
迭代/递归在与-environments一起使用时经常会出现问题tabular
:每个表格单元都形成自己的局部范围。因此,如果在每次迭代中都定义了一个临时宏,并且\inlist
-delimited-argument 的代码(它构成了每次迭代中要执行的代码,并且该临时宏可用于表示当前列表项)也用于&
创建另一个表格单元,那么临时宏的定义将在 之后消失&
。这意味着在作为 -delimited-argument 提供的代码中,\inlist
临时宏只能在第一个 之前用于表示当前列表项&
。所以问题是:该机制是否
tabular
也适用于诸如环境之类的事物?另一个问题是对空虚的处理:
- 如果嵌套在 之间的列表本身
\inlist...\endlist
为空,该怎么办? 什么都不做吗? 还是进行一次迭代,从而使要处理的单词为空? - 如果列表本身有一个前导空格标记该怎么办?什么都不做吗?这是否应该被视为第一个单词为空,以便进行一次迭代,从而使要处理的单词为空?
- 如果列表本身有一个尾随空格标记,该怎么办?什么都不做吗?这是否应该被视为最后一个为空的单词,以便进行一次迭代,从而使要处理的单词为空?
- 如果列表本身仅由空格标记组成,该怎么办?什么都不做吗?这应该被理解为两个空词被一个空格隔开吗?
- 你可以将连续的空格标记放入列表中间。例如,
列表开头有一个空项,后面有一个空项\def\spaceinsert#1{% \forword \myword in {#1one#1#1two#1three#1#1four#1five#1six#1}% }% \spaceinsert{ }{List member is `\myword''\par}
\one#1
,后面也是空的six
。
如果列表中间某处出现空项,该怎么办?
如果代码为空,您提供的代码将不执行任何操作。我不知道您是否喜欢这样一种编程风格,即保持简单、可读且易于维护,但代价是可能无法处理所有边缘情况?您是否有兴趣让代码对用户友好,这样就可以处理相当奇怪的用户输入,但代价是使其更加复杂。我私下为我写东西时更喜欢 further,因为我知道它的局限性
- 如果嵌套在 之间的列表本身
您提供的代码使用了分隔参数。因此,通常参数本身不应包含相应的分隔标记序列(除非通过嵌套在一对匹配的花括号之间而“隐藏”,而花括号在适当的时候需要处理这些花括号的剥离),否则这些序列可能会错误地匹配分隔符。
所以问题是:是否可以接受禁止用户在参数中使用标记序列?
另一个问题是:你的循环/递归/迭代的终止条件可以有多草率?
例如,在你的第二个例子中,的定义\@withword
是:\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% }
停止递归的条件是
\@withword
第 4 个参数为空。
我们来看看:\@withword\ThisListItem\do{In this iteration the item is: \ThisListItem}\inlist Item1 {}Item2 Item3 Item4 {}\endlist
处理时循环终止
{}Item2
。顺便说一句:每次迭代时,都会将另一个空的大括号组附加到
\@withword
第五个\endlist
分隔参数。当您混合使用定界和不定界参数时,您可能需要对括号剥离特别挑剔 - 让我们看看:
\@withword\ThisListItem\do{In this iteration the item is: \ThisListItem}\inlist Item1 {Item}2 Item3 Item4 {}\endlist
Item
在第一次迭代中,您会丢失短语周围的括号{Item}2
。宏定义中一个几乎众所周知的陷阱是将宏参数的占位符(
#1
、#2
、...#9
)放在\if..\else..\fi
表达式中的情况。如果在宏调用中,参数包含不平衡的\else
或\fi
,那么您将得到意外的行为,因为来自宏定义的 -token 与来自宏参数的或-token\if
匹配,而不是与来自宏定义的或匹配。情况可能变得非常复杂,也是因为-matching 与组嵌套无关。通常,您可以通过执行以下操作轻松规避类似的陷阱\else
\fi
\else
\fi
\if..\else..\fi
\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>}%
代替
\if.. % <tokens in case condition is true>% \else <tokens in case condition is false>% \fi
进一步的方法还解决了另一个问题:处理整个
\if..\else..\fi
-表达式,并在处理所选分支的标记之前删除未选定分支的标记。\fi
如果在分叉分支中重复调用另一个迭代,则可以防止 的累积,这可能会导致内存问题。假设您希望传递一些标记的字符串化,并使用
\@withword
第二个示例的变体执行以下操作:\@withword\ThisListItem\do{This is the token {\tt\expandafter\string\ThisListItem}}\inlist{\LaTeX} {\fi} {}\endlist
在第一次迭代中
#4
是\fi
。它被插入到后面\inlist
并错误地匹配事物 - 您会得到:\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%
在我看来,一个非常重要的方面是:
您正在实施的迭代例程的重点在哪里?
重点是排版结果,即您在查看 pdf 文件时可以看到的内容吗?
重点是获取一组可以传递给其他宏进行进一步处理的标记吗?
假设您有一个名称列表“Joe William Jack Averell”。
应用递归方法让 TeX 将短语“Hello, Joe!”、 “Hello, William!”、 “Hello, Jack!”、 “Hello, Averell!”放入 pdf 文件中是一项与获取一组标记不同的任务Hello, Joe! Hello, William! Hello, Jack! Hello, Averell!
。后者通常涉及一个宏参数,其中累积了运行基于递归的例程的结果。如果重点是在
tabular
环境之外的排版等,并且如果我既不介意令牌\endlist
不允许在参数中使用(除非嵌套在花括号中),也不介意使用列表项进行哈希加倍,我可能会做类似下面的事情,它会剥离围绕空格分隔项列表的整个项的一层花括号,以便您可以通过明确指定一个空的括号组来获得一个空项,并且您可以通过将\endlist
整个项嵌套在花括号内来获得带有空格令牌和/或令牌的列表项:\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
这是我的回答的第一部分。
我的答案的第二部分在https://tex.stackexchange.com/a/654692。