diff --git a/Cargo.lock b/Cargo.lock index ba035a72c..3c37e0ce6 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.1.18", + "nix", + "uucore", +] + [[package]] name = "uu_sum" version = "0.0.14" diff --git a/Cargo.toml b/Cargo.toml index 134f73925..4d71d2c51 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..d39786494 --- /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.24.1", 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..b2688cb8d --- /dev/null +++ b/src/uu/stty/src/flags.rs @@ -0,0 +1,332 @@ +// * 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 +// 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 +// spell-checker:ignore isig icanon iexten echoe crterase echok echonl noflsh xcase tostop echoprt prterase echoctl ctlecho echoke crtkill flusho extproc + +use crate::Flag; +use nix::sys::termios::{ControlFlags, InputFlags, LocalFlags, OutputFlags}; + +pub const CONTROL_FLAGS: [Flag; 8] = [ + Flag { + name: "parenb", + flag: ControlFlags::PARENB, + show: true, + sane: false, + }, + Flag { + name: "parodd", + flag: ControlFlags::PARODD, + show: true, + sane: false, + }, + Flag { + name: "cmspar", + flag: ControlFlags::CMSPAR, + show: true, + sane: false, + }, + Flag { + name: "hupcl", + flag: ControlFlags::HUPCL, + show: true, + sane: true, + }, + Flag { + name: "cstopb", + flag: ControlFlags::CSTOPB, + show: true, + sane: false, + }, + Flag { + name: "cread", + flag: ControlFlags::CREAD, + show: true, + sane: true, + }, + Flag { + name: "clocal", + flag: ControlFlags::CLOCAL, + show: true, + sane: false, + }, + Flag { + name: "crtscts", + flag: ControlFlags::CRTSCTS, + show: true, + sane: false, + }, +]; + +pub const INPUT_FLAGS: [Flag; 15] = [ + Flag { + name: "ignbrk", + flag: InputFlags::IGNBRK, + show: true, + sane: false, + }, + Flag { + name: "brkint", + flag: InputFlags::BRKINT, + show: true, + sane: true, + }, + Flag { + name: "ignpar", + flag: InputFlags::IGNPAR, + show: true, + sane: false, + }, + Flag { + name: "parmrk", + flag: InputFlags::PARMRK, + show: true, + sane: false, + }, + Flag { + name: "inpck", + flag: InputFlags::INPCK, + show: true, + sane: false, + }, + Flag { + name: "istrip", + flag: InputFlags::ISTRIP, + show: true, + sane: false, + }, + Flag { + name: "inlcr", + flag: InputFlags::INLCR, + show: true, + sane: false, + }, + Flag { + name: "igncr", + flag: InputFlags::IGNCR, + show: true, + sane: false, + }, + Flag { + name: "icrnl", + flag: InputFlags::ICRNL, + show: true, + sane: true, + }, + Flag { + name: "ixoff", + flag: InputFlags::IXOFF, + show: true, + sane: false, + }, + Flag { + name: "tandem", + flag: InputFlags::IXOFF, + show: false, + sane: false, + }, + Flag { + name: "ixon", + flag: InputFlags::IXON, + show: true, + sane: false, + }, + // not supported by nix + // Flag { + // name: "iuclc", + // flag: InputFlags::IUCLC, + // show: true, + // default: false, + // }, + Flag { + name: "ixany", + flag: InputFlags::IXANY, + show: true, + sane: false, + }, + Flag { + name: "imaxbel", + flag: InputFlags::IMAXBEL, + show: true, + sane: true, + }, + Flag { + name: "iutf8", + flag: InputFlags::IUTF8, + show: true, + sane: false, + }, +]; + +pub const OUTPUT_FLAGS: [Flag; 8] = [ + Flag { + name: "opost", + flag: OutputFlags::OPOST, + show: true, + sane: true, + }, + Flag { + name: "olcuc", + flag: OutputFlags::OLCUC, + show: true, + sane: false, + }, + Flag { + name: "ocrnl", + flag: OutputFlags::OCRNL, + show: true, + sane: false, + }, + Flag { + name: "onlcr", + flag: OutputFlags::ONLCR, + show: true, + sane: true, + }, + Flag { + name: "onocr", + flag: OutputFlags::ONOCR, + show: true, + sane: false, + }, + Flag { + name: "onlret", + flag: OutputFlags::ONLRET, + show: true, + sane: false, + }, + Flag { + name: "ofill", + flag: OutputFlags::OFILL, + show: true, + sane: false, + }, + Flag { + name: "ofdel", + flag: OutputFlags::OFDEL, + show: true, + sane: false, + }, +]; + +pub const LOCAL_FLAGS: [Flag; 18] = [ + Flag { + name: "isig", + flag: LocalFlags::ISIG, + show: true, + sane: true, + }, + Flag { + name: "icanon", + flag: LocalFlags::ICANON, + show: true, + sane: true, + }, + Flag { + name: "iexten", + flag: LocalFlags::IEXTEN, + show: true, + sane: true, + }, + Flag { + name: "echo", + flag: LocalFlags::ECHO, + show: true, + sane: true, + }, + Flag { + name: "echoe", + flag: LocalFlags::ECHOE, + show: true, + sane: true, + }, + Flag { + name: "crterase", + flag: LocalFlags::ECHOE, + show: false, + sane: true, + }, + Flag { + name: "echok", + flag: LocalFlags::ECHOK, + show: true, + sane: true, + }, + Flag { + name: "echonl", + flag: LocalFlags::ECHONL, + show: true, + sane: false, + }, + Flag { + name: "noflsh", + flag: LocalFlags::NOFLSH, + show: true, + sane: false, + }, + // Not supported by nix + // Flag { + // name: "xcase", + // flag: LocalFlags::XCASE, + // show: true, + // sane: false, + // }, + Flag { + name: "tostop", + flag: LocalFlags::TOSTOP, + show: true, + sane: false, + }, + Flag { + name: "echoprt", + flag: LocalFlags::ECHOPRT, + show: true, + sane: false, + }, + Flag { + name: "prterase", + flag: LocalFlags::ECHOPRT, + show: false, + sane: false, + }, + Flag { + name: "echoctl", + flag: LocalFlags::ECHOCTL, + show: true, + sane: true, + }, + Flag { + name: "ctlecho", + flag: LocalFlags::ECHOCTL, + show: false, + sane: true, + }, + Flag { + name: "echoke", + flag: LocalFlags::ECHOKE, + show: true, + sane: true, + }, + Flag { + name: "crtkill", + flag: LocalFlags::ECHOKE, + show: false, + sane: true, + }, + Flag { + name: "flusho", + flag: LocalFlags::FLUSHO, + show: true, + sane: false, + }, + Flag { + name: "extproc", + flag: LocalFlags::EXTPROC, + show: true, + sane: false, + }, +]; 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..52cd5995b --- /dev/null +++ b/src/uu/stty/src/stty.rs @@ -0,0 +1,240 @@ +// * 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 + +mod flags; + +use clap::{crate_version, Arg, ArgMatches, Command}; +use nix::sys::termios::{ + tcgetattr, tcsetattr, ControlFlags, InputFlags, LocalFlags, OutputFlags, Termios, +}; +use std::io::{self, stdout}; +use std::os::unix::io::{AsRawFd, RawFd}; +use uucore::error::UResult; +use uucore::{format_usage, InvalidEncodingHandling}; + +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."; + +pub struct Flag { + name: &'static str, + flag: T, + show: bool, + sane: bool, +} + +trait TermiosFlag { + fn is_in(&self, termios: &Termios) -> 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()), + }) + } +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = args + .collect_str(InvalidEncodingHandling::ConvertLossy) + .accept_any(); + + let matches = uu_app().get_matches_from(args); + + let opts = Options::from(&matches)?; + + stty(&opts) +} + +fn stty(opts: &Options) -> UResult<()> { + // TODO: Figure out the right error message + let mut termios = tcgetattr(opts.file).expect("Could not get terminal attributes"); + if let Some(settings) = &opts.settings { + for setting in settings { + apply_setting(&mut termios, setting); + } + + tcsetattr(opts.file, nix::sys::termios::SetArg::TCSANOW, &termios) + .expect("Could not write terminal attributes"); + } else { + print_settings(&termios, opts); + } + Ok(()) +} + +fn print_settings(termios: &Termios, opts: &Options) { + if print_flags(termios, opts, &CONTROL_FLAGS) { + println!(); + } + if print_flags(termios, opts, &INPUT_FLAGS) { + println!(); + } + if print_flags(termios, opts, &OUTPUT_FLAGS) { + println!(); + } + if print_flags(termios, opts, &LOCAL_FLAGS) { + println!(); + } +} + +fn print_flags(termios: &Termios, opts: &Options, flags: &[Flag]) -> bool +where + Flag: TermiosFlag, +{ + let mut printed = false; + for flag in flags { + if !flag.show { + continue; + } + let val = flag.is_in(termios); + if opts.all || val != flag.sane { + if !val { + print!("-"); + } + print!("{} ", flag.name); + printed = true; + } + } + printed +} + +fn apply_setting(termios: &mut Termios, s: &str) -> Option<()> { + if let Some(()) = apply_flag(termios, &CONTROL_FLAGS, s) { + return Some(()); + } + if let Some(()) = apply_flag(termios, &INPUT_FLAGS, s) { + return Some(()); + } + if let Some(()) = apply_flag(termios, &OUTPUT_FLAGS, s) { + return Some(()); + } + if let Some(()) = apply_flag(termios, &LOCAL_FLAGS, s) { + return Some(()); + } + None +} + +fn apply_flag(termios: &mut Termios, flags: &[Flag], name: &str) -> Option<()> +where + T: Copy, + Flag: TermiosFlag, +{ + let (remove, name) = strip_hyphen(name); + find(flags, name)?.apply(termios, !remove); + Some(()) +} + +fn strip_hyphen(s: &str) -> (bool, &str) { + match s.strip_prefix('-') { + Some(s) => (true, s), + None => (false, s), + } +} + +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)) + .arg(Arg::new(options::SAVE).short('g').long(options::SAVE)) + .arg( + Arg::new(options::FILE) + .short('F') + .long(options::FILE) + .takes_value(true) + .value_hint(clap::ValueHint::FilePath), + ) + .arg( + Arg::new(options::SETTINGS) + .takes_value(true) + .multiple_values(true), + ) +} + +impl TermiosFlag for Flag { + fn is_in(&self, termios: &Termios) -> bool { + termios.control_flags.contains(self.flag) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.control_flags.set(self.flag, val) + } +} + +impl TermiosFlag for Flag { + fn is_in(&self, termios: &Termios) -> bool { + termios.input_flags.contains(self.flag) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.input_flags.set(self.flag, val) + } +} + +impl TermiosFlag for Flag { + fn is_in(&self, termios: &Termios) -> bool { + termios.output_flags.contains(self.flag) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.output_flags.set(self.flag, val) + } +} + +impl TermiosFlag for Flag { + fn is_in(&self, termios: &Termios) -> bool { + termios.local_flags.contains(self.flag) + } + + fn apply(&self, termios: &mut Termios, val: bool) { + termios.local_flags.set(self.flag, val) + } +} + +fn find<'a, T>(flags: &'a [Flag], flag_name: &str) -> Option<&'a Flag> +where + T: Copy, +{ + flags.iter().find_map(|flag| { + if flag.name == flag_name { + Some(flag) + } else { + None + } + }) +}