Synopsis:
This page covers what happens once a file is admitted - how it is split, how it is
matched, and what the block that results looks like.
-
The expression compiles once per invocation, before traversal, so a malformed
pattern is refused before any file is opened.
-
A line is bounded by LF, CRLF, or bare CR, and an unterminated final line is a line.
Splitting happens before matching, which is why
., ^, and
$ agree across the four engines.
-
A match is one occurrence anywhere in the line. TextFinder asks the engine one
question and uses one bit of the answer, so greediness and capture groups are
unobservable.
/n and /L give four block forms, and one of the four is
different in kind.
- With both false the path line says everything, so evaluation of a file stops at its first match.
- Add the default expression and no file content is needed at all - every selected non-empty file is reported without being opened.
- The specification names the two costs it accepts for that speed rather than claiming there are none.
Two rules bind the implementation rather than describing the platform.
- Every line ends in a single LF, which three of the four target runtimes translate by default or by idiom.
- Emission is incremental - nothing is buffered to the end of a file, a directory, or the run.
1. One Compilation per Invocation
Each implementation compiles the /r expression exactly once per invocation and
reuses the compiled engine for every line of every file across every root path (§3.3).
A build that compiled per file would do the same work once per candidate rather than once,
and the requirement exists to rule that out.
The rule has a structural consequence every implementation shares: compilation happens
before traversal begins, so a malformed expression is refused before any file is opened.
§5.2 gives that its own diagnostic, invalid regex for switch: /r, and
exit code 1. A run with a bad pattern reads no files at all.
§6.1 names the engine per language rather than leaving it to the implementation -
std::regex with std::regex::ECMAScript for C++, the
regex crate for Rust, System.Text.RegularExpressions.Regex for
C#, and the re module for Python. What those four agree on, and what a pattern
outside the agreement costs, is on the
Contracts page.
The expression must not be empty. The default . is the way to match every
line, and /r "" draws empty expression for switch: /r rather than
matching everything.
2. Lines and Their Terminators
A line is a maximal run of characters bounded by a line terminator. Three terminators are
recognized, which covers the Windows, Linux, and macOS conventions:
| Terminator |
Bytes |
Convention |
| LF |
U+000A |
Linux, macOS |
| CRLF |
U+000D U+000A |
Windows |
| bare CR |
U+000D |
classic Mac OS |
If the final line of a file lacks a terminator, it is treated as a line all the same. Line
numbers count every line, including the ones that do not match, so a number reported under
/n is the number an editor shows.
Splitting happens before matching, and that ordering does more work than it appears to.
§6.1 leans on it: ., ^, and $ agree across the
four engines because a line holds no terminator by the time the engine sees it, so the
engines' differing treatment of a terminator inside the subject never arises.
3. The Anywhere-in-the-Line Match
The expression signifies a match by finding at least one occurrence anywhere within the
line. It is not anchored, and a pattern that should be anchored says so with
^ or $.
TextFinder asks the engine one question and uses one bit of the answer. Under
/L it reports the whole line, never a match position, a matched substring, or
a capture group. Greediness, alternation preference, and capture-group numbering are
therefore unobservable, which is the ground §6.1 stands on when it claims the four
engines differ on nothing TextFinder observes so long as the pattern is one they all
accept.
One consequence shows up in the output. A line holding three occurrences of the pattern
produces one detail line, not three: §3.4 says one detail line is emitted for every
matching line, however many occurrences that line holds.
4. Block Forms and Their Gating
/n and /L select the fields a detail line carries, and their four
combinations give four block forms. Both default to false.
| /n |
/L |
Detail line carries |
Detail lines per block |
| false |
false |
nothing - the block has none |
0; the path line says everything |
| true |
false |
the line number, after its indent |
one per matching line |
| false |
true |
the line text, after its indent |
one per matching line |
| true |
true |
number, - , text |
one per matching line |
The three-character separator - appears only when both fields are
present. Each detail line is indented exactly two spaces, and the indent is fixed in
§3.4 rather than left to the implementation, because a comparison of two runs would
otherwise diff on whitespace.
The first row is the interesting one. With no detail lines, a block's path line says
everything the block can say, so one block is emitted per matching file and evaluation of
that file stops at its first match. A 4,000-line file whose second line matches costs two
lines of reading.
Emission is incremental. A block's path line is written the moment that file's first match
is found, ahead of the detail line for that same match, and each further detail line as its
own line is evaluated. Nothing is buffered to the end of a file, a directory, or the run.
Announcements go out the same way, at the point TextFinder deals with the entry they name,
so none waits on the directory holding it to finish.
The gating interacts with /h exactly once. A file that matched is never
announced, its block already naming it, so a file announcement reports only a file that
produced no block. Under the default /h true the output holds blocks and error
announcements; under /h false every examined file appears exactly once, as
either a block or an announcement.
5. The No-Content Case and Its Two Costs
One state needs no file content at all. When the expression is the default .
and /n and /L are both false - the state any command
line reaches that overrides none of the three - a block carries only its path line and a
single match settles it. TextFinder reports every selected file of non-zero size without
opening it.
The size test still applies, since it reads only filesystem metadata, but the NUL and UTF-8
tests do not. No file is opened, so no file announcement arises: not searched,
which reports a file that was read and matched nothing, and not skipped, which
reports one a content test rejected. Every selected file matches, so every one produces a
block instead. A selected file of zero size produces neither a block nor an announcement,
having no line to match.
§3.3 names the two costs it accepts for the speed this buys:
- A file the content tests would have rejected, a binary file among them, is
reported.
- So is a file whose lines are all empty, which
. would not in fact have
matched.
Both are visible in a demonstration run. Invocation 2 of the C++ demonstration uses the
default expression and reports 46 lines against the project tree, one per selected
non-empty file, and it opens none of them.
The error announcements still work in this case, because they rest on metadata too: a file
above the size limit draws too large, and one whose metadata cannot be read
draws cannot open.
6. The Line That Closes a Run
A run that traversed writes one more line after its last block or announcement, and
§3.6 fixes its text:
accessed <fileCount> files, <dirCount> directories
It counts what the run reached rather than what it found, so it is not a record of matching
and nothing in §3.4 governs it. It names no path, which is why none of this page's
rendering and gating rules reach it: /h leaves it in place, no switch
suppresses it, and the two-level block form does not apply to a line that belongs to no
file. The
Overview page
states what each count includes.
Two of this page's cases meet it in a way worth naming. The no-content case of §5 opens
no file, yet every file it selects is counted, so a run under the default expression reports
a file count equal to the number of selected files whose size it read - including the
zero-length ones that produced no block at all. And a run whose expression matched nothing
still writes the line, so an empty result is now distinguishable from a search that never
reached a file: the first ends accessed 40 files, 7 directories, the second
accessed 0 files, 0 directories.
The counts move with the tree, which is why every demonstration capture in this project
states the date it was taken.
7. The LF Terminator
Every line of a block, every announcement, and the run summary of §3.6 is terminated by
a single LF on every platform. An implementation must prevent its runtime from translating that terminator to
CRLF, or the same tree would yield byte-different output on Windows and POSIX and the
comparison of §6 would hold only within a platform.
The rule binds the implementation rather than describing the platform, and that phrasing is
deliberate: three of the four target runtimes translate by default or by idiom. C#'s
Console.WriteLine uses Environment.NewLine; the C runtime behind
C++'s std::cout translates on Windows unless the descriptor is put into binary
mode. The rule lived in the C++ output specification for several turns, where it bound one
language, and was moved to the parent for exactly the C# reason.
The rule governs stdout alone. What terminator an implementation writes to stderr is
whatever its runtime and platform produce, and the specification does not fix it. The
reasoning is stated: comparing one implementation against another means comparing stdout,
a run that writes to stderr has written a usage diagnostic and traversed nothing, so it
has no search output to compare, and holding its stderr to the byte would test the platform
rather than the implementation.
One boundary case follows from the binary-mode requirement and caught the C++
implementation. Text the binary writes itself - help and the option listing - reaches the
same stdout, so it must be written after the output component has configured the stream,
not before. The C++ Entry specification states that as a step ordering with the reason
attached, and the integration suite tests it: "help text carries LF only, being written
after the sink configures stdout."