3

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.

  • The xstring package does that sort of thing, depending on whether you want the first, the nth, or the total number found. – John Kormylo Aug 11 at 14:32
5

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
| improve this answer | |
  • 1
    If 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}}% – Ulrich Diez Aug 11 at 9:34
  • @UlrichDiez Yes, that's much better (or at least more in-line with what e-TeX does :). Thanks! – Phelype Oleinik Aug 11 at 12:01
3

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

enter image description here

| improve this answer | |
3

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

enter image description here

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.