@ 261104.1

261028.1.2 · 014b1523953b0067a8c414d97aac13f3744c2dd7

Merge upstream/master; drop changelog note for zsh --format

Keep --format and the hidden --literal alias in the zsh completion, alongside upstream --exact. Omit the changelog line per review.

reading declarations

.github/workflows/CICD.yml

@@ -224,7 +224,7 @@ jobs:      - name: "Artifact upload: tarball"       id: upload-tarball-      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7+      uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7       with:         name: ${{ steps.package.outputs.PKG_NAME }}         path: ${{ steps.package.outputs.PKG_PATH }}@@ -232,7 +232,7 @@ jobs:      - name: "Artifact upload: Debian package"       id: upload-deb-      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7+      uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7       if: steps.debian-package.outputs.DPKG_NAME       with:         name: ${{ steps.debian-package.outputs.DPKG_NAME }}

CHANGELOG.md

@@ -4,11 +4,13 @@ -  ## Bugfixes+- Make `--ignore` and `--no-hidden` after `-u` override only their half of `-u`, see #2192 (@NavpreetSSidhu)+- Fix inline case-insensitive regex flags incorrectly making other `--and` patterns case-sensitive, see #2184 (@mabunemeh) - Accept `--format` templates that start with `-` when passed as a separate argument, see #2126 (@vulragrag-star) - Don't incorrectly escape newlines in error messages, see #2104 - Restore jemalloc as default allocator on supported systems - Fix the `cargo binstall` release URL, which named the crate (`fd-find`) instead of the binary (`fd`) and fell back to a third-party mirror-- Suggest the `--format` option and the hidden `--literal` alias in the zsh completion, which omitted both+- Fix integer overflow when parsing size constraints, rejecting values that exceed `u64::MAX` instead of panicking, see #2196 (@parneetsingh022).  # 10.5.0 @@ -27,6 +29,7 @@ - Also fire the "search pattern contains a path separator" diagnostic for `--and` patterns, not only the primary positional pattern. `--and` patterns are matched against the file name just like the primary pattern, so a path separator in them silently returned zero results. See #1873. - Fix bug where passing "-" as a directory argument didn't actually search that directory, see #849 (@Sean-Kenneth-Doherty). - Fix panic when `--changed-before`/`--changed-within` is given an out-of-range `@` Unix timestamp; the value is now rejected gracefully, see #2081 (@nikolauspschuetz).+- Various fixes and updates to zsh completion  # 10.4.2 

Cargo.lock

@@ -172,7 +172,7 @@ dependencies = [  "heck",  "proc-macro2",  "quote",- "syn",+ "syn 2.0.117", ]  [[package]]@@ -252,7 +252,7 @@ dependencies = [  "proc-macro-error2",  "proc-macro2",  "quote",- "syn",+ "syn 2.0.117", ]  [[package]]@@ -505,7 +505,7 @@ dependencies = [  "jiff-core",  "proc-macro2",  "quote",- "syn",+ "syn 2.0.117", ]  [[package]]@@ -654,7 +654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [  "proc-macro2",- "syn",+ "syn 2.0.117", ]  [[package]]@@ -676,7 +676,7 @@ dependencies = [  "proc-macro-error-attr2",  "proc-macro2",  "quote",- "syn",+ "syn 2.0.117", ]  [[package]]@@ -786,7 +786,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [  "proc-macro2",  "quote",- "syn",+ "syn 2.0.117", ]  [[package]]@@ -825,6 +825,17 @@ dependencies = [  "unicode-ident", ] +[[package]]+name = "syn"+version = "3.0.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]+ [[package]] name = "tempfile" version = "3.27.0"@@ -850,34 +861,34 @@ dependencies = [  [[package]] name = "test-case"-version = "3.3.1"+version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"+checksum = "124953e7f67cb0b2fcfb87e899e4ae5a64fb68e2e160767933cc67b646fb0042" dependencies = [  "test-case-macros", ]  [[package]] name = "test-case-core"-version = "3.3.1"+version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"+checksum = "bd097615b407247e102046ba34426bb594923ab3554cf0f3cdfed050f1a5a3e8" dependencies = [  "cfg-if",  "proc-macro2",  "quote",- "syn",+ "syn 3.0.6", ]  [[package]] name = "test-case-macros"-version = "3.3.1"+version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"+checksum = "f4ce35ece947bccac166e1ded639133827a953f69a48ba134d17f91108333082" dependencies = [  "proc-macro2",  "quote",- "syn",+ "syn 3.0.6",  "test-case-core", ] @@ -898,7 +909,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [  "proc-macro2",  "quote",- "syn",+ "syn 2.0.117", ]  [[package]]@@ -1151,7 +1162,7 @@ dependencies = [  "heck",  "indexmap",  "prettyplease",- "syn",+ "syn 2.0.117",  "wasm-metadata",  "wit-bindgen-core",  "wit-component",@@ -1167,7 +1178,7 @@ dependencies = [  "prettyplease",  "proc-macro2",  "quote",- "syn",+ "syn 2.0.117",  "wit-bindgen-core",  "wit-bindgen-rust", ]

contrib/completion/_fd

@@ -22,7 +22,7 @@ _fd() {    fd_types=(     {f,file}'\:"regular files"'-    {d,directory}'\:"directories"'+    {d,dir,directory}'\:"directories"'     {l,symlink}'\:"symbolic links"'     {e,empty}'\:"empty files or directories"'     {x,executable}'\:"executable (files)"'@@ -54,12 +54,12 @@ _fd() {      + '(no-ignore-full)' # all ignore files     '(no-ignore-partial)'{-I,--no-ignore}"[don't respect .(git|fd)ignore and global ignore files]"-    $no'(no-ignore-partial)*'{-u,--unrestricted}'[alias for --no-ignore, when repeated also alias for --hidden]'+    $no'(no-ignore-partial)*'{-u,--unrestricted}'[alias for --no-ignore --hidden]'      + no-ignore-partial # some ignore files     "(no-ignore-full --no-ignore-vcs)--no-ignore-vcs[don't respect .gitignore files]"     "!(no-ignore-full --no-global-ignore-file)--no-global-ignore-file[don't respect the global ignore file]"-    $no'(no-ignore-full --no-ignore-parent)--no-ignore-parent[]'+    "$no(no-ignore-full --no-ignore-parent)--no-ignore-parent[don't respect .(git|fd)ignore files in parent directories]"      + '(case)' # case-sensitivity     {-s,--case-sensitive}'[perform a case-sensitive search]'@@ -72,6 +72,7 @@ _fd() {     {-g,--glob}'[perform a glob-based search]'     {-F,--fixed-strings}'[treat pattern as literal string instead of a regex]'     '!--literal'+    '--exact[pattern must exactly match the full filename]'      + '(no-require-git)'     "$no(no-ignore-full --no-ignore-vcs --no-require-git)--no-require-git[don't require git repo to respect gitignores]"@@ -81,6 +82,7 @@ _fd() {      + '(follow)' # follow symlinks     {-L,--follow}'[follow symbolic links to directories]'+    '!--dereference'      + '(abs-path)' # show absolute paths     '(long-listing)'{-a,--absolute-path}'[show absolute paths instead of relative paths]'@@ -95,9 +97,13 @@ _fd() {     '(long-listing)'--format=[print results according to template]:format      + '(max-results)' # max number of results-    '(long-listing exec-cmds)--max-results=[limit number of search results to given count and quit]:count'+    '(long-listing exec-cmds quiet)--max-results=[limit number of search results to given count and quit]:count'     '(long-listing exec-cmds)-1[limit to a single search result and quit]' +    + '(quiet)'+    '(max-results)'{-q,--quiet}'[print nothing. exit code 1 if no matches]'+    '!(max-results)--has-results'+     + '(fs-errors)' # file-system errors     $no'--show-errors[enable the display of filesystem errors]' @@ -108,8 +114,9 @@ _fd() {      + dir-depth # directory depth     '(--exact-depth -d --max-depth)'{-d+,--max-depth=}'[set max directory depth to descend when searching]:depth'-    '!(--exact-depth -d --max-depth)--maxdepth:depth'+    '!(--exact-depth -d --max-depth)--maxdepth=:depth'     '(--exact-depth --min-depth)--min-depth=[set directory depth to descend before start searching]:depth'+    '!(--exact-depth --min-depth)--mindepth=:depth'     '(--exact-depth -d --max-depth --maxdepth --min-depth)--exact-depth=[only search at the exact given directory depth]:depth'      + prune # pruning@@ -144,6 +151,7 @@ _fd() {     ))'      '--hyperlink=-[add hyperlinks to output paths]::when:(auto never always)'+    '!--hyper=-'      + '(threads)'     {-j+,--threads=}'[set the number of threads for searching and executing]:number of threads'@@ -156,6 +164,8 @@ _fd() {     + other     '!(--max-buffer-time)--max-buffer-time=[set amount of time to buffer before showing output]:time (ms)' +    '*--ignore-contain=[Ignore dirs containing named file]:name'+     + '(about)' # about flags     '(: * -)'{-h,--help}'[display help message]'     '(: * -)'{-V,--version}'[display version information]'@@ -171,7 +181,7 @@ _fd() {     $no'(strip-cwd-prefix exec-cmds)--strip-cwd-prefix=-[When to strip ./]::when:(always never auto)'      + and-    '--and=[additional required search path]:pattern'+    '*--and=[additional required search pattern]:pattern'       + args # positional arguments@@ -221,7 +231,7 @@ _fd() {           'T:terabytes  (10^12 = 1000^4 bytes)'           'Ki:kibibytes  ( 2^10 = 1024   bytes)'           'Mi:mebibytes  ( 2^20 = 1024^2 bytes)'-          'Gi:gigibytes  ( 2^30 = 1024^3 bytes)'+          'Gi:gibibytes  ( 2^30 = 1024^3 bytes)'           'Ti:tebibytes  ( 2^40 = 1024^4 bytes)'         )         _describe -t units 'size limit units' suff -V 'units'

doc/fd.1

@@ -94,8 +94,8 @@ regardless of whether you’re searching in a git repository or not. The flag ca be overridden with '--require-git'. .TP .B \-\-no\-ignore\-parent-Show search results from files and directories that would otherwise be ignored by gitignore files in-parent directories.+Show search results from files and directories that would otherwise be ignored by+'.gitignore', '.ignore', or '.fdignore' files in parent directories. .TP .B \-s, \-\-case\-sensitive Perform a case-sensitive search. By default, fd uses case-insensitive searches, unless the@@ -210,7 +210,7 @@ Do not traverse into matching directories. .BI "\-t, \-\-type " filetype Filter search by type: .RS-.IP "f, file"+.IP "f, file" 20 regular files .IP "d, dir, directory" directories

src/cli.rs

@@ -4,8 +4,8 @@ use std::time::Duration;  use anyhow::anyhow; use clap::{-    Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser, ValueEnum, error::ErrorKind,-    value_parser,+    Arg, ArgAction, ArgGroup, ArgMatches, Command, Parser, ValueEnum, builder::ArgPredicate,+    error::ErrorKind, value_parser, }; #[cfg(feature = "completions")] use clap_complete::Shell;@@ -39,7 +39,11 @@ pub struct Opts {         long,         short = 'H',         help = "Search hidden files and directories",-        long_help+        long_help,+        default_value_ifs([+            ("no_hidden", ArgPredicate::IsPresent, Some("false")),+            ("rg_alias_hidden_ignore", ArgPredicate::IsPresent, Some("true")),+        ])     )]     pub hidden: bool, @@ -48,13 +52,17 @@ pub struct Opts {     no_hidden: (),      /// Show search results from files and directories that would otherwise be-    /// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file,+    /// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file.     /// The flag can be overridden with --ignore.     #[arg(         long,         short = 'I',         help = "Do not respect .(git|fd)ignore files",-        long_help+        long_help,+        default_value_ifs([+            ("ignore", ArgPredicate::IsPresent, Some("false")),+            ("rg_alias_hidden_ignore", ArgPredicate::IsPresent, Some("true")),+        ])     )]     pub no_ignore: bool, @@ -120,11 +128,14 @@ pub struct Opts {      /// Perform an unrestricted search, including ignored and hidden files. This is     /// an alias for '--no-ignore --hidden'.-    #[arg(long = "unrestricted", short = 'u', overrides_with_all(&["ignore", "no_hidden"]), action(ArgAction::Count), hide_short_help = true,-    help = "Unrestricted search, alias for '--no-ignore --hidden'",-        long_help,-        )]-    rg_alias_hidden_ignore: u8,+    #[arg(+        long = "unrestricted",+        short = 'u',+        hide_short_help = true,+        help = "Unrestricted search, alias for '--no-ignore --hidden'",+        long_help+    )]+    rg_alias_hidden_ignore: bool,      /// Case-sensitive search (default: smart case)     #[arg(@@ -552,7 +563,7 @@ pub struct Opts {     pub ignore_contain: Vec<String>,      /// Set number of threads to use for searching & executing (default: number-    /// of available CPU cores)+    /// of available CPU cores, up to a maximum of 64)     #[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]     pub threads: Option<NonZeroUsize>, @@ -742,11 +753,6 @@ impl Opts {         self.path.is_empty() && self.search_path.is_empty()     } -    #[inline]-    pub fn rg_alias_ignore(&self) -> bool {-        self.rg_alias_hidden_ignore > 0-    }-     pub fn max_depth(&self) -> Option<usize> {         self.max_depth.or(self.exact_depth)     }@@ -873,7 +879,7 @@ impl clap::FromArgMatches for Exec {             })             .transpose()             .map_err(|e| clap::Error::raw(ErrorKind::InvalidValue, e))?;-        Ok(Exec { command })+        Ok(Self { command })     }      fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> clap::error::Result<()> {

src/exec/mod.rs

@@ -33,13 +33,13 @@ pub struct CommandSet { }  impl CommandSet {-    pub fn new<I, T, S>(input: I) -> Result<CommandSet>+    pub fn new<I, T, S>(input: I) -> Result<Self>     where         I: IntoIterator<Item = T>,         T: IntoIterator<Item = S>,         S: AsRef<str>,     {-        Ok(CommandSet {+        Ok(Self {             mode: ExecutionMode::OneByOne,             commands: input                 .into_iter()@@ -48,13 +48,13 @@ impl CommandSet {         })     } -    pub fn new_batch<I, T, S>(input: I) -> Result<CommandSet>+    pub fn new_batch<I, T, S>(input: I) -> Result<Self>     where         I: IntoIterator<Item = T>,         T: IntoIterator<Item = S>,         S: AsRef<str>,     {-        Ok(CommandSet {+        Ok(Self {             mode: ExecutionMode::Batch,             commands: input                 .into_iter()@@ -217,7 +217,7 @@ struct CommandTemplate { }  impl CommandTemplate {-    fn new<I, S>(input: I, mode: ExecutionMode) -> Result<CommandTemplate>+    fn new<I, S>(input: I, mode: ExecutionMode) -> Result<Self>     where         I: IntoIterator<Item = S>,         S: AsRef<str>,@@ -252,7 +252,7 @@ impl CommandTemplate {             args.push(FormatTemplate::Tokens(vec![Token::Placeholder]));         } -        Ok(CommandTemplate { args })+        Ok(Self { args })     }      fn number_of_tokens(&self) -> usize {

src/exit_codes.rs

@@ -15,7 +15,7 @@ impl From<ExitCode> for i32 {     fn from(code: ExitCode) -> Self {         match code {             ExitCode::Success => 0,-            ExitCode::HasResults(has_results) => !has_results as i32,+            ExitCode::HasResults(has_results) => !has_results as Self,             ExitCode::GeneralError => 1,             ExitCode::KilledBySigint => 130,         }@@ -30,7 +30,7 @@ impl ExitCode {     /// Exit the process with the appropriate code.     pub fn exit(self) -> ! {         #[cfg(unix)]-        if self == ExitCode::KilledBySigint {+        if self == Self::KilledBySigint {             // Get rid of the SIGINT handler, if present, and raise SIGINT             unsafe {                 if signal(Signal::SIGINT, SigHandler::SigDfl).is_ok() {

src/filetypes.rs

@@ -1,10 +1,11 @@+use crate::cli::FileType; use crate::dir_entry; use crate::filesystem;  use faccess::PathExt;  /// Whether or not to show-#[derive(Default)]+#[derive(Default, Debug, PartialEq, Eq)] pub struct FileTypes {     pub files: bool,     pub directories: bool,@@ -18,6 +19,34 @@ pub struct FileTypes { }  impl FileTypes {+    pub fn from_types(types: &[FileType]) -> Self {+        let mut file_types = FileTypes::default();+        for value in types {+            match value {+                FileType::File => file_types.files = true,+                FileType::Directory => file_types.directories = true,+                FileType::Symlink => file_types.symlinks = true,+                FileType::Executable => {+                    file_types.executables_only = true;+                    file_types.files = true;+                }+                FileType::Empty => file_types.empty_only = true,+                FileType::BlockDevice => file_types.block_devices = true,+                FileType::CharDevice => file_types.char_devices = true,+                FileType::Socket => file_types.sockets = true,+                FileType::Pipe => file_types.pipes = true,+            }+        }++        // If only 'empty' was specified, search for both files and directories:+        if file_types.empty_only && !(file_types.files || file_types.directories) {+            file_types.files = true;+            file_types.directories = true;+        }++        file_types+    }+     pub fn should_ignore(&self, entry: &dir_entry::DirEntry) -> bool {         if let Some(ref entry_type) = entry.file_type() {             (!self.files && entry_type.is_file())@@ -41,3 +70,42 @@ impl FileTypes {         }     } }++#[cfg(test)]+mod tests {+    use super::*;++    #[test]+    fn empty_only_implies_files_and_directories() {+        let ft = FileTypes::from_types(&[FileType::Empty]);+        assert!(ft.empty_only);+        assert!(ft.files);+        assert!(ft.directories);+    }++    #[test]+    fn empty_with_explicit_file_keeps_directories_off() {+        let ft = FileTypes::from_types(&[FileType::Empty, FileType::File]);+        assert!(ft.empty_only);+        assert!(ft.files);+        assert!(!ft.directories);+    }++    #[test]+    fn executable_implies_files() {+        let ft = FileTypes::from_types(&[FileType::Executable]);+        assert!(ft.executables_only);+        assert!(ft.files);+        assert!(!ft.directories);+    }++    #[test]+    fn union_of_multiple_types() {+        let ft = FileTypes::from_types(&[FileType::File, FileType::Symlink]);+        assert!(ft.files);+        assert!(ft.symlinks);+        assert!(!ft.directories);+        assert!(!ft.executables_only);+        assert!(!ft.empty_only);+    }+}

src/filter/owner.rs

@@ -16,7 +16,7 @@ enum Check<T> { }  impl OwnerFilter {-    const IGNORE: Self = OwnerFilter {+    const IGNORE: Self = Self {         uid: Check::Ignore,         gid: Check::Ignore,     };@@ -54,7 +54,7 @@ impl OwnerFilter {             }         })?; -        Ok(OwnerFilter { uid, gid })+        Ok(Self { uid, gid })     }      /// If self is a no-op (ignore both uid and gid) then return `None`, otherwise wrap in a `Some`@@ -76,9 +76,9 @@ impl OwnerFilter { impl<T: PartialEq> Check<T> {     fn check(&self, v: T) -> bool {         match self {-            Check::Equal(x) => v == *x,-            Check::NotEq(x) => v != *x,-            Check::Ignore => true,+            Self::Equal(x) => v == *x,+            Self::NotEq(x) => v != *x,+            Self::Ignore => true,         }     } @@ -87,16 +87,16 @@ impl<T: PartialEq> Check<T> {         F: Fn(&str) -> Result<T>,     {         let (s, equality) = match s {-            Some("") | None => return Ok(Check::Ignore),+            Some("") | None => return Ok(Self::Ignore),             Some(s) if s.starts_with('!') => (&s[1..], false),             Some(s) => (s, true),         };          f(s).map(|x| {             if equality {-                Check::Equal(x)+                Self::Equal(x)             } else {-                Check::NotEq(x)+                Self::NotEq(x)             }         })     }

src/filter/size.rs

@@ -26,7 +26,7 @@ const TEBI: u64 = GIBI * 1024;  impl SizeFilter {     pub fn from_string(s: &str) -> anyhow::Result<Self> {-        SizeFilter::parse_opt(s)+        Self::parse_opt(s)             .ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", s))     } @@ -56,20 +56,21 @@ impl SizeFilter {             _ => return None,         }; -        let size = quantity * multiplier;+        let size = quantity.checked_mul(multiplier)?;+         match limit_kind {-            "+" => Some(SizeFilter::Min(size)),-            "-" => Some(SizeFilter::Max(size)),-            "" => Some(SizeFilter::Equals(size)),+            "+" => Some(Self::Min(size)),+            "-" => Some(Self::Max(size)),+            "" => Some(Self::Equals(size)),             _ => None,         }     }      pub fn is_within(&self, size: u64) -> bool {         match *self {-            SizeFilter::Max(limit) => size <= limit,-            SizeFilter::Min(limit) => size >= limit,-            SizeFilter::Equals(limit) => size == limit,+            Self::Max(limit) => size <= limit,+            Self::Min(limit) => size >= limit,+            Self::Equals(limit) => size == limit,         }     } }@@ -216,4 +217,42 @@ mod tests {         let f = SizeFilter::from_string("+1K").unwrap();         assert!(f.is_within(1000));     }++    #[test]+    fn ensure_size_multiplication_overflow_returns_error() {+        let invalid = [+            "+18446744073709551615k",+            "-18446744073709551615m",+            "18446744073709551615g",+            "+18446744073709551615ti",+            "-18446744073709551615tib",+        ];++        for input in invalid {+            assert!(+                SizeFilter::from_string(input).is_err(),+                "Expected {input:?} to fail due to overflow"+            );+        }+    }++    #[test]+    fn ensure_size_multiplication_boundary() {+        let max_valid = u64::MAX / KILO;++        assert_eq!(+            SizeFilter::from_string(&format!("{max_valid}k")).unwrap(),+            SizeFilter::Equals(max_valid * KILO)+        );++        assert!(SizeFilter::from_string(&format!("{}k", max_valid + 1)).is_err());+    }++    #[test]+    fn ensure_max_u64_does_not_overflow() {+        assert_eq!(+            SizeFilter::from_string("18446744073709551615b").unwrap(),+            SizeFilter::Equals(u64::MAX)+        );+    } }

src/filter/time.rs

@@ -46,18 +46,18 @@ impl TimeFilter {         }     } -    pub fn before(s: &str) -> Option<TimeFilter> {-        TimeFilter::from_str(s).map(TimeFilter::Before)+    pub fn before(s: &str) -> Option<Self> {+        Self::from_str(s).map(TimeFilter::Before)     } -    pub fn after(s: &str) -> Option<TimeFilter> {-        TimeFilter::from_str(s).map(TimeFilter::After)+    pub fn after(s: &str) -> Option<Self> {+        Self::from_str(s).map(TimeFilter::After)     }      pub fn applies_to(&self, t: &SystemTime) -> bool {         match self {-            TimeFilter::Before(limit) => t < limit,-            TimeFilter::After(limit) => t > limit,+            Self::Before(limit) => t < limit,+            Self::After(limit) => t > limit,         }     } }

src/fmt/mod.rs

@@ -27,12 +27,12 @@ pub enum Token { impl Display for Token {     fn fmt(&self, f: &mut Formatter) -> fmt::Result {         match *self {-            Token::Placeholder => f.write_str("{}")?,-            Token::Basename => f.write_str("{/}")?,-            Token::Parent => f.write_str("{//}")?,-            Token::NoExt => f.write_str("{.}")?,-            Token::BasenameNoExt => f.write_str("{/.}")?,-            Token::Text(ref string) => f.write_str(string)?,+            Self::Placeholder => f.write_str("{}")?,+            Self::Basename => f.write_str("{/}")?,+            Self::Parent => f.write_str("{//}")?,+            Self::NoExt => f.write_str("{.}")?,+            Self::BasenameNoExt => f.write_str("{/.}")?,+            Self::Text(ref string) => f.write_str(string)?,         }         Ok(())     }@@ -52,7 +52,7 @@ static PLACEHOLDERS: OnceLock<AhoCorasick> = OnceLock::new();  impl FormatTemplate {     pub fn has_tokens(&self) -> bool {-        matches!(self, FormatTemplate::Tokens(_))+        matches!(self, Self::Tokens(_))     }      pub fn parse(fmt: &str) -> Self {@@ -96,14 +96,14 @@ impl FormatTemplate {         }         if tokens.is_empty() {             // No placeholders were found, so just return the text-            return FormatTemplate::Text(buf);+            return Self::Text(buf);         }         // Add final text segment         if !buf.is_empty() {             tokens.push(Token::Text(buf));         }         debug_assert!(!tokens.is_empty());-        FormatTemplate::Tokens(tokens)+        Self::Tokens(tokens)     }      /// Generate a result string from this template. If path_separator is Some, then it will replace

src/hyperlink.rs

@@ -5,8 +5,8 @@ use std::path::{Path, PathBuf}; pub(crate) struct PathUrl(PathBuf);  impl PathUrl {-    pub(crate) fn new(path: &Path) -> Option<PathUrl> {-        Some(PathUrl(absolute_path(path).ok()?))+    pub(crate) fn new(path: &Path) -> Option<Self> {+        Some(Self(absolute_path(path).ok()?))     } } 

src/main.rs

@@ -315,14 +315,12 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config     Ok(Config {         case_sensitive,         full_path_base,-        ignore_hidden: !(opts.hidden || opts.rg_alias_ignore()),-        read_fdignore: !(opts.no_ignore || opts.rg_alias_ignore()),-        read_vcsignore: !(opts.no_ignore || opts.rg_alias_ignore() || opts.no_ignore_vcs),+        ignore_hidden: !opts.hidden,+        read_fdignore: !opts.no_ignore,+        read_vcsignore: !(opts.no_ignore || opts.no_ignore_vcs),         require_git_to_read_vcsignore: !opts.no_require_git,         read_parent_ignore: !opts.no_ignore_parent,-        read_global_ignore: !(opts.no_ignore-            || opts.rg_alias_ignore()-            || opts.no_global_ignore_file),+        read_global_ignore: !(opts.no_ignore || opts.no_global_ignore_file),         follow_links: opts.follow,         one_file_system: opts.one_file_system,         null_separator: opts.null_separator,@@ -335,34 +333,10 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config         ls_colors,         hyperlink,         interactive_terminal,-        file_types: opts.filetype.as_ref().map(|values| {-            use crate::cli::FileType::*;-            let mut file_types = FileTypes::default();-            for value in values {-                match value {-                    File => file_types.files = true,-                    Directory => file_types.directories = true,-                    Symlink => file_types.symlinks = true,-                    Executable => {-                        file_types.executables_only = true;-                        file_types.files = true;-                    }-                    Empty => file_types.empty_only = true,-                    BlockDevice => file_types.block_devices = true,-                    CharDevice => file_types.char_devices = true,-                    Socket => file_types.sockets = true,-                    Pipe => file_types.pipes = true,-                }-            }--            // If only 'empty' was specified, search for both files and directories:-            if file_types.empty_only && !(file_types.files || file_types.directories) {-                file_types.files = true;-                file_types.directories = true;-            }--            file_types-        }),+        file_types: opts+            .filetype+            .as_ref()+            .map(|values| FileTypes::from_types(values)),         extensions: opts             .extensions             .as_ref()

src/regex_helper.rs

@@ -3,36 +3,45 @@ use regex_syntax::hir::Hir;  /// Determine if a regex pattern contains a literal uppercase character. pub fn pattern_has_uppercase_char(pattern: &str) -> bool {-    let mut parser = ParserBuilder::new().utf8(false).build();--    parser+    // The HIR includes case-folded alternatives introduced by inline flags.+    // Smart case must consider the original literals, before case folding.+    regex_syntax::ast::parse::Parser::new()         .parse(pattern)-        .map(|hir| hir_has_uppercase_char(&hir))+        .map(|ast| regex_syntax::ast::visit(&ast, UppercaseVisitor(false)).unwrap_or(false))         .unwrap_or(false) } -/// Determine if a regex expression contains a literal uppercase character.-fn hir_has_uppercase_char(hir: &Hir) -> bool {-    use regex_syntax::hir::*;+struct UppercaseVisitor(bool); -    match hir.kind() {-        HirKind::Literal(Literal(bytes)) => match std::str::from_utf8(bytes) {-            Ok(s) => s.chars().any(|c| c.is_uppercase()),-            Err(_) => bytes.iter().any(|b| char::from(*b).is_uppercase()),-        },-        HirKind::Class(Class::Unicode(ranges)) => ranges-            .iter()-            .any(|r| r.start().is_uppercase() || r.end().is_uppercase()),-        HirKind::Class(Class::Bytes(ranges)) => ranges-            .iter()-            .any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()),-        HirKind::Capture(Capture { sub, .. }) | HirKind::Repetition(Repetition { sub, .. }) => {-            hir_has_uppercase_char(sub)-        }-        HirKind::Concat(hirs) | HirKind::Alternation(hirs) => {-            hirs.iter().any(hir_has_uppercase_char)+impl regex_syntax::ast::Visitor for UppercaseVisitor {+    type Output = bool;+    type Err = std::convert::Infallible;++    fn finish(self) -> Result<bool, Self::Err> {+        Ok(self.0)+    }++    fn visit_pre(&mut self, ast: &regex_syntax::ast::Ast) -> Result<(), Self::Err> {+        if let regex_syntax::ast::Ast::Literal(literal) = ast {+            self.0 |= literal.c.is_uppercase();         }-        _ => false,+        Ok(())+    }++    fn visit_class_set_item_pre(+        &mut self,+        item: &regex_syntax::ast::ClassSetItem,+    ) -> Result<(), Self::Err> {+        use regex_syntax::ast::ClassSetItem;++        self.0 |= match item {+            ClassSetItem::Literal(literal) => literal.c.is_uppercase(),+            ClassSetItem::Range(range) => {+                range.start.c.is_uppercase() || range.end.c.is_uppercase()+            }+            _ => false,+        };+        Ok(())     } } @@ -103,3 +112,19 @@ fn matches_strings_with_leading_dot_simple() {     assert!(!pattern_matches_strings_with_leading_dot("\\.gitignore"));     assert!(!pattern_matches_strings_with_leading_dot("^gitignore")); }++#[test]+fn pattern_has_uppercase_char_inline_flags() {+    for pattern in [+        "(?i)foo",+        "(?i:foo)",+        "(?i)[a-z]",+        "(?i)[a-z&&[^q]]",+        "(?i)é",+    ] {+        assert!(!pattern_has_uppercase_char(pattern), "{pattern}");+    }+    for pattern in ["(?i)Foo", "(?i)[A-Z]", r"(?i)\x46oo", "(?i)É"] {+        assert!(pattern_has_uppercase_char(pattern), "{pattern}");+    }+}

tests/tests.rs

@@ -536,6 +536,23 @@ fn test_smart_case() {     te.assert_output(&["\\AC"], "one/two/C.Foo2"); } +#[test]+fn test_smart_case_inline_flags_and_patterns() {+    let te = TestEnv::new(&[], &["foo-bar-1", "foo-BAR-2", "FOO-BAR-3"]);+    let all = "FOO-BAR-3\nfoo-BAR-2\nfoo-bar-1";++    te.assert_output(&["foo", "--and", "bar"], all);+    te.assert_output(&["(?i)foo", "--and", "bar"], all);+    te.assert_output(&["foo", "--and", "(?i)bar"], all);+    te.assert_output(&["(?i:foo)", "--and", "bar"], all);+    te.assert_output(&["--ignore-case", "(?i)foo", "--and", "bar"], all);+    te.assert_output(+        &["--case-sensitive", "(?i)foo", "--and", "bar"],+        "foo-bar-1",+    );+    te.assert_output(&["(?i)Foo", "--and", "bar"], "foo-bar-1");+}+ /// Case sensitivity (--case-sensitive) #[test] fn test_case_sensitive() {@@ -2927,6 +2944,18 @@ fn test_opposing(flag: &str, opposing_flags: &[&str]) {     ); } +#[test_case(&["-u", "--ignore"], &["--hidden"] ; "u ignore")]+#[test_case(&["-u", "--no-hidden"], &["--no-ignore"] ; "u no-hidden")]+#[test_case(&["--no-hidden", "-u"], &["--no-ignore"] ; "no-hidden u")]+fn test_unrestricted_partial_override(flags: &[&str], expected_flags: &[&str]) {+    let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);++    assert_eq!(+        te.assert_success_and_get_normalized_output(".", flags),+        te.assert_success_and_get_normalized_output(".", expected_flags),+    );+}+ /// Print error if search pattern starts with a dot and --hidden is not set /// (Unix only, hidden files on Windows work differently) #[test]