ls: across & commas formats and width parameter (#1869)

This commit is contained in:
Terts Diepraam 2021-03-22 18:24:23 +01:00 committed by GitHub
parent d86ee34bc6
commit de3f9b8186
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 290 additions and 42 deletions

View file

@ -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<u16>,
}
// 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::<u16>().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<Item = Cell>, 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(

View file

@ -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::<Vec<_>>())
.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::<Vec<_>>())
.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::<Vec<_>>())
.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"
);
}
}