Author SHA1 Message Date
Diomidis Spinellis cb4ea5ef15 Disable T in POSIX mode 2026-08-02 17:29:33 +03:00
mukunda katta 04023934fd ci: re-trigger checks (windows runner crates.io download flake) 2026-08-02 17:29:33 +03:00
mukunda kattaandDiomidis Spinellis addc2db04a feat: add T command (branch on no-substitution)
Implements the GNU `T LABEL` command: branch to LABEL if no s///
substitution has succeeded since the last input line was read or the
last t/T was taken. It is the inverse of `t` and shares the same
address and label parsing.

Wires 'T' into get_cmd_spec, resolve_branch_targets, and the processor
branch logic, resetting the substitution flag on evaluation just like
't'. Adds integration tests (simple branch + flag-clear) and a cmd-spec
unit test.

Closes #395

Co-authored-by: Diomidis Spinellis <dds@aueb.gr>
2026-08-02 17:27:58 +03:00
6 changed files with 117 additions and 5 deletions

No files matched your search

+3 -3
View File
@@ -361,11 +361,11 @@ impl Command {
#[derive(Debug)]
/// Command-specific data
/// After parsing, t, b Label elements are converted into BranchTarget ones.
/// After parsing, t, T, b Label elements are converted into BranchTarget ones.
pub enum CommandData {
None,
BranchTarget(Option<Rc<RefCell<Command>>>), // Commands for 'b', 't', '{'
Label(Option<String>), // Label name for 'b', 't', ':'
BranchTarget(Option<Rc<RefCell<Command>>>), // Commands for 'b', 't', 'T', '{'
Label(Option<String>), // Label name for 'b', 't', 'T', ':'
Path(PathBuf), // File path for 'r'
NamedWriter(Rc<RefCell<NamedWriter>>), // File output for 'w'
Number(usize), // Number for 'l', 'q', 'Q' (GNU)
+16 -2
View File
@@ -213,8 +213,8 @@ fn resolve_branch_targets(
resolve_branch_targets(Some(sub_head.clone()), context)?;
}
// Only for 't' or 'b' commands:
if matches!(cmd.code, 't' | 'b') {
// Only for 't', 'T', or 'b' commands:
if matches!(cmd.code, 't' | 'T' | 'b') {
// Take ownership of the current data
let old_data = mem::replace(&mut cmd.data, CommandData::None);
@@ -1574,6 +1574,10 @@ fn get_cmd_spec(
n_addr: 2,
handler: compile_subst_command,
}),
'T' if !posix => Ok(CommandSpec {
n_addr: 2,
handler: compile_label_command,
}),
'w' => Ok(CommandSpec {
n_addr: 2,
handler: compile_write_file_command,
@@ -1665,6 +1669,16 @@ mod tests {
);
}
#[test]
fn test_lookup_branch_commands() {
// b, t, and T all share compile_label_command and accept 2 addresses.
for code in ['b', 't', 'T'] {
let (lines, line) = make_providers("123abc");
let cmd = get_cmd_spec(&lines, &line, code, false).unwrap();
assert_eq!(cmd.n_addr, 2, "command `{code}` should accept 2 addresses");
}
}
// Utility to create a ScriptCharProvider from a &str
fn char_provider_from(s: &str) -> ScriptCharProvider {
ScriptCharProvider::new(s)
+17
View File
@@ -831,6 +831,23 @@ fn process_file(
// Branch to the end of the script.
break;
}
'T' if context.substitution_made => {
// A substitution was made since the last cycle or T,
// so reset the flag and fall through without branching.
context.substitution_made = false;
}
'T' => {
// Branch to the specified label or end if none is given
// if no substitution was made since last cycle or T.
let target = extract_variant!(command, BranchTarget);
if target.is_some() {
// New command to execute
current.clone_from(target);
continue;
}
// Branch to the end of the script.
break;
}
'w' => {
// Append the pattern space to the specified file.
let writer = extract_variant!(command, NamedWriter);
+53
View File
@@ -1414,6 +1414,59 @@ tb"#,
]
);
// T branches when NO substitution was made (inverse of t).
// Lines not ending in "2" leave s/2$/X/ with no match, so T branches to
// :skip and keeps the original line; lines ending in "2" fall through and
// get the "SUB " mark.
check_output!(
branch_no_sub_simple,
[
"-n",
"-e",
r#"
s/2$/X/
Tskip
s/^/SUB /
:skip
p
"#,
LINES1
]
);
// Check that T also clears the substitution-done flag, matching t.
// After s/// sets the flag, the first T sees a substitution so it does
// not branch but still clears the flag; the second T then sees no
// substitution and branches to :y. Reaching :y (CLEARED) rather than
// falling through to :x (NOTCLEARED) proves the first T cleared the flag.
check_output!(
branch_no_sub_clears,
[
"-n",
"-e",
r#"
s/^/^/
Tx
Ty
:x
s/^/NOTCLEARED /p
b
:y
s/^/CLEARED /p
"#,
LINES1
]
);
#[test]
fn test_branch_no_sub_non_posix() {
new_ucmd!()
.args(&["--posix", "T"])
.fails()
.code_is(1)
.stderr_contains("invalid command code");
}
////////////////////////////////////////////////////////////
// Text: a, c, i
+14
View File
@@ -0,0 +1,14 @@
CLEARED ^l1_1
CLEARED ^l1_2
CLEARED ^l1_3
CLEARED ^l1_4
CLEARED ^l1_5
CLEARED ^l1_6
CLEARED ^l1_7
CLEARED ^l1_8
CLEARED ^l1_9
CLEARED ^l1_10
CLEARED ^l1_11
CLEARED ^l1_12
CLEARED ^l1_13
CLEARED ^l1_14
+14
View File
@@ -0,0 +1,14 @@
l1_1
SUB l1_X
l1_3
l1_4
l1_5
l1_6
l1_7
l1_8
l1_9
l1_10
l1_11
SUB l1_1X
l1_13
l1_14