0

I often need to find a file, but I'm not sure what the name is, something like this:

$ find -iname '*foo*' -o -iname '*bar*' -o -iname '*blah*'

This is a little tedious. I'd like to create an alias that's easier to use, like this:

$ findany foo bar blah

Here's my attempt:

findany() {
    args=("-iname" "'*$1*'")
    shift
while [ $# -gt 0 ]; do
    args+=("-o" "-iname" "'*$1*'")
    shift
done

find ${args[@]}

}

The problem is it never yields any results, even though the files are right there:

$ ls
bar.txt  blah.txt  foo.txt

$ findany foo bar blah

nothing

If I add echo in front of the command, it looks correct:

$ findany foo bar blah                     
find -iname '*foo*' -o -iname '*bar*' -o -iname '*blah*'

And if I copy the output above and run it, it works fine:

$ find -iname '*foo*' -o -iname '*bar*' -o -iname '*blah*'
./bar.txt
./foo.txt
./blah.txt

I figure it has to do with argument splitting or quotes but I'm not sure how to debug it.

I normally use zsh but I verified the same behavior in bash.

3 Answers3

4

You were very close! But for some reason you'd included single quotes in the file names themselves, so they'd never match.

Also you must quote "${args[@]}" to have it work properly. Otherwise it's subject to word splitting, and any globs will expand in the shell before find sees them.

Try this instead (specifically for bash):

findany() {
    local args=('-iname' "*$1*")
    shift
while [ "$#" -gt 0 ]; do
    args+=('-o' '-iname' "*$1*")
    shift
done

find . "${args[@]}"

}

Chris Davies
  • 116,213
  • 16
  • 160
  • 287
4

With zsh, you could just use:

set -o extendedglob # usually in your ~/.zshrc, here used for (#i) for
                    # case insensitive matching
print -rC1 -- **/*(#i)(foo|bar|blah)*(ND)

To build that pattern from an array, you can use the j[|] parameter expansion flag to join the elements of the array with | combined with either the ~ parameter expansion flag so that that | be treated as a glob pattern operator, or with the ~ parameter expansion operator so that any wildcard character in the joined string (even those found in array elements) are treated as patterns.

findany1() print -rC1 -- **/*(#i)(${(~j[|])argv})*(ND)

Or:

findany2() print -rC1 -- **/*(#i)(${(j[|])~argv})*(ND)

For instance findany1 '??' would find the files whose name contains ?? while findany2 would find the files whose name contains at least 2 characters. You'd need findany2 '\?\?' or findany2 '[?][?]' to find the files whose name contains ?? with findany2.

To make the same distinction with find, that'd be:

findany2() (
  for arg do
    argv+=( -o -iname "$arg" )
    shift
  done
  shift
  find . "$@"
)
findany1() (
  set -o extendedglob
  for arg do
    argv+=( -o -iname "${arg//(#m)[][\\?*]/\\$MATCH}" )
    shift
  done
  shift
  find . "$@"
)

Where we escape the wildcards recognised by find (?, *, [...] and \) with \ (the only quoting operator supported by find/fnmatch()).

1
$ find -iname '*foo*' -o -iname '*bar*' -o -iname '*blah*'

that's one way to go about it, but frankly, it is result-wise similar to

shopt -s globstar  ## enable recursive globbing operator **
shopt -s extglob   ## enable (|) pattern lists
shopt -s nocasematch  ## take a guess!

echo */@(foo|bar|blah)*

(but it does that without help of find).

We can very quickly build a shell script from that.

#! /bin/bash -
shopt -s globstar  ## enable recursive globbing operator **
shopt -s extglob   ## enable (|) pattern lists
shopt -s nullglob  ## don't error if nothing matches
shopt -s nocasematch  ## take a guess!

IFS='|' # "$" joins with the first character of IFS pattern="/@(${})"

IFS= # do globbing but not splitting upon unquoted expansion: matches=( $pattern )

for element in "${matches[@]}"; do printf '%s\n' "${element}" done

If you want to have it as a function, just put pattern=… to done in a function declaration.

  • Interesting approach. I've never thought of using echo to list files. I'm not sure how to translate those bash options into zsh ones, and echo **/*+(foo|bar|blah)* returns "no matches found", but it works if I remove the +. – Big McLargeHuge Jan 12 '24 at 06:39
  • but *(pattern1|pattern2|pattern3)* means "zero or more matches of either pattern1, pattern2 or pattern3", and "zero matches" is probably not what you want :) – Marcus Müller Jan 12 '24 at 08:15
  • @MarcusMüller *(...) and +(...) are ksh operators. Here, in ksh (or bash -O extglob), you'd want @(a|b) rather than +(a|b). The zsh equivalent is (a|b) (and the equivalent of +(a|b) would be (a|b)## with extendedglob). Here with bash/ksh, you'd also need to disable word splitting which is another side effect of leaving a parameter expansion unquoted. In any case, $match (best not to use that variable in zsh as it has a special meaning there similar to bash's $BASH_REMATCH) should be quoted in ksh/bash. – Stéphane Chazelas Jan 12 '24 at 08:41
  • @StéphaneChazelas hope I got the place where I need to quote right. (also, to avoid confusion, renamed $match and $matcher) How would I disable word splitting in this context? – Marcus Müller Jan 12 '24 at 14:06
  • 1
    Marcus, See edit. – Stéphane Chazelas Jan 12 '24 at 15:00
  • @StéphaneChazelas aaaaaaaaah! thanks! – Marcus Müller Jan 12 '24 at 15:07