David Carlisle 最近针对以下问题提供了一个简洁且相当优雅的 TeX 解决方案:可扩展的“字符扫描”命令。不幸的是,David 的解决方案没有保留输入字符串中的空格。因此,给出 David 的代码:
\def\expandloop#1{\xloop#1\relax}
\def\xloop#1{%
\ifx\relax#1\else[#1]\expandafter\xloop\fi}
无论我们是否提供
\edef\zzzz{\expandloop{TEST1}}
或者
\edef\zzzz{\expandloop{ T ES T 1 }}
为什么会发生这种情况?
是否有一种解决方案,可以将空格字符与输入字符串中的其他字符相同地处理?
答案1
下面显示了三个循环的输出。
- 如上所述,原始版本删除了所有空间标记,即 catcode 10 的标记。
- 如果空格字符有 catcode 12(其他),那么它会被视为普通字符并且不会被删除。
- 变体循环会提前扫描以查找真实空格并使其安全,但正如@Ryan 的回答中所述,TeX 已将相邻的空格字符压缩为单个标记,因此
[ ]
每次运行空格时您只能获得一个标记。但是,此版本适用于普通的 catcode 10 个字符。 - 第四种是第一种的变体,它
\string
在循环中插入一个扩展,这样括号而不是括号组就会被视为单个标记。(正如 Bruno 所指出的,最初的三个版本并没有这样做)。
哪一个是最可接受的取决于你真正想要的是什么。
$ pdftex xloop2
This is pdfTeX, Version 3.1415926-2.3-1.40.12 (TeX Live 2011/Cygwin)
restricted \write18 enabled.
entering extended mode
(./xloop2.tex
2:[ ][ ][T][E][S][T][ ][ ][1]
3:[ ][T][E][S][T][ ][1]
3:[ ][T][ES][T][ ][1]
4:[T][{][E][S][}][T][1]
)
No pages of output.
Transcript written on xloop2.log.
\def\expandloop#1{\xloop#1\relax}
\def\xloop#1{%
\ifx\relax#1\else[#1]\expandafter\xloop\fi}
%%%%%%%%%%%%%%%%%%%%%%%%%%%
\edef\tmp{\def\noexpand\expandloopb##1{\noexpand\xloopb##1\relax\space \valign}}
\tmp
\def\xloopb#1 #2\valign{%
\ifx\relax#2\relax
\xxloopb#1%
\else
\xloopb#1\xloopb#2\valign
\fi}
\def\xxloopb#1{%
\ifx\relax#1\else[\ifx\xloopb#1\space\else#1\fi]\expandafter\xxloopb\fi}
%%%%%%%%%%%%%%%%%%%%%%%%%%%
\def\expandloopc#1{\xloopc#1!}
\def\xloopc{\expandafter\xxloopc\string}
\def\xxloopc#1{%
\ifx!#1\else[#1]\expandafter\xloopc\fi}
%%%%%%%%%%%%%%%%%%%%%
{\catcode`\ =12
\immediate\write20{2:\expandloop{ TEST 1}}}
\immediate\write20{3:\expandloopb{ TEST 1}}
\immediate\write20{3:\expandloopb{ T{ES}T 1}}
\immediate\write20{4:\expandloopc{ T{ES}T 1}}
\bye
答案2
可能有点过头了,但 Bruno Le Floch 在 LaTeX3 代码库中提出了“标记列表操作”的概念。这是一个通用概念,可以应用于不同的场景,例如\MakeUppercase 是否有纯粹可扩展的变体?。
我们没有为通用机制提供公共接口,因为不清楚是否需要这样做。因此,我在这里使用标准 TeX 编码重新编码了该方法。由于 LaTeX3 需要\pdfstrcmp
或等效功能,我正在加载pdftexmcds
包以提供\pdf@strcmp
。
\RequirePackage{pdftexcmds}
\makeatletter
\long\def\tl@action#1#2#3#4#5{%
\ifnum\iffalse{\fi`}=\z@\fi
\tl@action@loop#5\q@action@mark\q@action@stop
{#4}#1#2#3%
\tl@action@result{}%
}
\long\def\tl@action@loop#1\q@action@stop{%
\tl@if@head@N@type{#1}
{\tl@action@normal}
{%
\tl@if@head@grouped{#1}
{\tl@action@group}
{\tl@action@space}%
}%
#1\q@action@stop
}
\long\def\tl@action@normal#1#2\q@action@stop#3#4{%
\ifx\q@action@mark#1%
\expandafter\tl@action@end
\fi
#4{#3}#1%
\tl@action@loop#2\q@action@stop
{#3}#4%
}
\long\def\tl@action@end#1\tl@action@result#2{%
\ifnum`{=\z@}\fi
\z@
#2%
}
\long\def\tl@action@group#1#2\q@action@stop#3#4#5{%
#5{#3}{#1}%
\tl@action@loop#2\q@action@stop
{#3}#4#5%
}
\expandafter\long\expandafter\def\expandafter
\tl@action@space\space#1\q@action@stop#2#3#4#5{%
#5{#2}%
\tl@action@loop#1\q@action@stop
{#2}#3#4#5%
}
\long\def\tl@action@output#1#2\tl@action@result#3{%
#2%
\tl@action@result{#3#1}%
}
\def\q@action@mark{\q@action@mark}
\long\def\tl@if@head@N@type#1{%
\ifnum\pdf@strcmp
{\unexpanded\expandafter{\@firstofone#1{}}}{\unexpanded{#1{}}}=\z@
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi
}
\long\def\tl@if@head@grouped#1{%
\ifcat*\expandafter\@gobble\expandafter{\expandafter{\string#1?}}*%
\expandafter\@secondoftwo
\else
\expandafter\@firstoftwo
\fi
}
\long\def\tl@wrap#1{%
\unexpanded\expandafter{%
\romannumeral
\tl@action
\tl@wrap@normal
\tl@wrap@normal
\tl@wrap@space
{}
{#1}%
}%
}
\long\def\tl@wrap@normal#1#2{%
\tl@action@output{[#2]}%
}
\long\def\tl@wrap@space#1{%
\tl@action@output{[ ]}%
}
\makeatother
\documentclass{article}
\begin{document}
\makeatletter
\tl@wrap{abc de { } f}
\makeatother
\end{document}
在很多方面,仔细扫描空间和群组的想法与 Ryan 的相同,尽管这里的实现显然有点不同。(\tl@action
可以用于很多事情,因此有空洞和重复的论点!)
关于“为什么会发生这种情况?”这个问题,有两点需要注意。首先,TeX 会将多个空格读作一个空格。因此,
foo bar baz
将被解读为
foo bar baz
除非有类似的东西\obeyspaces
在起作用。后者必须在阅读材料之前设置,并且不可扩展。
其次,以下形式的宏
\def\foo#1{<do stuff>}
\foo
读取时会跳过后面的空格#1
。因此
\foo tokens
将跳过 之后的空格\foo
,并将其读取t
为第一个标记。为了避免这种情况,需要仔细控制标记,以免发生这种情况。
答案3
在 TeX 中,当宏被展开并收集其参数时,所有中间的空格都会被忽略。因此,\xloop
只需从一个字母跳到下一个字母并占用所有空间。我写道一个答案曾经有一个宏,它提供了一个完全可扩展的宏,用于“清理”文本:完全删除控制序列和括号。您可以修改该答案,例如,[]
通过进行以下更改来包围每个标记:\SanitizeTokens
用以下宏替换那里给出的:
\newcommand\SanitizeTokens[1]{%
\ifx\SanitizeStop#1%
\else
\ifx\SanitizedSpace#1%
\RealSpace
\else
\ifx\ #1%
\RealSpace
\else
\if\relax\noexpand#1%
[\string#1]%
\else
[#1]%
\fi
\fi
\fi
\expandafter\SanitizeTokens
\fi
}
这将产生以下效果:
\Sanitize{TEST1} = [T][E][S][T][1]
\Sanitize{ T E S T 1} = [T] [E] [S] [T] [1]
\Sanitize{T{EST}1} = [T][E][S][T][1]
\Sanitize{TEST\macro} = [T][E][S][T][\macro]
请注意,多个空格会被压缩。这是因为 TeX 本身在解析输入时会将重复的空格合并为一个空格。要解决这个问题,您必须使用 catcode 来解决问题,而且这是不可扩展的。可能的让花括号也包含在括号中,如果这对您来说是个问题。我不知道这是否可取。最后,我选择强制宏名称不扩展;\string
如果您愿意,您可以更改其中的行。
注意:您可能想知道,对于这个简单的更改,我编写的极其复杂的宏是否真的有必要。答案是肯定的:所有的复杂性都是为了小心翼翼地绕过宏解析器,以避免丢失空格和被组欺骗。