Automatically checking for accidental spaces in macros?

It is easy to forget a \relax or % at the line end of commands in the body of macros. For example, in writing

\def\myMacro{
\invokeX
\invokeY{}
\invokeZ
}


You must add a space swallower at the end of the first and third line, but not at the end of the second, fourth and fifth lines.

A simple global search/replace of {$ by {% will fix omissions such as that of the first line, and is not likely to add redundant swallowers. But, a global search/replace of }$ by }% will also add a percent after the fifth line.

For the benefit of Google search visitors, let me write the vi commands for these searches:

:%s/{$/{%/ :%s/}$/}%/


But as I mention, these are not perfect, and may not take care of other tricky cases.

So, my question is: Can you do better? Perhaps there is a clever redefinition of \def that would eliminate the need for these ugly additions, or perhaps there is some other automatic mechanism that would warn against such potential bugs?

-
You should mentioned that $ is the end-of-line marker in regular expressions used for the search&replace. Otherwise some people will wonder what you mean with replacing {$ with {%. –  Martin Scharrer Feb 27 '11 at 19:58
I edited your question, because your original example with \invokeM1 is equivalent to \invokeM 1 (digits cannot be part of the macro name (with the default catcodes)) and therefore all lines would need to eat spaces. –  Lev Bishop Feb 27 '11 at 20:14

I wrote some code to check a chunk of code. It counts how many spaces and how many spaces+end-line-characters appear in the argument of \cnel, and displays it to the terminal. It also runs the code: this way, it shouldn't be too disruptive.

% Should print 1 and 4.
\cnel{
\def\foo{b ar
}
}

% Just to stop the run (press <enter>)

% Should print 0 and 0, because there is no space nor end-of-line.
\cnel{%
\show\foo
}


The trick is to

• use \scantokens to read the argument with various catcode setups for the end-of-line character: either ignored or space. We first need to read it with catcode other because "ignored" it would disappear, and "space", it would become indistinguishable from a true space.

• use \lowercase to convert spaces to some other character (namely ^^S), then \detokenize, and search for the string ^^S. (That string is itself produced by detokenizing a lowercase space.)

.

% cnel = check if no end-line.
%
\catcode\@=11\relax
\newcount\cnel@count

\def\cnel{%
\begingroup%
\catcode\endlinechar=12\relax%
\cnel@aux%
}
\newcommand{\cnel@aux}[2][\^^S]{%
% Set spaces to lowercase to the first, optional, argument (default ^^S).
\lccode\ #1%
% First we count the number of spaces with a setting in which
% the end-line character is ignored. Then we count it after setting
% it to be a space instead.
\typeout{}%
\typeout{First, we ignore end-of-line characters.}%
\catcode\endlinechar=9\relax%
\scantokens{\cnel@count@spaces{#2}}%
\typeout{}%
\typeout{Then, they are counted as spaces.}%
\catcode\endlinechar=10\relax%
\scantokens{\cnel@count@spaces{#2}}%
% To execute the code, we keep this endline=space setting.
\scantokens{\gdef\cnel@gtmp{#2}}%
\endgroup%
\cnel@gtmp%
}

\newcommand{\cnel@count@spaces}[1]{%
\cnel@count0\relax%
\lowercase{\expandafter\cnel@w\detokenize{. .#1 }\relax%
\typeout{\unexpanded{#1}}%
}%
\typeout{Number of space(s): \the\cnel@count.}%
}
\def\cnel@w.#1.{%
\def\cnel@w@aux##1#1##2\relax{%
\ifempty@nTF{##2}{}{%
\cnel@w@aux##2\relax%
}%
}%
\cnel@w@aux%
}
\def\ifempty@nTF#1{%
\expandafter\ifx\expandafter a\detokenize{#1}a%
\expandafter\@firstoftwo%
\else%
\expandafter\@secondoftwo%
\fi%
}

-
Brilliant! I just cannot tell you how much I appreciate this. I think this should be packaged and advertised! –  Yossi Gil Mar 12 '11 at 10:30
@Yossi: I'll try to understand the code again this or next week-end, and perhaps turn it into an environment, and think a bit more about catcodes. –  Bruno Le Floch Mar 12 '11 at 16:02
@Bruno: I know this may be very time consuming. Should you decide to to do this, I trust the community will be very grateful. –  Yossi Gil Mar 13 '11 at 13:50
@Yossi: it's not that time consuming. I just can't figure out a good interface. And I can't remember why I treated spaces and end nf line character differently above. If you have any clue, I'd appreciate the help. –  Bruno Le Floch Mar 17 '11 at 10:09
@Yossi: I'm getting into unforseenly busy times, unfortunately. And my quick attempt at packaging tonight didn't work. So that will happen later. Sorry. --- About why eol are treated specially: the idea is that the user might want to ignore spaces which appear in the middle of the line, because those are more often intentional. Also I need to implement ignoring spaces outside braces. –  Bruno Le Floch Mar 18 '11 at 23:55

ConTeXt provides \starttexdefinition ... \stoptexdefinition environment that does exactly that. Using that you can write your command as:

\starttexdefinition myMacro
\invokeX
\invokeY{}
\invokeZ
\stoptexdefinition


I believe that LaTeX3 also provides a similar syntax.

-
Thanks Atidya. I applied Lev's correction to your answer as well. –  Yossi Gil Feb 28 '11 at 3:51

For LaTeX, using the expl3 package:

\ExplSyntaxOn
\def\myMacro{
\invokeX
\invokeY{}
\invokeZ
}
\ExplSyntaxOff


or even

\ExplSyntaxOn
\def \myMacro {
\invokeX
\invokeY { }
\invokeZ
}
\ExplSyntaxOff

-
You could add that when the ExplSyntax is on, spaces are ignored, the end of line character is a space, ~ is a genuine space, and : and _` are letters (usable in macro names). (Am I missing something?) –  Bruno Le Floch Feb 28 '11 at 10:58
{"extraction_info": {"found_math": true, "script_math_tex": 0, "script_math_asciimath": 0, "math_annotations": 0, "math_alttext": 0, "mathml": 0, "mathjax_tag": 0, "mathjax_inline_tex": 1, "mathjax_display_tex": 0, "mathjax_asciimath": 1, "img_math": 0, "codecogs_latex": 0, "wp_latex": 0, "mimetex.cgi": 0, "/images/math/codecogs": 0, "mathtex.cgi": 0, "katex": 0, "math-container": 0, "wp-katex-eq": 0, "align": 0, "equation": 0, "x-ck12": 0, "texerror": 0, "math_score": 0.7713568806648254, "perplexity": 2632.428931621855}, "config": {"markdown_headings": false, "markdown_code": true, "boilerplate_config": {"ratio_threshold": 0.18, "absolute_threshold": 10, "end_threshold": 15, "enable": true}, "remove_buttons": true, "remove_image_figures": true, "remove_link_clusters": true, "table_config": {"min_rows": 2, "min_cols": 3, "format": "plain"}, "remove_chinese": true, "remove_edit_buttons": true, "extract_latex": true}, "warc_path": "s3://commoncrawl/crawl-data/CC-MAIN-2014-15/segments/1397609532128.44/warc/CC-MAIN-20140416005212-00020-ip-10-147-4-33.ec2.internal.warc.gz"}