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
@@ -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 }}
@@ -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
@@ -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", ]
@@ -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'
@@ -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
@@ -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<()> {
@@ -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 {
@@ -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() {
@@ -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);+ }+}
@@ -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) } }) }
@@ -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)+ );+ } }
@@ -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, } } }
@@ -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
@@ -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()?)) } }
@@ -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()
@@ -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: ®ex_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: ®ex_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}");+ }+}
@@ -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]