Skip to content

Commit de27835

Browse files
authored
Merge pull request sharkdp#812 from yyogo/master
Append trailing path separators to directories
2 parents dcde6d3 + 41affe1 commit de27835

10 files changed

+282
-197
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
## Changes
1515

1616
- Changed `-u` flag to be equivalent to `-HI`. Multiple `-u` flags still allowed but do nothing, see #840 (@jacksontheel)
17+
- Directories are now printed with an additional path separator at the end: `foo/bar/`, see #436 and #812 (@yyogo)
1718

1819
## Other
1920

src/config.rs

+3
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ pub struct Config {
111111
/// The separator used to print file paths.
112112
pub path_separator: Option<String>,
113113

114+
/// The actual separator, either the system default separator or `path_separator`
115+
pub actual_path_separator: String,
116+
114117
/// The maximum number of search results
115118
pub max_results: Option<usize>,
116119

src/dir_entry.rs

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use std::{
2+
fs::{FileType, Metadata},
3+
path::{Path, PathBuf},
4+
};
5+
6+
use once_cell::unsync::OnceCell;
7+
8+
enum DirEntryInner {
9+
Normal(ignore::DirEntry),
10+
BrokenSymlink(PathBuf),
11+
}
12+
13+
pub struct DirEntry {
14+
inner: DirEntryInner,
15+
metadata: OnceCell<Option<Metadata>>,
16+
}
17+
18+
impl DirEntry {
19+
#[inline]
20+
pub fn normal(e: ignore::DirEntry) -> Self {
21+
Self {
22+
inner: DirEntryInner::Normal(e),
23+
metadata: OnceCell::new(),
24+
}
25+
}
26+
27+
pub fn broken_symlink(path: PathBuf) -> Self {
28+
Self {
29+
inner: DirEntryInner::BrokenSymlink(path),
30+
metadata: OnceCell::new(),
31+
}
32+
}
33+
34+
pub fn path(&self) -> &Path {
35+
match &self.inner {
36+
DirEntryInner::Normal(e) => e.path(),
37+
DirEntryInner::BrokenSymlink(pathbuf) => pathbuf.as_path(),
38+
}
39+
}
40+
41+
pub fn into_path(self) -> PathBuf {
42+
match self.inner {
43+
DirEntryInner::Normal(e) => e.into_path(),
44+
DirEntryInner::BrokenSymlink(p) => p,
45+
}
46+
}
47+
48+
pub fn file_type(&self) -> Option<FileType> {
49+
match &self.inner {
50+
DirEntryInner::Normal(e) => e.file_type(),
51+
DirEntryInner::BrokenSymlink(_) => self.metadata().map(|m| m.file_type()),
52+
}
53+
}
54+
55+
pub fn metadata(&self) -> Option<&Metadata> {
56+
self.metadata
57+
.get_or_init(|| match &self.inner {
58+
DirEntryInner::Normal(e) => e.metadata().ok(),
59+
DirEntryInner::BrokenSymlink(path) => path.symlink_metadata().ok(),
60+
})
61+
.as_ref()
62+
}
63+
64+
pub fn depth(&self) -> Option<usize> {
65+
match &self.inner {
66+
DirEntryInner::Normal(e) => Some(e.depth()),
67+
DirEntryInner::BrokenSymlink(_) => None,
68+
}
69+
}
70+
}
71+
72+
impl PartialEq for DirEntry {
73+
#[inline]
74+
fn eq(&self, other: &Self) -> bool {
75+
self.path() == other.path()
76+
}
77+
}
78+
impl Eq for DirEntry {}
79+
80+
impl PartialOrd for DirEntry {
81+
#[inline]
82+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
83+
self.path().partial_cmp(other.path())
84+
}
85+
}
86+
87+
impl Ord for DirEntry {
88+
#[inline]
89+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
90+
self.path().cmp(other.path())
91+
}
92+
}

src/exec/job.rs

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use std::path::PathBuf;
21
use std::sync::mpsc::Receiver;
32
use std::sync::{Arc, Mutex};
43

4+
use crate::dir_entry::DirEntry;
55
use crate::error::print_error;
66
use crate::exit_codes::{merge_exitcodes, ExitCode};
77
use crate::walk::WorkerResult;
@@ -25,8 +25,8 @@ pub fn job(
2525

2626
// Obtain the next result from the receiver, else if the channel
2727
// has closed, exit from the loop
28-
let value: PathBuf = match lock.recv() {
29-
Ok(WorkerResult::Entry(path)) => path,
28+
let dir_entry: DirEntry = match lock.recv() {
29+
Ok(WorkerResult::Entry(dir_entry)) => dir_entry,
3030
Ok(WorkerResult::Error(err)) => {
3131
if show_filesystem_errors {
3232
print_error(err.to_string());
@@ -39,7 +39,7 @@ pub fn job(
3939
// Drop the lock so that other threads can read from the receiver.
4040
drop(lock);
4141
// Generate a command, execute it and store its exit code.
42-
results.push(cmd.execute(&value, Arc::clone(&out_perm), buffer_output))
42+
results.push(cmd.execute(dir_entry.path(), Arc::clone(&out_perm), buffer_output))
4343
}
4444
// Returns error in case of any error.
4545
merge_exitcodes(results)
@@ -51,15 +51,17 @@ pub fn batch(
5151
show_filesystem_errors: bool,
5252
limit: usize,
5353
) -> ExitCode {
54-
let paths = rx.iter().filter_map(|value| match value {
55-
WorkerResult::Entry(path) => Some(path),
56-
WorkerResult::Error(err) => {
57-
if show_filesystem_errors {
58-
print_error(err.to_string());
54+
let paths = rx
55+
.into_iter()
56+
.filter_map(|worker_result| match worker_result {
57+
WorkerResult::Entry(dir_entry) => Some(dir_entry.into_path()),
58+
WorkerResult::Error(err) => {
59+
if show_filesystem_errors {
60+
print_error(err.to_string());
61+
}
62+
None
5963
}
60-
None
61-
}
62-
});
64+
});
6365
if limit == 0 {
6466
// no limit
6567
return cmd.execute_batch(paths);

src/filesystem.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
99

1010
use normpath::PathExt;
1111

12-
use crate::walk;
12+
use crate::dir_entry;
1313

1414
pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
1515
if path.is_absolute() {
@@ -51,7 +51,7 @@ pub fn is_executable(_: &fs::Metadata) -> bool {
5151
false
5252
}
5353

54-
pub fn is_empty(entry: &walk::DirEntry) -> bool {
54+
pub fn is_empty(entry: &dir_entry::DirEntry) -> bool {
5555
if let Some(file_type) = entry.file_type() {
5656
if file_type.is_dir() {
5757
if let Ok(mut entries) = fs::read_dir(entry.path()) {

src/filetypes.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
use crate::dir_entry;
12
use crate::filesystem;
2-
use crate::walk;
33

44
/// Whether or not to show
55
#[derive(Default)]
@@ -14,7 +14,7 @@ pub struct FileTypes {
1414
}
1515

1616
impl FileTypes {
17-
pub fn should_ignore(&self, entry: &walk::DirEntry) -> bool {
17+
pub fn should_ignore(&self, entry: &dir_entry::DirEntry) -> bool {
1818
if let Some(ref entry_type) = entry.file_type() {
1919
(!self.files && entry_type.is_file())
2020
|| (!self.directories && entry_type.is_dir())

src/main.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod app;
22
mod config;
3+
mod dir_entry;
34
mod error;
45
mod exec;
56
mod exit_codes;
@@ -221,6 +222,9 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
221222
let path_separator = matches
222223
.value_of("path-separator")
223224
.map_or_else(filesystem::default_path_separator, |s| Some(s.to_owned()));
225+
let actual_path_separator = path_separator
226+
.clone()
227+
.unwrap_or_else(|| std::path::MAIN_SEPARATOR.to_string());
224228
check_path_separator_length(path_separator.as_deref())?;
225229

226230
let size_limits = extract_size_limits(&matches)?;
@@ -367,6 +371,7 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result<Co
367371
owner_constraint,
368372
show_filesystem_errors: matches.is_present("show-errors"),
369373
path_separator,
374+
actual_path_separator,
370375
max_results: matches
371376
.value_of("max-results")
372377
.map(|n| n.parse::<usize>())

src/output.rs

+56-17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::path::Path;
55
use lscolors::{Indicator, LsColors, Style};
66

77
use crate::config::Config;
8+
use crate::dir_entry::DirEntry;
89
use crate::error::print_error;
910
use crate::exit_codes::ExitCode;
1011
use crate::filesystem::strip_current_dir;
@@ -13,18 +14,21 @@ fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
1314
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
1415
}
1516

16-
// TODO: this function is performance critical and can probably be optimized
17-
pub fn print_entry<W: Write>(stdout: &mut W, entry: &Path, config: &Config) {
18-
let path = if config.strip_cwd_prefix {
19-
strip_current_dir(entry)
17+
fn stripped_path<'a>(entry: &'a DirEntry, config: &Config) -> &'a Path {
18+
let path = entry.path();
19+
if config.strip_cwd_prefix {
20+
strip_current_dir(path)
2021
} else {
21-
entry
22-
};
22+
path
23+
}
24+
}
2325

26+
// TODO: this function is performance critical and can probably be optimized
27+
pub fn print_entry<W: Write>(stdout: &mut W, entry: &DirEntry, config: &Config) {
2428
let r = if let Some(ref ls_colors) = config.ls_colors {
25-
print_entry_colorized(stdout, path, config, ls_colors)
29+
print_entry_colorized(stdout, entry, config, ls_colors)
2630
} else {
27-
print_entry_uncolorized(stdout, path, config)
31+
print_entry_uncolorized(stdout, entry, config)
2832
};
2933

3034
if let Err(e) = r {
@@ -38,15 +42,39 @@ pub fn print_entry<W: Write>(stdout: &mut W, entry: &Path, config: &Config) {
3842
}
3943
}
4044

45+
// Display a trailing slash if the path is a directory and the config option is enabled.
46+
// If the path_separator option is set, display that instead.
47+
// The trailing slash will not be colored.
48+
#[inline]
49+
fn print_trailing_slash<W: Write>(
50+
stdout: &mut W,
51+
entry: &DirEntry,
52+
config: &Config,
53+
style: Option<&Style>,
54+
) -> io::Result<()> {
55+
if entry.file_type().map_or(false, |ft| ft.is_dir()) {
56+
write!(
57+
stdout,
58+
"{}",
59+
style
60+
.map(Style::to_ansi_term_style)
61+
.unwrap_or_default()
62+
.paint(&config.actual_path_separator)
63+
)?;
64+
}
65+
Ok(())
66+
}
67+
4168
// TODO: this function is performance critical and can probably be optimized
4269
fn print_entry_colorized<W: Write>(
4370
stdout: &mut W,
44-
path: &Path,
71+
entry: &DirEntry,
4572
config: &Config,
4673
ls_colors: &LsColors,
4774
) -> io::Result<()> {
4875
// Split the path between the parent and the last component
4976
let mut offset = 0;
77+
let path = stripped_path(entry, config);
5078
let path_str = path.to_string_lossy();
5179

5280
if let Some(parent) = path.parent() {
@@ -74,11 +102,18 @@ fn print_entry_colorized<W: Write>(
74102
}
75103

76104
let style = ls_colors
77-
.style_for_path(path)
105+
.style_for_path_with_metadata(path, entry.metadata())
78106
.map(Style::to_ansi_term_style)
79107
.unwrap_or_default();
80108
write!(stdout, "{}", style.paint(&path_str[offset..]))?;
81109

110+
print_trailing_slash(
111+
stdout,
112+
entry,
113+
config,
114+
ls_colors.style_for_indicator(Indicator::Directory),
115+
)?;
116+
82117
if config.null_separator {
83118
write!(stdout, "\0")?;
84119
} else {
@@ -91,42 +126,46 @@ fn print_entry_colorized<W: Write>(
91126
// TODO: this function is performance critical and can probably be optimized
92127
fn print_entry_uncolorized_base<W: Write>(
93128
stdout: &mut W,
94-
path: &Path,
129+
entry: &DirEntry,
95130
config: &Config,
96131
) -> io::Result<()> {
97132
let separator = if config.null_separator { "\0" } else { "\n" };
133+
let path = stripped_path(entry, config);
98134

99135
let mut path_string = path.to_string_lossy();
100136
if let Some(ref separator) = config.path_separator {
101137
*path_string.to_mut() = replace_path_separator(&path_string, separator);
102138
}
103-
write!(stdout, "{}{}", path_string, separator)
139+
write!(stdout, "{}", path_string)?;
140+
print_trailing_slash(stdout, entry, config, None)?;
141+
write!(stdout, "{}", separator)
104142
}
105143

106144
#[cfg(not(unix))]
107145
fn print_entry_uncolorized<W: Write>(
108146
stdout: &mut W,
109-
path: &Path,
147+
entry: &DirEntry,
110148
config: &Config,
111149
) -> io::Result<()> {
112-
print_entry_uncolorized_base(stdout, path, config)
150+
print_entry_uncolorized_base(stdout, entry, config)
113151
}
114152

115153
#[cfg(unix)]
116154
fn print_entry_uncolorized<W: Write>(
117155
stdout: &mut W,
118-
path: &Path,
156+
entry: &DirEntry,
119157
config: &Config,
120158
) -> io::Result<()> {
121159
use std::os::unix::ffi::OsStrExt;
122160

123161
if config.interactive_terminal || config.path_separator.is_some() {
124162
// Fall back to the base implementation
125-
print_entry_uncolorized_base(stdout, path, config)
163+
print_entry_uncolorized_base(stdout, entry, config)
126164
} else {
127165
// Print path as raw bytes, allowing invalid UTF-8 filenames to be passed to other processes
128166
let separator = if config.null_separator { b"\0" } else { b"\n" };
129-
stdout.write_all(path.as_os_str().as_bytes())?;
167+
stdout.write_all(stripped_path(entry, config).as_os_str().as_bytes())?;
168+
print_trailing_slash(stdout, entry, config, None)?;
130169
stdout.write_all(separator)
131170
}
132171
}

0 commit comments

Comments
 (0)