1
0
mirror of https://github.com/uutils/coreutils synced 2024-06-29 05:54:27 +00:00

ls: gnu color-norm test fix (#6481)

This commit is contained in:
sreehari prasad 2024-06-25 01:08:10 +05:30 committed by GitHub
parent 92c3de5387
commit 92665144c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 342 additions and 103 deletions

8
Cargo.lock generated
View File

@ -1383,9 +1383,9 @@ dependencies = [
[[package]]
name = "lscolors"
version = "0.16.0"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab0b209ec3976527806024406fe765474b9a1750a0ed4b8f0372364741f50e7b"
checksum = "02a5d67fc8a616f260ee9a36868547da09ac24178a4b84708cd8ea781372fbe4"
dependencies = [
"nu-ansi-term",
]
@ -1484,9 +1484,9 @@ dependencies = [
[[package]]
name = "nu-ansi-term"
version = "0.49.0"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68"
checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14"
dependencies = [
"windows-sys 0.48.0",
]

View File

@ -290,7 +290,7 @@ hostname = "0.4"
indicatif = "0.17.8"
itertools = "0.13.0"
libc = "0.2.153"
lscolors = { version = "0.16.0", default-features = false, features = [
lscolors = { version = "0.18.0", default-features = false, features = [
"gnu_legacy",
] }
memchr = "2.7.2"

162
src/uu/ls/src/colors.rs Normal file
View File

@ -0,0 +1,162 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use super::get_metadata_with_deref_opt;
use super::PathData;
use lscolors::{Indicator, LsColors, Style};
use std::fs::{DirEntry, Metadata};
use std::io::{BufWriter, Stdout};
/// 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
pub(crate) struct StyleManager<'a> {
/// last style that is applied, if `None` that means reset is applied.
pub(crate) current_style: Option<Style>,
/// `true` if the initial reset is applied
pub(crate) initial_reset_is_done: bool,
pub(crate) colors: &'a LsColors,
}
impl<'a> StyleManager<'a> {
pub(crate) fn new(colors: &'a LsColors) -> Self {
Self {
initial_reset_is_done: false,
current_style: None,
colors,
}
}
pub(crate) fn apply_style(&mut self, new_style: Option<&Style>, name: &str) -> String {
let mut style_code = String::new();
let mut force_suffix_reset: bool = false;
// if reset is done we need to apply normal style before applying new style
if self.is_reset() {
if let Some(norm_sty) = self.get_normal_style().copied() {
style_code.push_str(&self.get_style_code(&norm_sty));
}
}
if let Some(new_style) = new_style {
// we only need to apply a new style if it's not the same as the current
// style for example if normal is the current style and a file with
// normal style is to be printed we could skip printing new color
// codes
if !self.is_current_style(new_style) {
style_code.push_str(&self.reset(!self.initial_reset_is_done));
style_code.push_str(&self.get_style_code(new_style));
}
}
// if new style is None and current style is Normal we should reset it
else if matches!(self.get_normal_style().copied(), Some(norm_style) if self.is_current_style(&norm_style))
{
style_code.push_str(&self.reset(false));
// even though this is an unnecessary reset for gnu compatibility we allow it here
force_suffix_reset = true;
}
format!("{}{}{}", style_code, name, self.reset(force_suffix_reset))
}
/// Resets the current style and returns the default ANSI reset code to
/// reset all text formatting attributes. If `force` is true, the reset is
/// done even if the reset has been applied before.
pub(crate) fn reset(&mut self, force: bool) -> String {
// todo:
// We need to use style from `Indicator::Reset` but as of now ls colors
// uses a fallback mechanism and because of that if `Indicator::Reset`
// is not specified it would fallback to `Indicator::Normal` which seems
// to be non compatible with gnu
if self.current_style.is_some() || force {
self.initial_reset_is_done = true;
self.current_style = None;
return "\x1b[0m".to_string();
}
String::new()
}
pub(crate) fn get_style_code(&mut self, new_style: &Style) -> String {
self.current_style = Some(*new_style);
let mut nu_a_style = new_style.to_nu_ansi_term_style();
nu_a_style.prefix_with_reset = false;
let mut ret = nu_a_style.paint("").to_string();
// remove the suffix reset
ret.truncate(ret.len() - 4);
ret
}
pub(crate) fn is_current_style(&mut self, new_style: &Style) -> bool {
matches!(&self.current_style,Some(style) if style == new_style )
}
pub(crate) fn is_reset(&mut self) -> bool {
self.current_style.is_none()
}
pub(crate) fn get_normal_style(&self) -> Option<&Style> {
self.colors.style_for_indicator(Indicator::Normal)
}
pub(crate) fn apply_normal(&mut self) -> String {
if let Some(sty) = self.get_normal_style().copied() {
return self.get_style_code(&sty);
}
String::new()
}
pub(crate) fn apply_style_based_on_metadata(
&mut self,
path: &PathData,
md_option: Option<&Metadata>,
name: &str,
) -> String {
let style = self
.colors
.style_for_path_with_metadata(&path.p_buf, md_option);
self.apply_style(style, name)
}
pub(crate) fn apply_style_based_on_dir_entry(
&mut self,
dir_entry: &DirEntry,
name: &str,
) -> String {
let style = self.colors.style_for(dir_entry);
self.apply_style(style, name)
}
}
/// Colors the provided name based on the style determined for the given path
/// This function is quite long because it tries to leverage DirEntry to avoid
/// unnecessary calls to stat()
/// and manages the symlink errors
pub(crate) fn color_name(
name: &str,
path: &PathData,
style_manager: &mut StyleManager,
out: &mut BufWriter<Stdout>,
target_symlink: Option<&PathData>,
) -> String {
if !path.must_dereference {
// If we need to dereference (follow) a symlink, we will need to get the metadata
if let Some(de) = &path.de {
// There is a DirEntry, we don't need to get the metadata for the color
return style_manager.apply_style_based_on_dir_entry(de, name);
}
}
if let Some(target) = target_symlink {
// use the optional target_symlink
// Use fn get_metadata_with_deref_opt instead of get_metadata() here because ls
// should not exit with an err, if we are unable to obtain the target_metadata
let md_res = get_metadata_with_deref_opt(&target.p_buf, path.must_dereference);
let md = md_res.or(path.p_buf.symlink_metadata());
style_manager.apply_style_based_on_metadata(path, md.ok().as_ref(), name)
} else {
let md_option = path.get_metadata(out);
let symlink_metadata = path.p_buf.symlink_metadata().ok();
let md = md_option.or(symlink_metadata.as_ref());
style_manager.apply_style_based_on_metadata(path, md, name)
}
}

View File

@ -10,7 +10,7 @@ use clap::{
crate_version, Arg, ArgAction, Command,
};
use glob::{MatchOptions, Pattern};
use lscolors::{LsColors, Style};
use lscolors::LsColors;
use ansi_width::ansi_width;
use std::{cell::OnceCell, num::IntErrorKind};
@ -68,6 +68,8 @@ use uucore::{
use uucore::{help_about, help_section, help_usage, parse_glob, show, show_error, show_warning};
mod dired;
use dired::{is_dired_arg_present, DiredOutput};
mod colors;
use colors::{color_name, StyleManager};
#[cfg(not(feature = "selinux"))]
static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not enabled)";
#[cfg(feature = "selinux")]
@ -2038,7 +2040,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 mut style_manager = config.color.as_ref().map(StyleManager::new);
let initial_locs_len = locs.len();
for loc in locs {
@ -2072,6 +2074,15 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
sort_entries(&mut files, config, &mut out);
sort_entries(&mut dirs, config, &mut out);
if let Some(style_manager) = style_manager.as_mut() {
// ls will try to write a reset before anything is written if normal
// color is given
if style_manager.get_normal_style().is_some() {
let to_write = style_manager.reset(true);
write!(out, "{}", to_write)?;
}
}
display_items(&files, config, &mut out, &mut dired, &mut style_manager)?;
for (pos, path_data) in dirs.iter().enumerate() {
@ -2245,7 +2256,7 @@ fn enter_directory(
out: &mut BufWriter<Stdout>,
listed_ancestors: &mut HashSet<FileInformation>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
style_manager: &mut Option<StyleManager>,
) -> UResult<()> {
// Create vec of entries with initial dot files
let mut entries: Vec<PathData> = if config.files == Files::All {
@ -2469,7 +2480,7 @@ fn display_items(
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
style_manager: &mut Option<StyleManager>,
) -> UResult<()> {
// `-Z`, `--context`:
// Display the SELinux security context or '?' if none is found. When used with the `-l`
@ -2521,6 +2532,11 @@ fn display_items(
let padding = calculate_padding_collection(items, config, out);
// we need to apply normal color to non filename output
if let Some(style_manager) = style_manager {
write!(out, "{}", style_manager.apply_normal())?;
}
let mut names_vec = Vec::new();
for i in items {
let more_info = display_additional_leading_info(i, &padding, config, out)?;
@ -2691,10 +2707,15 @@ fn display_item_long(
config: &Config,
out: &mut BufWriter<Stdout>,
dired: &mut DiredOutput,
style_manager: &mut StyleManager,
style_manager: &mut Option<StyleManager>,
quoted: bool,
) -> UResult<()> {
let mut output_display: String = String::new();
// apply normal color to non filename outputs
if let Some(style_manager) = style_manager {
write!(output_display, "{}", style_manager.apply_normal()).unwrap();
}
if config.dired {
output_display += " ";
}
@ -3146,7 +3167,7 @@ fn display_item_name(
prefix_context: Option<usize>,
more_info: String,
out: &mut BufWriter<Stdout>,
style_manager: &mut StyleManager,
style_manager: &mut Option<StyleManager>,
) -> String {
// 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);
@ -3155,8 +3176,8 @@ fn display_item_name(
name = create_hyperlink(&name, path);
}
if let Some(ls_colors) = &config.color {
name = color_name(name, path, ls_colors, style_manager, out, None);
if let Some(style_manager) = style_manager {
name = color_name(&name, path, style_manager, out, None);
}
if config.format != Format::Long && !more_info.is_empty() {
@ -3202,7 +3223,7 @@ fn display_item_name(
// We might as well color the symlink output after the arrow.
// This makes extra system calls, but provides important information that
// people run `ls -l --color` are very interested in.
if let Some(ls_colors) = &config.color {
if let Some(style_manager) = style_manager {
// We get the absolute path to be able to construct PathData with valid Metadata.
// This is because relative symlinks will fail to get_metadata.
let mut absolute_target = target.clone();
@ -3228,9 +3249,8 @@ fn display_item_name(
name.push_str(&path.p_buf.read_link().unwrap().to_string_lossy());
} else {
name.push_str(&color_name(
escape_name(target.as_os_str(), &config.quoting_style),
&escape_name(target.as_os_str(), &config.quoting_style),
path,
ls_colors,
style_manager,
out,
Some(&target_data),
@ -3292,92 +3312,6 @@ fn create_hyperlink(name: &str, path: &PathData) -> String {
format!("\x1b]8;;file://{hostname}{absolute_path}\x07{name}\x1b]8;;\x07")
}
/// 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()
}
}
fn apply_style_based_on_metadata(
path: &PathData,
md_option: Option<&Metadata>,
ls_colors: &LsColors,
style_manager: &mut StyleManager,
name: &str,
) -> String {
match ls_colors.style_for_path_with_metadata(&path.p_buf, md_option) {
Some(style) => style_manager.apply_style(style, name),
None => name.to_owned(),
}
}
/// Colors the provided name based on the style determined for the given path
/// This function is quite long because it tries to leverage DirEntry to avoid
/// unnecessary calls to stat()
/// and manages the symlink errors
fn color_name(
name: String,
path: &PathData,
ls_colors: &LsColors,
style_manager: &mut StyleManager,
out: &mut BufWriter<Stdout>,
target_symlink: Option<&PathData>,
) -> String {
if !path.must_dereference {
// If we need to dereference (follow) a symlink, we will need to get the metadata
if let Some(de) = &path.de {
// There is a DirEntry, we don't need to get the metadata for the color
return match ls_colors.style_for(de) {
Some(style) => style_manager.apply_style(style, &name),
None => name,
};
}
}
if let Some(target) = target_symlink {
// use the optional target_symlink
// Use fn get_metadata_with_deref_opt instead of get_metadata() here because ls
// should not exit with an err, if we are unable to obtain the target_metadata
let md_res = get_metadata_with_deref_opt(target.p_buf.as_path(), path.must_dereference);
let md = md_res.or(path.p_buf.symlink_metadata());
apply_style_based_on_metadata(path, md.ok().as_ref(), ls_colors, style_manager, &name)
} else {
let md_option = path.get_metadata(out);
let symlink_metadata = path.p_buf.symlink_metadata().ok();
let md = md_option.or(symlink_metadata.as_ref());
apply_style_based_on_metadata(path, md, ls_colors, style_manager, &name)
}
}
#[cfg(not(unix))]
fn display_symlink_count(_metadata: &Metadata) -> String {
// Currently not sure of how to get this on Windows, so I'm punting.

View File

@ -4673,3 +4673,133 @@ fn test_acl_display() {
.succeeds()
.stdout_contains("+");
}
// Make sure that "ls --color" correctly applies color "normal" to text and
// files. Specifically, it should use the NORMAL setting to format non-file name
// output and file names that don't have a designated color (unless the FILE
// setting is also configured).
#[cfg(unix)]
#[test]
fn test_ls_color_norm() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("exe");
at.set_mode("exe", 0o755);
at.touch("no_color");
at.set_mode("no_color", 0o444);
let colors = "no=7:ex=01;32";
let strip = |input: &str| {
let re = Regex::new(r"-r.*norm").unwrap();
re.replace_all(input, "norm").to_string()
};
// Uncolored file names should inherit NORMAL attributes.
let expected = "\x1b[0m\x1b[07mnorm \x1b[0m\x1b[01;32mexe\x1b[0m\n\x1b[07mnorm no_color\x1b[0m"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", colors)
.env("TIME_STYLE", "+norm")
.arg("-gGU")
.arg("--color")
.arg("exe")
.arg("no_color")
.succeeds()
.stdout_str_apply(strip)
.stdout_contains(expected);
let expected = "\x1b[0m\x1b[07m\x1b[0m\x1b[01;32mexe\x1b[0m \x1b[07mno_color\x1b[0m\n"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", colors)
.env("TIME_STYLE", "+norm")
.arg("-xU")
.arg("--color")
.arg("exe")
.arg("no_color")
.succeeds()
.stdout_contains(expected);
let expected =
"\x1b[0m\x1b[07mnorm no_color\x1b[0m\n\x1b[07mnorm \x1b[0m\x1b[01;32mexe\x1b[0m\n"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", colors)
.env("TIME_STYLE", "+norm")
.arg("-gGU")
.arg("--color")
.arg("no_color")
.arg("exe")
.succeeds()
.stdout_str_apply(strip)
.stdout_contains(expected);
let expected = "\x1b[0m\x1b[07mno_color\x1b[0m \x1b[07m\x1b[0m\x1b[01;32mexe\x1b[0m"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", colors)
.env("TIME_STYLE", "+norm")
.arg("-xU")
.arg("--color")
.arg("no_color")
.arg("exe")
.succeeds()
.stdout_contains(expected);
// NORMAL does not override FILE
let expected = "\x1b[0m\x1b[07mnorm \x1b[0m\x1b[01mno_color\x1b[0m\n\x1b[07mnorm \x1b[0m\x1b[01;32mexe\x1b[0m\n"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", format!("{}:fi=1", colors))
.env("TIME_STYLE", "+norm")
.arg("-gGU")
.arg("--color")
.arg("no_color")
.arg("exe")
.succeeds()
.stdout_str_apply(strip)
.stdout_contains(expected);
// uncolored ordinary files that do _not_ inherit from NORMAL.
let expected =
"\x1b[0m\x1b[07mnorm \x1b[0mno_color\x1b[0m\n\x1b[07mnorm \x1b[0m\x1b[01;32mexe\x1b[0m\n"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", format!("{}:fi=", colors))
.env("TIME_STYLE", "+norm")
.arg("-gGU")
.arg("--color")
.arg("no_color")
.arg("exe")
.succeeds()
.stdout_str_apply(strip)
.stdout_contains(expected);
let expected =
"\x1b[0m\x1b[07mnorm \x1b[0mno_color\x1b[0m\n\x1b[07mnorm \x1b[0m\x1b[01;32mexe\x1b[0m\n"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", format!("{}:fi=0", colors))
.env("TIME_STYLE", "+norm")
.arg("-gGU")
.arg("--color")
.arg("no_color")
.arg("exe")
.succeeds()
.stdout_str_apply(strip)
.stdout_contains(expected);
// commas (-m), indicator chars (-F) and the "total" line, do not currently
// use NORMAL attributes
let expected = "\x1b[0m\x1b[07mno_color\x1b[0m, \x1b[07m\x1b[0m\x1b[01;32mexe\x1b[0m\n"; // spell-checker:disable-line
scene
.ucmd()
.env("LS_COLORS", colors)
.env("TIME_STYLE", "+norm")
.arg("-mU")
.arg("--color")
.arg("no_color")
.arg("exe")
.succeeds()
.stdout_contains(expected);
}

View File

@ -2,7 +2,7 @@
# `build-gnu.bash` ~ builds GNU coreutils (from supplied sources)
#
# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules xstrtol ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed
# spell-checker:ignore (paths) abmon deref discrim eacces getlimits getopt ginstall inacc infloop inotify reflink ; (misc) INT_OFLOW OFLOW baddecode submodules xstrtol ; (vars/env) SRCDIR vdir rcexp xpart dired OSTYPE ; (utils) gnproc greadlink gsed multihardlink
set -e
@ -343,3 +343,16 @@ test \$n_stat1 -ge \$n_stat2 \\' tests/ls/stat-free-color.sh
# no need to replicate this output with hashsum
sed -i -e "s|Try 'md5sum --help' for more information.\\\n||" tests/cksum/md5sum.pl
# Our ls command always outputs ANSI color codes prepended with a zero. However,
# in the case of GNU, it seems inconsistent. Nevertheless, it looks like it
# doesn't matter whether we prepend a zero or not.
sed -i -E 's/\^\[\[([1-9]m)/^[[0\1/g; s/\^\[\[m/^[[0m/g' tests/ls/color-norm.sh
# It says in the test itself that having more than one reset is a bug, so we
# don't need to replicate that behavior.
sed -i -E 's/(\^\[\[0m)+/\^\[\[0m/g' tests/ls/color-norm.sh
# GNU's ls seems to output color codes in the order given in the environment
# variable, but our ls seems to output them in a predefined order. Nevertheless,
# the order doesn't matter, so it's okay.
sed -i 's/44;37/37;44/' tests/ls/multihardlink.sh