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.

1

3 Answers 3

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[@]}"
}
9
  • I thought I needed the single quotes to prevent the shell from expanding the glob pattern. Commented Jan 12 at 6:46
  • @BigMcLargeHuge, double quotes already do stop globbing. Even if they didn't, those single quotes you had there, were part of a double-quoted string, so they don't work as quotes (e.g. "'$foo'" will still expand the variable). The syntax for putting words in an array is exactly the same as what you'd use when running a command, e.g. if find ... -o -iname '*blah*' works as a command, then array=(-o -iname '*blah*') works as an array assignment.
    – ilkkachu
    Commented Jan 12 at 7:56
  • Note that in zsh, you don't need to quote ${args[@]} for it to work properly. You can also write it $args[@] or $args. All 3 would discard empty elements which "$args[@]" would prevent, but having empty elements here would not make sense. Commented Jan 12 at 8:52
  • @StéphaneChazelas thank you for the zsh comparison. I was responding to the bash tag; happy to qualify the code to identify this if it's important Commented Jan 12 at 10:27
  • 1
    See Security implications of forgetting to quote a variable in bash/POSIX shells. Specifically the What about [ $# -gt 1 ] section. Commented Jan 12 at 12:10
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()).

3
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.

6
  • 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 +. Commented Jan 12 at 6: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 :) Commented Jan 12 at 8: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. Commented Jan 12 at 8: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? Commented Jan 12 at 14:06
  • 1
    Marcus, See edit. Commented Jan 12 at 15:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .