diff --git a/Cargo.toml b/Cargo.toml index 5d7bbad9b..85aad89a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,6 +128,7 @@ uzers = "0.12.1" windows-sys = { version = "0.61.2", features = [ "Win32_System_Console", "Win32_Foundation", + "Win32_Storage_FileSystem", ] } [build-dependencies] diff --git a/README.md b/README.md index fea6e62bf..24a6b68db 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ eza’s options are almost, but not quite, entirely unlike `ls`’s. Quick overv - **-x**, **--across**: sort the grid across, rather than downwards - **-F**, **--classify=(when)**: display type indicator by file names (always, auto, never) - **--colo[u]r=(when)**: when to use terminal colours (always, auto, never) -- **--colo[u]r-scale=(field)**: highlight levels of `field` distinctly(all, age, size) +- **--colo[u]r-scale=(field)**: highlight levels of `field` distinctly (all, age, size) - **--color-scale-mode=(mode)**: use gradient or fixed colors in --color-scale. valid options are `fixed` or `gradient` - **--icons=(when)**: when to display icons (always, auto, never) - **--hyperlink=(when)**: when to display entries as hyperlinks (always, auto, never) @@ -122,6 +122,7 @@ eza’s options are almost, but not quite, entirely unlike `ls`’s. Quick overv Click to expand - **-a**, **--all**: show hidden and 'dot' files +- **--show-dotfiles**: show dot-prefixed files without showing other hidden files - **-d**, **--treat-dirs-as-files**: list directories like regular files - **-L**, **--level=(depth)**: limit the depth of recursion - **-r**, **--reverse**: reverse the sort order diff --git a/completions/bash/eza b/completions/bash/eza index f1033d10e..b2adc6bd4 100644 --- a/completions/bash/eza +++ b/completions/bash/eza @@ -4,7 +4,7 @@ _eza() { prev=${COMP_WORDS[COMP_CWORD-1]} case "$prev" in - --help|-v|--version|--smart-group) + --help|-v|--version|--smart-group|--show-dotfiles) return ;; diff --git a/completions/fish/eza.fish b/completions/fish/eza.fish index 98e538b13..0b26d4759 100644 --- a/completions/fish/eza.fish +++ b/completions/fish/eza.fish @@ -57,6 +57,7 @@ complete -c eza -l group-directories-last -d "Sort directories after other files complete -c eza -l git-ignore -d "Ignore files mentioned in '.gitignore'" complete -c eza -s a -l all -d "Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories" complete -c eza -s A -l almost-all -d "Equivalent to --all; included for compatibility with `ls -A`" +complete -c eza -l show-dotfiles -d "Show dot-prefixed files without showing other hidden files" complete -c eza -s d -l treat-dirs-as-files -d "List directories like regular files" complete -c eza -s L -l level -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9" complete -c eza -s w -l width -d "Limits column output of grid, 0 implies auto-width" diff --git a/completions/nush/eza.nu b/completions/nush/eza.nu index 6558c9189..8e7e1f6d7 100644 --- a/completions/nush/eza.nu +++ b/completions/nush/eza.nu @@ -25,6 +25,7 @@ export extern "eza" [ --git-ignore # Ignore files mentioned in '.gitignore' --all(-a) # Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories --almost-all(-A) # Equivalent to --all; included for compatibility with `ls -A` + --show-dotfiles # Show dot-prefixed files without showing other hidden files --treat-dirs-as-files(-d) # List directories like regular files --level(-L): string # Limit the depth of recursion --width(-w) # Limits column output of grid, 0 implies auto-width diff --git a/completions/pwsh/_eza.ps1 b/completions/pwsh/_eza.ps1 index b7d3fd703..798c3ea7e 100644 --- a/completions/pwsh/_eza.ps1 +++ b/completions/pwsh/_eza.ps1 @@ -172,6 +172,7 @@ Register-ArgumentCompleter -Native -CommandName 'eza' -ScriptBlock { [CompletionResult]::new('--all' ,'filter' , [CompletionResultType]::ParameterName, 'show hidden and ''dot'' files. Use this twice to also show the ''.'' and ''..'' directories') # [CompletionResult]::new('-A' ,'filter' , [CompletionResultType]::ParameterName, 'equivalent to --all; included for compatibility with `ls -A`') # [CompletionResult]::new('--almost-all' ,'filter' , [CompletionResultType]::ParameterName, 'equivalent to --all; included for compatibility with `ls -A`') + [CompletionResult]::new('--show-dotfiles' ,'filter' , [CompletionResultType]::ParameterName, 'show dot-prefixed files without showing other hidden files') # [CompletionResult]::new('-d' ,'filter' , [CompletionResultType]::ParameterName, 'list directories as files; don''t list their contents') [CompletionResult]::new('--treat-dirs-as-files' ,'filter' , [CompletionResultType]::ParameterName, 'list directories as files; don''t list their contents') # [CompletionResult]::new('-D' ,'filter' , [CompletionResultType]::ParameterName, 'list only directories') diff --git a/man/eza.1.md b/man/eza.1.md index dc14382fb..ce220f8a3 100644 --- a/man/eza.1.md +++ b/man/eza.1.md @@ -144,6 +144,9 @@ Use this twice to also show the ‘`.`’ and ‘`..`’ directories. `-A`, `--almost-all` : Equivalent to --all; included for compatibility with `ls -A`. +`--show-dotfiles` +: Show dot-prefixed files without showing other hidden files. + `-d`, `--treat-dirs-as-files` : This flag, inherited from `ls`, changes how `eza` handles directory arguments. diff --git a/src/fs/dir.rs b/src/fs/dir.rs index 70ff56e80..56801dec5 100644 --- a/src/fs/dir.rs +++ b/src/fs/dir.rs @@ -89,6 +89,8 @@ impl Dir { inner: self.contents.iter(), dir: self, dotfiles: dots.shows_dotfiles(), + #[cfg(windows)] + windows_hidden: dots.shows_windows_hidden(), dots: dots.dots(), git, git_ignoring, @@ -122,6 +124,10 @@ pub struct Files<'dir, 'ig> { /// Whether to include dotfiles in the list. dotfiles: bool, + #[cfg(windows)] + /// Whether Windows hidden-attribute entries should be visible. + windows_hidden: bool, + /// Whether the `.` or `..` directories should be produced first, before /// any files have been listed. dots: DotsNext, @@ -184,7 +190,7 @@ impl<'dir> Files<'dir, '_> { // Windows has its own concept of hidden files, when dotfiles are // hidden Windows hidden files should also be filtered out #[cfg(windows)] - if !self.dotfiles && file.attributes().map_or(false, |a| a.hidden) { + if !self.windows_hidden && file.attributes().is_some_and(|a| a.hidden) { continue; } @@ -244,6 +250,9 @@ pub enum DotFilter { /// Show files and dotfiles, but hide `.` and `..`. Dotfiles, + /// Show dotfiles by name only, but keep platform hidden-attribute files hidden. + DotfilesByName, + /// Just show files, hiding anything beginning with a dot. #[default] JustFiles, @@ -255,16 +264,84 @@ impl DotFilter { match self { Self::JustFiles => false, Self::Dotfiles => true, + Self::DotfilesByName => true, Self::DotfilesAndDots => true, } } + #[cfg(windows)] + /// Whether this filter should reveal Windows hidden-attribute entries. + fn shows_windows_hidden(self) -> bool { + cfg!(windows) && matches!(self, Self::Dotfiles | Self::DotfilesAndDots) + } + /// Whether this filter should add dot directories to a listing. fn dots(self) -> DotsNext { match self { Self::JustFiles => DotsNext::Files, Self::Dotfiles => DotsNext::Files, + Self::DotfilesByName => DotsNext::Files, Self::DotfilesAndDots => DotsNext::Dot, } } } + +#[cfg(all(test, windows))] +mod tests { + use super::*; + use std::fs; + use std::os::windows::ffi::OsStrExt; + use std::path::Path; + use std::time::{SystemTime, UNIX_EPOCH}; + use windows_sys::Win32::Storage::FileSystem::{ + FILE_ATTRIBUTE_HIDDEN, GetFileAttributesW, SetFileAttributesW, + }; + + fn unique_temp_dir() -> std::path::PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + .as_nanos(); + let path = std::env::temp_dir().join(format!("eza-show-dotfiles-{nanos}")); + fs::create_dir_all(&path).expect("failed to create temp dir"); + path + } + + fn set_hidden(path: &Path) { + let wide = path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + unsafe { + let attrs = GetFileAttributesW(wide.as_ptr()); + assert_ne!(attrs, u32::MAX); + assert_ne!( + SetFileAttributesW(wide.as_ptr(), attrs | FILE_ATTRIBUTE_HIDDEN), + 0 + ); + } + } + + #[test] + fn show_dotfiles_does_not_show_windows_hidden_attributes() { + let path = unique_temp_dir(); + fs::write(path.join(".dotfile"), "").unwrap(); + fs::write(path.join("_underscore"), "").unwrap(); + fs::write(path.join("hidden.txt"), "").unwrap(); + set_hidden(&path.join("hidden.txt")); + + let dir = Dir::read_dir(path.clone()).unwrap(); + + let names: Vec<_> = dir + .files(DotFilter::DotfilesByName, None, false, false, false) + .map(|file| file.name) + .collect(); + + assert!(names.contains(&".dotfile".to_string())); + assert!(names.contains(&"_underscore".to_string())); + assert!(!names.contains(&"hidden.txt".to_string())); + + let _ = fs::remove_dir_all(path); + } +} diff --git a/src/options/filter.rs b/src/options/filter.rs index 1ecea7d8a..2c2a2ccfe 100644 --- a/src/options/filter.rs +++ b/src/options/filter.rs @@ -99,14 +99,17 @@ impl DotFilter { pub fn deduce(matches: &ArgMatches, strict: bool) -> Result { let all_count = matches.get_count("all"); let has_almost_all = matches.get_flag("almost-all"); + let show_dotfiles = matches.get_flag("show-dotfiles"); - match (all_count, has_almost_all) { - (0, false) => Ok(Self::JustFiles), + if has_almost_all { + return Ok(Self::Dotfiles); + } - // either a single --all or at least one --almost-all is given - (1, _) | (0, true) => Ok(Self::Dotfiles), - // more than one --all - (c, _) => { + match all_count { + 0 if show_dotfiles => Ok(Self::DotfilesByName), + 0 => Ok(Self::JustFiles), + 1 => Ok(Self::Dotfiles), + c => { if matches.get_flag("tree") { Err(OptionsError::TreeAllAll) } else if strict && c > 2 { @@ -250,6 +253,22 @@ mod tests { ); } + #[test] + fn deduce_dot_filter_show_dotfiles() { + assert_eq!( + DotFilter::deduce(&mock_cli(vec!["--show-dotfiles"]), false), + Ok(DotFilter::DotfilesByName) + ); + } + + #[test] + fn deduce_dot_filter_show_dotfiles_and_all() { + assert_eq!( + DotFilter::deduce(&mock_cli(vec!["--show-dotfiles", "--all"]), false), + Ok(DotFilter::Dotfiles) + ); + } + #[test] fn deduce_sort_field_default() { assert_eq!( diff --git a/src/options/parser.rs b/src/options/parser.rs index 231c27968..64d743acc 100644 --- a/src/options/parser.rs +++ b/src/options/parser.rs @@ -94,6 +94,7 @@ pub fn get_command() -> clap::Command { .next_help_heading("FILTERING OPTIONS") .arg(arg!(-a --all... "show hidden files. Use this twice to also show the '.' and '..' directories")) .arg(arg!(-A --"almost-all" "equivalent to --all; included for compatibility with `ls -A`")) + .arg(arg!(--"show-dotfiles" "show dot-prefixed files without showing other hidden files")) .arg(arg!(-d --"treat-dirs-as-files" "treat directories as files; don't list their contents") .alias("list-dirs") // TODO: compat alias to remove (above flag published in v0.23.4 / 2025-10-03) .conflicts_with_all(["recurse", "tree"])) diff --git a/tests/ptests/ptest_2439b7d68089135b.stdout b/tests/ptests/ptest_2439b7d68089135b.stdout index 5bd9aa127..2ecdd11ee 100644 --- a/tests/ptests/ptest_2439b7d68089135b.stdout +++ b/tests/ptests/ptest_2439b7d68089135b.stdout @@ -32,6 +32,7 @@ DISPLAY OPTIONS: FILTERING OPTIONS: -a, --all... show hidden files. Use this twice to also show the '.' and '..' directories -A, --almost-all equivalent to --all; included for compatibility with `ls -A` + --show-dotfiles show dot-prefixed files without showing other hidden files -d, --treat-dirs-as-files treat directories as files; don't list their contents -D, --only-dirs list only directories -f, --only-files list only files