diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index a935eef54..455b4f7b6 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -15,7 +15,6 @@ extern crate uucore; use clap::{App, Arg}; use number_prefix::NumberPrefix; -use std::cmp::Reverse; #[cfg(unix)] use std::collections::HashMap; use std::fs; @@ -30,6 +29,7 @@ use std::path::{Path, PathBuf}; #[cfg(unix)] use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; +use std::{cmp::Reverse, process::exit}; use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use time::{strftime, Timespec}; @@ -78,6 +78,8 @@ pub mod options { pub static ONELINE: &str = "1"; pub static LONG: &str = "long"; pub static COLUMNS: &str = "C"; + pub static ACROSS: &str = "x"; + pub static COMMAS: &str = "m"; pub static LONG_NO_OWNER: &str = "g"; pub static LONG_NO_GROUP: &str = "o"; } @@ -98,6 +100,7 @@ pub mod options { pub static HUMAN_READABLE: &str = "human-readable"; pub static SI: &str = "si"; } + pub static WIDTH: &str = "width"; pub static AUTHOR: &str = "author"; pub static NO_GROUP: &str = "no-group"; pub static FORMAT: &str = "format"; @@ -120,6 +123,8 @@ enum Format { Columns, Long, OneLine, + Across, + Commas, } enum Sort { @@ -166,6 +171,7 @@ struct Config { #[cfg(unix)] color: bool, long: LongFormat, + width: Option, } // Fields that can be removed or added to the long format @@ -183,6 +189,8 @@ impl Config { "long" | "verbose" => Format::Long, "single-column" => Format::OneLine, "columns" | "vertical" => Format::Columns, + "across" | "horizontal" => Format::Across, + "commas" => Format::Commas, // below should never happen as clap already restricts the values. _ => unreachable!("Invalid field for --format"), }, @@ -190,6 +198,10 @@ impl Config { ) } else if options.is_present(options::format::LONG) { (Format::Long, options::format::LONG) + } else if options.is_present(options::format::ACROSS) { + (Format::Across, options::format::ACROSS) + } else if options.is_present(options::format::COMMAS) { + (Format::Commas, options::format::COMMAS) } else { (Format::Columns, options::format::COLUMNS) }; @@ -316,6 +328,16 @@ impl Config { } }; + let width = options + .value_of(options::WIDTH) + .map(|x| { + x.parse::().unwrap_or_else(|_e| { + show_error!("invalid line width: ‘{}’", x); + exit(2); + }) + }) + .or_else(|| termsize::get().map(|s| s.cols)); + Config { format, files, @@ -334,6 +356,7 @@ impl Config { #[cfg(unix)] inode: options.is_present(options::INODE), long, + width, } } } @@ -354,13 +377,15 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(options::FORMAT) .help("Set the display format.") .takes_value(true) - .possible_values(&["long", "verbose", "single-column", "columns"]) + .possible_values(&["long", "verbose", "single-column", "columns", "vertical", "across", "horizontal", "commas"]) .hide_possible_values(true) .require_equals(true) .overrides_with_all(&[ options::FORMAT, options::format::COLUMNS, options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, ]), ) .arg( @@ -371,6 +396,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { options::FORMAT, options::format::COLUMNS, options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, ]), ) .arg( @@ -381,8 +408,33 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .overrides_with_all(&[ options::FORMAT, options::format::COLUMNS, - options::format::ONELINE, options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, + ]), + ) + .arg( + Arg::with_name(options::format::ACROSS) + .short(options::format::ACROSS) + .help("List entries in rows instead of in columns.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, + ]), + ) + .arg( + Arg::with_name(options::format::COMMAS) + .short(options::format::COMMAS) + .help("List entries separated by commas.") + .overrides_with_all(&[ + options::FORMAT, + options::format::COLUMNS, + options::format::LONG, + options::format::ACROSS, + options::format::COLUMNS, ]), ) // The next three arguments do not override with the other format @@ -601,6 +653,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(options::RECURSIVE) .help("List the contents of all directories recursively."), ) + .arg( + Arg::with_name(options::WIDTH) + .long(options::WIDTH) + .short("w") + .help("Assume that the terminal is COLS columns wide.") + .value_name("COLS") + .takes_value(true) + ) .arg( Arg::with_name(options::COLOR) .long(options::COLOR) @@ -779,47 +839,71 @@ fn display_items(items: &[PathBuf], strip: Option<&Path>, config: &Config) { display_item_long(item, strip, max_links, max_size, config); } } else { - if config.format != Format::OneLine { - let names = items.iter().filter_map(|i| { - let md = get_metadata(i, config); - match md { - Err(e) => { - let filename = get_file_name(i, strip); - show_error!("'{}': {}", filename, e); - None - } - Ok(md) => Some(display_file_name(&i, strip, &md, config)), - } - }); - - if let Some(size) = termsize::get() { - let mut grid = Grid::new(GridOptions { - filling: Filling::Spaces(2), - direction: Direction::TopToBottom, - }); - - for name in names { - grid.add(name); - } - - if let Some(output) = grid.fit_into_width(size.cols as usize) { - print!("{}", output); - return; - } - } - } - - // Couldn't display a grid, either because we don't know - // the terminal width or because fit_into_width failed - for i in items { + let names = items.iter().filter_map(|i| { let md = get_metadata(i, config); - if let Ok(md) = md { - println!("{}", display_file_name(&i, strip, &md, config).contents); + match md { + Err(e) => { + let filename = get_file_name(i, strip); + show_error!("'{}': {}", filename, e); + None + } + Ok(md) => Some(display_file_name(&i, strip, &md, config)), + } + }); + + match (&config.format, config.width) { + (Format::Columns, Some(width)) => display_grid(names, width, Direction::TopToBottom), + (Format::Across, Some(width)) => display_grid(names, width, Direction::LeftToRight), + (Format::Commas, width_opt) => { + let term_width = width_opt.unwrap_or(1); + let mut current_col = 0; + let mut names = names; + if let Some(name) = names.next() { + print!("{}", name.contents); + current_col = name.width as u16 + 2; + } + for name in names { + let name_width = name.width as u16; + if current_col + name_width + 1 > term_width { + current_col = name_width + 2; + print!(",\n{}", name.contents); + } else { + current_col += name_width + 2; + print!(", {}", name.contents); + } + } + // Current col is never zero again if names have been printed. + // So we print a newline. + if current_col > 0 { + println!(); + } + } + _ => { + for name in names { + println!("{}", name.contents); + } } } } } +fn display_grid(names: impl Iterator, width: u16, direction: Direction) { + let mut grid = Grid::new(GridOptions { + filling: Filling::Spaces(2), + direction, + }); + + for name in names { + grid.add(name); + } + + match grid.fit_into_width(width as usize) { + Some(output) => print!("{}", output), + // Width is too small for the grid, so we fit it in one column + None => print!("{}", grid.fit_into_columns(1)), + } +} + use uucore::fs::display_permissions; fn display_item_long( diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 4fd35a286..ecd288735 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -57,12 +57,72 @@ fn test_ls_a() { assert!(!result.stdout.contains("..")); } +#[test] +fn test_ls_width() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-width-1")); + at.touch(&at.plus_as_string("test-width-2")); + at.touch(&at.plus_as_string("test-width-3")); + at.touch(&at.plus_as_string("test-width-4")); + + for option in &["-w 100", "-w=100", "--width=100", "--width 100"] { + let result = scene + .ucmd() + .args(&option.split(" ").collect::>()) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-width-1 test-width-2 test-width-3 test-width-4\n", + ) + } + + for option in &["-w 50", "-w=50", "--width=50", "--width 50"] { + let result = scene + .ucmd() + .args(&option.split(" ").collect::>()) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-width-1 test-width-3\ntest-width-2 test-width-4\n", + ) + } + + for option in &[ + "-w 25", + "-w=25", + "--width=25", + "--width 25", + "-w 0", + "-w=0", + "--width=0", + "--width 0", + ] { + let result = scene + .ucmd() + .args(&option.split(" ").collect::>()) + .run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n", + ) + } +} + #[test] fn test_ls_columns() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; at.touch(&at.plus_as_string("test-columns-1")); at.touch(&at.plus_as_string("test-columns-2")); + at.touch(&at.plus_as_string("test-columns-3")); + at.touch(&at.plus_as_string("test-columns-4")); // Columns is the default let result = scene.ucmd().run(); @@ -71,9 +131,15 @@ fn test_ls_columns() { assert!(result.success); #[cfg(not(windows))] - assert_eq!(result.stdout, "test-columns-1\ntest-columns-2\n"); + assert_eq!( + result.stdout, + "test-columns-1\ntest-columns-2\ntest-columns-3\ntest-columns-4\n" + ); #[cfg(windows)] - assert_eq!(result.stdout, "test-columns-1 test-columns-2\n"); + assert_eq!( + result.stdout, + "test-columns-1 test-columns-2 test-columns-3 test-columns-4\n" + ); for option in &["-C", "--format=columns"] { let result = scene.ucmd().arg(option).run(); @@ -81,9 +147,107 @@ fn test_ls_columns() { println!("stdout = {:?}", result.stdout); assert!(result.success); #[cfg(not(windows))] - assert_eq!(result.stdout, "test-columns-1\ntest-columns-2\n"); + assert_eq!( + result.stdout, + "test-columns-1\ntest-columns-2\ntest-columns-3\ntest-columns-4\n" + ); #[cfg(windows)] - assert_eq!(result.stdout, "test-columns-1 test-columns-2\n"); + assert_eq!( + result.stdout, + "test-columns-1 test-columns-2 test-columns-3 test-columns-4\n" + ); + } + + for option in &["-C", "--format=columns"] { + let result = scene.ucmd().arg("-w=40").arg(option).run(); + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert!(result.success); + assert_eq!( + result.stdout, + "test-columns-1 test-columns-3\ntest-columns-2 test-columns-4\n" + ); + } +} + +#[test] +fn test_ls_across() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-across-1")); + at.touch(&at.plus_as_string("test-across-2")); + at.touch(&at.plus_as_string("test-across-3")); + at.touch(&at.plus_as_string("test-across-4")); + + for option in &["-x", "--format=across"] { + let result = scene.ucmd().arg(option).succeeds(); + // Because the test terminal has width 0, this is the same output as + // the columns option. + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + if cfg!(unix) { + assert_eq!( + result.stdout, + "test-across-1\ntest-across-2\ntest-across-3\ntest-across-4\n" + ); + } else { + assert_eq!( + result.stdout, + "test-across-1 test-across-2 test-across-3 test-across-4\n" + ); + } + } + + for option in &["-x", "--format=across"] { + let result = scene.ucmd().arg("-w=30").arg(option).run(); + // Because the test terminal has width 0, this is the same output as + // the columns option. + println!("stderr = {:?}", result.stderr); + println!("stdout = {:?}", result.stdout); + assert_eq!( + result.stdout, + "test-across-1 test-across-2\ntest-across-3 test-across-4\n" + ); + } +} + +#[test] +fn test_ls_commas() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch(&at.plus_as_string("test-commas-1")); + at.touch(&at.plus_as_string("test-commas-2")); + at.touch(&at.plus_as_string("test-commas-3")); + at.touch(&at.plus_as_string("test-commas-4")); + + for option in &["-m", "--format=commas"] { + let result = scene.ucmd().arg(option).succeeds(); + if cfg!(unix) { + assert_eq!( + result.stdout, + "test-commas-1,\ntest-commas-2,\ntest-commas-3,\ntest-commas-4\n" + ); + } else { + assert_eq!( + result.stdout, + "test-commas-1, test-commas-2, test-commas-3, test-commas-4\n" + ); + } + } + + for option in &["-m", "--format=commas"] { + let result = scene.ucmd().arg("-w=30").arg(option).succeeds(); + assert_eq!( + result.stdout, + "test-commas-1, test-commas-2,\ntest-commas-3, test-commas-4\n" + ); + } + for option in &["-m", "--format=commas"] { + let result = scene.ucmd().arg("-w=45").arg(option).succeeds(); + assert_eq!( + result.stdout, + "test-commas-1, test-commas-2, test-commas-3,\ntest-commas-4\n" + ); } }