Synopsis:
This page covers rust_textfinder_cmdline, the single place in the Rust
implementation where switch letters, argument syntax, and defaults are known.
-
One struct and four functions.
parse returns Result, whose
Err carries the complete diagnostic, so the caller's error path is one
print and a return.
-
It takes a slice the binary already decoded, so it never sees an undecodable
argument and defines no behavior for one - which is why its 29 tests run without
spawning a process.
The Default implementation is the sole authority in code for the nine
defaults.
#[derive(Default)] would not do, since four fields need a value the derive cannot produce.
- The unit suite asserts each default against the specification separately, rather than asserting the struct equals its own
Default - which would compare the code against itself.
Several rules here are written against a plausible wrong call rather than from scratch.
switch_letter walks a Chars iterator, because len() counts bytes and a two-character token holding one non-ASCII character would be refused by the wrong route.
str::trim is rejected for trimming too much, and char::is_ascii_whitespace for trimming one character too little - the omission that sent the whitespace rule up to the parent specification.
- The exhaustive
match carries an arm no input reaches, which the record marks as the compiler's requirement rather than a second validation.
1. Public Interface
rust_textfinder_cmdline converts a slice of argument strings into a struct that
controls the other two libraries. It is the single place in the Rust implementation where
switch letters, argument syntax, and defaults are known, and it performs no traversal, no
matching, no file I/O, and no stream writing.
The crate root lib.rs exports one struct and four functions.
pub struct ProgramCommands {
pub root_paths: Vec<String>, // /P
pub extensions: Vec<String>, // /p
pub regex_text: String, // /r
pub recurse: bool, // /s
pub suppress_on_no_match: bool, // /h
pub verbose: bool, // /v
pub help: bool, // /H
pub line_numbers: bool, // /n
pub matched_line: bool, // /L
}
pub fn parse(args: &[String]) -> Result<ProgramCommands, String>;
pub fn usage_line() -> String;
pub fn help_text() -> String;
pub fn options_text(commands: &ProgramCommands) -> String;
parse returns Result because
rust_textfinder_entry needs two things from a failure: the signal, and the text
to write. The Err variant carries the complete diagnostic, so the caller's
error path is one eprint! and a return rather than a lookup and a format.
parse takes &[String] rather than reading the arguments
itself. The binary collects them and decodes them, per Spec_Rust_TextFinder_Entry.md §4
step 1, so this library never sees an undecodable argument and defines no behavior for one.
The unit suite hands it hand-built slices as a consequence, which is why 29 tests run in
under a millisecond and none of them spawns a process.
ProgramCommands derives Clone and Debug and has no
invariants: every field combination the parser can produce is valid. Dirnav
borrows the value rust_textfinder_entry owns rather than cloning it, so that
value must outlive the Dirnav it was passed to. The borrow checker enforces
that rule, where the C++ implementation states the same lifetime requirement in prose and
relies on the caller.
The field names are the switch meanings spelled out rather than the switch letters.
suppress_on_no_match for /h is the one worth pausing on: it names
what the flag does rather than what it is called, so the gating check in
rust_textfinder_dirnav reads as a sentence.
2. Defaults Live in the Default Impl
The field comments are the switch-to-field mapping, and the Default
implementation is the sole authority in code for the defaults of Spec_TextFinder.md
§5.
impl Default for ProgramCommands {
fn default() -> Self {
ProgramCommands {
root_paths: vec![String::from(".")],
extensions: Vec::new(),
regex_text: String::from("."),
recurse: true,
suppress_on_no_match: true,
verbose: false,
help: false,
line_numbers: false,
matched_line: false,
}
}
}
ProgramCommands::default() equals the result of parsing an empty command line,
because parse starts from it and overwrites only what the arguments name. That
is what makes step 5 of the Entry startup sequence a one-liner: the bare command line prints
the listing of a default-constructed value.
#[derive(Default)] would not do. Three of the nine fields need a value the
derive cannot produce - . for root_paths, . for
regex_text, and true for recurse and
suppress_on_no_match - so the implementation is written out, and every default
§5 fixes appears once, in table order, in one block a reader can compare against the
specification line by line.
The wording in the specification is load-bearing: this is the sole authority
in code. Spec_TextFinder.md §5 remains the authority overall, and a
disagreement between the two is a defect in this crate. The unit suite asserts each of the
nine against §5 separately rather than asserting the struct equals its own
Default, which would compare the code against itself.
3. Parsing Rules
parse scans args[1..] left to right, alternating switch token and
argument token; args[0] is the program name and is not inspected. It stops at
the first violation and returns that diagnostic; no partial result is produced. It touches
no filesystem: root paths are not tested for existence, and extensions are not compared
against any file.
Four rules govern it.
- Switch tokens. A token in switch position is valid only when it is
exactly two characters, the first
/ or -, the second one of
the nine letters. A token with no introducer is not a switch; any other
introducer-led token, including a bare / or -, is an
unrecognized switch.
- Arguments. Each switch consumes the following token verbatim,
including when that token begins with
/ or -, since there are
no bare flags. A switch with no following token is missing its argument.
- Conversion. Boolean switches accept only
true or
false, compared with eq_ignore_ascii_case. /r and
/P take the token verbatim and each rejects an empty argument.
/p is normalized.
- Accumulation.
/P clears the default
vec![String::from(".")] on its first occurrence and appends thereafter,
preserving argument order. Every other switch overwrites any earlier value, silently
discarding it.
Rule 1 partitions the first two diagnostics of §5.2 rather than leaving them to
overlap. Before the partition, /ss, -abc, and a bare
/ each satisfied both conditions, and two implementations could have reported
different reason lines for the same token while both following the specification.
switch_letter reads the partition directly: it takes the first character from
an iterator and returns not a switch unless that character is an introducer,
then takes two more and returns unrecognized switch unless the second is one of
the nine letters and the third does not exist.
Walking a Chars iterator rather than indexing is what keeps the length test
correct for a multi-byte token. token.len() counts bytes, so a two-character
token holding one non-ASCII character reports 3 or more and would be refused as over-long
rather than as an unrecognized switch - the same outcome by the wrong route. An earlier
draft of the specification defended counting characters on the ground that the two differ,
and the review struck that sentence: both routes produce
unrecognized switch for every input, so the claim did not hold even though the
code it defended is the one to write.
Rule 4's first-occurrence-clears behavior needs one bit of state,
roots_supplied, and the code carries it as a local rather than inferring it
from the vector's contents. Inferring would work until a user typed -P ., which
is indistinguishable from the default by value.
The match over the switch letter carries a _ arm returning
unrecognized switch that no input reaches, since
switch_letter has already refused every letter outside the nine. Rust requires
a match over char to be exhaustive, so the arm is the compiler's
requirement rather than a second validation, and it returns the diagnostic the token would
have drawn had the earlier test not caught it.
Every violation returns the complete usage diagnostic Spec_TextFinder.md §5.2 binds -
its reason line, a newline, then usage_line() - built by one three-line
diagnostic helper, so the shape has one definition. Six of the seven rows are
this library's. The seventh, a malformed /r, is detected later:
Dirnav::new compiles the expression and returns its failure, and the binary
composes the diagnostic. Neither library composes it - the one supplies a string, the other
reports the failure, and the binary joins them.
Spec_Rust_TextFinder_Cmdline.md §6 carries the six reason lines and owns them.
Spec_TextFinder.md §2 leaves the wording of anything reaching stderr to each language,
and this implementation adopts §5.2's supplied wording unchanged, which costs nothing
and leaves its stderr comparable with the C++ implementation's.
There is no error condition for a duplicated switch or an empty /p list.
Duplicates resolve by rule 4, and an empty extension list means every file is searched.
4. Extension-List Normalization
The /p argument arrives as one token, the shell having already removed the
quotes. Normalization implements the /p rules of Spec_TextFinder.md §5 as
one iterator chain: split on commas, trim each item, strip one leading . if
present, discard empty items, and collect the survivors in order.
/// The six characters Spec_TextFinder.md section 5 names, and no others.
fn is_trimmed(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\u{000B}' | '\u{000C}' | '\r')
}
fn normalize_extensions(argument: &str) -> Vec<String> {
argument
.split(',')
.map(|item| item.trim_matches(is_trimmed))
.map(|item| item.strip_prefix('.').unwrap_or(item))
.filter(|item| !item.is_empty())
.map(String::from)
.collect()
}
" .rs , , txt " therefore normalizes to rs, txt, which is the pair
the option listing reports.
Two standard-library calls are rejected by name, and each stops an implementer writing
something plausible and wrong.
str::trim is not used. It trims every character Unicode
calls whitespace, a set including no-break space and the en and em spaces, so this
implementation would accept an extension list another rejects.
char::is_ascii_whitespace is not used either. It omits
vertical tab, which §5 names. That omission is the one-character difference that
sent the rule up to the parent specification: C's isspace in the C locale
includes U+000B, so /p "cpp,\vrs" would have normalized differently in the
two implementations. §5 now names the six characters, and all three implementations
test against that list and nothing else.
strip_prefix('.') removes the dot, and one call removes at most one, which is
the rule §5 states. unwrap_or(item) supplies the unstripped item when
there was no dot, so the chain needs no branch.
Duplicates are retained. They are harmless to the membership test
rust_textfinder_dirnav performs, and the specification says so rather than
leaving a reader to wonder whether the omission was an oversight. Case folding is not
applied here: the platform-dependent comparison §5 fixes is performed by
rust_textfinder_dirnav when it matches a file name against the list, because
that is where the platform question arises.
5. Help Text and Option Listing
Three functions render text the binary writes, and none of them writes it.
help_text() returns the text Spec_TextFinder.md §5.1 fixes with
<executable> replaced by rust_textfinder. The body is a
raw string literal, r#"..."#, so the fixture and the source agree by
construction and the backslashes and quotes inside it need no escaping.
usage_line() returns its first line - the line that terminates every
usage diagnostic - and help_text() is built from it, so the synopsis has
one definition.
options_text(commands) returns the resolved option set in the form
§5.3 fixes.
One function serves all three cases §5.3 calls for, since the text is the same in each
and only the caller's next move differs: /v true, after which traversal
follows; the bare command line, after which the process exits 0; and the invalid-regex
diagnostic, where the listing precedes that diagnostic whatever /v says and the
process then exits 1. The listing reflects whatever commands holds, so its
/v line reads false in the latter two unless /v was
itself typed.
The Rust specification adds one sentence about options_text that is worth more
than it looks: it chooses none of that form and must not be read as the place the form is
decided. A reader who wants to change the listing changes Spec_TextFinder.md §5.3.
The listing's construction shows where the form is fixed and where it is not. The six
boolean lines come from one loop over an array of switch-and-value pairs in §5 table
order, so no line can be forgotten and none can be reordered without moving an array
element. The /P, /p, and /r lines are written out,
because each has a rule of its own: one line per root path, the extension list joined by
", " with /p alone when the list is empty, and the expression
verbatim. The unit suite asserts that no line of the listing ends in whitespace, which is
the rule that lets this text serve as a test fixture.
All three functions end their returned string with a newline, and none writes to a stream.
The library opens no stream at all, which is what lets its unit suite check the rendered
text as a string rather than by capturing output.
6. Source
lib.rs in full - 188 lines, of which the help body is 21. The comments cite the
Rust specification, which cites Spec_TextFinder.md in turn.
Rust_Spec_driven_Cmdline/src/lib.rs
//! rust_textfinder_cmdline - command-line parsing for TextFinder.
//! Implements Spec_Rust_TextFinder_Cmdline.md, the Rust binding of Spec_TextFinder.md sections 4-5.
#[cfg(test)]
mod unit_tests;
#[derive(Clone, Debug)]
pub struct ProgramCommands {
pub root_paths: Vec<String>, // /P
pub extensions: Vec<String>, // /p
pub regex_text: String, // /r
pub recurse: bool, // /s
pub suppress_on_no_match: bool, // /h
pub verbose: bool, // /v
pub help: bool, // /H
pub line_numbers: bool, // /n
pub matched_line: bool, // /L
}
impl Default for ProgramCommands {
fn default() -> Self {
ProgramCommands {
root_paths: vec![String::from(".")],
extensions: Vec::new(),
regex_text: String::from("."),
recurse: true,
suppress_on_no_match: true,
verbose: false,
help: false,
line_numbers: false,
matched_line: false,
}
}
}
const EXECUTABLE: &str = "rust_textfinder";
const SWITCH_LETTERS: [char; 9] = ['P', 'p', 'r', 's', 'h', 'v', 'H', 'n', 'L'];
const HELP_BODY: &str = r#"
/P path (.) root path for traversal; repeat to add more root paths
/p "ext, ext" () comma-separated bare extensions to search; empty searches every file
/r regex (.) regular expression evaluated against each line
/s true|false (true) recurse into subdirectories
/h true|false (true) hide files that matched nothing; errors always appear
/v true|false (false) list the resolved option set before traversal
/H true|false (false) print this help and exit
/n true|false (false) add a detail line per match, carrying the line number
/L true|false (false) add a detail line per match, carrying the line text
A matching file prints its path on one line; /n and /L add indented detail
lines beneath it. A path is never printed twice. A search ends with a line
counting the files and directories it reached.
Switch introducers / and - are equivalent. Switch letters are case-sensitive,
so /h and /H differ. Every switch takes exactly one argument; there are no bare
flags. Arguments containing whitespace or commas must be quoted.
Run with no switches at all to list the resolved options and exit without
searching.
"#;
pub fn usage_line() -> String {
format!(
"usage: {EXECUTABLE} [/P path] [/p \"ext, ext\"] [/r regex] [/s bool] [/h bool] [/v bool] [/H bool] [/n bool] [/L bool]\n"
)
}
pub fn help_text() -> String {
format!("{}{}", usage_line(), HELP_BODY)
}
pub fn options_text(commands: &ProgramCommands) -> String {
let mut text = String::new();
for root in &commands.root_paths {
text.push_str("/P ");
text.push_str(root);
text.push('\n');
}
if commands.extensions.is_empty() {
text.push_str("/p\n");
} else {
text.push_str("/p ");
text.push_str(&commands.extensions.join(", "));
text.push('\n');
}
text.push_str("/r ");
text.push_str(&commands.regex_text);
text.push('\n');
for (switch, value) in [
("/s", commands.recurse),
("/h", commands.suppress_on_no_match),
("/v", commands.verbose),
("/H", commands.help),
("/n", commands.line_numbers),
("/L", commands.matched_line),
] {
text.push_str(switch);
text.push(' ');
text.push_str(if value { "true" } else { "false" });
text.push('\n');
}
text
}
pub fn parse(args: &[String]) -> Result<ProgramCommands, String> {
let mut commands = ProgramCommands::default();
let mut roots_supplied = false;
let mut index = 1;
while index < args.len() {
let token = args[index].as_str();
let letter = switch_letter(token)?;
let value = match args.get(index + 1) {
Some(next) => next.as_str(),
None => return Err(diagnostic(&format!("missing argument for switch: {token}"))),
};
match letter {
'P' => {
if value.is_empty() {
return Err(diagnostic(&format!("empty root path for switch: {token}")));
}
if !roots_supplied {
commands.root_paths.clear();
roots_supplied = true;
}
commands.root_paths.push(String::from(value));
}
'p' => commands.extensions = normalize_extensions(value),
'r' => {
if value.is_empty() {
return Err(diagnostic(&format!("empty expression for switch: {token}")));
}
commands.regex_text = String::from(value);
}
's' => commands.recurse = boolean(token, value)?,
'h' => commands.suppress_on_no_match = boolean(token, value)?,
'v' => commands.verbose = boolean(token, value)?,
'H' => commands.help = boolean(token, value)?,
'n' => commands.line_numbers = boolean(token, value)?,
'L' => commands.matched_line = boolean(token, value)?,
_ => return Err(diagnostic(&format!("unrecognized switch: {token}"))),
}
index += 2;
}
Ok(commands)
}
fn switch_letter(token: &str) -> Result<char, String> {
let mut chars = token.chars();
match chars.next() {
Some('/') | Some('-') => {}
_ => return Err(diagnostic(&format!("not a switch: {token}"))),
}
match (chars.next(), chars.next()) {
(Some(letter), None) if SWITCH_LETTERS.contains(&letter) => Ok(letter),
_ => Err(diagnostic(&format!("unrecognized switch: {token}"))),
}
}
fn boolean(switch: &str, value: &str) -> Result<bool, String> {
if value.eq_ignore_ascii_case("true") {
Ok(true)
} else if value.eq_ignore_ascii_case("false") {
Ok(false)
} else {
Err(diagnostic(&format!("invalid boolean for {switch}: {value}")))
}
}
fn diagnostic(reason: &str) -> String {
format!("{reason}\n{}", usage_line())
}
/// The six characters Spec_TextFinder.md section 5 names, and no others.
fn is_trimmed(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\u{000B}' | '\u{000C}' | '\r')
}
fn normalize_extensions(argument: &str) -> Vec<String> {
argument
.split(',')
.map(|item| item.trim_matches(is_trimmed))
.map(|item| item.strip_prefix('.').unwrap_or(item))
.filter(|item| !item.is_empty())
.map(String::from)
.collect()
}
#[cfg(test)] mod unit_tests; at the top is how the 29 assertions of the
Testing page reach the
private items beside them. The module compiles only under cargo test, so
nothing of the suite reaches the shipped library.
7. Prompt Records
This page carries none. Page_Structure.md §8 assigns it
Prompts_Spec_Rust_TextFinder_Cmdline.md and its Fix companion, and
neither was written: the Cmdline specification was produced in the same turn as the other
four documents, recorded at the project level. The two audit items that reach this component
- the struck char-counting claim and the whitespace set - sit on the
Process page.