2

I have encountered the following code to check for an empty argument:

\catcode`\@=11 % as in plain.tex
\long\def\blank#1{\bl@nk#1@@..\bl@nk}%
\long\def\bl@nk#1#2@#3#4\bl@nk{#3#4}
\catcode`\@=12

\long\def\test#1{\begingroup \toks0{[#1]}%
  \newlinechar`\/\message{/\the\toks0:
  \if\blank {#1}EMPTY\else NOT empty\fi%

Could someone explain how it works?

Especially, I don't understand the \if\blank {#1} part. What does it expand to if called e.g. with \test{} vs. \test{Test}?

3 Answers 3

6
\catcode`\@=11 % as in plain.tex
\long\def\blank#1{\bl@nk#1@@..\bl@nk}%
\long\def\bl@nk#1#2@#3#4\bl@nk{#3#4}
\catcode`\@=12

\long\def\test#1{\begingroup \toks0{[#1]}%
  \newlinechar`\/\message{/\the\toks0:
  \if\blank {#1}EMPTY\else NOT empty\fi%

In the code above you find the phrase "blank":

In colloquial TeX language a macro argument is said to be "blank" in case either it does not consist of any token at all, i.e., either it is empty, or it does consist only of explicit character tokens of category-code 10(space) and character-code 32, i.e., or it does consist of only of explicit space tokens. (The character code of a character token refers to the number which the character has in TeX's internal character representation scheme, which with traditional TeX-engines is ASCII and with XeTeX/LuaTeX is Unicode whereof ASCII is a strict subset.)

The test \if\blank{1⟨\blank's argument⟩}2E11M11P11T11Y11\elseN11O11T11explicit space token10e11m11p11t11y11\fi is about finding out whether it is/it is not the case that ⟨\blank's argument⟩ is empty or consists only of explicit space tokens (character-code 32, category-code 10).

The test \if\blank{1⟨\blank's argument⟩}2E11M11P11T11Y11\elseN11O11T11explicit space token10e11m11p11t11y11\fi relies on ⟨\blank's argument⟩ containing neither the token @11 nor the token \bl@nk as these tokens are delivered by the \blank-macro and used as \bl@nk's argument-delimiters and tokens coming from user-input/coming from ⟨\blank's argument⟩ should not erroneously match up these delimiters.

Two subtle aspects of the ways in which TeX works are taken advantage of:

  1. TeX keeps expanding expandable things when gathering the two tokens for \if-comparison=character-code-comparison.
  2. When TeX is gathering tokens belonging to an undelimited argument, all explicit space tokens preceding the outermost {1 of that argument/preceding the single token that forms the argument are discarded.

\blank{1⟨\blank's argument⟩}2 yields:

\bl@nk⟨\blank's argument⟩@11@11.12.12\bl@nk.

The gist of the test is about how TeX gathers the arguments of \bl@nk:

In case ⟨\blank's argument⟩ is blank,

  • the first @11 behind ⟨\blank's argument⟩ will be taken for \bl@nk's 1st (undelimited) argument.
  • the second @11 behind ⟨\blank's argument⟩ will be taken for the delimiter of \bl@nk's 2nd (⁠⁠@11-delimited) argument, which is empty.
  • the first .12 behind ⟨\blank's argument⟩ will be taken for \bl@nk's 3rd (undelimited) argument.
  • the second .12 behind ⟨\blank's argument⟩ will be taken for \bl@nk's 4th (⁠⁠\bl@nk-delimited) argument.

\bl@nk's 3rd and 4th argument are delivered, thus you have something like
\if.12.12E11M11P11T11Y11\elseN11O11T11explicit space token10e11m11p11t11y11\fi

The character-code of .12 is compared to the character-code of .12, so the \if-branch/true-branch is taken and the \else-branch/false-branch is discarded.

In case ⟨\blank's argument⟩ is not blank,

  • the first token which is not an explicit space token (, i.e., which is not an explicit character token of character code 32 and category code 10(space)) or the first {1}2-group of ⟨\blank's argument⟩ will be taken for \bl@nk's 1st (undelimited) argument.
  • the first @11 behind ⟨\blank's argument⟩ will be taken for the delimiter of \bl@nk's 2nd (⁠⁠@11-delimited) argument, which holds the remainder of ⟨\blank's argument⟩.
  • the second @11 behind ⟨\blank's argument⟩ will be taken for \bl@nk's 3rd (undelimited) argument.
  • the token-sequence .12.12 behind ⟨\blank's argument⟩ will be taken for \bl@nk's 4th (⁠⁠\bl@nk-delimited) argument.

\bl@nk's 3rd and 4th argument are delivered, thus you have something like
\if@11.12.12E11M11P11T11Y11\elseN11O11T11explicit space token10e11m11p11t11y11\fi

The character-code of @11 is compared to the character-code of .12, so the \if-branch/true-branch is discarded and the \else-branch/false-branch is taken.


The answers to How does TeX look for delimited arguments? might be of interest to you.

The answers to Expandable test for an empty token list—methods, performance, and robustness might be of interest to you.

4

if #1 is empty then

\if\blank{}

is

\if\bl@nk @@..\bl@nk

which is

\if ..

so true

If #1 is non empty, say ! then

\if\blank{|}

is

\if\bl@nk !@@..\bl@nk

which is

\if @..

which is false so the . is skipped to the \else


Note this test is not completely safe


\catcode`\@=11 % as in plain.tex
\long\def\blank#1{\bl@nk#1@@..\bl@nk}%
\long\def\bl@nk#1#2@#3#4\bl@nk{#3#4}


\if\blank{@@}EMPTY\else Not empty\N\fi


\bye

will typeset as ..EMPTY as the internal delimited arguments assume that the argument being tested never includes a catcode 11 @.

3

The first thing to remember is that \if will do recursive macro expansion until finding two unexpandable tokens.

The second important fact is that TeX will ignore explicit space tokens when looking for an undelimited macro argument.

Let's examine what happens with the calls

\if\blank{a}<true>\else<false>\fi
\if\blank{}<true>\else<false>\fi
\if\blank{ }<true>\else<false>\fi

\if\blank{a}<true>\else<false>\fi

TeX will expand \blank, so #1 will be a, to get

\if\bl@nk a@@..\bl@nk•<true>\else<false>\fi

(there is no space after the first \bl@nk, I used it only to see where the token ends). Now the macro \bl@nk will be expanded. It looks for an undelimited argument, then for an argument delimited by @, then for an undelimited argument and finally for an argument delimited by \bl@nk. In the present case,

#1 <- a
#2 <-    (empty)
#3 <- @
#4 <- ..

and will substitute the replacement text, so we obtain

\if @..<true>\else<false>\fi

Since @ and . haven't the same character code, the test will return false and so

<false>\fi

will remain in the input stream.

It would be similar if instead of \if\blank{a} we had \if\blank{abc}, because in this case we'd get

#1 <- a
#2 <- bc
#3 <- @
#4 <- ..

\if\blank{}<true>\else<false>\fi

At the first step we get

\if\bl@nk @@..\bl@nk<true>\else<false>\fi

(again, the space after the first \bl@nk does not exist). Now \bl@nk looks for its arguments:

#1 <- @
#2 <-    (empty)
#3 <- .
#4 <- .

and so the input stream will have

\if..<true>\else<false>\fi

and now the test returns true.

\if\blank{ }<true>\else<false>\fi

The same as in the previous case: we get

\if\bl@nk @@..\bl@nk<true>\else<false>\fi

but now the space after \bl@nk is what's obtained from the argument to \blank. Not a big deal! The second rule mentioned above applies and that space is ignored, leaving the things exactly as in the second case.

Note

As David Carlisle points out, this is not particularly robust, because if this is used in a context where @ has category code 11, the test might return true even if the argument is not blank, precisely if we call \if\blank{@@}. Indeed, in this case the first step yields

\if\bl@nk @@@@..\bl@nk<true>\else<false>\fi

and we have

#1 <- @
#2 <-    (empty)
#3 <- @
#4 <- @..

so we'd end up with

\if @@..<true>\else<false>\fi

and the test would return true. The two periods would appear in the input stream.

A safer test would be with very strange characters, say Q with category code 3 that's normally not found.

You must log in to answer this question.

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