Skip to content

stty: set control characters #7931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

willshuttleworth
Copy link

Added functionality to set mappings between control characters (erase, intr, kill, etc.) and specific characters given by the user (^C, for example).

I had to rework the argument processing as it was previously processing one flag at a time, but this feature requires processing of consecutive args (ie stty erase ^C). Added string_to_control_char() to convert strings like "^C", "X", "0x7F" to their ASCII value if they are valid control char mappings. Also added apply_char_mapping() to set new control char mappings using termios.

I'm a relatively new Rust programmer, so I'd love feedback on things that could be written more idiomatically or concisely. This PR is related to issue #7357.

Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/misc/stdbuf (fails in this run but passes in the 'main' branch)

Comment on lines 221 to 239
match apply_char_mapping(&mut termios, char_index, new_cc) {
Ok(_) => {}
Err(e) => match e {
ControlCharMappingError::IntOutOfRange => {
return Err(USimpleError::new(
1,
format!(
"invalid integer argument: '{new_cc}': Numerical result out of range"
),
));
}
ControlCharMappingError::MultipleChars => {
return Err(USimpleError::new(
1,
format!("invalid integer argument: '{new_cc}'"),
));
}
},
}
Copy link
Contributor

Choose a reason for hiding this comment

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

As you only handle the error case, you can simplify it with map_err:

Suggested change
match apply_char_mapping(&mut termios, char_index, new_cc) {
Ok(_) => {}
Err(e) => match e {
ControlCharMappingError::IntOutOfRange => {
return Err(USimpleError::new(
1,
format!(
"invalid integer argument: '{new_cc}': Numerical result out of range"
),
));
}
ControlCharMappingError::MultipleChars => {
return Err(USimpleError::new(
1,
format!("invalid integer argument: '{new_cc}'"),
));
}
},
}
apply_char_mapping(&mut termios, char_index, new_cc).map_err(|e| {
let message = match e {
ControlCharMappingError::IntOutOfRange => format!(
"invalid integer argument: '{new_cc}': Numerical result out of range"
),
ControlCharMappingError::MultipleChars => {
format!("invalid integer argument: '{new_cc}'")
}
};
USimpleError::new(1, message)
})?;

@@ -274,6 +307,15 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
Ok(())
}

fn is_control_char(option: &str) -> Option<SpecialCharacterIndices> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function name is a bit misleading: functions starting with is_ usually return a bool.

Copy link
Author

Choose a reason for hiding this comment

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

initially i was returning a bool but then changed it to an option, good catch

@willshuttleworth willshuttleworth force-pushed the stty-set-control-chars branch from 9d0ca57 to 8a6e390 Compare May 15, 2025 14:08
Comment on lines 504 to 510
match string_to_control_char(new_val) {
Ok(val) => {
termios.control_chars[control_char_index as usize] = val;
Ok(())
}
Err(e) => Err(e),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can simplify it to something like:

Suggested change
match string_to_control_char(new_val) {
Ok(val) => {
termios.control_chars[control_char_index as usize] = val;
Ok(())
}
Err(e) => Err(e),
}
string_to_control_char(new_val).and_then(|val| {
termios.control_chars[control_char_index as usize] = val;
Ok(())
})

// c. decimal, no prefix
// 3. Disabling the control character: '^-' or 'undef'
//
// This function returns the ascii value of valid control chars, or None if the input is invalid
Copy link
Contributor

Choose a reason for hiding this comment

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

The function returns a ControlCharMappingError in the error case, not None.

Comment on lines 528 to 535
let mut ascii_num: Option<u32> = None;
if let Some(hex) = s.strip_prefix("0x") {
ascii_num = u32::from_str_radix(hex, 16).ok();
} else if let Some(octal) = s.strip_prefix("0") {
ascii_num = u32::from_str_radix(octal, 8).ok();
} else if let Ok(decimal) = s.parse::<u32>() {
ascii_num = Some(decimal);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

ascii_num doesn't have to be mutable. And you can get rid of the last if let:

Suggested change
let mut ascii_num: Option<u32> = None;
if let Some(hex) = s.strip_prefix("0x") {
ascii_num = u32::from_str_radix(hex, 16).ok();
} else if let Some(octal) = s.strip_prefix("0") {
ascii_num = u32::from_str_radix(octal, 8).ok();
} else if let Ok(decimal) = s.parse::<u32>() {
ascii_num = Some(decimal);
}
let ascii_num = if let Some(hex) = s.strip_prefix("0x") {
u32::from_str_radix(hex, 16).ok()
} else if let Some(octal) = s.strip_prefix("0") {
u32::from_str_radix(octal, 8).ok()
} else {
s.parse::<u32>().ok()
};

Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/misc/usage_vs_getopt (fails in this run but passes in the 'main' branch)
Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)

@willshuttleworth
Copy link
Author

anything else you'd like me to fix? i can push the final review commit now if we're all good

@cakebaker
Copy link
Contributor

@willshuttleworth from my side it looks good, maybe @tertsdiepraam has something to add as he wrote most of the current implementation.

@willshuttleworth
Copy link
Author

@cakebaker i have committed the final code review related changes

Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)

@sylvestre sylvestre force-pushed the stty-set-control-chars branch from b65870b to 23f7c1b Compare May 19, 2025 09:19
@sylvestre sylvestre force-pushed the stty-set-control-chars branch from 23f7c1b to fdbd248 Compare May 20, 2025 07:45
@sylvestre
Copy link
Contributor

it would be nice to have tests

@willshuttleworth
Copy link
Author

the one issue with testing is that cargo test doesnt run in a tty, so i would need to do some reworking to test properly. there are two main options i can imagine now. this first is reworking uumain() so that control char parsing is done before the call to tcgetattr() and i can test for argument combinations that fail. or, i could rework the way testing is done so that a separate tty process is spawned and used. this crate seems to accomplish that. the first option seems to be the easier and more pragmatic approach, but i could be ignorant of better options.

@sylvestre
Copy link
Contributor

that would be ideal
you could also start by testing at function level (like cc_to_index)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants