Skip to content

Commit b22013a

Browse files
joknarfAlonely0sylvestre
authored
ls: ls -lF symlink target indicators (#11554)
--------- Co-authored-by: Guillem L. Jara <4lon3ly0@tutanota.com> Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
1 parent 179b977 commit b22013a

3 files changed

Lines changed: 253 additions & 81 deletions

File tree

src/uu/ls/src/config.rs

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ pub struct Config {
216216
// Dir and vdir needs access to this field
217217
pub quoting_style: QuotingStyle,
218218
pub(crate) locale_quoting: Option<LocaleQuoting>,
219-
pub(crate) indicator_style: IndicatorStyle,
219+
pub(crate) indicator_style: Option<IndicatorStyle>,
220220
pub(crate) time_format_recent: String, // Time format for recent dates
221221
pub(crate) time_format_older: Option<String>, // Time format for older dates (optional, if not present, time_format_recent is used)
222222
pub(crate) context: bool,
@@ -598,34 +598,28 @@ fn extract_quoting_style(
598598
/// # Returns
599599
///
600600
/// An [`IndicatorStyle`] variant representing the indicator style to use.
601-
fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle {
601+
fn extract_indicator_style(options: &clap::ArgMatches) -> Option<IndicatorStyle> {
602602
if let Some(field) = options.get_one::<String>(options::INDICATOR_STYLE) {
603603
match field.as_str() {
604-
"none" => IndicatorStyle::None,
605-
"file-type" => IndicatorStyle::FileType,
606-
"classify" => IndicatorStyle::Classify,
607-
"slash" => IndicatorStyle::Slash,
608-
&_ => IndicatorStyle::None,
604+
"none" => None,
605+
"file-type" => Some(IndicatorStyle::FileType),
606+
"classify" => Some(IndicatorStyle::Classify),
607+
"slash" => Some(IndicatorStyle::Slash),
608+
&_ => None,
609609
}
610610
} else if let Some(field) = options.get_one::<String>(options::indicator_style::CLASSIFY) {
611611
match field.as_str() {
612-
"never" | "no" | "none" => IndicatorStyle::None,
613-
"always" | "yes" | "force" => IndicatorStyle::Classify,
614-
"auto" | "tty" | "if-tty" => {
615-
if stdout().is_terminal() {
616-
IndicatorStyle::Classify
617-
} else {
618-
IndicatorStyle::None
619-
}
620-
}
621-
&_ => IndicatorStyle::None,
612+
"never" | "no" | "none" => None,
613+
"always" | "yes" | "force" => Some(IndicatorStyle::Classify),
614+
"auto" | "tty" | "if-tty" => stdout().is_terminal().then_some(IndicatorStyle::Classify),
615+
&_ => None,
622616
}
623617
} else if options.get_flag(options::indicator_style::SLASH) {
624-
IndicatorStyle::Slash
618+
Some(IndicatorStyle::Slash)
625619
} else if options.get_flag(options::indicator_style::FILE_TYPE) {
626-
IndicatorStyle::FileType
620+
Some(IndicatorStyle::FileType)
627621
} else {
628-
IndicatorStyle::None
622+
None
629623
}
630624
}
631625

@@ -957,7 +951,7 @@ impl Config {
957951
} else if options.get_flag(options::dereference::DIR_ARGS) {
958952
Dereference::DirArgs
959953
} else if options.get_flag(options::DIRECTORY)
960-
|| indicator_style == IndicatorStyle::Classify
954+
|| indicator_style == Some(IndicatorStyle::Classify)
961955
|| format == Format::Long
962956
{
963957
Dereference::None

src/uu/ls/src/display.rs

Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use crate::colors::{StyleManager, color_name};
5252
use crate::config::Files;
5353
use crate::dired::{self, DiredOutput};
5454
use crate::{Config, ListState, LsError, PathData, get_block_size};
55+
use lscolors::Indicator;
5556

5657
/// Width of the standard Unix permissions string (one file-type char plus
5758
/// nine permission bits, e.g. `drwxr-xr-x`).
@@ -91,9 +92,8 @@ pub(crate) struct DisplayItemName {
9192
pub(crate) dired_name_len: usize,
9293
}
9394

94-
#[derive(PartialEq, Eq)]
95+
#[derive(Clone, Copy, PartialEq, Eq)]
9596
pub(crate) enum IndicatorStyle {
96-
None,
9797
Slash,
9898
FileType,
9999
Classify,
@@ -705,35 +705,31 @@ fn display_item_name(
705705
}
706706
}
707707

708-
if config.indicator_style != IndicatorStyle::None {
709-
let sym = classify_file(path);
710-
711-
let char_opt = match config.indicator_style {
712-
IndicatorStyle::Classify => sym,
713-
IndicatorStyle::FileType => {
714-
// Don't append an asterisk.
715-
match sym {
716-
Some('*') => None,
717-
_ => sym,
718-
}
719-
}
720-
IndicatorStyle::Slash => {
721-
// Append only a slash.
722-
match sym {
723-
Some('/') => Some('/'),
724-
_ => None,
725-
}
726-
}
727-
IndicatorStyle::None => None,
728-
};
708+
let is_long_symlink = config.format == Format::Long
709+
&& path.file_type().is_some_and(FileType::is_symlink)
710+
&& !path.must_dereference;
729711

730-
if let Some(c) = char_opt {
712+
if !is_long_symlink {
713+
if let Some(c) = indicator_char(path, config.indicator_style) {
731714
let _ = name.write_char(c);
732715
}
733716
}
734717

735718
let dired_name_len = if config.dired { name.len() } else { 0 };
736719

720+
let has_mi_or_or = style_manager.as_ref().is_some_and(|sm| {
721+
sm.has_indicator_style(Indicator::OrphanedSymbolicLink)
722+
|| sm.has_indicator_style(Indicator::MissingFile)
723+
});
724+
// Only stat symlink target when:
725+
// 1. Color is enabled AND LS_COLORS has mi= or or=, OR
726+
// 2. Long format AND (--classify or --file-type)
727+
let should_stat_target = has_mi_or_or
728+
|| matches!(
729+
config.indicator_style,
730+
Some(IndicatorStyle::Classify) | Some(IndicatorStyle::FileType)
731+
);
732+
737733
if config.format == Format::Long
738734
&& path.file_type().is_some_and(FileType::is_symlink)
739735
&& !path.must_dereference
@@ -745,10 +741,11 @@ fn display_item_name(
745741
// We might as well color the symlink output after the arrow.
746742
// This makes extra system calls, but provides important information that
747743
// people run `ls -l --color` are very interested in.
748-
if let Some(style_manager) = &mut style_manager {
749-
let escaped_target = escape_name_with_locale(target_path.as_os_str(), config);
750-
// We get the absolute path to be able to construct PathData with valid Metadata.
751-
// This is because relative symlinks will fail to get_metadata.
744+
let escaped_target = escape_name_with_locale(target_path.as_os_str(), config);
745+
746+
// We get the absolute path to be able to construct PathData with valid Metadata.
747+
// This is because relative symlinks will fail to get_metadata.
748+
if should_stat_target {
752749
let absolute_target = if target_path.is_relative() {
753750
match path.path().parent() {
754751
Some(p) => &p.join(&target_path),
@@ -757,7 +754,6 @@ fn display_item_name(
757754
} else {
758755
&target_path
759756
};
760-
761757
match fs::canonicalize(absolute_target) {
762758
Ok(resolved_target) => {
763759
let target_data = PathData::new(
@@ -769,43 +765,62 @@ fn display_item_name(
769765
false,
770766
);
771767

772-
// Check if the target actually needs coloring
773-
let md_option: Option<Metadata> = target_data
774-
.metadata()
775-
.cloned()
776-
.or_else(|| target_data.p_buf.symlink_metadata().ok());
777-
let style = style_manager.colors.style_for_path_with_metadata(
778-
&target_data.p_buf,
779-
md_option.as_ref(),
780-
);
781-
782-
if style.is_some() {
783-
// Only apply coloring if there's actually a style
784-
name.push(color_name(
785-
escaped_target,
786-
&target_data,
787-
style_manager,
788-
None,
789-
is_wrap(name.len()),
790-
));
768+
let target_display = if let Some(style_manager) = style_manager {
769+
let md = match target_data.metadata() {
770+
Some(md) => Some(Cow::Borrowed(md)),
771+
None => {
772+
target_data.p_buf.symlink_metadata().ok().map(Cow::Owned)
773+
}
774+
};
775+
// Check if the target actually needs coloring
776+
if style_manager
777+
.colors
778+
.style_for_path_with_metadata(&target_data.p_buf, md.as_deref())
779+
.is_some()
780+
{
781+
// Only apply coloring if there's actually a style
782+
color_name(
783+
escaped_target,
784+
&target_data,
785+
style_manager,
786+
None,
787+
is_wrap(name.len()),
788+
)
789+
} else {
790+
// For regular files with no coloring, just use plain text
791+
escaped_target
792+
}
791793
} else {
792-
// For regular files with no coloring, just use plain text
793-
name.push(escaped_target);
794+
escaped_target
795+
};
796+
name.push(target_display);
797+
// Add appropriate indicator based on indicator_style
798+
if let Some(c) = indicator_char(&target_data, config.indicator_style) {
799+
if matches!(
800+
config.indicator_style,
801+
Some(IndicatorStyle::Classify)
802+
| Some(IndicatorStyle::FileType)
803+
| Some(IndicatorStyle::Slash)
804+
) {
805+
let _ = name.write_char(c);
806+
}
794807
}
795808
}
796809
Err(_) => {
797-
name.push(
798-
style_manager.apply_missing_target_style(
810+
if let Some(style_manager) = &mut style_manager {
811+
name.push(style_manager.apply_missing_target_style(
799812
escaped_target,
800813
is_wrap(name.len()),
801-
),
802-
);
814+
));
815+
} else {
816+
// If no coloring is required, we just use target as is.
817+
// with the right quoting
818+
name.push(escaped_target);
819+
}
803820
}
804821
}
805822
} else {
806-
// If no coloring is required, we just use target as is.
807-
// Apply the right quoting
808-
name.push(escape_name_with_locale(target_path.as_os_str(), config));
823+
name.push(&escaped_target);
809824
}
810825
}
811826
Err(err) => {
@@ -1118,6 +1133,23 @@ fn display_item_long(
11181133
Ok(())
11191134
}
11201135

1136+
fn indicator_char(path: &PathData, style: Option<IndicatorStyle>) -> Option<char> {
1137+
let style = style?;
1138+
let sym = classify_file(path);
1139+
1140+
match style {
1141+
IndicatorStyle::Classify => sym,
1142+
IndicatorStyle::FileType => match sym {
1143+
Some('*') => None,
1144+
_ => sym,
1145+
},
1146+
IndicatorStyle::Slash => match sym {
1147+
Some('/') => Some('/'),
1148+
_ => None,
1149+
},
1150+
}
1151+
}
1152+
11211153
fn classify_file(path: &PathData) -> Option<char> {
11221154
let file_type = path.file_type()?;
11231155

0 commit comments

Comments
 (0)