stty: add combination settings

This commit is contained in:
Will Shuttleworth
2025-07-10 08:19:27 -04:00
parent 745ae4b10f
commit 93fc669258
3 changed files with 185 additions and 12 deletions
+21
View File
@@ -9,6 +9,7 @@
// 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 sigquit sigtstp
// spell-checker:ignore cbreak decctlq evenp litout oddp
use crate::Flag;
@@ -365,3 +366,23 @@ pub const CONTROL_CHARS: &[(&str, S)] = &[
// Discards the current line.
("discard", S::VDISCARD),
];
/// This constant lists all possible combination settings, using a bool to represent if the setting is negatable
pub const COMBINATION_SETTINGS: &[(&str, bool)] = &[
("LCASE", true),
("lcase", true),
("cbreak", true),
("cooked", true),
("crt", false),
("dec", false),
("decctlq", true),
("ek", false),
("evenp", true),
("litout", true),
("nl", true),
("oddp", true),
("parity", true),
("pass8", true),
("raw", true),
("sane", false),
];
+148 -12
View File
@@ -4,16 +4,23 @@
// file that was distributed with this source code.
// spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime cflag lflag ispeed ospeed
// spell-checker:ignore tcsadrain
// 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 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 sigquit sigtstp
// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain
mod flags;
use crate::flags::AllFlags;
use crate::flags::COMBINATION_SETTINGS;
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, Termios,
cfgetospeed, cfsetospeed, tcgetattr, tcsetattr,
ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, SpecialCharacterIndices as S,
Termios, cfgetospeed, cfsetospeed, tcgetattr, tcsetattr,
};
use nix::{ioctl_read_bad, ioctl_write_ptr_bad};
use std::collections::HashMap;
@@ -106,6 +113,7 @@ enum Device {
Stdout(Stdout),
}
#[derive(Debug)]
enum ControlCharMappingError {
IntOutOfRange(String),
MultipleChars(String),
@@ -123,7 +131,7 @@ enum PrintSetting {
enum ArgOptions<'a> {
Flags(AllFlags<'a>),
Mapping((SpecialCharacterIndices, u8)),
Mapping((S, u8)),
Special(SpecialSetting),
Print(PrintSetting),
}
@@ -323,8 +331,7 @@ fn stty(opts: &Options) -> UResult<()> {
match args_iter.next() {
Some(min) => match parse_u8_or_err(min) {
Ok(n) => {
valid_args
.push(ArgOptions::Mapping((SpecialCharacterIndices::VMIN, n)));
valid_args.push(ArgOptions::Mapping((S::VMIN, n)));
}
Err(e) => return Err(USimpleError::new(1, e)),
},
@@ -341,8 +348,7 @@ fn stty(opts: &Options) -> UResult<()> {
} else if arg == "time" {
match args_iter.next() {
Some(time) => match parse_u8_or_err(time) {
Ok(n) => valid_args
.push(ArgOptions::Mapping((SpecialCharacterIndices::VTIME, n))),
Ok(n) => valid_args.push(ArgOptions::Mapping((S::VTIME, n))),
Err(e) => return Err(USimpleError::new(1, e)),
},
None => {
@@ -377,6 +383,9 @@ fn stty(opts: &Options) -> UResult<()> {
));
}
valid_args.push(flag.into());
// combination setting
} else if let Some(combo) = string_to_combo(arg) {
valid_args.append(&mut combo_to_flags(combo));
} else if arg == "rows" {
if let Some(rows) = args_iter.next() {
if let Some(n) = parse_rows_cols(rows) {
@@ -581,7 +590,7 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
Ok(())
}
fn cc_to_index(option: &str) -> Option<SpecialCharacterIndices> {
fn cc_to_index(option: &str) -> Option<S> {
for cc in CONTROL_CHARS {
if option == cc.0 {
return Some(cc.1);
@@ -590,6 +599,15 @@ fn cc_to_index(option: &str) -> Option<SpecialCharacterIndices> {
None
}
fn string_to_combo(arg: &str) -> Option<&str> {
let is_negated = arg.starts_with('-');
let name = arg.trim_start_matches('-');
COMBINATION_SETTINGS
.iter()
.find(|&&(combo_name, is_negatable)| name == combo_name && (!is_negated || is_negatable))
.map(|_| arg)
}
fn string_to_baud(arg: &str) -> Option<AllFlags> {
// BSDs use a u32 for the baud rate, so any decimal number applies.
#[cfg(any(
@@ -693,11 +711,11 @@ fn print_control_chars(termios: &Termios, opts: &Options) -> nix::Result<()> {
HashMap::from([
(
"min".to_string(),
termios.control_chars[SpecialCharacterIndices::VMIN as usize].to_string()
termios.control_chars[S::VMIN as usize].to_string()
),
(
"time".to_string(),
termios.control_chars[SpecialCharacterIndices::VTIME as usize].to_string()
termios.control_chars[S::VTIME as usize].to_string()
)
])
)
@@ -812,7 +830,7 @@ fn apply_baud_rate_flag(termios: &mut Termios, input: &AllFlags) {
}
}
fn apply_char_mapping(termios: &mut Termios, mapping: &(SpecialCharacterIndices, u8)) {
fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) {
termios.control_chars[mapping.0 as usize] = mapping.1;
}
@@ -889,6 +907,124 @@ fn string_to_control_char(s: &str) -> Result<u8, ControlCharMappingError> {
}
}
// decomposes a combination argument into a vec of corresponding flags
fn combo_to_flags(combo: &str) -> Vec<ArgOptions> {
let mut flags = Vec::new();
let mut ccs = Vec::new();
match combo {
"lcase" | "LCASE" => {
flags = vec!["xcase", "iuclc", "olcuc"];
}
"-lcase" | "-LCASE" => {
flags = vec!["-xcase", "-iuclc", "-olcuc"];
}
"cbreak" => {
flags = vec!["-icanon"];
}
"-cbreak" => {
flags = vec!["icanon"];
}
"cooked" | "-raw" => {
flags = vec![
"brkint", "ignpar", "istrip", "icrnl", "ixon", "opost", "isig", "icanon",
];
ccs = vec![(S::VEOF, "^D"), (S::VEOL, "")];
}
"crt" => {
flags = vec!["echoe", "echoctl", "echoke"];
}
"dec" => {
flags = vec!["echoe", "echoctl", "echoke", "-ixany"];
ccs = vec![(S::VINTR, "^C"), (S::VERASE, "^?"), (S::VKILL, "^U")];
}
"decctlq" => {
flags = vec!["ixany"];
}
"-decctlq" => {
flags = vec!["-ixany"];
}
"ek" => {
ccs = vec![(S::VERASE, "^?"), (S::VKILL, "^U")];
}
"evenp" | "parity" => {
flags = vec!["parenb", "-parodd", "cs7"];
}
"-evenp" | "-parity" => {
flags = vec!["-parenb", "cs8"];
}
"litout" => {
flags = vec!["-parenb", "-istrip", "-opost", "cs8"];
}
"-litout" => {
flags = vec!["parenb", "istrip", "opost", "cs7"];
}
"nl" => {
flags = vec!["-icrnl", "-onlcr"];
}
"-nl" => {
flags = vec!["icrnl", "-inlcr", "-igncr", "onlcr", "-ocrnl", "-onlret"];
}
"oddp" => {
flags = vec!["parenb", "parodd", "cs7"];
}
"-oddp" => {
flags = vec!["-parenb", "cs8"];
}
"pass8" => {
flags = vec!["-parenb", "-istrip", "cs8"];
}
"-pass8" => {
flags = vec!["parenb", "istrip", "cs7"];
}
"raw" | "-cooked" => {
flags = vec![
"-ignbrk", "-brkint", "-ignpar", "-parmrk", "-inpck", "-istrip", "-inlcr",
"-igncr", "-icrnl", "-ixon", "-ixoff", "-icanon", "-opost", "-isig", "-iuclc",
"-xcase", "-ixany", "-imaxbel",
];
// TODO: add 'min 1' and 'time 0' settings here after they have been implemented
}
"sane" => {
flags = vec![
"cread", "-ignbrk", "brkint", "-inlcr", "-igncr", "icrnl", "icanon", "iexten",
"echo", "echoe", "echok", "-echonl", "-noflsh", "-ixoff", "-iutf8", "-iuclc",
"-xcase", "-ixany", "imaxbel", "-olcuc", "-ocrnl", "opost", "-ofill", "onlcr",
"-onocr", "-onlret", "nl0", "cr0", "tab0", "bs0", "vt0", "ff0", "isig", "-tostop",
"-ofdel", "-echoprt", "echoctl", "echoke", "-extproc", "-flusho",
];
ccs = vec![
(S::VINTR, "^C"),
(S::VQUIT, "^\\"),
(S::VERASE, "^?"),
(S::VKILL, "^U"),
(S::VEOF, "^D"),
(S::VEOL, ""),
(S::VEOL2, ""),
#[cfg(target_os = "linux")]
(S::VSWTC, ""),
(S::VSTART, "^Q"),
(S::VSTOP, "^S"),
(S::VSUSP, "^Z"),
(S::VREPRINT, "^R"),
(S::VWERASE, "^W"),
(S::VLNEXT, "^V"),
(S::VDISCARD, "^O"),
];
}
_ => unreachable!("invalid combination setting: must have been caught earlier"),
}
let mut flags = flags
.iter()
.filter_map(|f| string_to_flag(f).map(ArgOptions::Flags))
.collect::<Vec<ArgOptions>>();
let mut ccs = ccs
.iter()
.map(|cc| ArgOptions::Mapping((cc.0, string_to_control_char(cc.1).unwrap())))
.collect::<Vec<ArgOptions>>();
flags.append(&mut ccs);
flags
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(uucore::crate_version!())
+16
View File
@@ -293,3 +293,19 @@ fn min_and_time() {
.fails()
.stderr_contains("invalid integer argument: '256': Value too large for defined data type");
}
#[test]
fn non_negatable_combo() {
new_ucmd!()
.args(&["-dec"])
.fails()
.stderr_contains("invalid argument '-dec'");
new_ucmd!()
.args(&["-crt"])
.fails()
.stderr_contains("invalid argument '-crt'");
new_ucmd!()
.args(&["-ek"])
.fails()
.stderr_contains("invalid argument '-ek'");
}