From 379230843412950e48fd95d8b659bb1d02bc22e1 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Thu, 20 Nov 2025 16:21:27 +0000 Subject: [PATCH 1/7] Adding TTY helper for unix to be able to create tests for stty and more --- tests/by-util/test_stty.rs | 39 +++++++++++++++++++++++------------ tests/uutests/src/lib/util.rs | 18 ++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 8f4aec5bd46..5524f490257 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -5,6 +5,7 @@ // spell-checker:ignore parenb parmrk ixany iuclc onlcr ofdel icanon noflsh econl igpar ispeed ospeed use uutests::new_ucmd; +use uutests::util::pty_path; #[test] fn test_invalid_arg() { @@ -12,31 +13,43 @@ fn test_invalid_arg() { } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn runs() { - new_ucmd!().succeeds(); +#[cfg(unix)] +fn test_basic() { + let (path, _controller, _replica) = pty_path(); + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_contains("speed"); } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn print_all() { - let res = new_ucmd!().args(&["--all"]).succeeds(); +#[cfg(unix)] +fn test_all_flag() { + let (path, _controller, _replica) = pty_path(); + let result = new_ucmd!().args(&["--all", "--file", &path]).succeeds(); - // Random selection of flags to check for for flag in [ "parenb", "parmrk", "ixany", "onlcr", "ofdel", "icanon", "noflsh", ] { - res.stdout_contains(flag); + result.stdout_contains(flag); } } #[test] -#[ignore = "Fails because cargo test does not run in a tty"] -fn sane_settings() { - new_ucmd!().args(&["intr", "^A"]).succeeds(); - new_ucmd!().succeeds().stdout_contains("intr = ^A"); +#[cfg(unix)] +fn test_sane() { + let (path, _controller, _replica) = pty_path(); + + new_ucmd!() + .args(&["--file", &path, "intr", "^A"]) + .succeeds(); + new_ucmd!() + .args(&["--file", &path]) + .succeeds() + .stdout_contains("intr = ^A"); + new_ucmd!().args(&["--file", &path, "sane"]).succeeds(); new_ucmd!() - .args(&["sane"]) + .args(&["--file", &path]) .succeeds() .stdout_str_check(|s| !s.contains("intr = ^A")); } diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index ebd97ee5e34..64ffa4400ca 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -2886,6 +2886,24 @@ pub fn whoami() -> String { }) } +/// Create a PTY (pseudo-terminal) for testing utilities that require a TTY. +/// +/// Returns a tuple of (path, controller_fd, replica_fd) where: +/// - path: The filesystem path to the PTY replica device +/// - controller_fd: The controller file descriptor +/// - replica_fd: The replica file descriptor +#[cfg(unix)] +pub fn pty_path() -> (String, OwnedFd, OwnedFd) { + use nix::pty::openpty; + use nix::unistd::ttyname; + let pty = openpty(None, None).expect("Failed to create PTY"); + let path = ttyname(&pty.slave) + .expect("Failed to get PTY path") + .to_string_lossy() + .to_string(); + (path, pty.master, pty.slave) +} + /// Add prefix 'g' for `util_name` if not on linux #[cfg(unix)] pub fn host_name_for(util_name: &str) -> Cow<'_, str> { From 7aaf6b4a809ab020881c7225c75f7ae8a408cd6b Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Thu, 20 Nov 2025 16:34:02 +0000 Subject: [PATCH 2/7] removing missing flag on github actions and spellcheck ignore --- tests/by-util/test_stty.rs | 6 ++---- tests/uutests/src/lib/util.rs | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 5524f490257..d6870d48fff 100644 --- a/tests/by-util/test_stty.rs +++ b/tests/by-util/test_stty.rs @@ -2,7 +2,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 +// spell-checker:ignore parenb parmrk ixany iuclc onlcr icanon noflsh econl igpar ispeed ospeed use uutests::new_ucmd; use uutests::util::pty_path; @@ -28,9 +28,7 @@ fn test_all_flag() { let (path, _controller, _replica) = pty_path(); let result = new_ucmd!().args(&["--all", "--file", &path]).succeeds(); - for flag in [ - "parenb", "parmrk", "ixany", "onlcr", "ofdel", "icanon", "noflsh", - ] { + for flag in ["parenb", "parmrk", "ixany", "onlcr", "icanon", "noflsh"] { result.stdout_contains(flag); } } diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 64ffa4400ca..4668e7ba8b3 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. //spell-checker: ignore (linux) rlimit prlimit coreutil ggroups uchild uncaptured scmd SHLVL canonicalized openpty //spell-checker: ignore (linux) winsize xpixel ypixel setrlimit FSIZE SIGBUS SIGSEGV sigbus tmpfs mksocket +//spell-checker: ignore (ToDO) ttyname #![allow(dead_code)] #![allow( From 18216d285862efbc6183e7ebc01258b47e960ba8 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Wed, 19 Nov 2025 10:58:12 -0800 Subject: [PATCH 3/7] Changing shell command to add recognizing a TTY for stty tests --- .github/workflows/GnuTests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/GnuTests.yml b/.github/workflows/GnuTests.yml index 09752fe5f92..94170e8bf47 100644 --- a/.github/workflows/GnuTests.yml +++ b/.github/workflows/GnuTests.yml @@ -114,7 +114,7 @@ jobs: ### Run tests as user - name: Run GNU tests - shell: bash + shell: 'script -q -e -c "bash {0}"' run: | ## Run GNU tests path_GNU='gnu' From cf27e1938a18f2739a9dc0337852a4cc19b85b01 Mon Sep 17 00:00:00 2001 From: Christopher Illarionova Date: Thu, 20 Nov 2025 16:09:09 +0000 Subject: [PATCH 4/7] Added fixes for exporting and importing state and using column env var --- src/uu/stty/src/stty.rs | 188 +++++++++++++++++++++++++++++++++------- 1 file changed, 159 insertions(+), 29 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index fdeee252df3..815a9b368aa 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -121,6 +121,7 @@ struct Options<'a> { save: bool, file: Device, settings: Option>, + columns: usize, } enum Device { @@ -149,6 +150,7 @@ enum ArgOptions<'a> { Mapping((S, u8)), Special(SpecialSetting), Print(PrintSetting), + SavedState(Vec), } impl<'a> From> for ArgOptions<'a> { @@ -177,9 +179,15 @@ impl AsRawFd for Device { impl<'a> Options<'a> { fn from(matches: &'a ArgMatches) -> io::Result { + let columns = std::env::var("COLUMNS") + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or(80); + Ok(Self { all: matches.get_flag(options::ALL), save: matches.get_flag(options::SAVE), + columns, file: match matches.get_one::(options::FILE) { // Two notes here: // 1. O_NONBLOCK is needed because according to GNU docs, a @@ -268,6 +276,23 @@ fn stty(opts: &Options) -> UResult<()> { let mut valid_args: Vec = Vec::new(); if let Some(args) = &opts.settings { + // Check if this looks like a saved state (all hex values) + if args.len() >= 4 && args.iter().all(|s| u32::from_str_radix(s, 16).is_ok()) { + // Parse as saved state + let mut state = Vec::new(); + for arg in args { + if let Ok(val) = u32::from_str_radix(arg, 16) { + state.push(val); + } else { + return invalid_arg(arg); + } + } + let mut termios = tcgetattr(opts.file.as_fd())?; + apply_saved_state(&mut termios, &state)?; + tcsetattr(opts.file.as_fd(), set_arg, &termios)?; + return Ok(()); + } + let mut args_iter = args.iter(); while let Some(&arg) = args_iter.next() { match arg { @@ -351,6 +376,28 @@ fn stty(opts: &Options) -> UResult<()> { valid_args.push(ArgOptions::Print(PrintSetting::Size)); } _ => { + // Try to parse saved format (hex string like "6d02:5:4bf:8a3b:...") + if arg.contains(':') { + let parts: Vec<&str> = arg.split(':').collect(); + if parts.len() >= 4 { + // Check if all parts are valid hex (any size) + let all_hex = parts.iter().all(|s| u32::from_str_radix(s, 16).is_ok()); + if all_hex { + // Parse saved state without accessing device yet + let mut state = Vec::new(); + for part in parts { + if let Ok(val) = u32::from_str_radix(part, 16) { + state.push(val); + } else { + return invalid_arg(arg); + } + } + valid_args.push(ArgOptions::SavedState(state)); + continue; + } + } + } + // control char if let Some(char_index) = cc_to_index(arg) { if let Some(mapping) = args_iter.next() { @@ -417,6 +464,9 @@ fn stty(opts: &Options) -> UResult<()> { ArgOptions::Print(setting) => { print_special_setting(setting, opts.file.as_raw_fd())?; } + ArgOptions::SavedState(state) => { + apply_saved_state(&mut termios, state)?; + } } } tcsetattr(opts.file.as_fd(), set_arg, &termios)?; @@ -564,19 +614,58 @@ fn string_to_combo(arg: &str) -> Option<&str> { } fn string_to_baud(arg: &str) -> Option> { - // BSDs use a u32 for the baud rate, so any decimal number applies. - #[cfg(any( - target_os = "freebsd", - target_os = "dragonfly", - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - ))] - if let Ok(n) = arg.parse::() { - return Some(AllFlags::Baud(n)); + // Normalize the input: trim whitespace and leading '+' + let normalized = arg.trim().trim_start_matches('+'); + + // Try to parse as a floating point number for rounding + if let Ok(f) = normalized.parse::() { + let rounded = f.round() as u32; + + // BSDs use a u32 for the baud rate, so any decimal number applies. + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + return Some(AllFlags::Baud(rounded)); + + // On other platforms, find the closest valid baud rate + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + { + let rounded_str = rounded.to_string(); + // First try exact match + for (text, baud_rate) in BAUD_RATES { + if *text == rounded_str { + return Some(AllFlags::Baud(*baud_rate)); + } + } + // If no exact match, find closest baud rate + let mut closest: Option<(u32, nix::sys::termios::BaudRate)> = None; + for (text, baud_rate) in BAUD_RATES { + if let Ok(rate_val) = text.parse::() { + let diff = rate_val.abs_diff(rounded); + if closest.is_none() || diff < closest.unwrap().0 { + closest = Some((diff, *baud_rate)); + } + } + } + if let Some((_, baud_rate)) = closest { + return Some(AllFlags::Baud(baud_rate)); + } + } } + // Fallback: try exact string match for non-numeric baud rate names #[cfg(not(any( target_os = "freebsd", target_os = "dragonfly", @@ -590,6 +679,7 @@ fn string_to_baud(arg: &str) -> Option> { return Some(AllFlags::Baud(*baud_rate)); } } + None } @@ -648,38 +738,52 @@ fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result { } fn print_control_chars(termios: &Termios, opts: &Options) -> nix::Result<()> { + let mut line = String::new(); + if !opts.all { // Print only control chars that differ from sane defaults - let mut printed = false; for (text, cc_index) in CONTROL_CHARS { let current_val = termios.control_chars[*cc_index as usize]; let sane_val = get_sane_control_char(*cc_index); if current_val != sane_val { - print!("{text} = {}; ", control_char_to_string(current_val)?); - printed = true; + let to_print = format!("{text} = {}; ", control_char_to_string(current_val)?); + if !line.is_empty() && line.len() + to_print.len() > opts.columns { + println!("{}", line.trim_end()); + line.clear(); + } + line.push_str(&to_print); } } - if printed { - println!(); + if !line.is_empty() { + println!("{}", line.trim_end()); } return Ok(()); } for (text, cc_index) in CONTROL_CHARS { - print!( + let to_print = format!( "{text} = {}; ", control_char_to_string(termios.control_chars[*cc_index as usize])? ); + if !line.is_empty() && line.len() + to_print.len() > opts.columns { + println!("{}", line.trim_end()); + line.clear(); + } + line.push_str(&to_print); } - println!( - "{}", - translate!("stty-output-min-time", + + let min_time = translate!("stty-output-min-time", "min" => termios.control_chars[S::VMIN as usize], "time" => termios.control_chars[S::VTIME as usize] - ) ); + if !line.is_empty() && line.len() + min_time.len() > opts.columns { + println!("{}", line.trim_end()); + line.clear(); + } + line.push_str(&min_time); + println!("{}", line.trim_end()); Ok(()) } @@ -712,7 +816,7 @@ fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> { } fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag]) { - let mut printed = false; + let mut line = String::new(); for &Flag { name, flag, @@ -725,21 +829,30 @@ fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag< continue; } let val = flag.is_in(termios, group); + let mut to_print = String::new(); + if group.is_some() { if val && (!sane || opts.all) { - print!("{name} "); - printed = true; + to_print = format!("{name} "); } } else if opts.all || val != sane { if !val { - print!("-"); + to_print.push('-'); + } + to_print.push_str(name); + to_print.push(' '); + } + + if !to_print.is_empty() { + if !line.is_empty() && line.len() + to_print.len() > opts.columns { + println!("{}", line.trim_end()); + line.clear(); } - print!("{name} "); - printed = true; + line.push_str(&to_print); } } - if printed { - println!(); + if !line.is_empty() { + println!("{}", line.trim_end()); } } @@ -794,6 +907,23 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) { termios.control_chars[mapping.0 as usize] = mapping.1; } +fn apply_saved_state(termios: &mut Termios, state: &[u32]) -> nix::Result<()> { + if state.len() >= 4 { + termios.input_flags = InputFlags::from_bits_truncate(state[0]); + termios.output_flags = OutputFlags::from_bits_truncate(state[1]); + termios.control_flags = ControlFlags::from_bits_truncate(state[2]); + termios.local_flags = LocalFlags::from_bits_truncate(state[3]); + + // Apply control characters (stored as u32 but used as u8) + for (i, &cc_val) in state.iter().skip(4).enumerate() { + if i < termios.control_chars.len() { + termios.control_chars[i] = cc_val as u8; + } + } + } + Ok(()) +} + fn apply_special_setting( _termios: &mut Termios, setting: &SpecialSetting, From 3149ce8703a8e5d54b04a2b28dc35bc8d5543087 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Thu, 20 Nov 2025 21:48:58 +0000 Subject: [PATCH 5/7] Adding a fix for stty row col to support hex inputs --- src/uu/stty/src/stty.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 815a9b368aa..cdbb5f9760d 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -521,10 +521,18 @@ fn parse_u8_or_err(arg: &str) -> Result { /// 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 + let n = if let Some(hex) = arg.strip_prefix("0x").or_else(|| arg.strip_prefix("0X")) { + u32::from_str_radix(hex, 16).ok()? + } else if let Some(octal) = arg.strip_prefix('0') { + if octal.is_empty() { + 0 + } else { + u32::from_str_radix(octal, 8).ok()? + } + } else { + arg.parse::().ok()? + }; + Some((n % (u16::MAX as u32 + 1)) as u16) } fn check_flag_group(flag: &Flag, remove: bool) -> bool { From e2c5b400e9dc55b29c79382bc7e4dda740bbcb06 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Thu, 20 Nov 2025 22:03:09 +0000 Subject: [PATCH 6/7] Adding validations to the string to baud method to pass GNU invalid stty tests --- src/uu/stty/src/stty.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index cdbb5f9760d..3ec22092e2c 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -622,6 +622,12 @@ fn string_to_combo(arg: &str) -> Option<&str> { } fn string_to_baud(arg: &str) -> Option> { + // Reject if has trailing/leading whitespace, multiple signs, negative, or invalid chars + if arg != arg.trim() || arg.starts_with('-') || arg.starts_with("++") + || arg.contains('E') || arg.contains('e') { + return None; + } + // Normalize the input: trim whitespace and leading '+' let normalized = arg.trim().trim_start_matches('+'); From 791ae7df2cbd612657da58adebd6eb1a25f185de Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Thu, 20 Nov 2025 22:24:08 +0000 Subject: [PATCH 7/7] When setting speed check for ispeed and ospeed flags --- src/uu/stty/src/flags.rs | 22 +++++- src/uu/stty/src/stty.rs | 148 +++++++++++++++++++++++++++------------ 2 files changed, 124 insertions(+), 46 deletions(-) diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index c10e7c04b39..4923f63d575 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -36,7 +36,7 @@ pub enum AllFlags<'a> { target_os = "netbsd", target_os = "openbsd" ))] - Baud(u32), + ISpeed(u32), #[cfg(not(any( target_os = "freebsd", target_os = "dragonfly", @@ -45,7 +45,25 @@ pub enum AllFlags<'a> { target_os = "netbsd", target_os = "openbsd" )))] - Baud(BaudRate), + ISpeed(BaudRate), + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + OSpeed(u32), + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + OSpeed(BaudRate), ControlFlags((&'a Flag, bool)), InputFlags((&'a Flag, bool)), LocalFlags((&'a Flag, bool)), diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 3ec22092e2c..8c1a03a89d3 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -20,7 +20,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use nix::libc::{O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, c_ushort}; use nix::sys::termios::{ ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, SpecialCharacterIndices as S, - Termios, cfgetospeed, cfsetospeed, tcgetattr, tcsetattr, + Termios, cfgetospeed, cfsetispeed, cfsetospeed, tcgetattr, tcsetattr, }; use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::fs::File; @@ -296,9 +296,28 @@ fn stty(opts: &Options) -> UResult<()> { let mut args_iter = args.iter(); while let Some(&arg) = args_iter.next() { match arg { - "ispeed" | "ospeed" => match args_iter.next() { + "ispeed" => match args_iter.next() { Some(speed) => { - if let Some(baud_flag) = string_to_baud(speed) { + if let Some(baud_flag) = string_to_baud(speed, true) { + valid_args.push(ArgOptions::Flags(baud_flag)); + } else { + return Err(USimpleError::new( + 1, + translate!( + "stty-error-invalid-speed", + "arg" => *arg, + "speed" => *speed, + ), + )); + } + } + None => { + return missing_arg(arg); + } + }, + "ospeed" => match args_iter.next() { + Some(speed) => { + if let Some(baud_flag) = string_to_baud(speed, false) { valid_args.push(ArgOptions::Flags(baud_flag)); } else { return Err(USimpleError::new( @@ -422,13 +441,16 @@ fn stty(opts: &Options) -> UResult<()> { } else { return missing_arg(arg); } - // baud rate - } else if let Some(baud_flag) = string_to_baud(arg) { - valid_args.push(ArgOptions::Flags(baud_flag)); + // baud rate (sets both ispeed and ospeed) + } else if let Some(ispeed_flag) = string_to_baud(arg, true) { + if let Some(ospeed_flag) = string_to_baud(arg, false) { + valid_args.push(ArgOptions::Flags(ispeed_flag)); + valid_args.push(ArgOptions::Flags(ospeed_flag)); + } // non control char flag } else if let Some(flag) = string_to_flag(arg) { let remove_group = match flag { - AllFlags::Baud(_) => false, + AllFlags::ISpeed(_) | AllFlags::OSpeed(_) => false, AllFlags::ControlFlags((flag, remove)) => { check_flag_group(flag, remove) } @@ -457,7 +479,7 @@ fn stty(opts: &Options) -> UResult<()> { for arg in &valid_args { match arg { ArgOptions::Mapping(mapping) => apply_char_mapping(&mut termios, mapping), - ArgOptions::Flags(flag) => apply_setting(&mut termios, flag), + ArgOptions::Flags(flag) => apply_setting(&mut termios, flag)?, ArgOptions::Special(setting) => { apply_special_setting(&mut termios, setting, opts.file.as_raw_fd())?; } @@ -621,13 +643,17 @@ fn string_to_combo(arg: &str) -> Option<&str> { .map(|_| arg) } -fn string_to_baud(arg: &str) -> Option> { - // Reject if has trailing/leading whitespace, multiple signs, negative, or invalid chars - if arg != arg.trim() || arg.starts_with('-') || arg.starts_with("++") - || arg.contains('E') || arg.contains('e') { +fn string_to_baud(arg: &str, is_ispeed: bool) -> Option> { + // Reject if has trailing whitespace, negative, multiple signs, or scientific notation + if arg != arg.trim_end() + || arg.trim().starts_with('-') + || arg.trim().starts_with("++") + || arg.contains('E') + || arg.contains('e') + { return None; } - + // Normalize the input: trim whitespace and leading '+' let normalized = arg.trim().trim_start_matches('+'); @@ -644,7 +670,11 @@ fn string_to_baud(arg: &str) -> Option> { target_os = "netbsd", target_os = "openbsd" ))] - return Some(AllFlags::Baud(rounded)); + return Some(if is_ispeed { + AllFlags::ISpeed(rounded) + } else { + AllFlags::OSpeed(rounded) + }); // On other platforms, find the closest valid baud rate #[cfg(not(any( @@ -660,7 +690,11 @@ fn string_to_baud(arg: &str) -> Option> { // First try exact match for (text, baud_rate) in BAUD_RATES { if *text == rounded_str { - return Some(AllFlags::Baud(*baud_rate)); + return Some(if is_ispeed { + AllFlags::ISpeed(*baud_rate) + } else { + AllFlags::OSpeed(*baud_rate) + }); } } // If no exact match, find closest baud rate @@ -674,7 +708,11 @@ fn string_to_baud(arg: &str) -> Option> { } } if let Some((_, baud_rate)) = closest { - return Some(AllFlags::Baud(baud_rate)); + return Some(if is_ispeed { + AllFlags::ISpeed(baud_rate) + } else { + AllFlags::OSpeed(baud_rate) + }); } } } @@ -690,7 +728,11 @@ fn string_to_baud(arg: &str) -> Option> { )))] for (text, baud_rate) in BAUD_RATES { if *text == arg { - return Some(AllFlags::Baud(*baud_rate)); + return Some(if is_ispeed { + AllFlags::ISpeed(*baud_rate) + } else { + AllFlags::OSpeed(*baud_rate) + }); } } @@ -871,9 +913,9 @@ fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag< } /// Apply a single setting -fn apply_setting(termios: &mut Termios, setting: &AllFlags) { +fn apply_setting(termios: &mut Termios, setting: &AllFlags) -> nix::Result<()> { match setting { - AllFlags::Baud(_) => apply_baud_rate_flag(termios, setting), + AllFlags::ISpeed(_) | AllFlags::OSpeed(_) => apply_baud_rate_flag(termios, setting)?, AllFlags::ControlFlags((setting, disable)) => { setting.flag.apply(termios, !disable); } @@ -887,34 +929,52 @@ fn apply_setting(termios: &mut Termios, setting: &AllFlags) { setting.flag.apply(termios, !disable); } } + Ok(()) } -fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) { - // BSDs use a u32 for the baud rate, so any decimal number applies. - #[cfg(any( - target_os = "freebsd", - target_os = "dragonfly", - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - ))] - if let AllFlags::Baud(n) = input { - cfsetospeed(termios, *n).expect("Failed to set baud rate"); - } - - // Other platforms use an enum. - #[cfg(not(any( - target_os = "freebsd", - target_os = "dragonfly", - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - )))] - if let AllFlags::Baud(br) = input { - cfsetospeed(termios, *br).expect("Failed to set baud rate"); +fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) -> nix::Result<()> { + match input { + // BSDs use a u32 for the baud rate + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + AllFlags::ISpeed(n) => cfsetispeed(termios, *n)?, + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + AllFlags::OSpeed(n) => cfsetospeed(termios, *n)?, + // Other platforms use an enum + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + AllFlags::ISpeed(br) => cfsetispeed(termios, *br)?, + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + AllFlags::OSpeed(br) => cfsetospeed(termios, *br)?, + _ => {} } + Ok(()) } fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) {