Skip to content

Commit dbfe7c5

Browse files
ChrisDryden3v1n0
andauthored
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) <[email protected]>
1 parent 7cce907 commit dbfe7c5

File tree

2 files changed

+185
-43
lines changed

2 files changed

+185
-43
lines changed

src/uu/stty/src/stty.rs

Lines changed: 128 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -542,8 +542,71 @@ fn print_special_setting(setting: &PrintSetting, fd: i32) -> nix::Result<()> {
542542
Ok(())
543543
}
544544

545-
fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
545+
/// Handles line wrapping for stty output to fit within terminal width
546+
struct WrappedPrinter {
547+
width: usize,
548+
current: usize,
549+
first_in_line: bool,
550+
}
551+
552+
impl WrappedPrinter {
553+
/// Creates a new printer with the specified terminal width.
554+
/// If term_size is None (typically when output is piped), falls back to
555+
/// the COLUMNS environment variable or a default width of 80 columns.
556+
fn new(term_size: Option<&TermSize>) -> Self {
557+
let columns = match term_size {
558+
Some(term_size) => term_size.columns,
559+
None => {
560+
const DEFAULT_TERM_WIDTH: u16 = 80;
561+
562+
std::env::var_os("COLUMNS")
563+
.and_then(|s| s.to_str()?.parse().ok())
564+
.filter(|&c| c > 0)
565+
.unwrap_or(DEFAULT_TERM_WIDTH)
566+
}
567+
};
568+
569+
Self {
570+
width: columns.max(1) as usize,
571+
current: 0,
572+
first_in_line: true,
573+
}
574+
}
575+
576+
fn print(&mut self, token: &str) {
577+
let token_len = self.prefix().chars().count() + token.chars().count();
578+
if self.current > 0 && self.current + token_len > self.width {
579+
println!();
580+
self.current = 0;
581+
self.first_in_line = true;
582+
}
583+
584+
print!("{}{}", self.prefix(), token);
585+
self.current += token_len;
586+
self.first_in_line = false;
587+
}
588+
589+
fn prefix(&self) -> &str {
590+
if self.first_in_line { "" } else { " " }
591+
}
592+
593+
fn flush(&mut self) {
594+
if self.current > 0 {
595+
println!();
596+
self.current = 0;
597+
self.first_in_line = false;
598+
}
599+
}
600+
}
601+
602+
fn print_terminal_size(
603+
termios: &Termios,
604+
opts: &Options,
605+
window_size: Option<&TermSize>,
606+
term_size: Option<&TermSize>,
607+
) -> nix::Result<()> {
546608
let speed = cfgetospeed(termios);
609+
let mut printer = WrappedPrinter::new(window_size);
547610

548611
// BSDs use a u32 for the baud rate, so we can simply print it.
549612
#[cfg(any(
@@ -554,7 +617,7 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
554617
target_os = "netbsd",
555618
target_os = "openbsd"
556619
))]
557-
print!("{} ", translate!("stty-output-speed", "speed" => speed));
620+
printer.print(&translate!("stty-output-speed", "speed" => speed));
558621

559622
// Other platforms need to use the baud rate enum, so printing the right value
560623
// becomes slightly more complicated.
@@ -568,17 +631,15 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
568631
)))]
569632
for (text, baud_rate) in BAUD_RATES {
570633
if *baud_rate == speed {
571-
print!("{} ", translate!("stty-output-speed", "speed" => (*text)));
634+
printer.print(&translate!("stty-output-speed", "speed" => (*text)));
572635
break;
573636
}
574637
}
575638

576639
if opts.all {
577-
let mut size = TermSize::default();
578-
unsafe { tiocgwinsz(opts.file.as_raw_fd(), &raw mut size)? };
579-
print!(
580-
"{} ",
581-
translate!("stty-output-rows-columns", "rows" => size.rows, "columns" => size.columns)
640+
let term_size = term_size.as_ref().expect("terminal size should be set");
641+
printer.print(
642+
&translate!("stty-output-rows-columns", "rows" => term_size.rows, "columns" => term_size.columns),
582643
);
583644
}
584645

@@ -588,10 +649,9 @@ fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> {
588649
// so we get the underlying libc::termios struct to get that information.
589650
let libc_termios: nix::libc::termios = termios.clone().into();
590651
let line = libc_termios.c_line;
591-
print!("{}", translate!("stty-output-line", "line" => line));
652+
printer.print(&translate!("stty-output-line", "line" => line));
592653
}
593-
594-
println!();
654+
printer.flush();
595655
Ok(())
596656
}
597657

@@ -759,39 +819,41 @@ fn control_char_to_string(cc: nix::libc::cc_t) -> nix::Result<String> {
759819
Ok(format!("{meta_prefix}{ctrl_prefix}{character}"))
760820
}
761821

762-
fn print_control_chars(termios: &Termios, opts: &Options) -> nix::Result<()> {
822+
fn print_control_chars(
823+
termios: &Termios,
824+
opts: &Options,
825+
term_size: Option<&TermSize>,
826+
) -> nix::Result<()> {
763827
if !opts.all {
764828
// Print only control chars that differ from sane defaults
765-
let mut printed = false;
829+
let mut printer = WrappedPrinter::new(term_size);
766830
for (text, cc_index) in CONTROL_CHARS {
767831
let current_val = termios.control_chars[*cc_index as usize];
768832
let sane_val = get_sane_control_char(*cc_index);
769833

770834
if current_val != sane_val {
771-
print!("{text} = {}; ", control_char_to_string(current_val)?);
772-
printed = true;
835+
printer.print(&format!(
836+
"{text} = {};",
837+
control_char_to_string(current_val)?
838+
));
773839
}
774840
}
775-
776-
if printed {
777-
println!();
778-
}
841+
printer.flush();
779842
return Ok(());
780843
}
781844

845+
let mut printer = WrappedPrinter::new(term_size);
782846
for (text, cc_index) in CONTROL_CHARS {
783-
print!(
784-
"{text} = {}; ",
847+
printer.print(&format!(
848+
"{text} = {};",
785849
control_char_to_string(termios.control_chars[*cc_index as usize])?
786-
);
850+
));
787851
}
788-
println!(
789-
"{}",
790-
translate!("stty-output-min-time",
852+
printer.print(&translate!("stty-output-min-time",
791853
"min" => termios.control_chars[S::VMIN as usize],
792854
"time" => termios.control_chars[S::VTIME as usize]
793-
)
794-
);
855+
));
856+
printer.flush();
795857
Ok(())
796858
}
797859

@@ -809,22 +871,48 @@ fn print_in_save_format(termios: &Termios) {
809871
println!();
810872
}
811873

874+
/// Gets terminal size using the tiocgwinsz ioctl system call.
875+
/// This queries the kernel for the current terminal window dimensions.
876+
fn get_terminal_size(fd: RawFd) -> nix::Result<TermSize> {
877+
let mut term_size = TermSize::default();
878+
unsafe { tiocgwinsz(fd, &raw mut term_size) }.map(|_| term_size)
879+
}
880+
812881
fn print_settings(termios: &Termios, opts: &Options) -> nix::Result<()> {
813882
if opts.save {
814883
print_in_save_format(termios);
815884
} else {
816-
print_terminal_size(termios, opts)?;
817-
print_control_chars(termios, opts)?;
818-
print_flags(termios, opts, CONTROL_FLAGS);
819-
print_flags(termios, opts, INPUT_FLAGS);
820-
print_flags(termios, opts, OUTPUT_FLAGS);
821-
print_flags(termios, opts, LOCAL_FLAGS);
885+
let device_fd = opts.file.as_raw_fd();
886+
let term_size = if opts.all {
887+
Some(get_terminal_size(device_fd)?)
888+
} else {
889+
get_terminal_size(device_fd).ok()
890+
};
891+
892+
let stdout_fd = stdout().as_raw_fd();
893+
let window_size = if device_fd == stdout_fd {
894+
&term_size
895+
} else {
896+
&get_terminal_size(stdout_fd).ok()
897+
};
898+
899+
print_terminal_size(termios, opts, window_size.as_ref(), term_size.as_ref())?;
900+
print_control_chars(termios, opts, window_size.as_ref())?;
901+
print_flags(termios, opts, CONTROL_FLAGS, window_size.as_ref());
902+
print_flags(termios, opts, INPUT_FLAGS, window_size.as_ref());
903+
print_flags(termios, opts, OUTPUT_FLAGS, window_size.as_ref());
904+
print_flags(termios, opts, LOCAL_FLAGS, window_size.as_ref());
822905
}
823906
Ok(())
824907
}
825908

826-
fn print_flags<T: TermiosFlag>(termios: &Termios, opts: &Options, flags: &[Flag<T>]) {
827-
let mut printed = false;
909+
fn print_flags<T: TermiosFlag>(
910+
termios: &Termios,
911+
opts: &Options,
912+
flags: &[Flag<T>],
913+
term_size: Option<&TermSize>,
914+
) {
915+
let mut printer = WrappedPrinter::new(term_size);
828916
for &Flag {
829917
name,
830918
flag,
@@ -839,20 +927,17 @@ fn print_flags<T: TermiosFlag>(termios: &Termios, opts: &Options, flags: &[Flag<
839927
let val = flag.is_in(termios, group);
840928
if group.is_some() {
841929
if val && (!sane || opts.all) {
842-
print!("{name} ");
843-
printed = true;
930+
printer.print(name);
844931
}
845932
} else if opts.all || val != sane {
846933
if !val {
847-
print!("-");
934+
printer.print(&format!("-{name}"));
935+
continue;
848936
}
849-
print!("{name} ");
850-
printed = true;
937+
printer.print(name);
851938
}
852939
}
853-
if printed {
854-
println!();
855-
}
940+
printer.flush();
856941
}
857942

858943
/// Apply a single setting

tests/by-util/test_stty.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,3 +556,60 @@ fn test_saved_state_with_control_chars() {
556556
.stderr_is(exp_result.stderr_str())
557557
.code_is(exp_result.code());
558558
}
559+
560+
#[test]
561+
#[cfg(unix)]
562+
fn test_columns_env_wrapping() {
563+
use std::process::Stdio;
564+
let (path, _controller, _replica) = pty_path();
565+
566+
// Must pipe output so stty uses COLUMNS env instead of actual terminal size
567+
for (columns, max_len) in [(20, 20), (40, 40), (50, 50)] {
568+
let result = new_ucmd!()
569+
.args(&["--all", "--file", &path])
570+
.env("COLUMNS", columns.to_string())
571+
.set_stdout(Stdio::piped())
572+
.succeeds();
573+
574+
for line in result.stdout_str().lines() {
575+
assert!(
576+
line.len() <= max_len,
577+
"Line exceeds COLUMNS={columns}: '{line}'"
578+
);
579+
}
580+
}
581+
582+
// Wide columns should allow longer lines
583+
let result = new_ucmd!()
584+
.args(&["--all", "--file", &path])
585+
.env("COLUMNS", "200")
586+
.set_stdout(Stdio::piped())
587+
.succeeds();
588+
let has_long_line = result.stdout_str().lines().any(|line| line.len() > 80);
589+
assert!(
590+
has_long_line,
591+
"Expected at least one line longer than 80 chars with COLUMNS=200"
592+
);
593+
594+
// Invalid values should fall back to default
595+
for invalid in ["invalid", "0", "-10"] {
596+
new_ucmd!()
597+
.args(&["--all", "--file", &path])
598+
.env("COLUMNS", invalid)
599+
.set_stdout(Stdio::piped())
600+
.succeeds();
601+
}
602+
603+
// Without --all flag
604+
let result = new_ucmd!()
605+
.args(&["--file", &path])
606+
.env("COLUMNS", "30")
607+
.set_stdout(Stdio::piped())
608+
.succeeds();
609+
for line in result.stdout_str().lines() {
610+
assert!(
611+
line.len() <= 30,
612+
"Line exceeds COLUMNS=30 without --all: '{line}'"
613+
);
614+
}
615+
}

0 commit comments

Comments
 (0)