diff --git a/Cargo.lock b/Cargo.lock index 1d0595470..29079ba87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,6 +445,7 @@ dependencies = [ "uu_split", "uu_stat", "uu_stdbuf", + "uu_stty", "uu_sum", "uu_sync", "uu_tac", @@ -2908,6 +2909,15 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_stty" +version = "0.0.14" +dependencies = [ + "clap 3.2.17", + "nix", + "uucore", +] + [[package]] name = "uu_sum" version = "0.0.14" diff --git a/Cargo.toml b/Cargo.toml index 455d45c8e..9fee3727c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,6 +190,7 @@ feat_require_unix = [ "nohup", "pathchk", "stat", + "stty", "timeout", "tty", "uname", @@ -348,6 +349,7 @@ sort = { optional=true, version="0.0.14", package="uu_sort", path="src/uu/so split = { optional=true, version="0.0.14", package="uu_split", path="src/uu/split" } stat = { optional=true, version="0.0.14", package="uu_stat", path="src/uu/stat" } stdbuf = { optional=true, version="0.0.14", package="uu_stdbuf", path="src/uu/stdbuf" } +stty = { optional=true, version="0.0.14", package="uu_stty", path="src/uu/stty" } sum = { optional=true, version="0.0.14", package="uu_sum", path="src/uu/sum" } sync = { optional=true, version="0.0.14", package="uu_sync", path="src/uu/sync" } tac = { optional=true, version="0.0.14", package="uu_tac", path="src/uu/tac" } diff --git a/src/uu/stty/Cargo.toml b/src/uu/stty/Cargo.toml new file mode 100644 index 000000000..919dd288b --- /dev/null +++ b/src/uu/stty/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "uu_stty" +version = "0.0.14" +authors = ["uutils developers"] +license = "MIT" +description = "stty ~ (uutils) print or change terminal characteristics" + +homepage = "https://github.com/uutils/coreutils" +repository = "https://github.com/uutils/coreutils/tree/main/src/uu/stty" +keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] +edition = "2021" + +[lib] +path = "src/stty.rs" + +[dependencies] +clap = { version = "3.1", features = ["wrap_help", "cargo"] } +uucore = { version=">=0.0.11", package="uucore", path="../../uucore" } +nix = { version="0.25", features = ["term"] } + +[[bin]] +name = "stty" +path = "src/main.rs" diff --git a/src/uu/stty/LICENSE b/src/uu/stty/LICENSE new file mode 120000 index 000000000..5853aaea5 --- /dev/null +++ b/src/uu/stty/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/src/uu/stty/src/flags.rs b/src/uu/stty/src/flags.rs new file mode 100644 index 000000000..2c3f00eec --- /dev/null +++ b/src/uu/stty/src/flags.rs @@ -0,0 +1,315 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE file +// * that was distributed with this source code. + +// spell-checker:ignore parenb parodd cmspar hupcl cstopb cread clocal crtscts CSIZE +// 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 ofill 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 + +use crate::Flag; + +#[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" +)))] +use nix::sys::termios::BaudRate; +use nix::sys::termios::{ControlFlags as C, InputFlags as I, LocalFlags as L, OutputFlags as O}; + +pub const CONTROL_FLAGS: &[Flag] = &[ + Flag::new("parenb", C::PARENB), + Flag::new("parodd", C::PARODD), + #[cfg(any( + target_os = "android", + all(target_os = "linux", not(target_arch = "mips")) + ))] + Flag::new("cmspar", C::CMSPAR), + Flag::new_grouped("cs5", C::CS5, C::CSIZE), + Flag::new_grouped("cs6", C::CS6, C::CSIZE), + Flag::new_grouped("cs7", C::CS7, C::CSIZE), + Flag::new_grouped("cs8", C::CS8, C::CSIZE).sane(), + Flag::new("hupcl", C::HUPCL), + Flag::new("cstopb", C::CSTOPB), + Flag::new("cread", C::CREAD).sane(), + Flag::new("clocal", C::CLOCAL), + Flag::new("crtscts", C::CRTSCTS), +]; + +pub const INPUT_FLAGS: &[Flag] = &[ + Flag::new("ignbrk", I::IGNBRK), + Flag::new("brkint", I::BRKINT).sane(), + Flag::new("ignpar", I::IGNPAR), + Flag::new("parmrk", I::PARMRK), + Flag::new("inpck", I::INPCK), + Flag::new("istrip", I::ISTRIP), + 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), + Flag::new("ixany", I::IXANY), + Flag::new("imaxbel", I::IMAXBEL).sane(), + #[cfg(any(target_os = "android", target_os = "linux", target_os = "macos"))] + Flag::new("iutf8", I::IUTF8), +]; + +pub const OUTPUT_FLAGS: &[Flag] = &[ + Flag::new("opost", O::OPOST).sane(), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "linux", + target_os = "openbsd" + ))] + Flag::new("olcuc", O::OLCUC), + Flag::new("ocrnl", O::OCRNL), + Flag::new("onlcr", O::ONLCR).sane(), + Flag::new("onocr", O::ONOCR), + Flag::new("onlret", O::ONLRET), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new("ofill", O::OFILL), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new("ofdel", O::OFDEL), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("nl0", O::NL0, O::NLDLY).sane(), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("nl1", O::NL1, O::NLDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("cr0", O::CR0, O::CRDLY).sane(), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("cr1", O::CR1, O::CRDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("cr2", O::CR2, O::CRDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("cr3", O::CR3, O::CRDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("tab0", O::TAB0, O::TABDLY).sane(), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("tab1", O::TAB1, O::TABDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("tab2", O::TAB2, O::TABDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("tab3", O::TAB3, O::TABDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("bs0", O::BS0, O::BSDLY).sane(), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("bs1", O::BS1, O::BSDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("vt0", O::VT0, O::VTDLY).sane(), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("vt1", O::VT1, O::VTDLY), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("ff0", O::FF0, O::FFDLY).sane(), + #[cfg(any( + target_os = "android", + target_os = "haiku", + target_os = "ios", + target_os = "linux", + target_os = "macos" + ))] + Flag::new_grouped("ff1", O::FF1, O::FFDLY), +]; + +pub const LOCAL_FLAGS: &[Flag] = &[ + Flag::new("isig", L::ISIG).sane(), + Flag::new("icanon", L::ICANON).sane(), + Flag::new("iexten", L::IEXTEN).sane(), + Flag::new("echo", L::ECHO).sane(), + Flag::new("echoe", L::ECHOE).sane(), + Flag::new("crterase", L::ECHOE).hidden().sane(), + 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), + Flag::new("tostop", L::TOSTOP), + Flag::new("echoprt", L::ECHOPRT), + Flag::new("prterase", L::ECHOPRT).hidden(), + Flag::new("echoctl", L::ECHOCTL).sane(), + Flag::new("ctlecho", L::ECHOCTL).sane().hidden(), + Flag::new("echoke", L::ECHOKE).sane(), + Flag::new("crtkill", L::ECHOKE).sane().hidden(), + Flag::new("flusho", L::FLUSHO), + Flag::new("extproc", L::EXTPROC), +]; + +// BSD's use u32 as baud rate, to using the enum is unnecessary. +#[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" +)))] +pub const BAUD_RATES: &[(&str, BaudRate)] = &[ + ("0", BaudRate::B0), + ("50", BaudRate::B50), + ("75", BaudRate::B75), + ("110", BaudRate::B110), + ("134", BaudRate::B134), + ("150", BaudRate::B150), + ("200", BaudRate::B200), + ("300", BaudRate::B300), + ("600", BaudRate::B600), + ("1200", BaudRate::B1200), + ("1800", BaudRate::B1800), + ("2400", BaudRate::B2400), + ("9600", BaudRate::B9600), + ("19200", BaudRate::B19200), + ("38400", BaudRate::B38400), + ("57600", BaudRate::B57600), + ("115200", BaudRate::B115200), + ("230400", BaudRate::B230400), + #[cfg(any(target_os = "android", target_os = "linux"))] + ("500000", BaudRate::B500000), + #[cfg(any(target_os = "android", target_os = "linux"))] + ("576000", BaudRate::B576000), + #[cfg(any(target_os = "android", target_os = "linux",))] + ("921600", BaudRate::B921600), + #[cfg(any(target_os = "android", target_os = "linux"))] + ("1000000", BaudRate::B1000000), + #[cfg(any(target_os = "android", target_os = "linux"))] + ("1152000", BaudRate::B1152000), + #[cfg(any(target_os = "android", target_os = "linux"))] + ("1500000", BaudRate::B1500000), + #[cfg(any(target_os = "android", target_os = "linux"))] + ("2000000", BaudRate::B2000000), + #[cfg(any( + target_os = "android", + all(target_os = "linux", not(target_arch = "sparc64")) + ))] + ("2500000", BaudRate::B2500000), + #[cfg(any( + target_os = "android", + all(target_os = "linux", not(target_arch = "sparc64")) + ))] + ("3000000", BaudRate::B3000000), + #[cfg(any( + target_os = "android", + all(target_os = "linux", not(target_arch = "sparc64")) + ))] + ("3500000", BaudRate::B3500000), + #[cfg(any( + target_os = "android", + all(target_os = "linux", not(target_arch = "sparc64")) + ))] + ("4000000", BaudRate::B4000000), +]; diff --git a/src/uu/stty/src/main.rs b/src/uu/stty/src/main.rs new file mode 100644 index 000000000..4f9b9799a --- /dev/null +++ b/src/uu/stty/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_stty); diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs new file mode 100644 index 000000000..23c1c357c --- /dev/null +++ b/src/uu/stty/src/stty.rs @@ -0,0 +1,401 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE file +// * that was distributed with this source code. + +// spell-checker:ignore tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed ushort + +mod flags; + +use clap::{crate_version, Arg, ArgMatches, Command}; +use nix::libc::{c_ushort, TIOCGWINSZ, TIOCSWINSZ}; +use nix::sys::termios::{ + cfgetospeed, tcgetattr, tcsetattr, ControlFlags, InputFlags, LocalFlags, OutputFlags, Termios, +}; +use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; +use std::io::{self, stdout}; +use std::ops::ControlFlow; +use std::os::unix::io::{AsRawFd, RawFd}; +use uucore::error::{UResult, USimpleError}; +use uucore::format_usage; + +#[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" +)))] +use flags::BAUD_RATES; +use flags::{CONTROL_FLAGS, INPUT_FLAGS, LOCAL_FLAGS, OUTPUT_FLAGS}; + +const NAME: &str = "stty"; +const USAGE: &str = "\ + {} [-F DEVICE | --file=DEVICE] [SETTING]... + {} [-F DEVICE | --file=DEVICE] [-a|--all] + {} [-F DEVICE | --file=DEVICE] [-g|--save]"; +const SUMMARY: &str = "Print or change terminal characteristics."; + +#[derive(Clone, Copy, Debug)] +pub struct Flag { + name: &'static str, + flag: T, + show: bool, + sane: bool, + group: Option, +} + +impl Flag { + pub const fn new(name: &'static str, flag: T) -> Self { + Self { + name, + flag, + show: true, + sane: false, + group: None, + } + } + + pub const fn new_grouped(name: &'static str, flag: T, group: T) -> Self { + Self { + name, + flag, + show: true, + sane: false, + group: Some(group), + } + } + + pub const fn hidden(mut self) -> Self { + self.show = false; + self + } + + pub const fn sane(mut self) -> Self { + self.sane = true; + self + } +} + +trait TermiosFlag: Copy { + fn is_in(&self, termios: &Termios, group: Option) -> bool; + fn apply(&self, termios: &mut Termios, val: bool); +} + +mod options { + pub const ALL: &str = "all"; + pub const SAVE: &str = "save"; + pub const FILE: &str = "file"; + pub const SETTINGS: &str = "settings"; +} + +struct Options<'a> { + all: bool, + save: bool, + file: RawFd, + settings: Option>, +} + +impl<'a> Options<'a> { + fn from(matches: &'a ArgMatches) -> io::Result { + Ok(Self { + all: matches.is_present(options::ALL), + save: matches.is_present(options::SAVE), + file: match matches.value_of(options::FILE) { + Some(_f) => todo!(), + None => stdout().as_raw_fd(), + }, + settings: matches.values_of(options::SETTINGS).map(|v| v.collect()), + }) + } +} + +// Needs to be repr(C) because we pass it to the ioctl calls. +#[repr(C)] +#[derive(Default, Debug)] +pub struct TermSize { + rows: c_ushort, + columns: c_ushort, + x: c_ushort, + y: c_ushort, +} + +ioctl_read_bad!( + /// Get terminal window size + tiocgwinsz, + TIOCGWINSZ, + TermSize +); + +ioctl_write_ptr_bad!( + /// Set terminal window size + tiocswinsz, + TIOCSWINSZ, + TermSize +); + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = args.collect_lossy(); + + let matches = uu_app().get_matches_from(args); + + let opts = Options::from(&matches)?; + + stty(&opts) +} + +fn stty(opts: &Options) -> UResult<()> { + if opts.save && opts.all { + return Err(USimpleError::new( + 1, + "the options for verbose and stty-readable output styles are mutually exclusive", + )); + } + + if opts.settings.is_some() && (opts.save || opts.all) { + return Err(USimpleError::new( + 1, + "when specifying an output style, modes may not be set", + )); + } + + // TODO: Figure out the right error message for when tcgetattr fails + let mut termios = tcgetattr(opts.file).expect("Could not get terminal attributes"); + + if let Some(settings) = &opts.settings { + for setting in settings { + if let ControlFlow::Break(false) = apply_setting(&mut termios, setting) { + return Err(USimpleError::new( + 1, + format!("invalid argument '{}'", setting), + )); + } + } + + tcsetattr(opts.file, nix::sys::termios::SetArg::TCSANOW, &termios) + .expect("Could not write terminal attributes"); + } else { + print_settings(&termios, opts).expect("TODO: make proper error here from nix error"); + } + Ok(()) +} + +fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { + let speed = cfgetospeed(termios); + + // BSDs use a u32 for the baud rate, so we can simply print it. + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + print!("speed {} baud; ", speed); + + // Other platforms need to use the baud rate enum, so printing the right value + // becomes slightly more complicated. + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + for (text, baud_rate) in BAUD_RATES { + if *baud_rate == speed { + print!("speed {} baud; ", text); + break; + } + } + + if opts.all { + let mut size = TermSize::default(); + unsafe { tiocgwinsz(opts.file, &mut size as *mut _)? }; + print!("rows {}; columns {}; ", size.rows, size.columns); + } + + #[cfg(any(target_os = "linux", target_os = "redox"))] + { + // For some reason the normal nix Termios struct does not expose the line, + // so we get the underlying libc::termios struct to get that information. + let libc_termios: nix::libc::termios = termios.clone().into(); + let line = libc_termios.c_line; + print!("line = {};", line); + } + + println!(); + Ok(()) +} + +fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> { + print_terminal_size(termios, opts)?; + print_flags(termios, opts, CONTROL_FLAGS); + print_flags(termios, opts, INPUT_FLAGS); + print_flags(termios, opts, OUTPUT_FLAGS); + print_flags(termios, opts, LOCAL_FLAGS); + Ok(()) +} + +fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag]) { + let mut printed = false; + for &Flag { + name, + flag, + show, + sane, + group, + } in flags + { + if !show { + continue; + } + let val = flag.is_in(termios, group); + if group.is_some() { + if val && (!sane || opts.all) { + print!("{} ", name); + printed = true; + } + } else if opts.all || val != sane { + if !val { + print!("-"); + } + print!("{} ", name); + printed = true; + } + } + if printed { + println!(); + } +} + +/// Apply a single setting +/// +/// The value inside the `Break` variant of the `ControlFlow` indicates whether +/// the setting has been applied. +fn apply_setting(termios: &mut Termios, s: &str) -> ControlFlow { + let (remove, name) = match s.strip_prefix('-') { + Some(s) => (true, s), + None => (false, s), + }; + apply_flag(termios, CONTROL_FLAGS, name, remove)?; + apply_flag(termios, INPUT_FLAGS, name, remove)?; + apply_flag(termios, OUTPUT_FLAGS, name, remove)?; + apply_flag(termios, LOCAL_FLAGS, name, remove)?; + ControlFlow::Break(false) +} + +/// Apply a flag to a slice of flags +/// +/// The value inside the `Break` variant of the `ControlFlow` indicates whether +/// the setting has been applied. +fn apply_flag( + termios: &mut Termios, + flags: &[Flag], + input: &str, + remove: bool, +) -> ControlFlow { + for Flag { + name, flag, group, .. + } in flags + { + if input == *name { + // Flags with groups cannot be removed + // Since the name matches, we can short circuit and don't have to check the other flags. + if remove && group.is_some() { + return ControlFlow::Break(false); + } + // If there is a group, the bits for that group should be cleared before applying the flag + if let Some(group) = group { + group.apply(termios, false); + } + flag.apply(termios, !remove); + return ControlFlow::Break(true); + } + } + ControlFlow::Continue(()) +} + +pub fn uu_app<'a>() -> Command<'a> { + Command::new(uucore::util_name()) + .name(NAME) + .version(crate_version!()) + .override_usage(format_usage(USAGE)) + .about(SUMMARY) + .infer_long_args(true) + .arg( + Arg::new(options::ALL) + .short('a') + .long(options::ALL) + .help("print all current settings in human-readable form"), + ) + .arg( + Arg::new(options::SAVE) + .short('g') + .long(options::SAVE) + .help("print all current settings in a stty-readable form"), + ) + .arg( + Arg::new(options::FILE) + .short('F') + .long(options::FILE) + .takes_value(true) + .value_hint(clap::ValueHint::FilePath) + .value_name("DEVICE") + .help("open and use the specified DEVICE instead of stdin"), + ) + .arg( + Arg::new(options::SETTINGS) + .takes_value(true) + .multiple_values(true) + .help("settings to change"), + ) +} + +impl TermiosFlag for ControlFlags { + fn is_in(&self, termios: &Termios, group: Option) -> bool { + termios.control_flags.contains(*self) + && group.map_or(true, |g| !termios.control_flags.intersects(g - *self)) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.control_flags.set(*self, val); + } +} + +impl TermiosFlag for InputFlags { + fn is_in(&self, termios: &Termios, group: Option) -> bool { + termios.input_flags.contains(*self) + && group.map_or(true, |g| !termios.input_flags.intersects(g - *self)) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.input_flags.set(*self, val); + } +} + +impl TermiosFlag for OutputFlags { + fn is_in(&self, termios: &Termios, group: Option) -> bool { + termios.output_flags.contains(*self) + && group.map_or(true, |g| !termios.output_flags.intersects(g - *self)) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.output_flags.set(*self, val); + } +} + +impl TermiosFlag for LocalFlags { + fn is_in(&self, termios: &Termios, group: Option) -> bool { + termios.local_flags.contains(*self) + && group.map_or(true, |g| !termios.local_flags.intersects(g - *self)) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.local_flags.set(*self, val); + } +} diff --git a/tests/by-util/test_stty.rs b/tests/by-util/test_stty.rs new file mode 100644 index 000000000..730ccdf37 --- /dev/null +++ b/tests/by-util/test_stty.rs @@ -0,0 +1,55 @@ +// spell-checker:ignore parenb parmrk ixany iuclc onlcr ofdel icanon noflsh + +use crate::common::util::*; + +#[test] +#[ignore = "Fails because cargo test does not run in a tty"] +fn runs() { + new_ucmd!().succeeds(); +} + +#[test] +#[ignore = "Fails because cargo test does not run in a tty"] +fn print_all() { + let res = new_ucmd!().succeeds(); + + // Random selection of flags to check for + for flag in [ + "parenb", "parmrk", "ixany", "iuclc", "onlcr", "ofdel", "icanon", "noflsh", + ] { + res.stdout_contains(flag); + } +} + +#[test] +fn save_and_setting() { + new_ucmd!() + .args(&["--save", "nl0"]) + .fails() + .stderr_contains("when specifying an output style, modes may not be set"); +} + +#[test] +fn all_and_setting() { + new_ucmd!() + .args(&["--all", "nl0"]) + .fails() + .stderr_contains("when specifying an output style, modes may not be set"); +} + +#[test] +fn save_and_all() { + new_ucmd!() + .args(&["--save", "--all"]) + .fails() + .stderr_contains( + "the options for verbose and stty-readable output styles are mutually exclusive", + ); + + new_ucmd!() + .args(&["--all", "--save"]) + .fails() + .stderr_contains( + "the options for verbose and stty-readable output styles are mutually exclusive", + ); +}