17

For indexing I wanted to write a macro \macroname that removes leading backslashes from macro names but leaves the names of environments untouched. That is

\macroname{\relax}   -->  relax
\macroname{itemize}  -->  itemize

where the --> is meant to be read as "expands to".

From my understanding, the following should work:

\newcommand\removebs[1]{\if#1\char92\else#1\fi}
\newcommand\macroname[1]{%
  \expandafter\removebs\detokenize{#1}}

However, its outcome is

\macroname{\relax}   -->  \relax   % literally
\macroname{itemize}  -->  itemize

By chance, actually, I found out that if I change the catcode of the backslash to "other" during the definition of the \removebs macro, it works:

{
  \catcode`\|=0
  |catcode`|\=12
  |global|def|removebs#1{|if#1\|else#1|fi}
}

\newcommand\macroname[1]{%
  \expandafter\removebs\detokenize{#1}}

Why is that? From TeX by Topic I'd expect that the comparison made by \if is what Philipp Lehman calls "category code agnostic".

Thanks for your answers!

1
  • 1
    The problem is that you are comparing a backslash character token with the token \char and they are different; \char92 is an instruction for printing the character 92 from the currect font.
    – egreg
    Commented Jan 26, 2012 at 8:00

3 Answers 3

14

Joseph has given a working solution. I'd like to explain what goes wrong with your code.

First attempt

\newcommand\removebs[1]{\if#1\char92\else#1\fi}
\newcommand\macroname[1]{%
  \expandafter\removebs\detokenize{#1}}

With \macroname{\relax} you get

\expandafter\removebs\detokenize{\relax}

and then (using to separate tokens and <space> do denote a space token)

\removebs • \ • r • e • l • a • x • <space>

which becomes

\if • \ • \char • 9 • 2 • \else • \ • \fi • r • e • l • a • x • <space>

and the comparison is between a category code 12 backslash and the token \char, which of course leads to false.

The code \char92 are instruction to print character 92 from the current font.

One might correct the code by checking against a real category code 12 backslash:

\makeatletter
\newcommand{\removebs}[1]{\if#1\@backslashchar\else#1\fi}
\makeatother

but the space produced by \detokenize after \relax would remain.

Second attempt

{
  \catcode`\|=0
  |catcode`|\=12
  |global|def|removebs#1{|if#1\|else#1|fi}
}

This works because it just implements the check against a category code 12 backslash, but it's not necessary, as the token is already available as \@backslashchar in the LaTeX kernel.

An alternative way, without global definitions and category changes, can be

\begingroup\lccode`\|=`\\
\lowercase{\endgroup\def\removebs#1{\if#1|\else#1\fi}}

where the only token that is converted to its lowercase equivalent is | (to a backslash).

Proposed code

% \makeatletter
% \newcommand{\removebs}[1]{\if#1\@backslashchar\else#1\fi}
% \makeatother
\begingroup\lccode`\|=`\\
\lowercase{\endgroup\def\removebs#1{\if#1|\else#1\fi}}
\newcommand{\macroname}[1]{\expandafter\removebs\string#1}

Since all you want is to get the argument letters, it doesn't matter if the first letter in \macroname{itemize} has category code 12.

5
  • The problem with an \if test here is that it will be true even if the input was not a control sequence to start with (for example if it is the string \ ).
    – Joseph Wright
    Commented Jan 26, 2012 at 9:36
  • Why? With \macroname{itemize} we do \string itemize and so #1 is i (of category code 12). Of course, if one plays with \escapechar things are different. The empty string is disallowed, of course.
    – egreg
    Commented Jan 26, 2012 at 11:38
  • I was thinking of {\catcode`/=0/catcode`\\=12/macroname{\}}, for example.
    – Joseph Wright
    Commented Jan 26, 2012 at 11:41
  • In that case nothing would be printed; but the macro is intended for a specific application, so going into all details probably is only a nice exercise. The case leaves a space token in the input stream.
    – egreg
    Commented Jan 26, 2012 at 11:47
  • Thanks for also answering my second question (without me asking it, of course) concerning the trailing space token. To keep things simple, I now went for \if#1\@backslashchar and replaced \detokenize with \string.
    – 5gon12eder
    Commented Jan 26, 2012 at 18:24
12

TeX works with tokens, and what you need to know is that a control sequence such as \foo is not tested by \if as a series of characters, but as a single 'unit'. There a a few primitives that will turn control sequences back into single tokens: \detokenize does this for a set of tokens, while \string does so for a single token. The latter is also available in all TeX versions (\detokenize requires e-TeX).

As you observe, detokenizing the input allows you to do a comparison to a category code 'other' \. An alternative approach would be to test if #1 is a control sequence, using the fact that \ifcat treats all (unexpanded) control sequences as identical. You can then use \string to convert to multiple tokens, and \@gobble to remove the very first character:

\documentclass{article}
\makeatletter
\newcommand{\removeabs}[1]{%
  \ifcat\relax\noexpand#1%
    \expandafter\expandafter\expandafter\@gobble\expandafter\string
  \fi
  #1%
}
\makeatother
\begin{document}
\removeabs{foo}
\removeabs{\bar}
\end{document}

The above is all fine as long as \escapechar is printable and not a space. Using some code from LaTeX3 'translated' back to primitives, you can set things up to be robust for all escape characters:

\documentclass{article}
\makeatletter
\newcommand{\removeabs}[1]{%
  \ifcat\relax\noexpand#1%
    \expandafter\removeabs@aux@i
  \fi
  #1%
}
\newcommand*{\removeabs@aux@i}{%
  \romannumeral
    \if\string\ \removeabs@aux@ii\fi
    \expandafter\removeabs@aux@iii\string
}
\newcommand{\removeabs@aux@ii}{}
\long\def\removeabs@aux@ii#1\removeabs@aux@iii{%
  -\number\fi\expandafter\z@
}
\newcommand{\removeabs@aux@iii}[1]{\z@}
\makeatother
\begin{document}
\removeabs{foo}
\removeabs{\bar}
\end{document}

(See the implementation of \cs_to_str:N in the LaTeX3 docs for what is going on here!)

0

My two cents:

\def\cwofcs#1#2 \endcs{\ifnum\the\catcode`#1=0#2\else#1#2\fi}
\def\nameof#1{\expandafter\cwofcs\detokenize{#1}\endcs}

Test:

\nameof\relax       % -->  relax
\nameof\itemize     % -->  itemize

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .