Skip to content

Conversation

@willshuttleworth
Copy link
Contributor

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.

@github-actions
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 252
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)
})?;

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
Contributor 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()
};

@github-actions
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
Contributor 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
Contributor Author

@cakebaker i have committed the final code review related changes

@github-actions
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 2 times, most recently from 23f7c1b to fdbd248 Compare May 20, 2025 07:45
@sylvestre
Copy link
Contributor

it would be nice to have tests

@willshuttleworth
Copy link
Contributor 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)

@willshuttleworth willshuttleworth force-pushed the stty-set-control-chars branch from 550aa8b to 20134d2 Compare May 24, 2025 17:41
@willshuttleworth
Copy link
Contributor Author

ok, i have reworked the earlier changes so that some testing can be done. the new approach is to parse and collect all arguments, return errors if necessary, then apply the new flags. none of the tty related calls are made until parsing is complete, so errors during parsing can be tested for.

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tail/inotify-dir-recreate (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)

@github-actions
Copy link

GNU testsuite comparison:

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

@github-actions
Copy link

github-actions bot commented Jun 2, 2025

GNU testsuite comparison:

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

Comment on lines 256 to 271
let mut remove_group = false;
match flag {
AllFlags::Baud(_) => {}
AllFlags::ControlFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
AllFlags::InputFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
AllFlags::LocalFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
AllFlags::OutputFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

You can simplify the match to:

Suggested change
let mut remove_group = false;
match flag {
AllFlags::Baud(_) => {}
AllFlags::ControlFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
AllFlags::InputFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
AllFlags::LocalFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
AllFlags::OutputFlags((flag, remove)) => {
remove_group = check_flag_group(flag, remove);
}
}
let remove_group = match flag {
AllFlags::Baud(_) => false,
AllFlags::ControlFlags((flag, remove)) => check_flag_group(flag, remove),
AllFlags::InputFlags((flag, remove)) => check_flag_group(flag, remove),
AllFlags::LocalFlags((flag, remove)) => check_flag_group(flag, remove),
AllFlags::OutputFlags((flag, remove)) => check_flag_group(flag, remove),
};

Comment on lines 307 to 310
if remove && flag.group.is_some() {
return true;
}
false
Copy link
Contributor

Choose a reason for hiding this comment

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

You can simplify it to:

Suggested change
if remove && flag.group.is_some() {
return true;
}
false
remove && flag.group.is_some()

Comment on lines 401 to 404
let (remove, name) = match option.strip_prefix('-') {
Some(s) => (true, s),
None => (false, option),
};
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 this with something like:

let remove = option.starts_with('-');
let name = option.trim_start_matches('-');

This assumes that option doesn't start with multiple -. Otherwise you could use strip_prefix with unwrap_or_else.

@willshuttleworth willshuttleworth force-pushed the stty-set-control-chars branch from e1eee82 to 0bd75fa Compare June 3, 2025 13:46
@willshuttleworth
Copy link
Contributor Author

@cakebaker i have integrated the changes from your review

@github-actions
Copy link

github-actions bot commented Jun 3, 2025

GNU testsuite comparison:

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

@cakebaker cakebaker force-pushed the stty-set-control-chars branch from 0bd75fa to 7ce7b5c Compare June 5, 2025 08:40
@github-actions
Copy link

github-actions bot commented Jun 5, 2025

GNU testsuite comparison:

Skip an intermittent issue tests/misc/tee (fails in this run but passes in the 'main' branch)
Skip an intermittent issue tests/tail/inotify-dir-recreate (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/timeout/timeout (passes in this run but fails in the 'main' branch)

@cakebaker cakebaker merged commit 61bd11a into uutils:main Jun 5, 2025
74 checks passed
@cakebaker
Copy link
Contributor

Thanks!

@willshuttleworth
Copy link
Contributor Author

thank you for all the help :) also, we can close this issue now.

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