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 $
\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\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\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\undefinedas true or false? – Leo Liu Jun 23 '11 at 17:00\undefinedis a control sequence (but an undefined one), so it should test true. – Martin Scharrer Jun 23 '11 at 17:02