Synopsis:
This page covers rust_textfinder_entry, the binary. It wires the three
libraries together and does no matching and no file I/O of its own.
-
It owns the skip list, the lifetime of all three values, and the exit code. Here the
borrow checker enforces the lifetime rule rather than leaving it to convention.
-
114 lines of code against a 106-line specification. Everything harder than wiring
lives in a library.
Eleven startup steps, one more than C++ needs, and the order is the specification rather
than a consequence of it.
- Step 1 uses
args_os, because std::env::args panics on an argument that is not valid Unicode and a command line the user typed must not crash the program.
- Step 3 constructs the sink first, and here it matters because the sink owns the process's only stdout handle - so the binary's own text shares one buffer with the search output.
- Step 10 asks for the run summary, supplying the one fact the library cannot have: that step 9 is finished.
Two places where the type system shapes the code rather than the specification.
- A
thread_local! RefCell holds the skip list, which recovers the one-argument signature without unsafe and without a lock.
- Steps 8 and 9 are one
match rather than two statements, because the sink cannot be written to directly while the Dirnav borrowing it exists.
1. What the Binary Owns
rust_textfinder_entry is the command-line entry point. It collects the
program's arguments, parses them through rust_textfinder_cmdline, wires the
three libraries together, and drives traversal. Matching and file I/O belong to
rust_textfinder_dirnav and rust_textfinder_output; the binary
performs neither.
The binary does write process-level text of its own - help, the resolved option listing, and
startup diagnostics - but it composes none of it. Each is a string
rust_textfinder_cmdline hands it, and the binary decides only when to write it
and to which stream. Block lines and announcements it neither composes nor writes.
Three things it owns outright:
- The skip list, and
add_skip_directory as the build-time
extension point Spec_TextFinder.md §3.5 defines. Section 3 covers it.
- The lifetime of all three values.
Dirnav borrows rather
than clones, so the StdoutSink, the finalized skip list, and the parsed
commands must outlive it. All three are locals of main, and the borrow
checker enforces the rule rather than leaving it to convention.
- The exit code. Section 4 covers the three values and how they map
onto this binary's failure modes.
It is 114 lines, and the specification that fixes it is 106. That ratio is the point of the
component: the design goal is to make the wiring itself readable, and everything harder than
wiring lives in a library.
2. The Startup Sequence
fn main() -> ExitCode performs eleven steps in order. The order is the
specification rather than a consequence of it: three of the eleven are placed where they are
for reasons that would not survive rearranging.
| Step |
What happens |
| 1 |
Collect the arguments from std::env::args_os and decode each to a String. An argument that will not decode writes invalid argument encoding: <token> to stderr and returns 2, having parsed nothing |
| 2 |
Invoke parse on the decoded arguments. On Err, write the returned usage diagnostic to stderr unaltered and return 1. A malformed /r is not detected here; step 8 reaches it |
| 3 |
Construct the StdoutSink. On None, write cannot initialize output to stderr and return 2 |
| 4 |
If /H true, write help_text() through the sink and return 0 |
| 5 |
If the argument list holds one element, write options_text(&commands) through the sink and return 0. This is the bare command line of §3.1 |
| 6 |
If /v true, write options_text(&commands) through the sink before traversal begins |
| 7 |
Finalize the skip list: the defaults, then the add_skip_directory calls compiled into the binary, then RefCell::take into a local that outlives the traversal |
| 8 |
Construct the Dirnav, generic over StdoutSink. Regex compilation happens here, and Dirnav::new returns Err when /r will not compile |
| 9 |
For each root path, in the order /P gave them, invoke search on the same value |
| 10 |
Call emit_run_summary on that same value, once, after the last root path returns |
| 11 |
Return exit code 0 |
Step 10 is the whole of this binary's part in the run summary.
Spec_TextFinder.md §3.6 puts the two counts in
rust_textfinder_dirnav, so this binary supplies no count and composes no text.
What it supplies is the one fact the library cannot have: that step 9 is finished. The call
sits inside the Ok arm of step 8, where the Dirnav value is in
scope, and it is the reason that arm ends in a statement rather than in the loop.
Every exit above returns before step 9, which is what makes the summary a mark of a run that
traversed. /H, the bare command line, a parse failure, an undecodable argument,
and a malformed /r each leave at their own step, and none of them reaches step
10 - so none writes a summary, which is exactly what §3.6 asks. The integration suite
asserts the absence for three of those five cases.
Step 1 uses args_os and not args.
std::env::args panics on an argument that is not valid Unicode, and a command
line the user typed must not crash the program. The decode is an explicit
into_string per argument, and its Err carries the undecodable
OsString, which to_string_lossy renders for the diagnostic. That
is the only rendering available for text that is not valid Unicode.
Step 3 precedes every write to stdout. The sink owns the process's only
stdout handle, so the binary's own help text and option listing pass through it rather than
through a handle of their own. They therefore share one buffer with the search output and
reach the stream in the order written. The C++ implementation needs this ordering for a
second reason, since constructing its sink also sets the stream mode; Rust needs no stream
mode, and the ordering still holds because of the shared buffer.
Step 5 reads the argument list's length and nothing else. It inspects no
element, because "no switch at all" is exactly "no tokens after the program name".
commands here is ProgramCommands::default(), so the listing names
every default and its /v line reads false. Steps 5 and 6 are
mutually exclusive: a command line bearing /v is not bare.
Step 8 is where a bad pattern is refused, and it takes four sub-steps to
report:
- If step 6 did not already write it, write
options_text(&commands)
through the sink now, so the user sees the /r line carrying the expression
that failed.
- Call
flush on the sink, so the listing reaches the stream ahead of the
diagnostic that explains it. The sink buffers stdout without per-line flushing, so
without this the two arrive out of order.
- Compose the diagnostic -
invalid regex for switch: /r, a newline, then
usage_line() - and write it to stderr. The binary composes it; neither
library does, the one supplying only usage_line() and the other only the
failure that triggers it.
- Return exit code 1, having traversed nothing.
The error text the regex crate puts in its own Error is not
written, and the specification gives a reason that outlives any wording change: that text
belongs to the crate, so a crate upgrade would alter this program's output with no document
in this tree recording the change.
Steps 8 and 9 are one match in the source rather than two statements, and the
borrow checker is why. The Dirnav borrows the sink for as long as it lives, so
the sink cannot be written to directly while the Dirnav exists. The
match runs the traversal inside its Ok arm and yields a
bool; the failure path then writes its listing after the borrow has ended. The
specification asks only that the listing precede the diagnostic, and this arrangement
satisfies both that and the borrow checker.
Step 9 passes a root-path failure to nobody. A root that cannot be searched - unopenable, a
symbolic link, or neither a regular file nor a directory - is announced by
rust_textfinder_dirnav itself, and the binary neither formats nor inspects that
notice. No root-path outcome affects the exit code.
3. The Skip List, RefCell, and thread_local
The binary owns the process-wide skip list, initialized with the 11 defaults of
Spec_TextFinder.md §3.2, and implements the extension point with that section's own
one-argument signature:
thread_local! {
static SKIP_LIST: RefCell<SkipList> = RefCell::new(default_skip_list());
}
fn default_skip_list() -> SkipList;
fn add_skip_directory(name: &str);
add_skip_directory takes the name alone, as §3.5 writes it, and reaches
the list through the cell. The interior mutability a RefCell supplies is what
makes that possible without an unsafe block and without a synchronization
primitive: the borrow is checked at run time rather than at compile time, and the check
costs one integer comparison per call.
The declaration is a thread_local! rather than a plain static. A
static must be Sync, and RefCell is deliberately not,
its borrow flag being an ordinary integer rather than an atomic one.
thread_local! carries no Sync bound, since each thread receives
its own value, and that costs this binary nothing: the list is built before traversal begins
and traversal runs on one thread.
Neither function is pub, and both are defined in the binary crate, so no
library and no test can call them; the calls that extend the list are written into the
binary's source and compiled with it. add_skip_directory ignores a name the
list already holds, and #[allow(dead_code)] marks it so that a build adding no
directory compiles without a warning. The source carries a
fn extend_skip_list() with an empty body, which is where such calls go.
That is the narrow reading of §3.5, which requires each language's specification to say
whether the function is exported from a library or confined to the component that owns the
list, and does not require it to be callable from outside the program. The C++
implementation reads it the same way, for the same reason.
Step 7 then takes the finished list out of the cell with RefCell::take and
holds it in a local. That local is what the Dirnav borrows, so the borrow lasts
the whole traversal rather than the body of a closure passed to with. The
Dirnav takes it as a shared borrow, so it consults the list and cannot modify
it - a property the type system carries rather than a comment.
4. Exit Codes
The three codes Spec_TextFinder.md §3.4 fixes map onto four failure modes. No other
value is returned from main.
| Code |
Cause in this binary |
What reaches which stream |
| 0 |
Startup and traversal completed; or /H; or the bare command line |
Search output, help, or the option listing on stdout. Match count and unopenable roots do not change it |
| 1 |
Parse failure, or a malformed /r |
A usage diagnostic on stderr. Parse failure leaves stdout empty; a malformed /r puts the option listing there first |
| 2 |
An argument would not decode, or StdoutSink::new returned None |
invalid argument encoding: <token> or cannot initialize output on stderr, no usage line, stdout empty |
Only one exit-code-1 path writes to stdout, and the asymmetry is deliberate: §5.2
requires the listing ahead of the invalid-regex diagnostic so the /r line shows
what failed, and leaves stdout empty for every other violation.
Every one of those strings is fixed in the Rust specifications rather than the project
specification. §2 leaves the wording of anything reaching stderr to each language, so
Spec_Rust_TextFinder_Cmdline.md §6 carries the six reason lines parse
produces and Spec_Rust_TextFinder_Entry.md §6 carries the seventh,
invalid regex for switch: /r, plus the two code-2 strings. What binds everyone
is the shape of a usage diagnostic, the destination, the exit codes, and what each failure
leaves on stdout. Rust adopts §5.2's supplied wording without changing a character, so
its stderr stays comparable with the C++ implementation's, which §6 no longer requires
but does not forbid.
An undecodable argument taking code 2 rather than code 1 is a decision, and the
specification records why either would look defensible. §5.2 fixes no reason line for
the case, since it cannot arise in an implementation whose argument vector carries bytes,
and §3.4 defines code 1 as the code that accompanies a §5.2 diagnostic. The binary
cannot form the command line it would have to validate, so the failure precedes
command-line validation rather than resulting from it.
A malformed regex taking code 1 is a decision too, and it went the other way: the expression
is something the user typed, so the failure is a usage failure even though it surfaces
inside a library constructor.
5. Why Every Exit Is a Return
Every exit in the sequence returns from main, never calling
std::process::exit. The StdoutSink is a local of
main, and only a return drops it and flushes its stdout buffer. Steps 4, 5, and
8 each write to stdout and then leave, and std::process::exit would discard
what they wrote.
ExitCode is what makes that practical. main returns
ExitCode rather than (), so a numeric status and a normal return
are the same act, and ExitCode::from(2) reads as plainly as
std::process::exit(2) would while leaving every destructor to run.
One rule pairs with it: nothing writes to stderr before stdout has been flushed. Steps 1 and
2 predate the sink and so have no buffer to flush; step 3 fails before one exists; step 8
flushes explicitly. rust_textfinder_output applies the same rule to its own
output failed notice.
Both rules exist because the sink defers flushing to Drop, which the
Output page covers. Every
exit being a return is the half of that requirement this component owns.
Nothing in the sequence panics. No step calls unwrap or expect,
and every failure it can meet resolves to a diagnostic and an exit code. That is a claim a
reader can check against the 113 lines of Section 7 rather than take on trust, which is why
the specification states it as a property of the sequence rather than as advice.
6. Stated Limits
Three non-goals, and one of them is a portability limit rather than a design choice.
- No per-file state in the binary. A single
Dirnav value
is reused across every root path, and it carries no state from one search
call to the next.
- Every argument must be valid Unicode. The binary takes
args_os and decodes, and an argument that will not decode is refused with
exit code 2. On Windows that admits any command line the shell can express, since the
arguments arrive as UTF-16; on POSIX it refuses a path holding bytes that are not valid
UTF-8. Spec_TextFinder.md §4 leaves the argument type and its encoding to this
document, so this is a stated limit of the Rust implementation rather than a departure
from the parent specification.
- No configuration file, and no runtime means of extending the skip
list.
The second limit cuts both ways against C++, and the specification says so. This
implementation accepts non-ASCII root paths and expressions the C++ implementation cannot
carry, since C++ takes main's char* argv[] undecoded, and it
refuses POSIX paths that the C++ implementation passes through as bytes. §6's
consistency guarantee speaks only to the command lines all four implementations accept.
7. Source
main.rs in full, then the package manifest. The step numbers in the comments
are the eleven steps of Section 2, and the citations name the specification section each step
satisfies.
main.rs
//! rust_textfinder_entry - the TextFinder binary.
//! Implements Spec_Rust_TextFinder_Entry.md.
use rust_textfinder_cmdline::{help_text, options_text, parse, usage_line, ProgramCommands};
use rust_textfinder_dirnav::{Dirnav, SkipList};
use rust_textfinder_output::StdoutSink;
use std::cell::RefCell;
use std::path::Path;
use std::process::ExitCode;
thread_local! {
static SKIP_LIST: RefCell<SkipList> = RefCell::new(default_skip_list());
}
fn default_skip_list() -> SkipList {
[
"archive",
".git",
".svn",
".hg",
"build",
"out",
"target",
"bin",
"obj",
"__pycache__",
"node_modules",
]
.iter()
.map(|name| String::from(*name))
.collect()
}
/// The build-time extension point of Spec_TextFinder.md section 3.5.
#[allow(dead_code)]
fn add_skip_directory(name: &str) {
SKIP_LIST.with(|list| {
let mut list = list.borrow_mut();
if !list.iter().any(|held| held == name) {
list.push(String::from(name));
}
});
}
/// Every call compiled in here runs before traversal begins. None is at present.
fn extend_skip_list() {}
fn main() -> ExitCode {
let mut args: Vec<String> = Vec::new();
for argument in std::env::args_os() {
match argument.into_string() {
Ok(text) => args.push(text),
Err(raw) => {
eprintln!("invalid argument encoding: {}", raw.to_string_lossy());
return ExitCode::from(2);
}
}
}
let commands: ProgramCommands = match parse(&args) {
Ok(commands) => commands,
Err(diagnostic) => {
eprint!("{diagnostic}");
return ExitCode::from(1);
}
};
let mut sink = match StdoutSink::new() {
Some(sink) => sink,
None => {
eprintln!("cannot initialize output");
return ExitCode::from(2);
}
};
if commands.help {
sink.write_text(&help_text());
return ExitCode::SUCCESS;
}
if args.len() == 1 {
sink.write_text(&options_text(&commands));
return ExitCode::SUCCESS;
}
if commands.verbose {
sink.write_text(&options_text(&commands));
}
extend_skip_list();
let skips: SkipList = SKIP_LIST.with(|list| list.take());
let compiled = match Dirnav::new(&mut sink, &skips, &commands) {
Ok(mut navigator) => {
for root in &commands.root_paths {
navigator.search(Path::new(root));
}
navigator.emit_run_summary(); // only main knows the last root has returned
true
}
Err(_) => false,
};
if !compiled {
if !commands.verbose {
sink.write_text(&options_text(&commands));
}
sink.flush();
eprint!("invalid regex for switch: /r\n{}", usage_line());
return ExitCode::from(1);
}
ExitCode::SUCCESS
}
Rust_Spec_driven_TextFinder_Entry/Cargo.toml
[package]
name = "rust_textfinder_entry"
version = "0.1.0"
edition = "2021"
rust-version = "1.70"
[[bin]]
name = "rust_textfinder"
path = "src/main.rs"
[dependencies]
rust_textfinder_cmdline = { path = "../Rust_Spec_driven_Cmdline" }
rust_textfinder_dirnav = { path = "../Rust_Spec_driven_Dirnav" }
rust_textfinder_output = { path = "../Rust_Spec_driven_Output" }
The [[bin]] section is what separates the package name from the executable
name. Without it Cargo would produce rust_textfinder_entry.exe, and
Spec_TextFinder.md §5.1 fixes the help text's synopsis around
rust_textfinder.
8. Prompt Records
This page carries none. Page_Structure.md §8 assigns it
Prompts_Spec_Rust_TextFinder_Entry.md and its Fix companion, and
neither was written: the Entry specification was produced in the same turn as the other four
documents, recorded at the project level. The audit that reshaped it sits on the
Process page, where three
of its four behavioral items reach this component.