Skip to content

Latest commit

 

History

History
355 lines (268 loc) · 29.1 KB

File metadata and controls

355 lines (268 loc) · 29.1 KB

POSIX.1-2024 Conformance Audits — screen/ utilities

This file collects per-utility POSIX conformance audits for the terminal-screen utilities crate. Each audit follows the playbook in audits.md.

Crate: screen/stty, tabs, tput (three POSIX utilities) plus the shared osdata.rs termios data tables used by stty. Date: 2026-06-18 Method: static spec-vs-code audit against the sliced POSIX.1-2024 tree (~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/{stty,tabs,tput}.md), with every Critical/Major claim behaviorally confirmed by running the release binaries (a PTY harness via pty.fork() was used for the TTY-only stty set-mode paths).


Cross-cutting observations

  • osdata.rs (262 lines) is stty-private (mod osdata; in stty.rs:13); tabs/tput do not use it. It holds the speed table (load_speeds), the ^c→control-char translation table (load_cchar_xlat), and the master termios parameter table (load_params). One data defect lives here: the speed map keys "54" to B50 (osdata.rs:32) — see stty #6.
  • i18n is split. All three call setlocale(LC_ALL, "") + textdomain + bind_textdomain_codeset in main. tabs and tput wrap their runtime diagnostics in gettext(); stty does not — every Error::other(...) / format! diagnostic in stty.rs is hardcoded English (stty #8), so LC_MESSAGES is inert for stty.
  • terminfo dependency. tabs and tput are thin front-ends over the terminfo crate (Database::from_env/from_name + capability expansion). stty is a direct termios(3) client (termios crate). All three honor the ASYNCHRONOUS EVENTS = Default requirement (no signal handling needed; none present — correct).
  • Diagnostic prefix. stty propagates errors through main() -> Result<…, Box<dyn Error>>, so the runtime prints Error: <msg> (Rust's default) rather than the conventional stty: <msg>. tabs prefixes tabs:; tput prefixes neither util name (stty #8, tput #T6 — Minor).

stty

Implementation: screen/stty.rs (814 lines) + screen/osdata.rs (262 lines) Tests: screen/tests/stty/mod.rs (26 lines — no executable tests; the file is a comment explaining that stty needs a TTY and is verified only by manual testing) Spec: POSIX.1-2024 (IEEE Std 1003.1-2024), Vol. 3 §3, pp. 3453–3462 Reference slice: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/stty.md Date: 2026-06-18

TL;DR

The display paths (-a long form, -g compact save, default short form) and the termios flag/cchar tables are broad and largely conforming, and the utility correctly uses standard input for both get and set (the spec's security RATIONALE). But the set path — stty's primary job — is severely broken on the golden path: stty <single-operand> (e.g. stty sane, stty raw, stty cs8) panics (assert!(args.operands.len() > 1)), and every negation operand (-echo, -icanon, …) is rejected by clap as an unknown option because the positional has no allow_hyphen_values. On top of those two crashes/rejections: single-character control-character assignment (stty erase x) is unsupported, the Issue-8 rows/cols/size window-size operands are entirely absent, and the speed table mis-keys B50 as "54". Diagnostics are hardcoded English.

Priority issues

Resolved 2026-06-18 (branch screen-audit). All stty findings fixed across Phases 1–3: #1/#2 unbreak the set path (single-operand panic removed, allow_hyphen_values for negation), #3 single-char cchar assignment, #4 rows/cols/size, #5 hardened save-blob parse, #6 "50" speed key (+ cfg-gated higher bauds), #7 cchar_to_str display, #8 gettext + stty: prefix, #9 ek/sane reviewed-conforming, #10 reject operands with -a/-g. stty gained a full test suite (8 pure-function unit tests + 9 PTY regressions via the portable-pty harness) where it previously had none.

Critical

  • #1 — stty <single mode operand> panics (assertion failure, exit 101). ✓ fixed (Phase 1): assertion changed to !args.operands.is_empty(); regression test_stty_single_operand_no_panic. stty.rs:642 (assert!(args.operands.len() > 1)) at the top of stty_set_long. stty_set (stty.rs:778-784) routes any non-pfmt1 operand list to stty_set_long, including a list of length 1. So stty sane, stty raw, stty cs8, stty parenb, stty -8 etc. all abort. Behaviorally confirmed under a PTY: stty parenbpanicked at screen/stty.rs:642 … assertion failed: args.operands.len() > 1, exit 101. This is the most common stty usage form. Fix: change the assertion to >= 1 (or delete it; the while idx < len loop already handles any length).
  • #2 — Negation operands (-echo, -icanon, -parenb, …) are rejected by clap before stty ever sees them. ✓ fixed (Phase 1): #[arg(allow_hyphen_values = true)] on operands; -a/-g still parse as options; regression test_stty_negation_operand_applied. stty.rs:34-59 declares operands: Vec<String> with no allow_hyphen_values/trailing_var_arg. clap treats a leading-- token as an option cluster. Behaviorally confirmed: stty -echoerror: unexpected argument '-e' found, exit 2. The internal set logic does implement negation (stty.rs:657-665 strip_prefix("-")), so the entire negation surface is dead code. Fix: add #[arg(allow_hyphen_values = true)] (or trailing_var_arg) to the operands field so -flag operands reach stty_set_long.

Major

  • #3 — Single-character control-character assignment is unsupported. ✓ fixed (Phase 2): a 1-char op_arg now sets the control char to that byte (rejecting code points > 0xff); regression test_stty_single_char_cchar_assignment. stty.rs:418-441 (set_ti_cchar_oparg) accepts only ^-/undef/^c; any other argument that is not a 2-char ^-sequence returns "Invalid cchar specification". POSIX 116246: "If string is a single character, the control character shall be set to that character." Behaviorally confirmed: stty erase xInvalid cchar specification, exit 1. Fix: when op_arg.chars().count() == 1, set *cc = that_char as cc_t.
  • #4 — Issue-8 window-size operands rows, cols, and the size informational query are entirely missing. ✓ fixed (Phase 2): rows/cols/size handled in stty_set_long via get_winsize/set_winsize (libc::ioctl TIOCGWINSZ/TIOCSWINSZ on stdin); size prints "%d %d\n"; regression test_stty_rows_cols_size. Added by Austin Group Defects 1053/1532/1687 (CHANGE HISTORY, 116429). POSIX 116290-116301 mandates rows number, cols number (via tcsetwinsize()), and size (write "%1d %1d\n", <rows>, <cols> to stdout). grep -nE '"rows"|"cols"|"size"|winsize|TIOCGWINSZ' stty.rs osdata.rs0 matches. size is the only Informational Query; without it stty size is "Unknown operand". Fix: add rows/cols operands using TIOCSWINSZ, and a size query using TIOCGWINSZ.
  • #5 — saved settings combination mode only round-trips this implementation's own pfmt1: blob. ✓ hardened (Phase 3): the assert_eq!(parts[0], HDR_SAVE) is now a returned diagnostic ("invalid saved-settings format"); the round-trip contract is otherwise retained. stty.rs:778-784 recognizes a saved-settings operand only when it starts_with(HDR_SAVE) ("pfmt1", stty.rs:32). That is the format -g emits (stty_show_compact, stty.rs:292-319), so stty "$(stty -g)" round-trips — CONFORMS in spirit. Flagged Major only because the -g form is not the spec's "one line of printable portable-character-set tokens excluding whitespace" guarantee tested for portability, and a malformed blob path has an assert_eq!(parts[0], HDR_SAVE) (stty.rs:488) guarded only by the starts_with check — safe today, but brittle. No fix required if the round-trip contract is the only goal; documented for the next auditor.
  • #6 — Speed table mis-keys B50 as "54"; stty 50 and ispeed/ospeed 50 fail. ✓ fixed (Phase 3): key corrected to "50"; cfg-gated 460800/921600 added for non-Apple; regressions test_speed_table_50_not_54 (unit) + test_stty_speed_50. osdata.rs:32 (("54", libc::B50)). The decimal baud string for B50 is "50", not "54". Consequences: stty 50 / stty ispeed 50set_ti_speed lookup miss → Error::other("Invalid speed") (stty.rs:451); and stty 54 wrongly programs B50. Fix: change the key to "50". (Also Minor: the table omits 460800/921600 and the non-decimal exta/extb aliases.)

Minor

  • #7 — Non-printable / printable-but-unmapped control chars are displayed as a raw decimal value. ✓ fixed (Phase 3): a shared cchar_to_str helper renders <undef>/^c/literal-char/\NNN-octal, used by both show_cchars and stty_show_short; regressions test_cchar_to_str_* (unit) + test_stty_printable_cchar_rendering. stty.rs:251-253 (show_cchars) and stty.rs:146-150 (stty_show_short): when a c_cc value is not in the ^c reverse map and not \0, the code prints format!("{}", ti.c_cc[idx]) (a decimal integer). POSIX 116347-116351: the value shall be "either the character, or some visual representation of the character if it is non-printable, or <undef>". A printable assignment (e.g. erase set to @) prints 64 instead of @ or ^?-style. Fix: print the literal char when printable, else a ^X/\NNN-style visual representation.
  • #8 — All stty diagnostics are hardcoded English and lack the stty: prefix. ✓ fixed (Phase 3): diagnostics routed through gettext(); main now catches the error and prints stty: <msg> to stderr, returning ExitCode::FAILURE. Every Error::other("…")/format!("Unknown operand {}", …) in stty.rs (:451, 504, 508, 687, 703, 712, 752, etc.) is plain English, not gettext()-wrapped; and because they bubble through main() -> Result<…, Box<dyn Error>>, the user sees Error: <msg> rather than stty: <msg>. POSIX 116317-116319 (LC_MESSAGES). Fix: wrap strings in gettext() and emit via an explicit eprintln!("stty: {}", …) path (matching tabs).
  • #9 — ek and sane use hardcoded control-char defaults rather than the device's "system defaults". stty.rs:610-635. POSIX 116287 (ek: "system defaults") / 116288 (sane: "reasonable, unspecified, values"). The hardcoded 0x7f/0x15/0x03/… are a reasonable set and sane's values are explicitly "unspecified", so this CONFORMS. Reviewed (Phase 3): no behavior change — there is no portable "system default" termios to consult; the chosen values match common practice (and coreutils sane). Documented as conforming.
  • #10 — -a/-g silently ignore any accompanying operands. ✓ fixed (Phase 3): run now rejects operands combined with -a/-g ("operands cannot be combined with -a or -g") and exits non-zero; regression test_stty_operands_with_all_rejected. stty.rs:797-811: when args.all/args.save is set, operands are never consulted. The SYNOPSIS separates stty [-a|-g] from stty operand...; stty -a sane should arguably be a usage error. clap's group = "mode" correctly makes -a and -g mutually exclusive, but neither conflicts with operands. Fix: reject operands when -a/-g is present.

Detailed conformance matrix

SYNOPSIS / argv parsing

  • -a and -g mutually exclusive — stty.rs:40,48 share clap group = "mode".
  • Negation operands reach the set path (#2 ✓) — operands now has allow_hyphen_values (stty.rs:57).
  • Bundled/standalone positive operands accepted — stty cs8 cs7 reaches the set path (confirmed; failed only at tcsetattr on the test PTY, i.e. parsing/dispatch worked).
  • -- end-of-options handled (clap default).

OPTIONS

Opt Status Notes (file:line)
-a CONFORMS stty.rs:797-799stty_show_long; emits speed … baud;, flag groups, cchars.
-g PARTIAL stty.rs:801-802stty_show_compact emits a pfmt1:-prefixed colon-joined blob; round-trips with this stty (#5) but is a private form.

OPERANDS

  • Control Modesparenb/parodd/cs5-8/hupcl/hup/cstopb/cread/clocal present (osdata.rs:152-164); baud number/ispeed/ospeed present (speed table fixed, #6 ✓).
  • Input Modes — all present (osdata.rs:178-192); reachable now that the set path is fixed (#1/#2 ✓).
  • Output Modesopost (Base) + XSI onlcr/ocrnl/onocr/onlret/ofill/ofdel/cr0-3/nl0-1/tab0-3/tabs/bs0-1/ff0-1/vt0-1 present (osdata.rs:196-222).
  • Local Modesisig/icanon/iexten/echo/echoe/echok/echonl/noflsh/tostop present (osdata.rs:226-237).
  • Special Control Character Assignmentseof/eol/erase/intr/kill/quit/susp/start/stop mapped (osdata.rs:247-255); ^c table complete (osdata.rs:63-124); ^-/undef→0 handled; single-char form now supported (#3 ✓).
  • min/timeosdata.rs:259-260 + stty.rs:742-756 parse numeric u8 args.
  • Combination Modesevenp/parity/oddp/-parity/-evenp/-oddp/raw/cooked/nl/-nl/ek/sane/tabs/-tabs handled (stty.rs:533-638); raw matches the POSIX literal recipe (cs8, disable erase/kill/intr/quit/eof/eol, -opost, -inpck); saved settings pfmt1 round-trip hardened (#5 ✓).
  • Terminal Window Size (rows/cols) and size query now supported (#4 ✓).

STDIN / INPUT FILES

  • Terminal state is read from and written to standard inputstty.rs:794 Termios::from_fd(STDIN_FILENO), stty.rs:523,771 tcsetattr(STDIN_FILENO, …). Matches POSIX RATIONALE 116401-116406 (security: stdin, not stdout).
  • No data is read from stdin — only tcgetattr/tcsetattr ioctls.
  • INPUT FILES "None" — no file operands consumed.

ENVIRONMENT VARIABLES

Var Status Notes
LANG/LC_ALL/LC_CTYPE PARTIAL setlocale(LC_ALL, "") called (stty.rs:787); affects clap help, but byte/char interpretation of operands is plain Rust str.
LC_MESSAGES MISSING (effect) (#8) diagnostics hardcoded English, never gettext()-wrapped.
NLSPATH (XSI) MISSING No message-catalog wiring.

ASYNCHRONOUS EVENTS

  • Default — no handlers required or present (grep -nE 'SIGCONT|SIGWINCH|signal' stty.rs → 0).

STDOUT / STDERR

  • With set-operands and no Informational Query: no stdout — set path writes nothing to stdout (stty.rs:778-784).
  • -a speed line: "speed %d baud;" when ispeed==ospeed, else "ispeed … ospeed …"stty.rs:74-78 (ti_baud_str). CONFORMS.
  • -a control chars: "%s = %s;" with <undef> for disabled — stty.rs:256, 247-254. CONFORMS (except value rendering, #7).
  • size query output "%d %d\n" now emitted (#4 ✓).
  • Diagnostics → stderr — via Error:/eprintln (but English & misprefixed, #8).

EXIT STATUS / CONSEQUENCES OF ERRORS

  • 0 on success; >0 on error — main returns Ok/Err (stty.rs:786-814).
  • No panic on stty <single operand> (#1 ✓) — the abort is removed; single operands exit 0.
  • First bad operand aborts the operand list — stty_set_long returns Err on the first failure (stty.rs:687-714). CONSEQUENCES OF ERRORS = Default, so abort is permitted.

Test coverage signal

screen/tests/stty/mod.rs contains no #[test] — only a comment that stty needs a TTY. The unit-test-able pure functions are untested.

Not covered (all map to findings):

  • stty <single operand> does not panic (#1) — test_stty_single_operand_no_panic (PTY harness).
  • Negation operand -echo reaches the set path (#2) — test_stty_negation_operand_applied.
  • stty erase x single-char assignment (#3) — test_stty_single_char_cchar_assignment.
  • rows/cols/size (#4) — test_stty_rows_cols_size.
  • set_ti_flag/speed_to_str/build_flagstr/cchar_to_str pure-function unit tests (no TTY needed) — stty.rs mod tests.
  • stty 50 selects B50 (#6) — test_speed_table_50_not_54 + test_stty_speed_50.

tabs

Implementation: screen/tabs.rs (426 lines, incl. 11 unit tests at :358-426) Tests: screen/tests/tabs/mod.rs (95 lines, 5 integration #[test]s) + the in-file unit tests Spec: POSIX.1-2024 (IEEE Std 1003.1-2024), Vol. 3 §3, pp. 3463–3466 (XSI / interactive-utility option) Reference slice: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/tabs.md Date: 2026-06-18

TL;DR

tabs is in good shape. It supports the full XSI option set (-a -a2 -c -c2 -c3 -f -p -s -u), the repetitive -0..-9 form (mapping -0→clear, default→-8), the n[[sep[+]n]...] operand with comma/blank separators and +N increments with strictly-ascending validation, -T type/TERM, and uses the terminfo hardware-tab capabilities (tbc/hts) as the spec intends. Diagnostics are gettext()-wrapped. The notable gaps are spec-Minor: a missing-TERM/-T condition errors out instead of falling back to an "unspecified default terminal type", MAX_COLUMN is hardcoded at 160, and a leading +N first operand is tolerated.

Priority issues

Resolved 2026-06-18 (branch screen-audit, Phase 4). All tabs findings fixed (strict scope): #T1 default-terminal fallback, #T2/#T4 width-derived cap + beyond-width rejection, #T3 leading-+ rejection, #T5 non-terminal stdout guard. Added preset/-0/default/leading-+/beyond-width unit tests plus PTY set-path and non-terminal-stdout integration tests.

Major

  • #T1 — Unset TERM with no -T errors instead of using a default terminal type. ✓ fixed (Phase 4): the no--T path now falls back from_env → ansi → dumb → vt100 before erroring. tabs.rs:326-337: Database::from_env() failing (TERM unset/null) prints "cannot determine terminal type" and exits 1. POSIX 116447-116449 / 116507-116508: when -T is absent and TERM is unset/null, "an unspecified default terminal type shall be used." Erroring is a divergence from the mandated fallback. Fix: fall back to a built-in default (e.g. dumb/ansi) instead of hard-failing. (Borderline Minor — the spec leaves the default "unspecified", but mandates that one be used.)

Minor

  • #T2 — MAX_COLUMN hardcoded at 160. ✓ fixed (Phase 4): max_column() derives the cap from TIOCGWINSZ ws_col (fallback DEFAULT_MAX_COLUMN = 160); generate_repetitive_tabs(interval, max); regression test_generate_repetitive_tabs_respects_max. tabs.rs:19, used by generate_repetitive_tabs (tabs.rs:156-169). POSIX 116446: "The maximum number of tab stops allowed is terminal-dependent." A fixed cap is acceptable but should ideally derive from the terminal width (terminfo cols). Fix: query terminfo cols or document the cap.
  • #T3 — A leading +N first operand is silently accepted. ✓ fixed (Phase 4): a leading + is now rejected ("first tab stop cannot be an increment"); regression test_parse_tabstops_leading_increment_rejected. tabs.rs:117-146 (parse_tabstops): the first token +5 is treated as 0+5. POSIX 116485-116486: the +-increment applies to any value "except the first one". Fix: reject a leading +.
  • #T4 — Custom-operand path does not enforce n ≤ a single column max / first stop ≥ 1 vs preset semantics. ✓ fixed (Phase 4): parse_cmd_line rejects any explicit stop beyond max_column() ("tab stop larger than terminal width"); regression test_tabstop_beyond_width_rejected. parse_tabstops rejects a literal 0 (tabs.rs:127-129) and non-ascending order (:140-142).
  • #T5 — Output written even when stdout is not a terminal. ✓ fixed (Phase 4, strict): set_hw_tabs now guards on libc::isatty(STDOUT_FILENO) and errors ("standard output is not a terminal") rather than dumping escapes into a pipe; regression test_tabs_non_terminal_stdout_errors. set_hw_tabs (tabs.rs:249-285). POSIX 116512-116514: "If standard output is not a terminal, undefined results occur."

Detailed conformance matrix

SYNOPSIS / OPTIONS

Opt Status Notes (file:line)
-n (-0..-9) CONFORMS tabs.rs:42-71, 174-204; -0→clear, default (no opt)→-8 (tabs.rs:244-245).
-a 1,10,16,36,72 CONFORMS tabs.rs:207-209.
-a2 1,10,16,40,72 CONFORMS multi-char option via preprocess_args -a2--a2 (tabs.rs:23-33, 76).
-c 1,8,12,16,20,55 CONFORMS tabs.rs:213-215.
-c2 1,6,10,14,49 CONFORMS tabs.rs:217-218.
-c3 1,6,10,…,67 CONFORMS tabs.rs:219-223.
-f 1,7,11,15,19,23 CONFORMS tabs.rs:224-226.
-p 1,5,9,…,61 CONFORMS tabs.rs:227-231.
-s 1,10,55 CONFORMS tabs.rs:232-234.
-u 1,12,20,44 CONFORMS tabs.rs:235-237.
-T type CONFORMS tabs.rs:313-325; TERM fallback at :326-337 (but see #T1).

OPERANDS / STDIN / INPUT FILES

  • n[[sep[+]n]...] with comma or blank separators — tabs.rs:111 splits on ,/whitespace.
  • +N increment relative to previous value — tabs.rs:117-137.
  • Strictly-ascending positive integers enforced — tabs.rs:127-142.
  • Leading +N first operand rejected (#T3 ✓).
  • STDIN "Not used" — no stdin reads.
  • INPUT FILES "None".

ENVIRONMENT VARIABLES

Var Status Notes
LANG/LC_ALL/LC_CTYPE CONFORMS setlocale(LC_ALL, "") at tabs.rs:288.
LC_MESSAGES CONFORMS (plumbed) diagnostics wrapped in gettext() (tabs.rs:125,141,149,254,262,275,319,332,344).
NLSPATH (XSI) MISSING no catalog wiring (tree-wide gap).
TERM CONFORMS read via Database::from_env; falls back to a default terminal when unset (#T1 ✓).

ASYNCHRONOUS EVENTS / STDOUT / STDERR / EXIT STATUS

  • Default — no signal handling needed (none present).
  • Clear+set sequence to stdout in unspecified format — set_hw_tabs (tabs.rs:249-285).
  • Unsupported terminal → diagnostic to stderr + exit >0 — tabs.rs:253-256 ("terminal does not support setting tab stops").
  • Diagnostics → stderr only — all via eprintln! with tabs: prefix.
  • 0 success / >0 error — tabs.rs:355 / various ExitCode::from(1).

Test coverage signal

Good for a TTY-dependent utility: 11 unit tests cover parse_tabstops (comma/blank/mixed separators, increments, non-ascending, invalid, zero) and generate_repetitive_tabs; 5 integration tests cover -T unknown, --help, --version, and error paths.

Not covered:

  • Each XSI preset (-a/-c3/…) yields its exact documented list — test_xsi_presets.
  • -0 produces an empty stop list; default equals -8test_rep0_and_default.
  • Leading +N rejection (#T3) — test_parse_tabstops_leading_increment_rejected.

tput

Implementation: screen/tput.rs (173 lines) Tests: screen/tests/tput/mod.rs (60 lines, 3 integration #[test]s) Spec: POSIX.1-2024 (IEEE Std 1003.1-2024), Vol. 3 §3, pp. 3504–3506 Reference slice: ~/tmp/posix.2024/sliced/xcu-shell-and-utilities/3-utilities/tput.md Date: 2026-06-18

TL;DR

tput is the cleanest of the three. It supports exactly the three POSIX-locale operands (clear/init/reset), maps the full POSIX exit-status ladder (0/2/3/4/>4), continues past unavailable capabilities without error, processes multiple operands, and honors -T/TERM. The gaps are Minor: init/reset emit the filename of an if/rf (init/reset-file) capability instead of its contents, an invalid operand aborts the whole list before any valid operand runs, and there is no init→fallback for a missing reset string.

Priority issues

Resolved 2026-06-18 (branch screen-audit, Phase 5). All tput findings fixed (strict scope): #T1 if/rf file contents emitted + iprog run, #T2 reset→init fallback, #T3 valid operands run before an invalid-operand exit 4. Added clear / valid-then-invalid / multi-valid integration tests.

Minor

  • #T1 — init/reset emit the init/reset file capability literally instead of catting its contents. ✓ fixed (Phase 5): emit_cap_file reads the if/rf filename (via cap.as_ref()) and writes its contents; run_cap_prog runs the iprog (cap::InitProg) program. tput.rs:42-45 (InitFile) and :60-63 (ResetFile) expand and write the capability value, which for if/rf is a pathname; the spec intent is to send the file's contents. The code comments acknowledge this ("for now just output the capability"). Few terminfo entries use if/rf, so impact is low. Fix: read the named file and write its bytes. Also iprog (init program) is not executed.
  • #T2 — reset does not fall back to init strings when reset strings are absent. ✓ fixed (Phase 5, strict): tput_reset now tracks whether any rs1/rs2/rf/rs3 was emitted and falls back to tput_init when none are present (historical parity). tput.rs:53-69.
  • #T3 — An invalid operand aborts all earlier valid operands. ✓ fixed (Phase 5, strict): the early-abort pre-validation is replaced — when at least one operand is valid, terminfo loads and operands run in order, so tput clear bogus clears then exits 4; an all-invalid list still short-circuits to exit 4 (outranking no-terminfo exit 3) without needing a terminal. Regressions test_tput_valid_then_invalid_operand, test_tput_multiple_valid_operands. tput.rs:132-137.

Detailed conformance matrix

SYNOPSIS / OPTIONS / OPERANDS

  • -T typetput.rs:28-29, 151-157 (Database::from_name).
  • TERM fallback — tput.rs:141-150 (Database::from_env); unset/unknown → exit 3 with diagnostic.
  • cleartput.rs:71-77 emits ClearScreen (clear).
  • inittput.rs:35-51 emits is1/is2 + if-contents + iprog + is3 (#T1 ✓).
  • resettput.rs:53-69 emits rs1/rs2 + rf-contents + rs3, falling back to init when none present (#T1/#T2 ✓).
  • Unsupported operation is not an error — tput.rs:36-49,54-67,72-75 skip absent capabilities via if let Some(...); process_operand returns Ok (POSIX 117940-117941). CONFORMS.

STDIN / INPUT FILES / ENVIRONMENT

  • STDIN "Not used"; INPUT FILES "None".
  • LANG/LC_ALL/LC_CTYPE/LC_MESSAGESsetlocale(LC_ALL, "") (tput.rs:112); diagnostics gettext()-wrapped (tput.rs:104,134,146,154).
  • NLSPATH (XSI) MISSING — no catalog wiring (tree-wide gap).
  • TERM — read via Database::from_env (tput.rs:141).

ASYNCHRONOUS EVENTS / STDOUT / STDERR

  • Default — no handlers needed (none present).
  • Sequences written to stdout — *.expand().to(io::stdout()) throughout.
  • Diagnostics → stderr only — all via eprintln!.

EXIT STATUS / CONSEQUENCES OF ERRORS

  • 0 success — tput.rs:18, 163-172.
  • 2 usage error — tput.rs:20, 120-127 (clap parse failure; missing required operand → exit 2, confirmed by test_tput_no_operand).
  • 3 no terminfo — tput.rs:21, 143-157 (confirmed by test_tput_invalid_terminal_type).
  • 4 invalid operand — tput.rs:22, 104-106, 132-137 (confirmed by test_tput_invalid_operand).
  • >4 (5) other error — tput.rs:23, 89-101.
  • Continue past unavailable capability — tput.rs:165-170 keeps the worst exit code and continues. CONFORMS to CONSEQUENCES OF ERRORS.
  • Valid operands run before an invalid-operand exit 4 (#T3 ✓).
Note: tput.rs:23 defines EXIT_OTHER_ERROR = 5
  • Reserved exit 1 ("Boolean operand not set") is correctly not used (commented out at tput.rs:19); POSIX RATIONALE 118012-118014 keeps 1 reserved for Boolean operands this implementation does not support. CONFORMS.

Test coverage signal

3 integration tests cover the three error exit codes (4 invalid operand, 3 bad -T, 2 missing operand). clear/init/reset output is verified only by manual testing (terminfo-dependent).

Not covered:

  • tput clear emits the terminal's clear sequence — test_tput_clear_xterm.
  • Multi-operand continue / valid-before-invalid behavior (#T3) — test_tput_valid_then_invalid_operand, test_tput_multiple_valid_operands.
  • init/reset file-capability (if/rf/iprog) handling (#T1) — exercised manually (few terminfo entries carry these caps; no portable fixture).

Suggested PR groupings

  • PR A — "stty: fix the set-mode golden path" (Critical): stty #1 (assertion → >= 1), stty #2 (allow_hyphen_values on operands). Smallest unit that makes stty sane/stty -echo work at all. Add the PTY-harness regression tests from this audit.
  • PR B — "stty: control-char + window-size operands": stty #3 (single-char assignment), stty #4 (rows/cols/size via TIOC[GS]WINSZ).
  • PR C — "stty: data + display fixes": stty #6 ("50" speed key), stty #7 (printable cchar rendering), stty #8 (gettext + stty: prefix), stty #10 (-a/-g + operands).
  • PR D — "stty: pure-function tests": unit tests for parse_tabstops-equivalent helpers, set_ti_flag, merge_map, speed_to_str (no TTY needed); closes the empty-test-file gap.
  • PR E — "tabs: terminal-default + operand polish": #T1 (default terminal on unset TERM), #T3 (leading +N), #T2 (cols-derived max).
  • PR F — "tput: init/reset file handling": #T1 (cat if/rf contents, run iprog), #T2 (init fallback), optionally #T3 (operand ordering).