rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs (ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r), fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like client of the process interface. search-test.nu: 42 cases match native tools.
192 lines
5.3 KiB
Rust
192 lines
5.3 KiB
Rust
use std::cell::OnceCell;
|
|
use std::ffi::OsString;
|
|
use std::fs::{FileType, Metadata};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use lscolors::{Colorable, LsColors, Style};
|
|
|
|
use crate::config::Config;
|
|
use crate::filesystem::strip_current_dir;
|
|
|
|
#[derive(Debug)]
|
|
enum DirEntryInner {
|
|
Normal(ignore::DirEntry),
|
|
// Broken symlinks reach us as walk errors rather than entries, so we carry
|
|
// over the depth the walker recorded on the error.
|
|
BrokenSymlink { path: PathBuf, depth: Option<usize> },
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct DirEntry {
|
|
inner: DirEntryInner,
|
|
metadata: OnceCell<Option<Metadata>>,
|
|
style: OnceCell<Option<Style>>,
|
|
}
|
|
|
|
impl DirEntry {
|
|
#[inline]
|
|
pub fn normal(e: ignore::DirEntry) -> Self {
|
|
Self {
|
|
inner: DirEntryInner::Normal(e),
|
|
metadata: OnceCell::new(),
|
|
style: OnceCell::new(),
|
|
}
|
|
}
|
|
|
|
pub fn broken_symlink(path: PathBuf, depth: Option<usize>) -> Self {
|
|
Self {
|
|
inner: DirEntryInner::BrokenSymlink { path, depth },
|
|
metadata: OnceCell::new(),
|
|
style: OnceCell::new(),
|
|
}
|
|
}
|
|
|
|
pub fn path(&self) -> &Path {
|
|
match &self.inner {
|
|
DirEntryInner::Normal(e) => e.path(),
|
|
DirEntryInner::BrokenSymlink { path, .. } => path.as_path(),
|
|
}
|
|
}
|
|
|
|
pub fn into_path(self) -> PathBuf {
|
|
match self.inner {
|
|
DirEntryInner::Normal(e) => e.into_path(),
|
|
DirEntryInner::BrokenSymlink { path, .. } => path,
|
|
}
|
|
}
|
|
|
|
/// Returns the path as it should be presented to the user.
|
|
/// When stripping `./` would leave the path starting with `-`, keep the original
|
|
/// (with `./`) so downstream tools don't interpret it as an option.
|
|
pub fn stripped_path(&self, config: &Config) -> &Path {
|
|
let path = self.path();
|
|
if config.strip_cwd_prefix {
|
|
let stripped = strip_current_dir(path);
|
|
if starts_with_dash(stripped) {
|
|
path
|
|
} else {
|
|
stripped
|
|
}
|
|
} else {
|
|
path
|
|
}
|
|
}
|
|
|
|
/// Returns the path as it should be presented to the user.
|
|
pub fn into_stripped_path(self, config: &Config) -> PathBuf {
|
|
if config.strip_cwd_prefix {
|
|
self.stripped_path(config).to_path_buf()
|
|
} else {
|
|
self.into_path()
|
|
}
|
|
}
|
|
|
|
pub fn file_type(&self) -> Option<FileType> {
|
|
match &self.inner {
|
|
DirEntryInner::Normal(e) => e.file_type(),
|
|
DirEntryInner::BrokenSymlink { .. } => self.metadata().map(|m| m.file_type()),
|
|
}
|
|
}
|
|
|
|
pub fn metadata(&self) -> Option<&Metadata> {
|
|
self.metadata
|
|
.get_or_init(|| match &self.inner {
|
|
DirEntryInner::Normal(e) => e.metadata().ok(),
|
|
DirEntryInner::BrokenSymlink { path, .. } => path.symlink_metadata().ok(),
|
|
})
|
|
.as_ref()
|
|
}
|
|
|
|
pub fn depth(&self) -> Option<usize> {
|
|
match &self.inner {
|
|
DirEntryInner::Normal(e) => Some(e.depth()),
|
|
DirEntryInner::BrokenSymlink { depth, .. } => *depth,
|
|
}
|
|
}
|
|
|
|
pub fn style(&self, ls_colors: &LsColors) -> Option<&Style> {
|
|
self.style
|
|
.get_or_init(|| ls_colors.style_for(self).cloned())
|
|
.as_ref()
|
|
}
|
|
}
|
|
|
|
fn starts_with_dash(path: &Path) -> bool {
|
|
path.as_os_str().as_encoded_bytes().first() == Some(&b'-')
|
|
}
|
|
|
|
impl PartialEq for DirEntry {
|
|
#[inline]
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.path() == other.path()
|
|
}
|
|
}
|
|
|
|
impl Eq for DirEntry {}
|
|
|
|
impl PartialOrd for DirEntry {
|
|
#[inline]
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for DirEntry {
|
|
#[inline]
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
self.path().cmp(other.path())
|
|
}
|
|
}
|
|
|
|
impl Colorable for DirEntry {
|
|
fn path(&self) -> PathBuf {
|
|
self.path().to_owned()
|
|
}
|
|
|
|
fn file_name(&self) -> OsString {
|
|
let name = match &self.inner {
|
|
DirEntryInner::Normal(e) => e.file_name(),
|
|
DirEntryInner::BrokenSymlink { path, .. } => {
|
|
// Path::file_name() only works if the last component is Normal,
|
|
// but we want it for all component types, so we open code it.
|
|
// Copied from LsColors::style_for_path_with_metadata().
|
|
path.components()
|
|
.next_back()
|
|
.map(|c| c.as_os_str())
|
|
.unwrap_or_else(|| path.as_os_str())
|
|
}
|
|
};
|
|
name.to_owned()
|
|
}
|
|
|
|
fn file_type(&self) -> Option<FileType> {
|
|
self.file_type()
|
|
}
|
|
|
|
fn metadata(&self) -> Option<Metadata> {
|
|
self.metadata().cloned()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::starts_with_dash;
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn dash_prefixed_paths_detected() {
|
|
assert!(starts_with_dash(Path::new("-rf")));
|
|
assert!(starts_with_dash(Path::new("--delete")));
|
|
assert!(starts_with_dash(Path::new("-")));
|
|
}
|
|
|
|
#[test]
|
|
fn safe_paths_not_flagged() {
|
|
assert!(!starts_with_dash(Path::new("foo")));
|
|
assert!(!starts_with_dash(Path::new("./foo")));
|
|
assert!(!starts_with_dash(Path::new("sub/-rf")));
|
|
assert!(!starts_with_dash(Path::new("")));
|
|
assert!(!starts_with_dash(Path::new(" -rf")));
|
|
}
|
|
}
|