Skip to content
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0dd8afb
feat(stty): support restoring settings from -g save format
mattsu2020 Nov 12, 2025
7e19f74
refactor(stty): simplify error types and iterator usage
mattsu2020 Nov 12, 2025
2485911
fix(stty): align error types and argument handling
mattsu2020 Nov 12, 2025
6c1d363
refactor(stty): simplify error helpers and missing arg handling
mattsu2020 Nov 12, 2025
8ccd314
fix(stty): correct block indentation for missing_arg handling
mattsu2020 Nov 12, 2025
17abac4
fix(stty): correctly parse and apply 64-bit termios flag bits
mattsu2020 Nov 12, 2025
8c84686
refactor(stty): remove redundant casts in parse_save_format
mattsu2020 Nov 12, 2025
7fd9246
fix(stty): ensure flag bit masks are converted before truncation
mattsu2020 Nov 12, 2025
767a74f
fix(stty): parse bitflags using tcflag_t for save format
mattsu2020 Nov 12, 2025
1754286
chore(stty): update spell-checker ignores for new flag terms
mattsu2020 Nov 12, 2025
582470a
chore(stty): extend spell-check ignore list for cflags
mattsu2020 Nov 13, 2025
6d1f375
fix(stty): distinguish invalid options from missing args
mattsu2020 Nov 13, 2025
0afdf35
fix(stty): align unknown arg handling with project expectations
mattsu2020 Nov 13, 2025
4f4ec2c
Merge branch 'uutils:main' into stty_fix
mattsu2020 Nov 13, 2025
24fa079
refactor(stty): optimize termios fetching and fix save format parsing
mattsu2020 Nov 21, 2025
7aed8d4
refactor(stty): translate Japanese comments to English
mattsu2020 Nov 21, 2025
5d30e89
refactor(stty): lazily fetch and cache base termios to optimize perfo…
mattsu2020 Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 108 additions & 20 deletions src/uu/stty/src/stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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 tcsadrain
// spell-checker:ignore cbreak decctlq evenp litout oddp tcsadrain tcflag bitflag lflags cflags

mod flags;

Expand Down Expand Up @@ -211,7 +211,7 @@ impl<'a> Options<'a> {
},
settings: matches
.get_many::<String>(options::SETTINGS)
.map(|v| v.map(|s| s.as_ref()).collect()),
.map(|v| v.map(String::as_str).collect()),
})
}
}
Expand Down Expand Up @@ -264,12 +264,47 @@ fn stty(opts: &Options) -> UResult<()> {
));
}

// termios を最初に必要になるまで遅延取得し、1 度だけキャッシュする
let mut base_termios: Option<Termios> = None;
let mut get_base_termios = |fd: BorrowedFd<'_>| -> nix::Result<Termios> {
if let Some(ref termios) = base_termios {
Ok(termios.clone())
} else {
let termios = tcgetattr(fd)?;
base_termios = Some(termios.clone());
Ok(termios)
}
};

let mut set_arg = SetArg::TCSADRAIN;
let mut valid_args: Vec<ArgOptions> = Vec::new();

// GNU compatible: If the first argument is a -g save format (hexadecimal colon-separated),
// restore termios from it before applying the remaining arguments.
let mut restored_from_save: Option<Termios> = None;
let mut settings_iter: Box<dyn Iterator<Item = &str>> = Box::new([].iter().copied());

if let Some(args) = &opts.settings {
let mut args_iter = args.iter();
while let Some(&arg) = args_iter.next() {
if let Some((first, rest)) = args.split_first() {
let base = get_base_termios(opts.file.as_fd())?;
let mut restored = base.clone();
match parse_save_format(first, &mut restored) {
Ok(()) => {
restored_from_save = Some(restored);
settings_iter = Box::new(rest.iter().copied());
}
// GNU stty errors immediately when the first argument looks like save format but cannot be parsed
Err(e) if first.contains(':') => return Err(e),
Err(_) => {
settings_iter = Box::new(args.iter().map(|s| &**s));
}
}
}
}

if restored_from_save.is_some() || opts.settings.is_some() {
let mut args_iter = settings_iter;
while let Some(arg) = args_iter.next() {
match arg {
"ispeed" | "ospeed" => match args_iter.next() {
Some(speed) => {
Expand Down Expand Up @@ -396,15 +431,21 @@ fn stty(opts: &Options) -> UResult<()> {
// combination setting
} else if let Some(combo) = string_to_combo(arg) {
valid_args.append(&mut combo_to_flags(combo));
} else if arg.starts_with('-') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhlhl, this implies that doing stty foo bar would pass, until we fail parsing... While to me we should just fail as GNU does in that case with stty: invalid argument 'foo'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Let's do that.

// For unknown options beginning with '-', treat them as invalid arguments.
return invalid_arg(arg);
} else {
// For bare words that are not recognized as flags, baud rates,
// control chars, or combinations, treat them as invalid arguments
// to match project test expectations.
return invalid_arg(arg);
}
}
}
}

// TODO: Figure out the right error message for when tcgetattr fails
let mut termios = tcgetattr(opts.file.as_fd())?;
let base = get_base_termios(opts.file.as_fd())?;
let mut termios = restored_from_save.unwrap_or(base);

// iterate over valid_args, match on the arg type, do the matching apply function
for arg in &valid_args {
Expand All @@ -421,35 +462,28 @@ fn stty(opts: &Options) -> UResult<()> {
}
tcsetattr(opts.file.as_fd(), set_arg, &termios)?;
} else {
// TODO: Figure out the right error message for when tcgetattr fails
let termios = tcgetattr(opts.file.as_fd())?;
print_settings(&termios, opts)?;
let base = get_base_termios(opts.file.as_fd())?;
print_settings(&base, opts)?;
}
Ok(())
}

fn missing_arg<T>(arg: &str) -> Result<T, Box<dyn UError>> {
Err::<T, Box<dyn UError>>(USimpleError::new(
Err(USimpleError::new(
1,
translate!(
"stty-error-missing-argument",
"arg" => *arg
),
translate!("stty-error-missing-argument", "arg" => arg.to_string()),
))
}

fn invalid_arg<T>(arg: &str) -> Result<T, Box<dyn UError>> {
Err::<T, Box<dyn UError>>(USimpleError::new(
Err(USimpleError::new(
1,
translate!(
"stty-error-invalid-argument",
"arg" => *arg
),
translate!("stty-error-invalid-argument", "arg" => arg.to_string()),
))
}

fn invalid_integer_arg<T>(arg: &str) -> Result<T, Box<dyn UError>> {
Err::<T, Box<dyn UError>>(USimpleError::new(
Err(USimpleError::new(
1,
translate!(
"stty-error-invalid-integer-argument",
Expand Down Expand Up @@ -697,6 +731,60 @@ fn print_in_save_format(termios: &Termios) {
println!();
}

/// GNU stty -g compatibility: restore Termios from the colon-separated hexadecimal representation
/// produced by print_in_save_format. Caller clones before passing and this function overwrites it.
fn parse_save_format(s: &str, termios: &mut Termios) -> Result<(), Box<dyn UError>> {
// Expect four flag values + a variable-length sequence of cc_t values (at least one).
let parts: Vec<&str> = s.split(':').collect();
if parts.len() < 5 {
return Err(USimpleError::new(
1,
translate!("stty-error-invalid-argument", "arg" => s.to_string()),
));
}

// Parse a hex string into tcflag_t (shared helper) to match the underlying bitflag type.
fn parse_hex_tcflag(x: &str, original: &str) -> Result<nix::libc::tcflag_t, Box<dyn UError>> {
nix::libc::tcflag_t::from_str_radix(x, 16).map_err(|_| {
USimpleError::new(
1,
translate!("stty-error-invalid-argument", "arg" => original.to_string()),
)
})
}

let iflags_bits = parse_hex_tcflag(parts[0], s)?;
let oflags_bits = parse_hex_tcflag(parts[1], s)?;
let cflags_bits = parse_hex_tcflag(parts[2], s)?;
let lflags_bits = parse_hex_tcflag(parts[3], s)?;

// Remaining segments are control_chars.
let cc_hex = &parts[4..];

termios.input_flags = InputFlags::from_bits_truncate(iflags_bits);
termios.output_flags = OutputFlags::from_bits_truncate(oflags_bits);
termios.control_flags = ControlFlags::from_bits_truncate(cflags_bits);
termios.local_flags = LocalFlags::from_bits_truncate(lflags_bits);

// The length of control_chars depends on the runtime environment; fill only up to the
// length of the local array.
let max_cc = termios.control_chars.len();
for (i, &hex) in cc_hex.iter().enumerate() {
if i >= max_cc {
break;
}
let val = u32::from_str_radix(hex, 16).map_err(|_| {
USimpleError::new(
1,
translate!("stty-error-invalid-argument", "arg" => s.to_string()),
)
})?;
termios.control_chars[i] = (val & 0xff) as u8;
}

Ok(())
}

fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> {
if opts.save {
print_in_save_format(termios);
Expand Down
Loading