From aa58eff8d667833e149c7d182261d6afe058b531 Mon Sep 17 00:00:00 2001 From: Will Shuttleworth Date: Wed, 18 Jun 2025 11:44:41 -0400 Subject: [PATCH 1/3] stty: add rows/cols settings --- src/uu/stty/src/stty.rs | 55 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 7866ecaad..9ac8a8c9e 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -108,9 +108,15 @@ enum ControlCharMappingError { MultipleChars, } +enum SpecialSettings { + Rows(u16), + Cols(u16), +} + enum ArgOptions<'a> { Flags(AllFlags<'a>), Mapping((SpecialCharacterIndices, u8)), + Special(SpecialSettings), } impl<'a> From> for ArgOptions<'a> { @@ -280,6 +286,32 @@ fn stty(opts: &Options) -> UResult<()> { return Err(USimpleError::new(1, format!("invalid argument '{arg}'"))); } valid_args.push(flag.into()); + } else if *arg == "rows" { + if let Some(rows) = args_iter.next() { + if let Some(n) = parse_rows_cols(rows) { + valid_args.push(ArgOptions::Special(SpecialSettings::Rows(n))); + } else { + return Err(USimpleError::new( + 1, + format!("invalid integer argument: '{rows}'"), + )); + } + } else { + return Err(USimpleError::new(1, format!("missing argument to '{arg}'"))); + } + } else if *arg == "columns" || *arg == "cols" { + if let Some(cols) = args_iter.next() { + if let Some(n) = parse_rows_cols(cols) { + valid_args.push(ArgOptions::Special(SpecialSettings::Cols(n))); + } else { + return Err(USimpleError::new( + 1, + format!("invalid integer argument: '{cols}'"), + )); + } + } else { + return Err(USimpleError::new(1, format!("missing argument to '{arg}'"))); + } // not a valid control char or flag } else { return Err(USimpleError::new(1, format!("invalid argument '{arg}'"))); @@ -294,6 +326,9 @@ fn stty(opts: &Options) -> UResult<()> { match arg { ArgOptions::Mapping(mapping) => apply_char_mapping(&mut termios, mapping), ArgOptions::Flags(flag) => apply_setting(&mut termios, flag), + ArgOptions::Special(setting) => { + apply_special_setting(setting, opts.file.as_raw_fd())?; + } } } tcsetattr( @@ -310,6 +345,15 @@ fn stty(opts: &Options) -> UResult<()> { Ok(()) } +// GNU uses an unsigned 32 bit integer for row/col sizes, but then wraps around 16 bits +// this function returns Some(n), where n is a u16 row/col size, or None if the string arg cannot be parsed as a u32 +fn parse_rows_cols(arg: &str) -> Option { + if let Ok(n) = arg.parse::() { + return Some((n % (u16::MAX as u32 + 1)) as u16); + } + None +} + fn check_flag_group(flag: &Flag, remove: bool) -> bool { remove && flag.group.is_some() } @@ -588,6 +632,17 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(SpecialCharacterIndices, termios.control_chars[mapping.0 as usize] = mapping.1; } +fn apply_special_setting(setting: &SpecialSettings, fd: i32) -> nix::Result<()> { + let mut size = TermSize::default(); + unsafe { tiocgwinsz(fd, &raw mut size)? }; + match setting { + SpecialSetting::Rows(n) => size.rows = *n, + SpecialSetting::Cols(n) => size.columns = *n, + } + unsafe { tiocswinsz(fd, &raw mut size)? }; + Ok(()) +} + // GNU stty defines some valid values for the control character mappings // 1. Standard character, can be a a single char (ie 'C') or hat notation (ie '^C') // 2. Integer From 93ac65593620a63930b9d3242ca2959ebfcaedc5 Mon Sep 17 00:00:00 2001 From: Will Shuttleworth Date: Wed, 18 Jun 2025 11:59:10 -0400 Subject: [PATCH 2/3] stty: add tests for setting rows/cols --- tests/by-util/test_stty.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 864888fc2..c567b9a8b 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -201,3 +201,37 @@ fn set_mapping() { .succeeds() .stdout_contains("intr = ^C"); } + +#[test] +fn row_column_sizes() { + new_ucmd!() + .args(&["rows", "-1"]) + .fails() + .stderr_contains("invalid integer argument: '-1'"); + + new_ucmd!() + .args(&["columns", "-1"]) + .fails() + .stderr_contains("invalid integer argument: '-1'"); + + // overflow the u32 used for row/col counts + new_ucmd!() + .args(&["cols", "4294967296"]) + .fails() + .stderr_contains("invalid integer argument: '4294967296'"); + + new_ucmd!() + .args(&["rows", ""]) + .fails() + .stderr_contains("invalid integer argument: ''"); + + new_ucmd!() + .args(&["columns"]) + .fails() + .stderr_contains("missing argument to 'columns'"); + + new_ucmd!() + .args(&["rows"]) + .fails() + .stderr_contains("missing argument to 'rows'"); +} From 10f8d775605a545bf2a04a2a2524ecd5a5632d2e Mon Sep 17 00:00:00 2001 From: Will Shuttleworth Date: Wed, 18 Jun 2025 12:57:03 -0400 Subject: [PATCH 3/3] stty: add option to print terminal size --- src/uu/stty/src/stty.rs | 33 +++++++++++++++++++++++++++------ tests/by-util/test_stty.rs | 8 ++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 9ac8a8c9e..6c392401c 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -108,15 +108,20 @@ enum ControlCharMappingError { MultipleChars, } -enum SpecialSettings { +enum SpecialSetting { Rows(u16), Cols(u16), } +enum PrintSetting { + Size, +} + enum ArgOptions<'a> { Flags(AllFlags<'a>), Mapping((SpecialCharacterIndices, u8)), - Special(SpecialSettings), + Special(SpecialSetting), + Print(PrintSetting), } impl<'a> From> for ArgOptions<'a> { @@ -289,7 +294,7 @@ fn stty(opts: &Options) -> UResult<()> { } else if *arg == "rows" { if let Some(rows) = args_iter.next() { if let Some(n) = parse_rows_cols(rows) { - valid_args.push(ArgOptions::Special(SpecialSettings::Rows(n))); + valid_args.push(ArgOptions::Special(SpecialSetting::Rows(n))); } else { return Err(USimpleError::new( 1, @@ -302,7 +307,7 @@ fn stty(opts: &Options) -> UResult<()> { } else if *arg == "columns" || *arg == "cols" { if let Some(cols) = args_iter.next() { if let Some(n) = parse_rows_cols(cols) { - valid_args.push(ArgOptions::Special(SpecialSettings::Cols(n))); + valid_args.push(ArgOptions::Special(SpecialSetting::Cols(n))); } else { return Err(USimpleError::new( 1, @@ -312,7 +317,9 @@ fn stty(opts: &Options) -> UResult<()> { } else { return Err(USimpleError::new(1, format!("missing argument to '{arg}'"))); } - // not a valid control char or flag + } else if *arg == "size" { + valid_args.push(ArgOptions::Print(PrintSetting::Size)); + // not a valid option } else { return Err(USimpleError::new(1, format!("invalid argument '{arg}'"))); } @@ -329,6 +336,9 @@ fn stty(opts: &Options) -> UResult<()> { ArgOptions::Special(setting) => { apply_special_setting(setting, opts.file.as_raw_fd())?; } + ArgOptions::Print(setting) => { + print_special_setting(setting, opts.file.as_raw_fd())?; + } } } tcsetattr( @@ -358,6 +368,17 @@ fn check_flag_group(flag: &Flag, remove: bool) -> bool { remove && flag.group.is_some() } +fn print_special_setting(setting: &PrintSetting, fd: i32) -> nix::Result<()> { + match setting { + PrintSetting::Size => { + let mut size = TermSize::default(); + unsafe { tiocgwinsz(fd, &raw mut size)? }; + println!("{} {}", size.rows, size.columns); + } + } + Ok(()) +} + fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { let speed = cfgetospeed(termios); @@ -632,7 +653,7 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(SpecialCharacterIndices, termios.control_chars[mapping.0 as usize] = mapping.1; } -fn apply_special_setting(setting: &SpecialSettings, fd: i32) -> nix::Result<()> { +fn apply_special_setting(setting: &SpecialSetting, fd: i32) -> nix::Result<()> { let mut size = TermSize::default(); unsafe { tiocgwinsz(fd, &raw mut size)? }; match setting { diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index c567b9a8b..382178dd7 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -48,6 +48,14 @@ fn all_and_setting() { .stderr_contains("when specifying an output style, modes may not be set"); } +#[test] +fn all_and_print_setting() { + new_ucmd!() + .args(&["--all", "size"]) + .fails() + .stderr_contains("when specifying an output style, modes may not be set"); +} + #[test] fn save_and_all() { new_ucmd!()