Merge pull request #5603 from sylvestre/gnu-legacy

ls: Match the gnu behavior for colors
This commit is contained in:
Sylvestre Ledru 2023-12-09 17:19:15 +01:00 committed by GitHub
commit 0308e015a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 131 additions and 49 deletions

View file

@ -289,7 +289,7 @@ indicatif = "0.17"
itertools = "0.12.0"
libc = "0.2.150"
lscolors = { version = "0.16.0", default-features = false, features = [
"nu-ansi-term",
"gnu_legacy",
] }
memchr = "2"
memmap2 = "0.9"

View file

@ -10,7 +10,8 @@ use clap::{
crate_version, Arg, ArgAction, Command,
};
use glob::{MatchOptions, Pattern};
use lscolors::LsColors;
use lscolors::{LsColors, Style};
use number_prefix::NumberPrefix;
use std::{cell::OnceCell, num::IntErrorKind};
use std::{collections::HashSet, io::IsTerminal};
@ -1900,6 +1901,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
let mut dirs = Vec::<PathData>::new();
let mut out = BufWriter::new(stdout());
let mut dired = DiredOutput::default();
let mut style_manager = StyleManager::new();
let initial_locs_len = locs.len();
for loc in locs {
@ -1933,7 +1935,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
sort_entries(&mut files, config, &mut out);
sort_entries(&mut dirs, config, &mut out);
display_items(&files, config, &mut out, &mut dired)?;
display_items(&files, config, &mut out, &mut dired, &mut style_manager)?;
for (pos, path_data) in dirs.iter().enumerate() {
// Do read_dir call here to match GNU semantics by printing
@ -1985,6 +1987,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
&mut out,
&mut listed_ancestors,
&mut dired,
&mut style_manager,
)?;
}
if config.dired {
@ -2101,6 +2104,7 @@ fn enter_directory(
out: &mut BufWriter<Stdout>,
listed_ancestors: &mut HashSet<FileInformation>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> {
// Create vec of entries with initial dot files
let mut entries: Vec<PathData> = if config.files == Files::All {
@ -2153,7 +2157,7 @@ fn enter_directory(
}
}
display_items(&entries, config, out, dired)?;
display_items(&entries, config, out, dired, style_manager)?;
if config.recursive {
for e in entries
@ -2194,7 +2198,15 @@ fn enter_directory(
show_dir_name(&e.p_buf, out);
writeln!(out)?;
enter_directory(e, rd, config, out, listed_ancestors, dired)?;
enter_directory(
e,
rd,
config,
out,
listed_ancestors,
dired,
style_manager,
)?;
listed_ancestors
.remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?);
} else {
@ -2316,6 +2328,7 @@ fn display_items(
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> {
// `-Z`, `--context`:
// Display the SELinux security context or '?' if none is found. When used with the `-l`
@ -2338,7 +2351,7 @@ fn display_items(
display_additional_leading_info(item, &padding_collection, config, out)?;
write!(out, "{more_info}")?;
}
display_item_long(item, &padding_collection, config, out, dired)?;
display_item_long(item, &padding_collection, config, out, dired, style_manager)?;
}
} else {
let mut longest_context_len = 1;
@ -2358,7 +2371,7 @@ fn display_items(
for i in items {
let more_info = display_additional_leading_info(i, &padding, config, out)?;
let cell = display_file_name(i, config, prefix_context, more_info, out);
let cell = display_file_name(i, config, prefix_context, more_info, out, style_manager);
names_vec.push(cell);
}
@ -2513,6 +2526,7 @@ fn display_item_long(
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
) -> UResult<()> {
let mut output_display: String = String::new();
if config.dired {
@ -2605,7 +2619,8 @@ fn display_item_long(
write!(output_display, " {} ", display_date(md, config)).unwrap();
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
let displayed_file =
display_file_name(item, config, None, String::new(), out, style_manager).contents;
if config.dired {
let (start, end) = dired::calculate_dired(
&dired.dired_positions,
@ -2687,7 +2702,8 @@ fn display_item_long(
write!(output_display, " {}", pad_right("?", padding.uname)).unwrap();
}
let displayed_file = display_file_name(item, config, None, String::new(), out).contents;
let displayed_file =
display_file_name(item, config, None, String::new(), out, style_manager).contents;
let date_len = 12;
write!(
@ -2985,6 +3001,7 @@ fn display_file_name(
prefix_context: Option<usize>,
more_info: String,
out: &mut BufWriter<Stdout>,
style_manager: &mut StyleManager,
) -> Cell {
// This is our return value. We start by `&path.display_name` and modify it along the way.
let mut name = escape_name(&path.display_name, &config.quoting_style);
@ -3008,13 +3025,14 @@ fn display_file_name(
if let Some(ls_colors) = &config.color {
let md = path.md(out);
name = if md.is_some() {
color_name(name, &path.p_buf, md, ls_colors)
color_name(name, &path.p_buf, md, ls_colors, style_manager)
} else {
color_name(
name,
&path.p_buf,
path.p_buf.symlink_metadata().ok().as_ref(),
ls_colors,
style_manager,
)
};
}
@ -3103,6 +3121,7 @@ fn display_file_name(
&target_data.p_buf,
Some(&target_metadata),
ls_colors,
style_manager,
));
}
} else {
@ -3137,11 +3156,50 @@ fn display_file_name(
}
}
fn color_name(name: String, path: &Path, md: Option<&Metadata>, ls_colors: &LsColors) -> String {
match ls_colors.style_for_path_with_metadata(path, md) {
Some(style) => {
return style.to_nu_ansi_term_style().paint(name).to_string();
/// We need this struct to be able to store the previous style.
/// This because we need to check the previous value in case we don't need
/// the reset
struct StyleManager {
current_style: Option<Style>,
}
impl StyleManager {
fn new() -> Self {
Self {
current_style: None,
}
}
fn apply_style(&mut self, new_style: &Style, name: &str) -> String {
if let Some(current) = &self.current_style {
if *current == *new_style {
// Current style is the same as new style, apply without reset.
let mut style = new_style.to_nu_ansi_term_style();
style.prefix_with_reset = false;
return style.paint(name).to_string();
}
}
// We are getting a new style, we need to reset it
self.current_style = Some(new_style.clone());
new_style
.to_nu_ansi_term_style()
.reset_before_style()
.paint(name)
.to_string()
}
}
/// Colors the provided name based on the style determined for the given path.
fn color_name(
name: String,
path: &Path,
md: Option<&Metadata>,
ls_colors: &LsColors,
style_manager: &mut StyleManager,
) -> String {
match ls_colors.style_for_path_with_metadata(path, md) {
Some(style) => style_manager.apply_style(style, &name),
None => name,
}
}

View file

@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs
// spell-checker:ignore (words) READMECAREFULLY birthtime doesntexist oneline somebackup lrwx somefile somegroup somehiddenbackup somehiddenfile tabsize aaaaaaaa bbbb cccc dddddddd ncccc neee naaaaa nbcdef nfffff dired subdired tmpfs mdir
#[cfg(any(unix, feature = "feat_selinux"))]
use crate::common::util::expected_result;
@ -864,11 +864,11 @@ fn test_ls_zero() {
.succeeds()
.stdout_only("\"0-test-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00");
scene
.ucmd()
.args(&["--zero", "--color=always"])
.succeeds()
.stdout_only("\x1b[1;34m0-test-zero\x1b[0m\x002-test-zero\x003-test-zero\x00");
let result = scene.ucmd().args(&["--zero", "--color=always"]).succeeds();
assert_eq!(
result.stdout_str(),
"\u{1b}[0m\u{1b}[01;34m0-test-zero\x1b[0m\x002-test-zero\x003-test-zero\x00"
);
scene
.ucmd()
@ -921,12 +921,9 @@ fn test_ls_zero() {
"\"0-test-zero\"\x00\"1\\ntest-zero\"\x00\"2-test-zero\"\x00\"3-test-zero\"\x00",
);
scene
.ucmd()
.args(&["--zero", "--color=always"])
.succeeds()
.stdout_only(
"\x1b[1;34m0-test-zero\x1b[0m\x001\ntest-zero\x002-test-zero\x003-test-zero\x00",
let result = scene.ucmd().args(&["--zero", "--color=always"]).succeeds();
assert_eq!(result.stdout_str(),
"\u{1b}[0m\u{1b}[01;34m0-test-zero\x1b[0m\x001\ntest-zero\x002-test-zero\x003-test-zero\x00",
);
scene
@ -1202,12 +1199,21 @@ fn test_ls_long_symlink_color() {
}
fn capture_colored_string(input: &str) -> (Color, Name) {
let colored_name = Regex::new(r"\x1b\[([0-9;]+)m(.+)\x1b\[0m").unwrap();
// Input can be:
// \u{1b}[0m\u{1b}[01;36mln-dir3\u{1b}[0m
// \u{1b}[0m\u{1b}[01;34m./dir1/dir2/dir3\u{1b}[0m
// \u{1b}[0m\u{1b}[01;36mln-file-invalid\u{1b}[0m
// \u{1b}[01;36mdir1/invalid-target\u{1b}[0m
let colored_name = Regex::new(r"(?:\x1b\[0m\x1b)?\[([0-9;]+)m(.+)\x1b\[0m").unwrap();
match colored_name.captures(input) {
Some(captures) => (
Some(captures) => {
dbg!(captures.get(1).unwrap().as_str().to_string());
dbg!(captures.get(2).unwrap().as_str().to_string());
return (
captures.get(1).unwrap().as_str().to_string(),
captures.get(2).unwrap().as_str().to_string(),
),
);
}
None => (String::new(), input.to_string()),
}
}
@ -1995,9 +2001,9 @@ fn test_ls_color() {
at.touch(nested_file);
at.touch("test-color");
let a_with_colors = "\x1b[1;34ma\x1b[0m";
let z_with_colors = "\x1b[1;34mz\x1b[0m";
let nested_dir_with_colors = "\x1b[1;34mnested_dir\x1b[0m"; // spell-checker:disable-line
let a_with_colors = "\x1b[0m\x1b[01;34ma\x1b[0m";
let z_with_colors = "\x1b[01;34mz\x1b[0m\n";
let nested_dir_with_colors = "\x1b[0m\x1b[01;34mnested_dir\x1b[0m\x0anested_file"; // spell-checker:disable-line
// Color is disabled by default
let result = scene.ucmd().succeeds();
@ -2006,12 +2012,9 @@ fn test_ls_color() {
// Color should be enabled
for param in ["--color", "--col", "--color=always", "--col=always"] {
scene
.ucmd()
.arg(param)
.succeeds()
.stdout_contains(a_with_colors)
.stdout_contains(z_with_colors);
let result = scene.ucmd().arg(param).succeeds();
assert!(result.stdout_str().contains(a_with_colors));
assert!(result.stdout_str().contains(z_with_colors));
}
// Color should be disabled
@ -2020,12 +2023,8 @@ fn test_ls_color() {
assert!(!result.stdout_str().contains(z_with_colors));
// Nested dir should be shown and colored
scene
.ucmd()
.arg("--color")
.arg("a")
.succeeds()
.stdout_contains(nested_dir_with_colors);
let result = scene.ucmd().arg("--color").arg("a").succeeds();
assert!(result.stdout_str().contains(nested_dir_with_colors));
// No output
scene
@ -2037,13 +2036,18 @@ fn test_ls_color() {
// The colors must not mess up the grid layout
at.touch("b");
scene
let result = scene
.ucmd()
.arg("--color")
.arg("-w=15")
.arg("-C")
.succeeds()
.stdout_only(format!("{a_with_colors} test-color\nb {z_with_colors}\n"));
.succeeds();
let expected = format!("{} test-color\x0ab {}", a_with_colors, z_with_colors);
assert_eq!(
result.stdout_str().escape_default().to_string(),
expected.escape_default().to_string()
);
assert_eq!(result.stdout_str(), expected);
}
#[cfg(unix)]
@ -3885,3 +3889,23 @@ fn test_ls_hyperlink() {
.succeeds()
.stdout_is(format!("{file}\n"));
}
#[test]
fn test_ls_color_do_not_reset() {
let scene: TestScenario = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("example");
at.mkdir("example/a");
at.mkdir("example/b");
let result = scene
.ucmd()
.arg("--color=always")
.arg("example/")
.succeeds();
// the second color code should not have a reset
assert_eq!(
result.stdout_str().escape_default().to_string(),
"\\u{1b}[0m\\u{1b}[01;34ma\\u{1b}[0m\\n\\u{1b}[01;34mb\\u{1b}[0m\\n"
);
}