Spec-Driven

Spec-Driven Behavior: Command Line

nine switches, one help text, seven diagnostics, and why the text is fixed

Synopsis:
This page covers the command line - the syntax, the nine switches, and the three blocks of fixed text an implementation reproduces rather than describes.
  • Five syntax rules, uniform enough that a parser needs no special cases. One case-sensitive letter behind / or -, exactly one argument each, no bare flags.
  • Nine switches, every one with a default, so any partial command line resolves to a full option set. A language spec may extend the table but must not redefine a row.
  • A repeated switch is last-wins, except /P, which accumulates root paths in the order given.
Three texts are quoted in the specification, not described, so implementations cannot drift apart in wording they were handed.
  • The 22-line help text, which is user documentation and a test fixture at once.
  • Seven usage diagnostics. The specification binds their shape, destination, and exit code, and leaves the wording to each language.
  • The nine-line resolved option listing, which appears in three cases and never ends a line in whitespace.
Several rows carry a decision the record can account for.
  • /r rejects an empty argument, because the claim that an empty expression matches every line did not hold across engines.
  • An invalid regex is the one diagnostic that writes to stdout first - a pattern the engine rejects is a pattern whose effect the user cannot see.

1.  Switch Syntax

Spec_TextFinder.md §4 fixes the syntax in five rules, and the shape is uniform enough that a parser needs no special cases.
  • A switch is one case-sensitive letter behind an introducer. The two introducers / and - are equivalent, so /P and -P name the same switch. Because letters are case-sensitive, /h and /H denote different commands.
  • Every switch takes exactly one argument, supplied as the next whitespace-separated token. There are no bare flags, which is why a boolean switch reads /s true rather than /s.
  • Boolean values are true or false, matched case-insensitively. TRUE, True, and true are equivalent.
  • An argument holding whitespace or commas is double-quoted. The calling shell removes the quotes before TextFinder sees its argument vector, and TextFinder performs no quote processing of its own.
  • A repeated switch other than /P resolves last-wins, earlier occurrences silently discarded. /P accumulates instead, each occurrence adding a root path traversed in the order given.
A command line that violates these rules draws the corresponding usage diagnostic from §5.2 on stderr and exits non-zero, without traversing. One thing §4 declines to fix: the form in which the program receives its arguments. The type of the argument vector and the character encoding it carries are properties of the language and platform, so each language's own specification states its argument type and any encoding limit that follows. Where two implementations differ there, they differ in which command lines reach a search at all, and §6's consistency guarantee speaks only to the command lines both accept. The C++ implementation takes main's char* argv[] and states the ASCII limit that follows on Windows.

2.  The Nine Switches

Omitting a switch supplies its default, so any partial command line resolves to a full option set. A language-specific specification may extend this table but must not redefine a row of it.
Switch Argument (default) Meaning
/P path (.) Root path for traversal, absolute or relative. Repeat to add more; the paths are traversed in the order given
/p "ext, ext" ("") Extensions to search. An empty list searches every file, including files with no extension; a non-empty list excludes files with no extension
/r regex (".") Regular expression evaluated against each line, compiled once per invocation. It must not be empty; the default . is the way to match every line
/s bool (true) Recursive search. When false, the files directly within the root path are searched but no subdirectory is entered
/h bool (true) Suppress the file announcements of §3.4. When false, every examined file appears in the output exactly once
/v bool (false) List the resolved option set at the top of output, in the form §5.3 fixes
/H bool (false) Print the help text of §5.1 to stdout, exit 0, and do not traverse
/n bool (false) Give a block one detail line per matching line, carrying the 1-based line number
/L bool (false) Give a block one detail line per matching line, carrying that line's text
Two rows carry rules an implementation cannot decide for itself. The /p row defines an extension as the file's last dot-suffix, a leading dot on the name notwithstanding, so .gitignore has extension gitignore and a dot-file is searched like any other. It also fixes normalization: each item is trimmed, loses one leading dot if present, and empty items are discarded, so "cpp,,rs" and "cpp, rs" name the same two extensions. And it fixes comparison as case-sensitive on POSIX and case-insensitive on Windows. The /r row rejects an empty argument. That is a rule with a history: an earlier draft claimed an empty expression compiles and matches every line, and the review found the claim did not hold across engines. The answer was to require the default . instead and give the empty case a diagnostic of its own.

3.  The Help Text

§5.1 does not describe the help text; it reproduces it. Every implementation prints exactly this, with <executable> replaced by its own executable name, and its first line alone serves as the usage line that terminates every usage diagnostic.
Spec_TextFinder.md §5.1
usage: <executable> [/P path] [/p "ext, ext"] [/r regex] [/s bool] [/h bool] [/v bool] [/H bool] [/n bool] [/L bool]

  /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.
The block does two jobs at once. It is user-facing documentation, and it is a test fixture: each integration suite compares /H output against it and checks that every one of the nine switches is listed, and the C# command-line suite asserts its line count. Its last sentence about output was added when §3.6 was: a search now ends with a line counting the files and directories it reached, and a help text that described the block form without naming that line would leave a reader with one line of every run unexplained. The addition cost three fixtures a revision, which is what fixed text costs when it changes and the reason a change to this block is not made lightly. The text lived in the C++ command-line specification for several turns before moving here. There it looked like cross-language determinism and bound one language. The move is what makes it binding on four.

4.  The Seven Usage Diagnostics

A usage diagnostic reports a command line TextFinder will not act on. Every implementation writes it to stderr as a reason line, a newline, then the usage line of §5.1, and exits 1. <token> is the offending token and <switch> the switch, each reproduced exactly as typed, preserving the introducer. §5.2 binds that shape, the destination, the exit code, and what reaches stdout. It does not bind the wording. The reason lines below are the text the specification supplies, and a language whose idiom calls for different wording fixes its own in its component specification and owns it from there. All three implementations built so far adopt these unchanged, so the table is what they emit; Section 6 covers why the line falls where it does.
Condition Reason line
Token in switch position has no / or - introducer not a switch: <token>
Introducer-led token that is not a switch defined in §5 unrecognized switch: <token>
Switch is the last token, with no argument following missing argument for switch: <switch>
Boolean switch given a value other than true or false invalid boolean for <switch>: <token>
/P given an empty argument empty root path for switch: <switch>
/r given an empty argument empty expression for switch: <switch>
/r given an expression the engine will not compile invalid regex for switch: /r
The first two rows partition rather than overlap, and the partition took a decision to settle: a token with no introducer is not a switch, and any other introducer-led token, a bare / or - included, is an unrecognized switch. Before that rule, /ss, -abc, and a bare / each satisfied both conditions. Six of the seven leave stdout empty. The seventh is the exception, and the reason is stated in the specification: a pattern the engine rejects is a pattern whose effect the user cannot see, so before writing invalid regex for switch: /r TextFinder writes the §5.3 listing to stdout whatever /v says. The listing's /r line carries the offending expression verbatim, which is the point of emitting it. Under /v true the listing has already been written and is not repeated. Failures that are not about what the user typed are not usage diagnostics at all. They carry no usage line, their text is specified per implementation, and their exit code is the 2 that §3.4 fixes.

5.  The Resolved Option Listing

The listing appears before traversal begins, in three cases: under /v true, where it precedes the output it explains and traversal follows; on the bare command line of §3.1, where nothing follows and the process exits 0; and ahead of the invalid-regex diagnostic, where nothing follows and the process exits 1. One key/value pair per line, in the order the §5 table gives, each line formatted <switch> <value> with a single separating space and terminated by the single LF §3.4 fixes. /P emits one line per root path; /p emits the normalized extension list joined by , ; /r emits the expression verbatim as typed; every boolean emits true or false in lower case. A command line of -v true alone produces these nine lines:
Spec_TextFinder.md §5.3
/P .
/p
/r .
/s true
/h true
/v true
/H false
/n false
/L false
An empty /p list emits /p alone, with no separating space and no trailing space, so that no line of the listing ends in whitespace. The specification gives the reason: a fixture holding this text survives an editor that strips trailing space. A rule with no stated reason gets tidied away. The /v line reads true only in the listing /v itself asked for. The other two cases list an option set in which /v was never set, so a bare command line emits the nine lines above with /v false in place of /v true.

6.  Where the Line Falls: stdout, Not stderr

Three blocks of text appear on this page, and the specification treats two of them differently from the third. The help text and the nine-line listing are fixed byte for byte. The seven reason lines are not. The dividing line is the stream. §2 states what the project specification fixes: what a user sees on stdout - the blocks and announcements of §3.4, the help text of §5.1, the listing of §5.3, and the exit codes. Everything else belongs to a component specification, in its own language's idiom, and the wording of anything written to stderr is named among those. §6 scopes the consistency guarantee to match: two implementations given the same command line write the same bytes to stdout and return the same exit code, and what they write to stderr is each one's own. The comparison the project wants is a line-for-line diff of two runs over one tree, and that diff runs over stdout. Anything an implementation is free to word its own way would defeat it at the first difference, and a reviewer would then have to decide whether the difference is a defect or a preference. Quoting the help text and the listing removes the question where it matters. §5.3 says as much about the listing: an implementation free to choose its form would make two listings uncomparable. Two implementations still agree on more than the guarantee requires. §5.2 binds which command lines are refused, the shape of the refusal, the exit code, and what each refusal leaves on stdout - so a test can check every one of those across implementations. Only the words differ, and all three implementations built so far have chosen not to differ in them either. Relaxing the wording also retired a claim the project could not keep. §3.4 governs the stdout terminator and leaves the stderr terminator to the runtime and platform, so diagnostic text was never comparable to the byte across platforms even while §5.2 said it was fixed. The C++ integration suite found that contradiction the hard way: eight assertions failed on a CRLF-against-LF difference in exactly the two outputs that bypass the output component, and the suite normalizes line endings in those two comparisons. The specification now says what the suite always had to do.