Is there a TeX macro (in plain or AmS-TeX) that wil search for a specific string of characters? Specifically, I want a macro which will be part of an if/then macro of the form, if a given string xyz appears in a word, then ..., otherwise something else.
3 Answers
In plain TeX
\input listofitems
\def\findstring#1#2#3#4{%
\setsepchar{#1}%
\readlist\mylist{#2}%
\ifnum\mylistlen>1\relax#3\else#4\fi
}
\findstring{abc}{My string ab*c does not contain it}{Found}{Not Found}
\findstring{abc}{My string abc does contain it}{Found}{Not Found}
\bye
This is a copy of expl3
's \tl_if_in:nn
(\IfContainedIn
) and \str_if_in:nn
(\StrIfContainedIn
). The former takes into account differences in catcodes, while the latter treats everything as catcode-12 (except for spaces, which are catcode-10). The syntax is \StrIfContainedIn{<string>}{<substring>}{<true>}{<false>}
. The macro defines an auxiliary which is delimited by the <substring>
, then uses that to check if the <string>
contains the <substring>
. There is extra safety against using this macro in table cells and for empty arguments (see the documentation of l3tl
for implementation details).
If you are using a TeX engine with ε-TeX (\detokenize
is needed to make a string out of the argument and for a robust \IfEmpty
test), then:
\catcode`\@=11
\def\@firstoftwo#1#2{#1}
\def\@secondoftwo#1#2{#2}
\def\return@if@false{\expandafter\@secondoftwo\romannumeral}
\def\return@if@true{\expandafter\@firstoftwo\romannumeral}
\chardef\exp@end=0
%
\def\IfEmpty#1{%
\if\relax\detokenize{#1}\relax
\return@if@true
\else
\return@if@false
\fi\exp@end}
%
\def\IfContainedIn#1#2{%
\relax\iffalse{\fi
\def\if@contained@aux##1#2{}%
\expandafter\IfEmpty\expandafter{\if@contained@aux#1{}{}#2}%
{\return@if@false}{\return@if@true}%
\iffalse}\fi
\exp@end}
%
\def\StrIfContainedIn#1#2{%
\edef\@@tmpa{\noexpand\IfContainedIn
{\detokenize{#1}}{\detokenize{#2}}}\@@tmpa
{\return@if@true}{\return@if@false}%
\exp@end}
\catcode`\@=12
\StrIfContainedIn{hello}{el}{T}{F}
\StrIfContainedIn{hello}{le}{T}{F}
\bye
Otherwise, if you are using Knuth's TeX, then you need a poorman's \detokenize
:
\def\@firstoftwo#1#2{#1}
\def\@secondoftwo#1#2{#2}
\def\strip@prefix#1>{}
\newtoks\myscratchtoks
\long\def\poorman@detokenize#1#2{%
\myscratchtoks{#2}\edef#1{\the\myscratchtoks}%
\edef#1{\expandafter\strip@prefix\meaning#1}}%
\def\return@if@false{\expandafter\@secondoftwo\romannumeral}
\def\return@if@true{\expandafter\@firstoftwo\romannumeral}
\chardef\exp@end=0
%
\def\IfEmpty#1{%
\poorman@detokenize\@@tmpa{#1}%
\if\relax\@@tmpa\relax
\return@if@true
\else
\return@if@false
\fi\exp@end}
%
\def\IfContainedIn#1#2{%
\relax\iffalse{\fi
\def\if@contained@aux##1#2{}%
\expandafter\IfEmpty\expandafter{\if@contained@aux#1{}{}#2}%
{\return@if@false}{\return@if@true}%
\iffalse}\fi
\exp@end}
%
\def\StrIfContainedIn#1#2{%
\poorman@detokenize\@@tmpa{#1}%
\poorman@detokenize\@@tmpb{#2}%
\edef\@@tmpa{\noexpand\IfContainedIn
{\@@tmpa}{\@@tmpb}}\@@tmpa
{\return@if@true}{\return@if@false}%
\exp@end}
\catcode`\@=12
\StrIfContainedIn{hello}{el}{T}{F}
\StrIfContainedIn{hello}{le}{T}{F}
\bye
-
1If you wish
\detokenize
's doubling of hashes, do:\newtoks\myscratchtoks\long\def\poorman@detokenize#1#2{\myscratchtoks{#2}\edef#1{\the\myscratchtoks}\edef#1{\expandafter\strip@prefix\meaning#1}}%
Aug 11, 2020 at 9:34 -
@UlrichDiez Yes, that's much better (or at least more in-line with what e-TeX does :). Thanks! Aug 11, 2020 at 12:01
A different approach from my other answer. This uses tokcycle
, instead of listofitems
. The expanded capability here is the ability to look deep down inside group content for the desired string, which is not possible with my other simpler answer. Likewise, macro names and groups can be part of the search string.
\input tokcycle
\def\findinstring#1#2#3#4{\begingroup%
\stripgroupingtrue
\runcount=0\relax%
\tokcycle{\nextctltok{##1}}
{\nextctltok{\opengroup}\processtoks{##1}\nextctltok{\closegroup}}
{\nextctltok{##1}}
{\nextctltok{\tcspace}}{#1}%
\edef\numlet{\the\runcount}%
\expandafter\def\expandafter\searchword\expandafter{\the\cytoks}%
\aftertokcycle{\expandafter\ifx\matchfound T#3\else#4\fi}%
\runcount=0\relax%
\def\matchfound{F}%
\tokcycle{\nextcmptok{##1}}
{\nextcmptok{\opengroup}\processtoks{##1}\nextcmptok{\closegroup}}
{\nextcmptok{##1}}
{\nextcmptok{\tcspace}}{#2}%
\endgroup}
\newcount\runcount
\catcode`@=11
\def\rotcytoks#1{\cytoks\expandafter\expandafter\expandafter{%
\expandafter\tc@gobble\the\cytoks#1}}
\catcode`@=12
\def\testmatch#1{\ifx#1\searchword\gdef\matchfound{T}\fi}%
\def\rotoradd#1#2{\runcount=\numexpr\runcount+1\relax%
\ifnum\the\runcount>\numlet\relax#1\else#2\fi\expandafter
\def\expandafter\tmp\expandafter{\the\cytoks}}
\def\nextcmptok#1{\rotoradd{\rotcytoks{#1}}{\addcytoks{#1}}\testmatch{\tmp}}
\def\nextctltok#1{\runcount=\numexpr\runcount+1\relax\addcytoks{#1}}
\findinstring{a\notmymac{b c}}{gf{vf{a\mymac{b c}g}gh}hn}{Found}{Not Found}
\findinstring{a\mymac{b c}}{gf{vf{a\mymac{b c}g}gh}hn}{Found}{Not Found}
\bye