13

At comp.text.tex the following scheme was suggested for testing if a string contains the parameter character with catcode 6.

\def\endcheck{\endcheck}
\def\second#1#2{#2}
\def\gobbletocheck#1\endcheck#2#3{#2}
\def\checkhash#1{\docheckhash#1\endcheck} 
\def\docheckhash#1{%
  \ifx#1\endcheck 
    \expandafter\second 
  \else 
    \ifx#1##% 
      \expandafter\expandafter\expandafter\gobbletocheck 
    \else 
      \expandafter\expandafter\expandafter\docheckhash 
    \fi
  \fi 
} 

This can be called as

\checkhash{abc#def}{YES}{NO} 
\checkhash{abcdef}{YES}{NO} 

Obviously, as suggested by its author, this test fails if the string to test contains brace groups with the first two tokens identical, e.g.,

\checkhash{ab{cc}def}{YES}{NO}

Please can anyone suggest a better test? Please note that the catcode of the hash character must be 6, not 12.

An expandable solution will be beautiful but not a necessity.

  • Can we use e-TeX? – Joseph Wright May 20 '13 at 16:20
  • Yes, an eTeX solution is welcome. – Ahmed Musa May 20 '13 at 18:46
13

This is easy, but not expandable:

\documentclass{article}
\usepackage{xparse,l3regex}
\ExplSyntaxOn
\NewDocumentCommand{\checkhash}{mmm}
 {
  \musa_checkhash:nnn { #1 } { #2 } { #3 }
 }

\cs_new_protected:Npn \musa_checkhash:nnn #1 #2 #3
 {
  \regex_match:nnTF { \cP. } { #1 } { #2 } { #3 }
 }
\ExplSyntaxOff

\begin{document}

\checkhash{abc#def}{YES}{NO}

\checkhash{abcdef}{YES}{NO}

\checkhash{ab{cc}def}{YES}{NO}

\checkhash{ab{c#c}def}{YES}{NO}

\end{document}

The output is

YES
NO
NO
YES

The l3regex module of the expl3 (LaTeX3 experimental) bundle furnishes regex searches and replacements. In addition to a great part of the common regex machinery, it allows to take care of category codes. A character, character range or wild card can be preceded by a prefix telling what category code we want (these prefixes can be used both in the search and in the replacement text). The prefixes are

\cC \cB \cE \cM \cT \cP \cU \cD \cS \cL \cO \cA

corresponding, respectively, to control sequence, begin group (catcode 1), end group (catcode 2), math shift (catcode 3), alignment (catcode 4), parameter (catcode 6), superscript (catcode 7), subscript (catcode 8), space (catcode 10), letter (catcode 11), other character (catcode 12), active character (catcode 13). The missing codes can never reach the tokens that reach the mouth of TeX, so they don't have a prefix (escape, 0; end-of-line, 5; ignored, 9; comment, 14; invalid, 15). There is also \cC for denoting a control sequence.

We check, in the code, whether any character having category code 6 appears in the token list given as argument: the period is a wild card representing any character (actually token, in l3regex); preceding it with \cP means that only characters with category code 6 will match.

If only #6 characters are to be considered, then the regex to use would be

\cP\#

Note that spaces in a regex expression are ignored; use (i.e. backslash and space) for denoting a space that is part of a regex.

  • One thing to keep in mind is that this will only find explicit macro parameter characters (not implicit ones created by \let\@sharp #), because l3regex does not care about the \meaning of tokens. Actually, I'm not 100% sure that l3regex is robust against receiving implicit macro parameter characters. – Bruno Le Floch May 21 '13 at 17:44
10

As noted a test such as

 \ifx#1\endcheck 

is true if the first two tokens of #1 are equal. That part is easy to fix. When using \ifx tests always put the guard token first

 \ifx\endcheck#1 

Now you only get bad result if the input token is \endcheck you can make that less likely by having a harder to type token as the guard token.

So this is an expandable test removing two levels of brace groups (not more, although it can be extended)

\def\endcheck{\endcheck}
\def\second#1#2{#2}
\def\gobbletocheck#1\endcheck#2#3{#2}
\def\checkhash#1{\docheckhash#1\endcheck}
\def\firstofone#1{#1}
\def\docheckhash#1{\expandafter\xdocheckhash\firstofone#1}
\def\xdocheckhash#1{%
  \ifx\endcheck #1%
    \expandafter\second 
  \else 
    \ifx###1% 
      \expandafter\expandafter\expandafter\gobbletocheck 
    \else 
      \expandafter\expandafter\expandafter\docheckhash 
    \fi
  \fi 
} 

This can be called as

\checkhash{abc#def}{YES}{NO} 

\checkhash{abcdef}{YES}{NO} 


\checkhash{ab{cc}def}{YES}{NO}

\checkhash{ab{#d}f}{YES}{NO} 

\checkhash{ab{{#d}}f}{YES}{NO} 

\bye

this reports YES NO NO YES YES

  • 1
    \checkhash, \docheckhash, and \xdocheckhash are removing one argument group each. Thus the following example with argument group nesting depth four fails: \checkhash{{{{a#}}}}{yes}{NO} -> NO – Heiko Oberdiek May 20 '13 at 23:38
  • @HeikoOberdiek yes s as noted above, the code doesn't work for arbitrary depth, you can easily increase the depth by adding more firstofone calls, removing brace levels to arbitrary depth whilst staying expandable is also possible but complicates th eloop perhaps more than needed. – David Carlisle May 21 '13 at 4:52
9

An expandable solution using expl3, which finds explicit and implicit catcode 6 tokens at any level of nesting:

\documentclass{article}
\usepackage{expl3}
\ExplSyntaxOn
\prg_new_conditional:Npnn \musa_if_param:n #1 { TF }
  { \__musa_if_param:n { #1 ? } }
\cs_new:Npn \__musa_if_param:n #1
  {
    \tl_if_empty:nTF {#1} { \prg_return_false: }
      {
        \tl_if_head_is_N_type:nTF {#1}
          {
            \tl_if_head_eq_catcode:nNTF {#1} ##
              { \prg_return_true: }
              { \exp_args:No \__musa_if_param:n { \use_none:n #1 } }
          }
          { \exp_args:No \__musa_if_param:n { \use:n #1 } }
      }
  }
\musa_if_param:nTF { 123 ~ \foobar } { \error } { }
\musa_if_param:nTF { 12 { { 3 } 4 # } } { } { \error }
\ExplSyntaxOff
\stop
  • 2
    It breaks with ! Argument of \use:n has an extra } if space tokens are used, e.g. \musa_if_param:nTF { ~ } { \error } { }. – Heiko Oberdiek May 21 '13 at 18:03
  • @HeikoOberdiek: Thanks. Fixed, by first appending a non-# normal token. This way, \use:n is always safe, may remove more than one space, and may remove braces, but we don't care about those anyway. – Bruno Le Floch May 22 '13 at 9:36
8

Here is an expandable plain TeX solution. Please let me know if it is foolproof.

\let\do\noexpand
\edef\x#1{\def#1\do\ifx##1\detokenize{macro parameter character}##2\relax}
\x\bcheckhash{\ifx\checkhash#2\checkhash}
\def\endcheck{\endcheck}
\def\gobbletocheck#1\endcheck#2#3{#2}
\def\checkhash#1{\gocheckhash#1\endcheck}
\def\second#1#2{#2}
\edef\x{%
  \def\do\gocheckhash##1{%
    \do\ifx\do\endcheck##1%
      \do\expandafter\do\second
    \do\else
      \do\expandafter\do\bcheckhash\do\expandafter\do\ifx
      \do\meaning##1{\relax}\detokenize{macro parameter character}\relax
        \do\expandafter\do\expandafter\do\expandafter\do\gocheckhash
      \do\else
        \do\expandafter\do\expandafter\do\expandafter\do\gobbletocheck
      \do\fi
    \do\fi
  }%
}\x

Tests

\edef\x{\checkhash{ab{cc}de}{Y}{N}}
\edef\y{\checkhash{ab{cc}#de}{Y}{N}}
  • It seems good, but it checks only at the outer brace level. – egreg May 20 '13 at 20:48
  • Something in Appendix D might apply (finding asterisks at any brace level in an expandable way). – egreg May 20 '13 at 20:55
  • App. D of the TeXBook or TeX by Topic? Which page? – Ahmed Musa May 20 '13 at 21:45
  • The TeXbook, pages 375–376. However the check for a * at any group level is not expandable (and I guess it can't be). – egreg May 20 '13 at 22:01
  • @egreg: it is possible expandably. See (untested) code in my answer. – Bruno Le Floch May 21 '13 at 17:55
2

We can safely explore an infinite nest of brace groups by the following expandable scheme.

\makeatletter
\begingroup
\lccode`\&=1 \catcode`\&=7
\lccode`\*=1 \catcode`\*=11
\lccode`\?=1 \catcode`\?=8
\lowercase{\endgroup
  \gdef\if@func#1#2{\if@func@i#1x?#2*?}
  \gdef\if@func@i#1#2?#3#4?{%
    \csname @\ifx#1#3first\else second\fi oftwo\endcsname
  }
  \gdef\if@blank#1{%
    \csname @\ifcat?\detokenize\expandafter{\@gobble#1.}?%
    first\else second\fi oftwo\endcsname
  }
  \gdef\gobbletoendhashcheck#1&#2#3{#2}
  \gdef\checkforhash#1{\docheckhash@i#1&}
  \gdef\docheckhash@i#1{%
    \if@blank{#1}{%
      \docheckhash@i
    }{%
      \expandafter\expandafter\expandafter\docheckhash@ii\expandafter
      \@cdr\detokenize{#1}\@nil?{#1}%
    }%
  }
  \gdef\docheckhash@ii#1?#2{%
    \if@func{##}{#2}{%
      \docheckhash@iii{##}%
    }{%
      \if@blank{#1}{\docheckhash@iii{#2}}{\docheckhash@i#2}%
    }%
  }
  \gdef\docheckhash@iii#1{%
    \if@func{&}{#1}
      \@secondoftwo
      {\if@func{##}{#1}\gobbletoendhashcheck\docheckhash@i}%
  }
}
\makeatother

Tests

\def\y#1{\edef\x{\checkforhash{#1}{Y}{N}}\typeout{\x}}
\y{a{}d}
\y{ab{{{{cc}}}}de}
\y{ab{{cc}}{{#}}de}
  • 1
    It is very often better to edit the previous solution rather than posting a new one. A few comments on that one: (1) I suspect that you intended to wrap the code in \lowercase, otherwise the \lccode assignments are not needed; (2) The use of \@cdr fails if the argument of \docheckhash is blank (only made of explicit (32, 10) spaces), in particular \checkhash{{}} fails. – Bruno Le Floch May 22 '13 at 21:41
  • I think you forgot to save your edit: I am seeing the same code as yesterday (and the page says "edited yesterday"). – Bruno Le Floch May 23 '13 at 9:59

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.