Synopsis:
This page covers rust_textfinder_output, the sink. It receives fully formed
strings, writes each as one line, and adds nothing but the terminator.
-
Two write methods differing only in what they add, and the placement is the access
control -
write_text is inherent rather than part of the trait, so
traversal cannot emit unterminated text.
-
It takes no configuration. Dirnav formats every line in full, so there is nothing
left here to parameterize.
The constructor is interesting for what it does not do and for what it refuses.
- Rust writes the bytes it is given, so the LF requirement is met by doing nothing - where the C++ sink needs a mode call, two headers, and a failure path.
new yields Some once and None thereafter, because two buffers over one stdout reorder the output silently and only under load.
Default is deliberately not implemented, since it would offer callers a constructor that silently produces a second buffer.
Failure is absorbed rather than reported, and the specification states the three
properties that follow.
- Flush first, one notice rather than one per discarded line, and nothing reaches a caller.
- A method returning
() where the operation returns a Result is usually a smell; here it is the interface the structure document asked for.
- The flush on drop is named separately, being the write most likely to be the first one that fails.
1. The Sink
rust_textfinder_output receives fully formed strings from
rust_textfinder_dirnav, writes each as one line to stdout, and absorbs any
write failure so that neither the traversal nor the binary has to reason about it.
pub struct StdoutSink { /* private */ }
impl StdoutSink {
pub fn new() -> Option<Self>;
pub fn write_text(&mut self, text: &str);
pub fn flush(&mut self);
}
impl Output for StdoutSink {
fn output(&mut self, text: &str);
}
impl Drop for StdoutSink {
fn drop(&mut self);
}
Two methods write, and they differ only in what they add.
output, the trait method, writes the string it is given followed by the
single LF Spec_TextFinder.md §3.4 fixes. It is the method
rust_textfinder_dirnav reaches, and the trait is the only thing that
library knows about this one.
write_text writes the string verbatim, adding nothing. It exists for the
help text of §5.1 and the option listing of §5.3, which
rust_textfinder_cmdline returns already newline-terminated.
write_text is an inherent method rather than part of the trait, and that
placement is the access control: rust_textfinder_dirnav holds an
O: Output and can reach only what the trait declares, so it cannot emit
unterminated text. Both route through one private emit(text, terminate), so the
failed-state check and the write have one definition.
The type takes no configuration. rust_textfinder_dirnav formats every line in
full before emitting it - a block's path line, a block's indented detail lines, and every
announcement alike - so there is nothing left here to parameterize. The library does not
know a block line from an announcement, does not indent a detail line, and does not apply
/h, /n, or /L.
Once constructed, neither method panics and neither reports failure to a caller.
2. No Stream Mode to Set
Spec_TextFinder.md §3.4 obliges an implementation to prevent its runtime translating
the LF terminator to CRLF. Rust's standard output writes the bytes it is given on every
platform and performs no such translation, so this implementation meets the requirement by
doing nothing. No #[cfg] on the target platform appears in this crate.
That is worth a section because the
C++ sink meets the same
requirement with a constructor that calls _setmode(1, _O_BINARY), can fail
trying, and needs two headers in a global module fragment to do it. One requirement, one
parent specification, and two components whose constructors have nothing in common.
The comparison also explains a difference in the startup sequences. C++ must construct its
sink before any write to stdout, because the mode is a property of the stream and the help
text would otherwise carry CRLF; Rust must construct its sink before any write to stdout for
a different reason, because the sink owns the only handle. Both specifications place the
step identically and give different reasons, which is what §2's delegation to each
language's idiom looks like in practice.
output adds the terminator and nothing else - no prefix, no separator, no
trailing content - because the string arrives fully formatted.
3. One Sink, One Buffer
new takes no arguments and yields Some the first time it is called
and None every time after, so that the process holds one sink and one only. The
flag lives in a thread_local! Cell<bool>, and
Drop clears it, so a sink dropped before another is created releases the right
to make one.
thread_local! {
static SINK_TAKEN: Cell<bool> = Cell::new(false);
}
The reason is the buffer of Section 5. Two sinks would wrap the same stdout with two
independent BufWriters, and their contents would reach the stream in the order
the buffers happened to fill rather than the order the lines were written - which would
break the emission order §3.4 fixes, silently and only under load. A rule against a
second sink is cheaper to enforce than a defect that appears only on long searches.
None is the only way construction fails, and
rust_textfinder_entry builds exactly one sink, so its own code cannot provoke
it. It handles the case all the same, and the specification gives the reason: a constructor
with a failure mode that callers ignore is a constructor whose failure mode will eventually
be reached. That None is one of the two paths to exit code 2, which
Spec_TextFinder.md §3.4 fixes for a failure that is not about the command line.
Default is not implemented, and the omission is deliberate rather than an
oversight: Default::default must return Self, and this type cannot
promise one. A #[derive(Default)] here would offer callers a constructor that
silently produces a second buffer.
The thread_local! choice is the same one the binary makes for its skip list,
and for the same reason: interior mutability without unsafe and without a
synchronization primitive, on a value only one thread reaches. A plain static
would require Sync, and Cell is not.
4. Error Handling
A write that fails - a closed pipe, a full disk - sets an internal failed state. On the
first such failure the library flushes stdout and then writes the single line
output failed to stderr. Thereafter it discards every string it is given and
writes nothing more, to stdout or stderr.
Three properties of that sequence are stated in the specification rather than left to the
code.
- The flush comes first, so every line already buffered reaches the
stream ahead of the notice explaining why the lines stop. It is best-effort: whatever
broke the write may break the flush too, and the code discards that
Result with let _ = rather than acting on it.
- The failed state is permanent. One notice, not one per discarded
line, which is what keeps a broken pipe from turning a long search into a long stderr
transcript.
- The failure reaches no caller. All three methods swallow the
io::Result their writes return. The library never panics, never returns a
status, and never lets the failure reach rust_textfinder_dirnav, which goes
on traversing. unwrap and expect appear nowhere in it.
A method returning () where the operation returns a Result is
usually a smell, and here it is the interface the structure document asked for. The
Output trait's method returns nothing, so absorbing the failure is the
contract; the alternative is a trait method returning Result and a traversal
that must decide what to do about a sink it was given rather than chose.
The flush performed when the value is dropped obeys the same rule, and §6 of the
specification explains why that case is named separately: the final flush is the write most
likely to be the first one that fails, being the only one that must reach the stream and, on
a short search, the only one that reaches it at all. Nothing follows it, so nothing is left
to discard.
A failed write does not affect the exit code, which the Entry specification reserves for
command-line and startup failures. A run whose output went nowhere still exits 0: the exit
code answers whether TextFinder could do what it was asked, not whether the reader received
it.
5. Buffering and the One Flush
std::io::stdout returns a handle that flushes on every newline, and every line
this library writes ends in one, so writing through it directly would flush once per emitted
line. A search emitting 60 lines would pay 60 system calls for nothing. The sink therefore
holds a std::io::BufWriter wrapping that handle, which defers the write until
its buffer fills.
No flush is performed per line. The stream is flushed when the value is dropped, and
rust_textfinder_entry reaches that drop on every path out of the program, since
each of its exits returns from main rather than calling
std::process::exit. Those two facts are one requirement stated in two
documents, and the
Entry page covers the other
half.
Drop is implemented explicitly rather than left to BufWriter's
own, and the specification says why: BufWriter discards a failing flush without
notice, and §6 fixes what this one does with it instead. The inherent
flush performs the same write on demand, for the one case that needs the buffer
drained before the process ends.
One rule keeps the deferral from reordering the output:
stdout is flushed before any write to stderr.
This library applies the rule to its own output failed notice, and
rust_textfinder_entry applies it to the diagnostic that follows the option
listing on an invalid /r. Invocation 10 of the demonstration is that case: nine
listing lines on stdout, then two diagnostic lines on stderr, in that order.
Because this value owns the only stdout handle in the process, the binary's help text and
option listing pass through write_text rather than through a handle of their
own. They therefore share this buffer and reach the stream in the order written, and the
ordering rule above is the only coordination needed.
6. Source
lib.rs in full. It is the smallest source in the project at 83 lines, and the
ratio to its 110-line specification is the point of the component: a sink that does one
thing can be specified exhaustively.
Rust_Spec_driven_Output/src/lib.rs
//! rust_textfinder_output - the process's one stdout sink.
//! Implements Spec_Rust_TextFinder_Output.md.
use rust_textfinder_dirnav::Output;
use std::cell::Cell;
use std::io::{BufWriter, Stdout, Write};
#[cfg(test)]
mod unit_tests;
thread_local! {
static SINK_TAKEN: Cell<bool> = Cell::new(false);
}
pub struct StdoutSink {
writer: BufWriter<Stdout>,
failed: bool,
}
impl StdoutSink {
pub fn new() -> Option<Self> {
let available = SINK_TAKEN.with(|taken| {
let free = !taken.get();
taken.set(true);
free
});
if available {
Some(StdoutSink { writer: BufWriter::new(std::io::stdout()), failed: false })
} else {
None
}
}
/// Writes the text verbatim. The help text and option listing arrive already terminated.
pub fn write_text(&mut self, text: &str) {
self.emit(text, false);
}
pub fn flush(&mut self) {
if self.failed {
return;
}
if self.writer.flush().is_err() {
self.fail();
}
}
fn emit(&mut self, text: &str, terminate: bool) {
if self.failed {
return;
}
let mut wrote = self.writer.write_all(text.as_bytes());
if wrote.is_ok() && terminate {
wrote = self.writer.write_all(b"\n");
}
if wrote.is_err() {
self.fail();
}
}
/// Spec section 6: flush what is buffered, then write one notice, then discard everything after.
fn fail(&mut self) {
self.failed = true;
let _ = self.writer.flush();
let _ = writeln!(std::io::stderr(), "output failed");
}
}
impl Output for StdoutSink {
fn output(&mut self, text: &str) {
self.emit(text, true);
}
}
impl Drop for StdoutSink {
fn drop(&mut self) {
if !self.failed && self.writer.flush().is_err() {
self.failed = true;
let _ = writeln!(std::io::stderr(), "output failed");
}
SINK_TAKEN.with(|taken| taken.set(false));
}
}
Drop repeats the failure handling rather than calling fail,
because fail flushes first and the flush is what has just been attempted. The
duplication is four lines and the alternative is a flush of a buffer that was reported
unwritable one statement earlier.
7. Prompt Records
This page carries none. Page_Structure.md §8 assigns it
Prompts_Spec_Rust_TextFinder_Output.md and its Fix companion, and
neither was written: the Output specification was produced in the same turn as the other
four documents, recorded at the project level. Three audit items reach this component - the
missing flush, the unspecified failing flush in Drop, and the
second sink nothing prevented - and all three sit on the
Process page.