You can use l3regex
:
\documentclass{article}
\usepackage{expl3,xparse,l3regex}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\ExplSyntaxOn
\NewDocumentCommand\showcatcodes { v }
{
\regex_extract_all:nnN { . } { #1 } \l_tmpa_seq
\seq_map_inline:Nn \l_tmpa_seq
{ \ulrike_value_catcode:x { \tl_to_str:n {##1} } }
}
\cs_new_protected:Nn \ulrike_value_catcode:n
{
#1\textsubscript{\char_value_catcode:n { `#1 }}
}
\cs_generate_variant:Nn \ulrike_value_catcode:n { x }
\ExplSyntaxOff
\begin{document}
\showcatcodes{abcde123!$_ ^€\{\}}
\end{document}

This is the output with XeLaTeX (after removing inputenc
and fontenc
):

Final variant with \textvisiblespace
and monospaced fonts for the characters:
\documentclass{article}
\usepackage{expl3,xparse,l3regex}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\ExplSyntaxOn
\NewDocumentCommand\showcatcodes { v }
{
\group_begin:
\ttfamily
\regex_extract_all:nnN { . } { #1 } \l_tmpa_seq
\seq_map_inline:Nn \l_tmpa_seq
{ \ulrike_value_catcode:x { \tl_to_str:n {##1} } }
\group_end:
}
\cs_new_protected:Nn \ulrike_value_catcode:n
{
\tl_if_blank:nTF { #1 } { \textvisiblespace } { #1 }
\textsubscript{\normalfont\char_value_catcode:n { `#1 }}
}
\cs_generate_variant:Nn \ulrike_value_catcode:n { x }
\ExplSyntaxOff
\begin{document}
\showcatcodes{abcde123!$_ ^€\{\}}
\end{document}

\tl_show_analysis:N
\def\showcatcodes#1#2\relax{% \string#1% \ifcat ###1$_{6}$\else% \ifcat $_{4}$\else% \ifcat A#1$_{11}$\else% \ifcat .#1$_{12}$\else% \ifcat $#1$_{3}$\else% \ifcat _#1$_{8}$\else% \ifcat ^#1$_{7}$\else% \ifcat \noexpand\relax\noexpand#1$_{0}$\else% \fi\fi\fi\fi\fi\fi\fi\fi% \ifx\relax#2\relax\else\showcatcodes#2\relax\fi% }
will show\{
and\}
and other macros as catcode 0, but it does not handle actual braces{
and}
. Note invocation as\showcatcodes abc&de#123!$_ ^€\{\}\relax
\tl_show_analysis:N
but it returns things like\abc
as a single token so I'm not sure if it is suitable.