Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
152fff5
Implemented saved state parser for stty
ChrisDryden Nov 25, 2025
7e592b9
Add compatibility to macos flag type
ChrisDryden Nov 25, 2025
6dd1b00
Added many example state parsing integration tests with GNU compatibi…
ChrisDryden Nov 25, 2025
a2c519e
Spelling and formatting fixes
ChrisDryden Nov 25, 2025
9039c3b
Matching behaviour of adding the help command after invocations and s…
ChrisDryden Nov 25, 2025
c89b03c
GNU tests were being skipped because they were not at the sufficient …
ChrisDryden Nov 25, 2025
b64342c
Fixed messaging error for invalid states to not show full path
ChrisDryden Nov 25, 2025
cb9320b
Normalizing the test output and reverting lib change
ChrisDryden Nov 25, 2025
7a3b317
Discovered that the limit depends on platform specific values derived…
ChrisDryden Nov 25, 2025
5aabaff
Spelling fixes and setting flags to 0 for cross platform compatibility
ChrisDryden Nov 26, 2025
0ad8ea1
Clippy fixes
ChrisDryden Nov 26, 2025
7abc1ce
Disabling tests due to invalid printing of control chars and using GN…
ChrisDryden Nov 26, 2025
b533d0d
Redisabling failing test as outside of the scope of this PR
ChrisDryden Nov 26, 2025
10b049a
Adding g prefix support to normalize stderr
ChrisDryden Nov 26, 2025
8180a86
Spell checker fixes
ChrisDryden Nov 26, 2025
9225590
Normalizing command for both gnu and uutils output
ChrisDryden Nov 26, 2025
d018f87
removing single value from testing since it can be interpreted as Bau…
ChrisDryden Nov 26, 2025
c571552
Fixing spelling mistake
ChrisDryden Nov 26, 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
50 changes: 49 additions & 1 deletion src/uu/stty/src/stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ enum ArgOptions<'a> {
Mapping((S, u8)),
Special(SpecialSetting),
Print(PrintSetting),
SavedState(Vec<u32>),
}

impl<'a> From<AllFlags<'a>> for ArgOptions<'a> {
Expand Down Expand Up @@ -352,8 +353,12 @@ fn stty(opts: &Options) -> UResult<()> {
valid_args.push(ArgOptions::Print(PrintSetting::Size));
}
_ => {
// Try to parse saved format (hex string like "6d02:5:4bf:8a3b:...")
if let Some(state) = parse_saved_state(arg) {
valid_args.push(ArgOptions::SavedState(state));
}
// control char
if let Some(char_index) = cc_to_index(arg) {
else if let Some(char_index) = cc_to_index(arg) {
if let Some(mapping) = args_iter.next() {
let cc_mapping = string_to_control_char(mapping).map_err(|e| {
let message = match e {
Expand Down Expand Up @@ -418,6 +423,9 @@ fn stty(opts: &Options) -> UResult<()> {
ArgOptions::Print(setting) => {
print_special_setting(setting, opts.file.as_raw_fd())?;
}
ArgOptions::SavedState(state) => {
apply_saved_state(&mut termios, state)?;
}
}
}
tcsetattr(opts.file.as_fd(), set_arg, &termios)?;
Expand Down Expand Up @@ -478,6 +486,29 @@ fn parse_rows_cols(arg: &str) -> Option<u16> {
None
}

fn parse_saved_state(arg: &str) -> Option<Vec<u32>> {
let parts: Vec<&str> = arg.split(':').collect();

if parts.len() < 4 {
return None;
}

let is_valid_hex = |s: &&str| s.is_empty() || u32::from_str_radix(s, 16).is_ok();
if !parts.iter().all(is_valid_hex) {
return None;
}

let parse_hex = |part: &&str| {
if part.is_empty() {
0
} else {
u32::from_str_radix(part, 16).unwrap()
}
};

Some(parts.iter().map(parse_hex).collect())
}

fn check_flag_group<T>(flag: &Flag<T>, remove: bool) -> bool {
remove && flag.group.is_some()
}
Expand Down Expand Up @@ -857,6 +888,23 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) {
termios.control_chars[mapping.0 as usize] = mapping.1;
}

fn apply_saved_state(termios: &mut Termios, state: &[u32]) -> nix::Result<()> {
if state.len() >= 4 {
termios.input_flags = InputFlags::from_bits_truncate(state[0] as _);
termios.output_flags = OutputFlags::from_bits_truncate(state[1] as _);
termios.control_flags = ControlFlags::from_bits_truncate(state[2] as _);
termios.local_flags = LocalFlags::from_bits_truncate(state[3] as _);

// Apply control characters (stored as u32 but used as u8)
for (i, &cc_val) in state.iter().skip(4).enumerate() {
if i < termios.control_chars.len() {
termios.control_chars[i] = cc_val as u8;
}
}
}
Ok(())
}

fn apply_special_setting(
_termios: &mut Termios,
setting: &SpecialSetting,
Expand Down
53 changes: 53 additions & 0 deletions tests/by-util/test_stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,56 @@ fn non_negatable_combo() {
.fails()
.stderr_contains("invalid argument '-ek'");
}

#[test]
#[cfg(unix)]
fn test_save_and_restore() {
let (path, _controller, _replica) = pty_path();
let saved = new_ucmd!()
.args(&["--save", "--file", &path])
.succeeds()
.stdout_move_str();

let saved = saved.trim();
assert!(saved.contains(':'));

new_ucmd!().args(&["--file", &path, saved]).succeeds();
}

#[test]
#[cfg(unix)]
fn test_save_with_g_flag() {
let (path, _controller, _replica) = pty_path();
let saved = new_ucmd!()
.args(&["-g", "--file", &path])
.succeeds()
.stdout_move_str();

let saved = saved.trim();
assert!(saved.contains(':'));

new_ucmd!().args(&["--file", &path, saved]).succeeds();
}

#[test]
#[cfg(unix)]
fn test_save_restore_after_change() {
let (path, _controller, _replica) = pty_path();
let saved = new_ucmd!()
.args(&["--save", "--file", &path])
.succeeds()
.stdout_move_str();

let saved = saved.trim();

new_ucmd!()
.args(&["--file", &path, "intr", "^A"])
.succeeds();

new_ucmd!().args(&["--file", &path, saved]).succeeds();

new_ucmd!()
.args(&["--file", &path])
.succeeds()
.stdout_str_check(|s| !s.contains("intr = ^A"));
}
Loading