From dfc353e89cb2beb045733b60fc4f33a2db6343ab Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:26:25 +0300 Subject: [PATCH 1/3] stty: clear the previous value when setting a grouped flag Settings such as cs5-cs8, nl0/nl1, crN, tabN, bsN, vtN and ffN select one value of a multi-bit field. They were applied by setting their bits on top of the current value, so the field was never cleared: `cs7` on a terminal in `cs8` was ignored, `cs5`/`tab0` (value 0) were no-ops and `tab2` after `tab1` produced a mix of both. This also affected the combination settings that use them, such as `evenp`, `oddp` and `sane`. Clear the field before setting the selected value. Fixes #11963 --- src/uu/stty/src/stty.rs | 25 +++++++-------- tests/by-util/test_stty.rs | 62 +++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index d6e46d90916..5d045d36dcf 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -980,22 +980,23 @@ fn print_flags( fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> nix::Result<()> { match setting { AllFlags::Baud(_, _) => apply_baud_rate_flag(termios, setting)?, - AllFlags::ControlFlags((setting, disable)) => { - setting.flag.apply(termios, !disable); - } - AllFlags::InputFlags((setting, disable)) => { - setting.flag.apply(termios, !disable); - } - AllFlags::LocalFlags((setting, disable)) => { - setting.flag.apply(termios, !disable); - } - AllFlags::OutputFlags((setting, disable)) => { - setting.flag.apply(termios, !disable); - } + AllFlags::ControlFlags((setting, disable)) => apply_flag(termios, setting, !disable), + AllFlags::InputFlags((setting, disable)) => apply_flag(termios, setting, !disable), + AllFlags::LocalFlags((setting, disable)) => apply_flag(termios, setting, !disable), + AllFlags::OutputFlags((setting, disable)) => apply_flag(termios, setting, !disable), } Ok(()) } +fn apply_flag(termios: &mut Termios, setting: &Flag, val: bool) { + // A grouped flag selects one value of a multi-bit field (e.g. `cs7` in CSIZE), + // so the bits of the previously selected value have to be cleared first. + if let (Some(group), true) = (setting.group, val) { + group.apply(termios, false); + } + setting.flag.apply(termios, val); +} + fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) -> nix::Result<()> { if let AllFlags::Baud(rate, baud_type) = input { match baud_type { diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 157758db903..975fec993f5 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore parenb parmrk ixany iuclc onlcr ofdel icanon noflsh econl igpar ispeed ospeed NCCS nonhex gstty notachar cbreak evenp oddp CSIZE +// spell-checker:ignore parenb parmrk ixany iuclc onlcr ofdel icanon noflsh econl igpar ispeed ospeed NCCS nonhex gstty notachar cbreak evenp oddp CSIZE tabdly #![cfg(unix)] @@ -64,6 +64,66 @@ fn test_sane() { .stdout_str_check(|s| !s.contains("intr = ^A")); } +/// Returns the settings from `group` that `stty --all` reports for the terminal at `path`. +fn shown_settings(path: &str, group: &[&'static str]) -> Vec<&'static str> { + let stdout = new_ucmd!() + .args(&["--all", "--file", path]) + .succeeds() + .stdout_move_str(); + let words: Vec<&str> = stdout.split_whitespace().collect(); + group + .iter() + .copied() + .filter(|s| words.contains(s)) + .collect() +} + +#[test] +fn test_grouped_flag_replaces_previous_value() { + let (path, _controller, _replica) = pty_path(); + let csize = ["cs5", "cs6", "cs7", "cs8"]; + + for (setting, expected) in [ + ("cs8", "cs8"), + ("cs7", "cs7"), + ("cs5", "cs5"), + ("cs6", "cs6"), + ("cs8", "cs8"), + ("evenp", "cs7"), + ("-evenp", "cs8"), + ] { + new_ucmd!().args(&["--file", &path, setting]).succeeds(); + assert_eq!(shown_settings(&path, &csize), [expected], "after {setting}"); + } +} + +#[test] +#[cfg(any( + target_vendor = "apple", + target_os = "android", + target_os = "haiku", + target_os = "linux" +))] +fn test_grouped_output_flag_replaces_previous_value() { + let (path, _controller, _replica) = pty_path(); + let tabdly = ["tab0", "tab1", "tab2", "tab3"]; + + for (setting, expected) in [ + ("tab1", "tab1"), + ("tab2", "tab2"), + ("tab0", "tab0"), + ("tab3", "tab3"), + ("sane", "tab0"), + ] { + new_ucmd!().args(&["--file", &path, setting]).succeeds(); + assert_eq!( + shown_settings(&path, &tabdly), + [expected], + "after {setting}" + ); + } +} + #[test] fn save_and_setting() { new_ucmd!() From de7e85572e5f26cf1e65a2993371e726035f259a Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:20:16 +0300 Subject: [PATCH 2/3] stty: test the character size on a termios value, not a pseudo-terminal Linux pseudo-terminals always keep cs8, so the integration test for the CSIZE group failed there. Check that group in a unit test that applies the settings to a termios value instead, and keep the pseudo-terminal test for the TABDLY group. --- src/uu/stty/src/stty.rs | 34 ++++++++++++++++++++++++++++++++++ tests/by-util/test_stty.rs | 25 ++++++------------------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 5d045d36dcf..62891d39c9c 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -1587,6 +1587,40 @@ mod tests { assert!(!check_flag_group(&flag_without_group, false)); } + // The character size is tested here rather than on a pseudo-terminal, + // because Linux pseudo-terminals always keep cs8. + #[test] + fn test_apply_grouped_flag_replaces_previous_value() { + // SAFETY: all-zero bytes are a valid `termios`. + let mut termios = Termios::from(unsafe { std::mem::zeroed::() }); + let apply = |termios: &mut Termios, arg: &str| { + let flags = string_to_flag(arg) + .map_or_else(|| combo_to_flags(arg), |flag| vec![ArgOptions::Flags(flag)]); + for flag in flags { + if let ArgOptions::Flags(flag) = flag { + apply_setting(termios, &flag).unwrap(); + } + } + }; + + for (arg, expected) in [ + ("cs8", ControlFlags::CS8), + ("cs7", ControlFlags::CS7), + ("cs5", ControlFlags::CS5), + ("cs6", ControlFlags::CS6), + ("cs8", ControlFlags::CS8), + ("evenp", ControlFlags::CS7), + ("-evenp", ControlFlags::CS8), + ] { + apply(&mut termios, arg); + assert_eq!( + termios.control_flags & ControlFlags::CSIZE, + expected, + "after {arg}" + ); + } + } + // Additional tests for get_sane_control_char #[test] fn test_get_sane_control_char_all_defined() { diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 975fec993f5..1423c8f6a2f 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -65,6 +65,12 @@ fn test_sane() { } /// Returns the settings from `group` that `stty --all` reports for the terminal at `path`. +#[cfg(any( + target_vendor = "apple", + target_os = "android", + target_os = "haiku", + target_os = "linux" +))] fn shown_settings(path: &str, group: &[&'static str]) -> Vec<&'static str> { let stdout = new_ucmd!() .args(&["--all", "--file", path]) @@ -78,25 +84,6 @@ fn shown_settings(path: &str, group: &[&'static str]) -> Vec<&'static str> { .collect() } -#[test] -fn test_grouped_flag_replaces_previous_value() { - let (path, _controller, _replica) = pty_path(); - let csize = ["cs5", "cs6", "cs7", "cs8"]; - - for (setting, expected) in [ - ("cs8", "cs8"), - ("cs7", "cs7"), - ("cs5", "cs5"), - ("cs6", "cs6"), - ("cs8", "cs8"), - ("evenp", "cs7"), - ("-evenp", "cs8"), - ] { - new_ucmd!().args(&["--file", &path, setting]).succeeds(); - assert_eq!(shown_settings(&path, &csize), [expected], "after {setting}"); - } -} - #[test] #[cfg(any( target_vendor = "apple", From 220b7fe9963946bc52ec34b981ac0831532190ee Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:06:50 +0300 Subject: [PATCH 3/3] stty: use a let chain in apply_flag --- src/uu/stty/src/stty.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 62891d39c9c..8bbe71eac7a 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -991,7 +991,7 @@ fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> nix::Result<()> { fn apply_flag(termios: &mut Termios, setting: &Flag, val: bool) { // A grouped flag selects one value of a multi-bit field (e.g. `cs7` in CSIZE), // so the bits of the previously selected value have to be cleared first. - if let (Some(group), true) = (setting.group, val) { + if val && let Some(group) = setting.group { group.apply(termios, false); } setting.flag.apply(termios, val);