diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index 971e99afcdb..a2d6759417b 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -372,6 +372,7 @@ UCRT brkint cstopb decctlq +dsusp echoctl echoe echoke @@ -383,6 +384,7 @@ istrip litout opost parodd +solarish ENOTTY # translation tests diff --git a/src/uu/stty/build.rs b/src/uu/stty/build.rs index 320a109d007..0c5b94c10cf 100644 --- a/src/uu/stty/build.rs +++ b/src/uu/stty/build.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore solarish + use cfg_aliases::cfg_aliases; fn main() { @@ -14,5 +16,6 @@ fn main() { target_os = "netbsd", target_os = "openbsd" ) }, + solarish: { any(target_os = "illumos", target_os = "solaris") }, }; } diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs index d25936628d2..b341812a419 100644 --- a/src/uu/stty/src/flags.rs +++ b/src/uu/stty/src/flags.rs @@ -7,9 +7,10 @@ // spell-checker:ignore ignbrk brkint ignpar parmrk inpck istrip inlcr igncr icrnl ixoff ixon iuclc ixany imaxbel iutf // spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofdel nldly crdly tabdly bsdly vtdly ffdly // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc -// spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase +// spell-checker:ignore lnext rprnt susp dsusp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase VDSUSP // spell-checker:ignore sigquit sigtstp // spell-checker:ignore cbreak decctlq evenp litout oddp +// spell-checker:ignore cdtrdsr CDTRDSR ofill OFILL VFLUSHO VSTATUS noncanonical VMIN deciseconds noncanonical VTIME use crate::Flag; @@ -54,11 +55,15 @@ pub const CONTROL_FLAGS: &[Flag] = &[ Flag::new_grouped("cs7", C::CS7, C::CSIZE), Flag::new_grouped("cs8", C::CS8, C::CSIZE).sane(), Flag::new("hupcl", C::HUPCL), + // GNU's `hup`, hidden as GNU never displays it (OMIT). + Flag::new("hup", C::HUPCL).hidden(), Flag::new("cstopb", C::CSTOPB), Flag::new("cread", C::CREAD).sane(), Flag::new("clocal", C::CLOCAL), - #[cfg(not(target_os = "redox"))] + #[cfg(not(any(target_os = "redox", target_os = "aix")))] Flag::new("crtscts", C::CRTSCTS), + // Not supported by nix and libc. + // Flag::new("cdtrdsr", C::CDTRDSR), ]; pub const INPUT_FLAGS: &[Flag] = &[ @@ -71,14 +76,22 @@ pub const INPUT_FLAGS: &[Flag] = &[ Flag::new("inlcr", I::INLCR), Flag::new("igncr", I::IGNCR), Flag::new("icrnl", I::ICRNL).sane(), - Flag::new("ixoff", I::IXOFF), - Flag::new("tandem", I::IXOFF), Flag::new("ixon", I::IXON), - // not supported by nix - // Flag::new("iuclc", I::IUCLC), - #[cfg(not(target_os = "redox"))] + Flag::new("ixoff", I::IXOFF), + Flag::new("tandem", I::IXOFF).hidden(), + #[cfg(any( + target_os = "aix", + target_os = "android", + target_os = "cygwin", + target_os = "haiku", + target_os = "hurd", + target_os = "linux", + target_os = "nto", + solarish + ))] + Flag::new("iuclc", I::IUCLC), Flag::new("ixany", I::IXANY), - #[cfg(not(target_os = "redox"))] + #[cfg(not(any(target_os = "redox", target_os = "haiku")))] Flag::new("imaxbel", I::IMAXBEL).sane(), #[cfg(any(target_vendor = "apple", target_os = "android", target_os = "linux"))] Flag::new("iutf8", I::IUTF8), @@ -97,6 +110,20 @@ pub const OUTPUT_FLAGS: &[Flag] = &[ Flag::new("onlcr", O::ONLCR).sane(), Flag::new("onocr", O::ONOCR), Flag::new("onlret", O::ONLRET), + #[cfg(any( + target_vendor = "apple", + target_os = "aix", + target_os = "android", + target_os = "cygwin", + target_os = "fuchsia", + target_os = "haiku", + target_os = "hurd", + target_os = "linux", + target_os = "nto", + target_os = "redox", + solarish + ))] + Flag::new("ofill", O::OFILL), #[cfg(any( target_vendor = "apple", target_os = "android", @@ -228,8 +255,14 @@ pub const LOCAL_FLAGS: &[Flag] = &[ Flag::new("echok", L::ECHOK).sane(), Flag::new("echonl", L::ECHONL), Flag::new("noflsh", L::NOFLSH), - // Not supported by nix - // Flag::new("xcase", L::XCASE), + #[cfg(any( + target_os = "aix", + target_os = "android", + target_os = "haiku", + target_os = "nto", + target_os = "linux", + ))] + Flag::new("xcase", L::XCASE), Flag::new("tostop", L::TOSTOP), #[cfg(not(any(target_os = "cygwin", target_os = "redox")))] Flag::new("echoprt", L::ECHOPRT), @@ -245,7 +278,12 @@ pub const LOCAL_FLAGS: &[Flag] = &[ Flag::new("crtkill", L::ECHOKE).sane().hidden(), #[cfg(not(target_os = "redox"))] Flag::new("flusho", L::FLUSHO), - #[cfg(not(any(target_os = "cygwin", target_os = "redox")))] + #[cfg(not(any( + target_os = "cygwin", + target_os = "redox", + target_os = "aix", + target_os = "haiku" + )))] Flag::new("extproc", L::EXTPROC), ]; @@ -276,7 +314,9 @@ pub const BAUD_RATES: &[(&str, BaudRate)] = &[ target_os = "cygwin", target_os = "fuchsia", target_os = "hurd", - target_os = "redox" + target_os = "redox", + target_os = "aix", + target_os = "haiku" )))] ("460800", BaudRate::B460800), #[cfg(any(target_os = "android", target_os = "linux"))] @@ -288,7 +328,9 @@ pub const BAUD_RATES: &[(&str, BaudRate)] = &[ target_os = "cygwin", target_os = "fuchsia", target_os = "hurd", - target_os = "redox" + target_os = "redox", + target_os = "aix", + target_os = "haiku" )))] ("921600", BaudRate::B921600), #[cfg(any(target_os = "android", target_os = "linux"))] @@ -348,14 +390,33 @@ pub const CONTROL_CHARS: &[(&str, S)] = &[ ("stop", S::VSTOP), // Sends a suspend signal (SIGTSTP). ("susp", S::VSUSP), + #[cfg(any(bsd, solarish, target_os = "aix"))] + // Sends a delayed suspend signal (SIGTSTP). + ("dsusp", S::VDSUSP), + #[cfg(not(target_os = "haiku"))] // Reprints the current line. ("rprnt", S::VREPRINT), + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] // Deletes the last word typed. ("werase", S::VWERASE), + #[cfg(not(target_os = "haiku"))] // Enters literal mode (next character is taken literally). ("lnext", S::VLNEXT), + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] // Discards the current line. ("discard", S::VDISCARD), + // deprecated compat option. + // Not supported by nix and libc. + // ("flush", S::VFLUSHO), + #[cfg(any(bsd, target_os = "illumos"))] + // Status character + ("status", S::VSTATUS), + // Minimum number of characters for noncanonical read. + // We handle this manually. + // ("min", S::VMIN), + // Timeout in deciseconds for noncanonical read. + // We handle this manually. + // ("time", S::VTIME), ]; /// This constant lists all possible combination settings, using a bool to represent if the setting is negatable diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index f4d71ae857a..8b3ad0b9a2a 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -8,7 +8,7 @@ // spell-checker:ignore ignbrk brkint ignpar parmrk inpck istrip inlcr igncr icrnl ixoff ixon iuclc ixany imaxbel iutf // spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofdel nldly crdly tabdly bsdly vtdly ffdly ofill // spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc -// spell-checker:ignore lnext rprnt susp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase +// spell-checker:ignore lnext rprnt susp dsusp swtch vdiscard veof veol verase vintr vkill vlnext vquit vreprint vstart vstop vsusp vswtc vwerase werase // spell-checker:ignore sigquit sigtstp // spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain exta extb NCCS cfsetispeed // spell-checker:ignore notaflag notacombo notabaud @@ -45,18 +45,22 @@ use flags::{CONTROL_CHARS, CONTROL_FLAGS, INPUT_FLAGS, LOCAL_FLAGS, OUTPUT_FLAGS const ASCII_DEL: u8 = 127; // Sane defaults for control characters. -const SANE_CONTROL_CHARS: [(S, u8); 12] = [ - (S::VINTR, 3), // ^C - (S::VQUIT, 28), // ^\ - (S::VERASE, 127), // DEL - (S::VKILL, 21), // ^U - (S::VEOF, 4), // ^D - (S::VSTART, 17), // ^Q - (S::VSTOP, 19), // ^S - (S::VSUSP, 26), // ^Z +const SANE_CONTROL_CHARS: &[(S, u8)] = &[ + (S::VINTR, 3), // ^C + (S::VQUIT, 28), // ^\ + (S::VERASE, 127), // DEL + (S::VKILL, 21), // ^U + (S::VEOF, 4), // ^D + (S::VSTART, 17), // ^Q + (S::VSTOP, 19), // ^S + (S::VSUSP, 26), // ^Z + #[cfg(not(target_os = "haiku"))] (S::VREPRINT, 18), // ^R - (S::VWERASE, 23), // ^W - (S::VLNEXT, 22), // ^V + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] + (S::VWERASE, 23), // ^W + #[cfg(not(target_os = "haiku"))] + (S::VLNEXT, 22), // ^V + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] (S::VDISCARD, 15), // ^O ]; @@ -607,12 +611,25 @@ fn print_terminal_size( ); } - #[cfg(any(target_os = "linux", target_os = "redox"))] + #[cfg(any(target_os = "linux", target_os = "android", target_os = "haiku"))] { let line = termios.line_discipline; printer.print(&translate!("stty-output-line", "line" => line)); } - #[cfg(not(any(target_os = "linux", target_os = "redox")))] + #[cfg(target_os = "redox")] + { + // nix does not expose the line discipline on Redox, so we get the + // underlying libc::termios struct to read that information. + let libc_termios: nix::libc::termios = termios.clone().into(); + let line = libc_termios.c_line; + printer.print(&translate!("stty-output-line", "line" => line)); + } + #[cfg(not(any( + target_os = "linux", + target_os = "android", + target_os = "haiku", + target_os = "redox" + )))] let _ = termios; // remove this hack when we switched to rustix completely printer.flush(); Ok(()) @@ -964,7 +981,7 @@ fn apply_special_setting( SpecialSetting::Rows(n) => size.ws_row = *n, SpecialSetting::Cols(n) => size.ws_col = *n, #[cfg_attr( - not(any(target_os = "linux", target_os = "android")), + not(any(target_os = "linux", target_os = "android", target_os = "haiku")), expect(unused_variables) )] SpecialSetting::Line(n) => { @@ -973,6 +990,12 @@ fn apply_special_setting( { _termios.line_discipline = *n; } + // On Haiku the field is a `c_char`, so convert from `u8` first. + #[cfg(target_os = "haiku")] + { + _termios.line_discipline = + (*n).try_into().map_err(|_| nix::errno::Errno::ERANGE)?; + } } } tcsetwinsize(fd, size)?; @@ -1049,10 +1072,18 @@ fn combo_to_flags(combo: &str) -> Vec> { (S::VSTART, "^Q"), (S::VSTOP, "^S"), (S::VSUSP, "^Z"), + #[cfg(not(target_os = "haiku"))] (S::VREPRINT, "^R"), + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] (S::VWERASE, "^W"), + #[cfg(not(target_os = "haiku"))] (S::VLNEXT, "^V"), + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] (S::VDISCARD, "^O"), + #[cfg(any(bsd, solarish, target_os = "aix"))] + (S::VDSUSP, "^Y"), + #[cfg(any(bsd, target_os = "illumos"))] + (S::VSTATUS, "^T"), ], _ => &[], }; @@ -1109,7 +1140,7 @@ fn combo_to_flags(combo: &str) -> Vec> { } fn get_sane_control_char(cc_index: S) -> u8 { - for (sane_index, sane_val) in SANE_CONTROL_CHARS { + for &(sane_index, sane_val) in SANE_CONTROL_CHARS { if sane_index == cc_index { return sane_val; } @@ -1121,12 +1152,18 @@ fn get_sane_control_char(cc_index: S) -> u8 { S::VMIN => 1, #[cfg(not(any( all(target_os = "linux", target_arch = "sparc64"), - target_os = "illumos", - target_os = "solaris" + solarish, + target_os = "aix", + target_os = "haiku" )))] S::VTIME => 0, #[cfg(target_os = "linux")] S::VSWTC => 0, + // BSD-family sane defaults (GNU uses CDSUSP = ^Y, CSTATUS = ^T). + #[cfg(any(bsd, solarish, target_os = "aix"))] + S::VDSUSP => 25, // ^Y + #[cfg(any(bsd, target_os = "illumos"))] + S::VSTATUS => 20, // ^T _ => 0, } } @@ -1266,6 +1303,18 @@ mod tests { fn test_combo_to_flags_sane() { let flags = combo_to_flags("sane"); assert!(flags.len() > 5); // sane sets multiple flags + + let has_mapping = |cc: S, val: u8| { + flags + .iter() + .any(|f| matches!(f, ArgOptions::Mapping((idx, v)) if *idx == cc && *v == val)) + }; + // sane always resets the standard control characters. + assert!(has_mapping(S::VINTR, 3)); // ^C + #[cfg(any(bsd, solarish, target_os = "aix"))] + assert!(has_mapping(S::VDSUSP, 25)); // ^Y + #[cfg(any(bsd, target_os = "illumos"))] + assert!(has_mapping(S::VSTATUS, 20)); // ^T } #[test] @@ -1421,9 +1470,13 @@ mod tests { assert_eq!(cc_to_index("start"), Some(S::VSTART)); assert_eq!(cc_to_index("stop"), Some(S::VSTOP)); assert_eq!(cc_to_index("susp"), Some(S::VSUSP)); + #[cfg(not(target_os = "haiku"))] assert_eq!(cc_to_index("rprnt"), Some(S::VREPRINT)); + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] assert_eq!(cc_to_index("werase"), Some(S::VWERASE)); + #[cfg(not(target_os = "haiku"))] assert_eq!(cc_to_index("lnext"), Some(S::VLNEXT)); + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] assert_eq!(cc_to_index("discard"), Some(S::VDISCARD)); } @@ -1454,12 +1507,28 @@ mod tests { assert_eq!(get_sane_control_char(S::VSTART), 17); // ^Q assert_eq!(get_sane_control_char(S::VSTOP), 19); // ^S assert_eq!(get_sane_control_char(S::VSUSP), 26); // ^Z + #[cfg(not(target_os = "haiku"))] assert_eq!(get_sane_control_char(S::VREPRINT), 18); // ^R + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] assert_eq!(get_sane_control_char(S::VWERASE), 23); // ^W + #[cfg(not(target_os = "haiku"))] assert_eq!(get_sane_control_char(S::VLNEXT), 22); // ^V + #[cfg(not(any(target_os = "aix", target_os = "haiku")))] assert_eq!(get_sane_control_char(S::VDISCARD), 15); // ^O } + #[cfg(any(bsd, solarish, target_os = "aix"))] + #[test] + fn test_get_sane_control_char_dsusp() { + assert_eq!(get_sane_control_char(S::VDSUSP), 25); // ^Y + } + + #[cfg(any(bsd, target_os = "illumos"))] + #[test] + fn test_get_sane_control_char_status() { + assert_eq!(get_sane_control_char(S::VSTATUS), 20); // ^T + } + // Tests for parse_u8_or_err #[test] fn test_parse_u8_or_err_valid() { diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs index 157758db903..1efb7685baa 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 dsusp evenp oddp CSIZE #![cfg(unix)] @@ -39,11 +39,145 @@ fn test_basic() { #[test] fn test_all_flag() { let (path, _controller, _replica) = pty_path(); + // The pty defaults for `status`/`dsusp` differ per platform (OpenBSD + // leaves `status` at \377), so set the sane values explicitly and check + // that they round-trip through the mapping. + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos" + ))] + new_ucmd!() + .args(&["--file", &path, "status", "^T"]) + .succeeds(); + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "aix", + target_os = "illumos", + target_os = "solaris" + ))] + new_ucmd!() + .args(&["--file", &path, "dsusp", "^Y"]) + .succeeds(); let result = new_ucmd!().args(&["--all", "--file", &path]).succeeds(); for flag in ["parenb", "parmrk", "ixany", "onlcr", "icanon", "noflsh"] { result.stdout_contains(flag); } + + // Tokens are matched on the whitespace-split output, so the checks are + // stable against the line wrapping of `--all`. + let tokens: Vec<&str> = result.stdout_str().split_whitespace().collect(); + let position = |token: &str| tokens.iter().position(|&t| t == token); + let flag_position = |name: &str| { + tokens + .iter() + .position(|&t| t.trim_start_matches('-') == name) + }; + + // `tandem` is GNU's hidden alias of `ixoff` and is never printed. + assert!(!tokens.contains(&"tandem")); + + // GNU prints `ixon` before `ixoff`, whichever their state. + assert!(flag_position("ixon").unwrap() < flag_position("ixoff").unwrap()); + + // GNU-compatibility entries that are only defined on some platforms; they + // are off by default, so check their negated form (polarity). + #[cfg(any( + target_os = "aix", + target_os = "android", + target_os = "cygwin", + target_os = "haiku", + target_os = "hurd", + target_os = "illumos", + target_os = "nto", + target_os = "solaris", + target_os = "linux" + ))] + assert!(position("-iuclc").unwrap() > flag_position("ixoff").unwrap()); + #[cfg(any( + target_vendor = "apple", + target_os = "aix", + target_os = "android", + target_os = "cygwin", + target_os = "fuchsia", + target_os = "haiku", + target_os = "hurd", + target_os = "illumos", + target_os = "linux", + target_os = "nto", + target_os = "redox", + target_os = "solaris" + ))] + assert!(position("-ofill").is_some()); + #[cfg(any( + target_os = "aix", + target_os = "android", + target_os = "haiku", + target_os = "nto", + target_os = "linux", + ))] + assert!(position("-xcase").is_some()); + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "aix", + target_os = "illumos", + target_os = "solaris" + ))] + { + // `dsusp` resets to ^Y on the sane defaults; `--all` prints it as + // `dsusp = ^Y;`. + let dsusp = position("dsusp").unwrap(); + assert_eq!(tokens[dsusp + 2], "^Y;"); + } + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + ))] + { + // `status` resets to ^T on sane defaults; `--all` prints it as + // `status = ^T;`. + let status = position("status").unwrap(); + assert_eq!(tokens[status + 2], "^T;"); + } +} + +#[test] +fn test_hup_alias() { + // `hup` is GNU's hidden alias of `hupcl` and is never printed itself. + let (path, _controller, _replica) = pty_path(); + + new_ucmd!().args(&["--file", &path, "-hup"]).succeeds(); + new_ucmd!() + .args(&["--file", &path, "--all"]) + .succeeds() + .stdout_contains("-hupcl"); + + new_ucmd!().args(&["--file", &path, "hup"]).succeeds(); + new_ucmd!() + .args(&["--file", &path, "--all"]) + .succeeds() + .stdout_contains("hupcl") + .stdout_does_not_contain("-hupcl"); } #[test] @@ -320,7 +454,7 @@ fn test_row_column_hex_octal() { } #[test] -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "haiku"))] fn line() { new_ucmd!() .args(&["line"])