mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
stty: columns env support and integration testing (#9490)
* stty: Wrap parameters when using --all Do the same as GNU stty when it has to prints the parameter, doing proper text wrapping * Adding integration tests for the COLUMNS env variable support in stty * Adding integration tests for the COLUMNS env variable support in stty --------- Co-authored-by: Marco Trevisan (Treviño) <mail@3v1n0.net>
This commit is contained in:
co-authored by
Marco Trevisan
parent
7cce907c0e
commit
dbfe7c56ab
+128
-43
@@ -542,8 +542,71 @@ fn print_special_setting(setting: &PrintSetting, fd: i32) -> nix::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
|
||||
/// Handles line wrapping for stty output to fit within terminal width
|
||||
struct WrappedPrinter {
|
||||
width: usize,
|
||||
current: usize,
|
||||
first_in_line: bool,
|
||||
}
|
||||
|
||||
impl WrappedPrinter {
|
||||
/// Creates a new printer with the specified terminal width.
|
||||
/// If term_size is None (typically when output is piped), falls back to
|
||||
/// the COLUMNS environment variable or a default width of 80 columns.
|
||||
fn new(term_size: Option<&TermSize>) -> Self {
|
||||
let columns = match term_size {
|
||||
Some(term_size) => term_size.columns,
|
||||
None => {
|
||||
const DEFAULT_TERM_WIDTH: u16 = 80;
|
||||
|
||||
std::env::var_os("COLUMNS")
|
||||
.and_then(|s| s.to_str()?.parse().ok())
|
||||
.filter(|&c| c > 0)
|
||||
.unwrap_or(DEFAULT_TERM_WIDTH)
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
width: columns.max(1) as usize,
|
||||
current: 0,
|
||||
first_in_line: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn print(&mut self, token: &str) {
|
||||
let token_len = self.prefix().chars().count() + token.chars().count();
|
||||
if self.current > 0 && self.current + token_len > self.width {
|
||||
println!();
|
||||
self.current = 0;
|
||||
self.first_in_line = true;
|
||||
}
|
||||
|
||||
print!("{}{}", self.prefix(), token);
|
||||
self.current += token_len;
|
||||
self.first_in_line = false;
|
||||
}
|
||||
|
||||
fn prefix(&self) -> &str {
|
||||
if self.first_in_line { "" } else { " " }
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
if self.current > 0 {
|
||||
println!();
|
||||
self.current = 0;
|
||||
self.first_in_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_terminal_size(
|
||||
termios: &Termios,
|
||||
opts: &Options,
|
||||
window_size: Option<&TermSize>,
|
||||
term_size: Option<&TermSize>,
|
||||
) -> nix::Result<()> {
|
||||
let speed = cfgetospeed(termios);
|
||||
let mut printer = WrappedPrinter::new(window_size);
|
||||
|
||||
// BSDs use a u32 for the baud rate, so we can simply print it.
|
||||
#[cfg(any(
|
||||
@@ -554,7 +617,7 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
print!("{} ", translate!("stty-output-speed", "speed" => speed));
|
||||
printer.print(&translate!("stty-output-speed", "speed" => speed));
|
||||
|
||||
// Other platforms need to use the baud rate enum, so printing the right value
|
||||
// becomes slightly more complicated.
|
||||
@@ -568,17 +631,15 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
|
||||
)))]
|
||||
for (text, baud_rate) in BAUD_RATES {
|
||||
if *baud_rate == speed {
|
||||
print!("{} ", translate!("stty-output-speed", "speed" => (*text)));
|
||||
printer.print(&translate!("stty-output-speed", "speed" => (*text)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if opts.all {
|
||||
let mut size = TermSize::default();
|
||||
unsafe { tiocgwinsz(opts.file.as_raw_fd(), &raw mut size)? };
|
||||
print!(
|
||||
"{} ",
|
||||
translate!("stty-output-rows-columns", "rows" => size.rows, "columns" => size.columns)
|
||||
let term_size = term_size.as_ref().expect("terminal size should be set");
|
||||
printer.print(
|
||||
&translate!("stty-output-rows-columns", "rows" => term_size.rows, "columns" => term_size.columns),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -588,10 +649,9 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
|
||||
// so we get the underlying libc::termios struct to get that information.
|
||||
let libc_termios: nix::libc::termios = termios.clone().into();
|
||||
let line = libc_termios.c_line;
|
||||
print!("{}", translate!("stty-output-line", "line" => line));
|
||||
printer.print(&translate!("stty-output-line", "line" => line));
|
||||
}
|
||||
|
||||
println!();
|
||||
printer.flush();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -759,39 +819,41 @@ fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result<String> {
|
||||
Ok(format!("{meta_prefix}{ctrl_prefix}{character}"))
|
||||
}
|
||||
|
||||
fn print_control_chars(termios: &Termios, opts: &Options) -> nix::Result<()> {
|
||||
fn print_control_chars(
|
||||
termios: &Termios,
|
||||
opts: &Options,
|
||||
term_size: Option<&TermSize>,
|
||||
) -> nix::Result<()> {
|
||||
if !opts.all {
|
||||
// Print only control chars that differ from sane defaults
|
||||
let mut printed = false;
|
||||
let mut printer = WrappedPrinter::new(term_size);
|
||||
for (text, cc_index) in CONTROL_CHARS {
|
||||
let current_val = termios.control_chars[*cc_index as usize];
|
||||
let sane_val = get_sane_control_char(*cc_index);
|
||||
|
||||
if current_val != sane_val {
|
||||
print!("{text} = {}; ", control_char_to_string(current_val)?);
|
||||
printed = true;
|
||||
printer.print(&format!(
|
||||
"{text} = {};",
|
||||
control_char_to_string(current_val)?
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if printed {
|
||||
println!();
|
||||
}
|
||||
printer.flush();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut printer = WrappedPrinter::new(term_size);
|
||||
for (text, cc_index) in CONTROL_CHARS {
|
||||
print!(
|
||||
"{text} = {}; ",
|
||||
printer.print(&format!(
|
||||
"{text} = {};",
|
||||
control_char_to_string(termios.control_chars[*cc_index as usize])?
|
||||
);
|
||||
));
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
translate!("stty-output-min-time",
|
||||
printer.print(&translate!("stty-output-min-time",
|
||||
"min" => termios.control_chars[S::VMIN as usize],
|
||||
"time" => termios.control_chars[S::VTIME as usize]
|
||||
)
|
||||
);
|
||||
));
|
||||
printer.flush();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -809,22 +871,48 @@ fn print_in_save_format(termios: &Termios) {
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Gets terminal size using the tiocgwinsz ioctl system call.
|
||||
/// This queries the kernel for the current terminal window dimensions.
|
||||
fn get_terminal_size(fd: RawFd) -> nix::Result<TermSize> {
|
||||
let mut term_size = TermSize::default();
|
||||
unsafe { tiocgwinsz(fd, &raw mut term_size) }.map(|_| term_size)
|
||||
}
|
||||
|
||||
fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> {
|
||||
if opts.save {
|
||||
print_in_save_format(termios);
|
||||
} else {
|
||||
print_terminal_size(termios, opts)?;
|
||||
print_control_chars(termios, opts)?;
|
||||
print_flags(termios, opts, CONTROL_FLAGS);
|
||||
print_flags(termios, opts, INPUT_FLAGS);
|
||||
print_flags(termios, opts, OUTPUT_FLAGS);
|
||||
print_flags(termios, opts, LOCAL_FLAGS);
|
||||
let device_fd = opts.file.as_raw_fd();
|
||||
let term_size = if opts.all {
|
||||
Some(get_terminal_size(device_fd)?)
|
||||
} else {
|
||||
get_terminal_size(device_fd).ok()
|
||||
};
|
||||
|
||||
let stdout_fd = stdout().as_raw_fd();
|
||||
let window_size = if device_fd == stdout_fd {
|
||||
&term_size
|
||||
} else {
|
||||
&get_terminal_size(stdout_fd).ok()
|
||||
};
|
||||
|
||||
print_terminal_size(termios, opts, window_size.as_ref(), term_size.as_ref())?;
|
||||
print_control_chars(termios, opts, window_size.as_ref())?;
|
||||
print_flags(termios, opts, CONTROL_FLAGS, window_size.as_ref());
|
||||
print_flags(termios, opts, INPUT_FLAGS, window_size.as_ref());
|
||||
print_flags(termios, opts, OUTPUT_FLAGS, window_size.as_ref());
|
||||
print_flags(termios, opts, LOCAL_FLAGS, window_size.as_ref());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_flags<T: TermiosFlag>(termios: &Termios, opts: &Options, flags: &[Flag<T>]) {
|
||||
let mut printed = false;
|
||||
fn print_flags<T: TermiosFlag>(
|
||||
termios: &Termios,
|
||||
opts: &Options,
|
||||
flags: &[Flag<T>],
|
||||
term_size: Option<&TermSize>,
|
||||
) {
|
||||
let mut printer = WrappedPrinter::new(term_size);
|
||||
for &Flag {
|
||||
name,
|
||||
flag,
|
||||
@@ -839,20 +927,17 @@ fn print_flags<T: TermiosFlag>(termios: &Termios, opts: &Options, flags: &[Flag<
|
||||
let val = flag.is_in(termios, group);
|
||||
if group.is_some() {
|
||||
if val && (!sane || opts.all) {
|
||||
print!("{name} ");
|
||||
printed = true;
|
||||
printer.print(name);
|
||||
}
|
||||
} else if opts.all || val != sane {
|
||||
if !val {
|
||||
print!("-");
|
||||
printer.print(&format!("-{name}"));
|
||||
continue;
|
||||
}
|
||||
print!("{name} ");
|
||||
printed = true;
|
||||
printer.print(name);
|
||||
}
|
||||
}
|
||||
if printed {
|
||||
println!();
|
||||
}
|
||||
printer.flush();
|
||||
}
|
||||
|
||||
/// Apply a single setting
|
||||
|
||||
@@ -556,3 +556,60 @@ fn test_saved_state_with_control_chars() {
|
||||
.stderr_is(exp_result.stderr_str())
|
||||
.code_is(exp_result.code());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_columns_env_wrapping() {
|
||||
use std::process::Stdio;
|
||||
let (path, _controller, _replica) = pty_path();
|
||||
|
||||
// Must pipe output so stty uses COLUMNS env instead of actual terminal size
|
||||
for (columns, max_len) in [(20, 20), (40, 40), (50, 50)] {
|
||||
let result = new_ucmd!()
|
||||
.args(&["--all", "--file", &path])
|
||||
.env("COLUMNS", columns.to_string())
|
||||
.set_stdout(Stdio::piped())
|
||||
.succeeds();
|
||||
|
||||
for line in result.stdout_str().lines() {
|
||||
assert!(
|
||||
line.len() <= max_len,
|
||||
"Line exceeds COLUMNS={columns}: '{line}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wide columns should allow longer lines
|
||||
let result = new_ucmd!()
|
||||
.args(&["--all", "--file", &path])
|
||||
.env("COLUMNS", "200")
|
||||
.set_stdout(Stdio::piped())
|
||||
.succeeds();
|
||||
let has_long_line = result.stdout_str().lines().any(|line| line.len() > 80);
|
||||
assert!(
|
||||
has_long_line,
|
||||
"Expected at least one line longer than 80 chars with COLUMNS=200"
|
||||
);
|
||||
|
||||
// Invalid values should fall back to default
|
||||
for invalid in ["invalid", "0", "-10"] {
|
||||
new_ucmd!()
|
||||
.args(&["--all", "--file", &path])
|
||||
.env("COLUMNS", invalid)
|
||||
.set_stdout(Stdio::piped())
|
||||
.succeeds();
|
||||
}
|
||||
|
||||
// Without --all flag
|
||||
let result = new_ucmd!()
|
||||
.args(&["--file", &path])
|
||||
.env("COLUMNS", "30")
|
||||
.set_stdout(Stdio::piped())
|
||||
.succeeds();
|
||||
for line in result.stdout_str().lines() {
|
||||
assert!(
|
||||
line.len() <= 30,
|
||||
"Line exceeds COLUMNS=30 without --all: '{line}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user