ls: ls -lF symlink target indicators (#11554)

---------

Co-authored-by: Guillem L. Jara <4lon3ly0@tutanota.com>
Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
joknarf
2026-05-28 13:08:08 +02:00
committed by GitHub
co-authored by Guillem L. Jara Sylvestre Ledru
parent 179b977b93
commit b22013ab79
3 changed files with 253 additions and 81 deletions
+149 -3
View File
@@ -1280,7 +1280,14 @@ fn test_ls_long_symlink_color() {
];
// We are only interested in lines or the ls output that are symlinks. These start with "lrwx".
let result = scene.ucmd().arg("-laR").arg("--color").arg(".").succeeds();
// Use --file-type to ensure symlink targets are stat'd and colored
let result = scene
.ucmd()
.arg("-laR")
.arg("--color")
.arg("--file-type")
.arg(".")
.succeeds();
let mut result_lines = result
.stdout_str()
.lines()
@@ -1521,6 +1528,41 @@ fn test_ls_dangling_symlink_or_and_missing_colors() {
assert_eq!(captures.name("target").unwrap().as_str(), "34");
}
#[test]
#[cfg(unix)]
fn test_ls_symlink_to_dir_with_mi_colors() {
// When LS_COLORS contains mi=, ln=, di=, ls -lp should stat the symlink target,
// color the link with ln color, the target with di color, and append '/' to the target.
use std::os::unix::fs::symlink;
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.mkdir("target_dir");
symlink("target_dir", at.plus("link")).unwrap();
let stdout = ts
.ucmd()
.env("LS_COLORS", "mi=41:ln=1;36:di=1;34")
.arg("-lp")
.arg("--color=always")
.arg("link")
.succeeds()
.stdout_str()
.to_string();
// Regex to capture link color and target color
let color_regex = Regex::new(
r"\x1b\[0m\x1b\[(?P<link>[0-9;]*)[m]link\x1b\[0m -> \x1b\[(?P<target>[0-9;]*)[m]target_dir\x1b\[0m/",
)
.unwrap();
let captures = color_regex
.captures(&stdout)
.expect("failed to capture symlink colors");
assert_eq!(captures.name("link").unwrap().as_str(), "1;36");
assert_eq!(captures.name("target").unwrap().as_str(), "1;34");
}
#[test]
/// Mirrors GNU `tests/ls/ls-misc.pl::sl-dangle4`.
fn test_ls_dangling_symlink_ln_or_priority() {
@@ -3400,6 +3442,110 @@ fn test_ls_indicator_style() {
}
}
#[test]
#[cfg(not(windows))]
fn test_ls_indicator_style_symlink_target_long() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("dir");
assert!(at.dir_exists("dir"));
at.symlink_dir("dir", "dir_link");
assert!(at.is_symlink("dir_link"));
scene
.ucmd()
.arg("--classify")
.arg("-l")
.arg("dir_link")
.succeeds()
.stdout_contains("dir_link -> ")
.stdout_does_not_contain("dir_link@ -> ")
.stdout_contains("/dir/");
}
#[test]
#[cfg(unix)]
fn test_ls_indicator_style_filetype_symlink_target_long() {
// GNU `ls -l --file-type` does append `/` to a symlink target that resolves to a
// directory unlike `ls -lp`
use std::os::unix::fs::symlink;
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("dir");
assert!(at.dir_exists("dir"));
// Use a relative-path symlink so the displayed target matches GNU output.
symlink("dir", at.plus("dir_link")).unwrap();
assert!(at.is_symlink("dir_link"));
scene
.ucmd()
.arg("--file-type")
.arg("-l")
.arg("dir_link")
.succeeds()
.stdout_contains("dir_link -> dir/")
.stdout_does_not_contain("dir_link/");
}
#[test]
#[cfg(unix)]
fn test_ls_indicator_style_filetype_symlink_to_executable_target_long() {
use std::fs;
use std::os::unix::fs::{PermissionsExt, symlink};
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("exec_target");
let mut perms = fs::metadata(at.plus("exec_target")).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(at.plus("exec_target"), perms).unwrap();
symlink("exec_target", at.plus("link")).unwrap();
assert!(at.is_symlink("link"));
scene
.ucmd()
.arg("--file-type")
.arg("-l")
.arg("link")
.succeeds()
.stdout_contains("link -> exec_target")
.stdout_does_not_contain("exec_target*");
}
#[test]
#[cfg(unix)]
fn test_ls_indicator_style_slash_symlink_target_long() {
// GNU `ls -lp` does NOT append `/` to a symlink target that resolves to a
// directory — the slash indicator style only applies to real directories.
use std::os::unix::fs::symlink;
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir("dir");
assert!(at.dir_exists("dir"));
// Use a relative-path symlink so the displayed target matches GNU output.
symlink("dir", at.plus("dir_link")).unwrap();
assert!(at.is_symlink("dir_link"));
scene
.ucmd()
.arg("-lp")
.arg("dir_link")
.succeeds()
.stdout_contains("dir_link -> dir\n")
.stdout_does_not_contain("dir_link/")
.stdout_does_not_contain("-> dir/");
}
// Essentially the same test as above, but only test symlinks and directories,
// not pipes or sockets.
#[test]
@@ -5024,7 +5170,7 @@ fn test_symlink_target_extension_color() {
at.touch("archive.tar.gz");
at.relative_symlink_file("archive.tar.gz", "link");
let out = ucmd
.env("LS_COLORS", "*.tar.gz=31")
.env("LS_COLORS", "*.tar.gz=31:or=33")
.args(&["-l", "--color=always", "link"])
.succeeds()
.stdout_move_str();
@@ -6045,7 +6191,7 @@ fn test_ls_hyperlink_symlink_target_handling() {
let result = scene
.ucmd()
.args(&["-l", "--hyperlink", "--color"])
.args(&["-l", "--hyperlink", "--color", "--file-type"])
.succeeds();
let output = result.stdout_str();