# Test if token is a control sequence

Sometimes more complex (La)TeX macros test the next input token and branch dependent on its type. I'm aware how to test for catcodes and character codes, but sometimes I like to handle control sequences (macros, primitives, i.e. \ followed by letters (catcode 11) and also the product of \csname ...\endcsname) in a special way.

Is there a suitable test which tells if a token, read as macro argument, is a control sequence? The token must not be expanded beforehand. I like to avoid using \string and looking for a leading \, because this depends on the value of \escapechar which could have been changed locally. Using \meaning and testing for a leading macro doesn't work for primitives like \relax.

So basically I'm looking for a \@ifcontrolsequence{<token>}{<true>}{<false>} macro which will give true for e.g. \relax, \empty, \custommacro and false for any other token. Active characters could be taken as an exception and treated either way. The macro should of course not break on any special tokens.

• I don't understand what you mean by "command sequence". How do you want to treat \topskip? how about \normalbaselineskip? The texbook defines "control words" as catcode==0 followed by one or more catcode==11, ending just before the next catcode!=11 or eol. – Lev Bishop Jun 23 '11 at 14:39
• @Lev: Thanks, I apparently got confused a little with the terminology. I checked The TeXbook again and found that the correct term there is "control sequences" (see e.g. page 7p), not "command sequences", which come in two flavors: control words (\letters) and control symbols (\'). I most likely wrongly remembered command instead of control because LaTeX calls them this way (e.g. \newcommand). – Martin Scharrer Jun 23 '11 at 15:51
• So you'd like to get true for all of \def, \relax, \empty, \', \topskip, \undefined, \normalbaselineskip? And false for \topskip=2pt, some text, \csname relax\endcsname, \relax  (note trailing space), {\relax}? – Lev Bishop Jun 23 '11 at 16:00
• @Lev: I'm actually talking about single tokens only. So your everything in your false list would be kind of invalid. But this reminds me that I need to validate the input first. – Martin Scharrer Jun 23 '11 at 16:16
• Do you want to test \undefined as true or false? – Leo Liu Jun 23 '11 at 17:00

\makeatletter
\def\@ifismacro#1{%
\begingroup\escapechar=-1
\edef\x{\endgroup\def\noexpand\first{\string#1}}\x
\begingroup\escapechar=\\
\edef\x{\endgroup\def\noexpand\second{\string#1}}\x
\ifnum\pdfstrcmp{\first}{\second}=\z@
\expandafter\@secondoftwo % no backslash in front
\else
\expandafter\@firstoftwo  % backslash in front
\fi}
\def\report#1{\@ifismacro{#1}{\message{CS}}{\message{NON CS}}}
\makeatother

\report{A}
\report{\"}
\let\pippo=a
\report{\pippo}


The problem with this approach is that is not completely expandable, as it relies on assignments to \escapechar, while being independent of the value it has at the moment the test is performed.

This test distinguishes the last case, which is not possible with \ifcat. Nor with \ifcsmacro of etoolbox, it seems.

• It is not possible to have a purely expandable solution: \escapechar=-1\let\*=* makes * and \* indistinguishable expandably, except by delimited macro arguments (but you can't have that for all characters). Same problem with active characters. – Bruno Le Floch Jun 23 '11 at 18:36
• @Bruno: that's what I suspected. If one doesn't need to cope with borderline cases, \ifcat may be sufficient. – egreg Jun 23 '11 at 19:27
• it depends on your definition of borderline case. A very weak form of the test would be \ifcat$\expandafter\@gobble\string#1$ (with the relevant \@firstoftwo and \@secondoftwo), if we don't care about spaces, and the case where \escapechar is 32 or non-printable. – Bruno Le Floch Jun 23 '11 at 21:33
• @Bruno: it all depends on the use Martin wants to make of this test and what he expects the argument to be. Of course one can devise tricky input to defy any attempt to build the test in the most general way; but normal input, in many cases, is just normal input. – egreg Jun 23 '11 at 22:21

It is not possible to get a full solution expandably. The code below gives correct results as long as the escape character is printable (\escapechar between 0 and 255 inclusive). It relies on the fact that in that case, applying \string to our argument will give more than one character. In fact, we treat the case \escapechar=32 (space) separately, because TeX ignores spaces when grabbing an undelimited argument.

The weird-looking \noexpand at various places are needed to cater for \outer macros: it lets them to \relax for long enough to grab them in an argument. Note that #1 never appears in text that may be skipped in a conditional (because it may be \outer).

In the case where the escape character is unprintable, and \string#1 gives a single character, then we need some more investigation to distinguish usual cases (but the solution cannot be complete).

\makeatletter
\newcommand{\ifcs}{\expandafter\ifcs@i\noexpand}
\newcommand{\ifcs@T}[3]{#2}
\newcommand{\ifcs@F}[3]{#3}

% "normal" escapechar
\newcommand{\ifcs@i}[1]
{%
\ifcat$\ifcat*\string#1\fi$%
\expandafter \expandafter
\expandafter \ifcs@test
\else
\expandafter \expandafter
\expandafter \ifcs@T
\fi
\noexpand #1%
}


We could be done here if we didn't care about special cases for \escapechar: just replace the end of the conditional by \expandafter \@secondoftwo \else \expandafter \@firstoftwo \fi (removing \noexpand #1 as well). But it's not too expensive to distinguish between various escapechars (I doubt that the test I give here is anywhere close to optimal).

\newcommand{\ifcs@test}
{%
\ifcase \expandafter\@gobble\string\2 % (space)
\ifcat\@sptoken\string\1 \else 0 \fi
\expandafter \ifcs@unprintable
\or
\expandafter \ifcs@space
\else
\expandafter \ifcs@F
\fi
}

% \escapechar=32
\newcommand{\ifcs@space}[1]
{%
\unless\ifcat\@sptoken\string#1%
\expandafter \expandafter
\expandafter \ifcs@F
\else
\expandafter \expandafter
\expandafter \ifcs@space@i
\fi
\noexpand #1%
}
\newcommand{\ifcs@space@i}[1]% \string#1 starts with space
{%
\ifcat$\romannumeral-0\string#1$%
\expandafter \@secondoftwo
\else
\expandafter \@firstoftwo
\fi
}


Case \escapechar < 0 or > 255. This I didn't test much, but it is not possible to provide a full solution, since for instance the control sequence \a, and an active a let to one another and let to a character are indistinguishable expandably. (Identical for \ifcat, \if, \ifx, \meaning, and \string)

\newcommand{\ifcs@unprintable}[1]
{%
\ifcat\relax\noexpand#1%
\expandafter \expandafter
\expandafter \ifcs@T
\else
\expandafter \expandafter
\expandafter \ifcs@unprintable@i
\fi
\noexpand #1%
}
\newcommand{\ifcs@unprintable@i}[1]
{%
\expandafter\ifx\csname \string#1\endcsname#1%
\expandafter \@firstoftwo % can be wrong
\else
\expandafter \@secondoftwo
\fi
}

\def\test{\expandafter\test@\noexpand}
\def\test@#1{\ifcs#1{\message{T}}{\message{F}}
\outer\def\foo{}

\escapechar=-1\relax
\test\foo
\test\a
\test\ %
\expandafter\test\csname \space a\endcsname
\test a
\test ^
\test $ Have a look at the etoolbox package and its \ifcsmacro test. \documentclass{article} \usepackage{etoolbox} \begin{document} % \def\custommacro{foo} % \def\custommacro{\empty} % \def\custommacro{\relax} % \let\custommacro\box \ifcsmacro{box}{True}{False} \ifcsmacro{custommacro}{True}{False} \end{document}  • Thanks, I had a look at it, but I don't think its usable. \ifcsmacro is a variation of \ifdefmacro which takes a command sequence name. So the input is provided to \csname ...\endcsname (expansion!) and then the \meaning of the resulting macro is tested if it starts with macro:. So it tests if the argument is the name of a defined macro, not a primitive. I can't see how an argument is passed to it safely without expansion. Using \ifdefmacro directly is better suited for analyzing input tokens (it uses \meaning#1), but it explicitly excludes primitives. (Idea: \pdfprimitive?) – Martin Scharrer Jun 23 '11 at 15:40 What you want is \ifiscs in etextools package. And I find out that there is a bug to test #. Examples: \documentclass{article} \usepackage{etextools} \begin{document} % yes \ifiscs{\fi}{yes}{no}\par \ifiscs{\undefined}{yes}{no}\par \ifiscs{\,}{yes}{no}\par % no \ifiscs{}{yes}{no}\par \ifiscs{ }{yes}{no}\par \ifiscs{ }{yes}{no}\par \ifiscs{~}{yes}{no}\par \ifiscs{_}{yes}{no}\par \ifiscs{^}{yes}{no}\par \ifiscs{&}{yes}{no}\par \ifiscs{$}{yes}{no}\par
\ifiscs{汉}{yes}{no}\par
\ifiscs{foo}{yes}{no}

% wrong: should be no, but get yes
\ifiscs{#}{yes}{no} is wrong

% I cannot test braces { }, comment char % and escape char \

\makeatletter
% yes
\ifiscs{\@sptoken}{yes}{no}

\catcode\@=0
% yes
\ifiscs{@asdf}{yes}{no}

\end{document}

• it uses \detokenize behind the scenes, which doesn't do well for #. Note that \escapechar=-1\relax\ifiscs{\^}{}{\ERROR} is wrong. – Bruno Le Floch Jun 23 '11 at 18:51

Thanks for all the interesting answers so far. I also searched for a solution and stumbled over the \if in The TeXBook:

\if<token1><token2> (test if character codes agree)
TEX will expand macros following \if until two unexpandable tokens are found. If either token is a control sequence, TEX considers it to have character code 256 and category code 16, unless the current equivalent of that control sequence has been \let equal to a non-active character token. In this way, each token specifies a (character code, category code) pair. The condition is true if the character codes are equal, independent of the category codes.

So comparing the \noexpanded argument with a known control sequence such as \relax should work, except it was let to a character. This excludes \bgroup, \egroup and \@sptoken etc., but I actually can life well with that. Active characters can be included by also testing for the catcode.

I would appreciate feedback to this:

\documentclass{article}

\usepackage[T1]{fontenc}

\makeatletter
\def\Ifiscs#1{%
\if\noexpand#1\relax
\expandafter\@firstoftwo
\else
\ifcat\noexpand~\noexpand#1%
\expandafter\expandafter
\expandafter\@firstoftwo
\else
\expandafter\expandafter
\expandafter\@secondoftwo
\fi
\fi
}

\def\test#1{%
\message{^^J\string#1:\space}%
\Ifiscs{#1}%
{\message{YES}}%
{\message{NO}}%
}
\makeatother

\begin{document}

\test{}
\test{\relax}
\test{\empty}
\test{word}
\test{\test}
\test{\document}
\test{\newcommand}
\test{\noexpand}
\test{\_}
\test{\^}
\test{\&}
\test{\$} \test{$}
\test{&}
\test{^}
\test{_}
\test{.}
\test{~}
\test{+}
\test{@}
\test{ }
\test{\ }
\test{\space}
\test{\@sptoken}
\test{#}
\test{\bgroup}
\test{\begingroup}
\test{汉}
\test{á}
\test{ä}
\test{ß}
\test{è}

\message{^^J}

\end{document}


### Test results:

:  NO
\relax:  YES
\empty:  YES
word:  NO
\test:  YES
\document:  YES
\newcommand:  YES
\noexpand:  YES
\_:  YES
\^:  YES
\&:  YES
\$: YES$:  NO
&:  NO
^:  NO
_:  NO
.:  NO
~:  YES
+:  NO
@:  NO
:  NO
\ :  YES
\space:  YES
\@sptoken:  NO
#:  NO
\bgroup:  NO
\begingroup:  YES

á:  NO
ä:  NO
ß:  NO
è:  NO

• That's exactly what I said in a comment, i.e., that in "non borderline cases", \ifcat would have sufficed. If all you need is to discriminate between "commands" and characters, using \if or \ifcat is just the same: \relax is considered has having character code 256 and category code 16, in this context. – egreg Jun 29 '11 at 13:28
• @egreg: Thanks. What borderline cases are you aware off? You mean the characters-let-to-control-sequence case or also others? – Martin Scharrer Jun 29 '11 at 13:33
• In some cases a character may be active because special things are done with it in a particular context. Bruno will surely point out other possible glitches. – egreg Jun 29 '11 at 13:47
• @Martin: you don't need two tests: simply using \ifcat\relax\noexpand#1 is just as robust. This will still fail for \bgroup. If you mind, then you can add in the false (i.e., not yet detected as a cs) branch the test \ifcat$\expandafter\@gobble\string#1$, always true for characters and almost always false for cs. The only case that would go through that is single character control sequences let equal to a character and this only when \escapechar=-1. – Bruno Le Floch Aug 8 '11 at 12:38
• I discovered \ifcat$\expandafter\@gobble\string#1$` independently yesterday. Please note that it fails if #1 is a single space token. – pts Aug 31 '17 at 12:28