2

I am trying to define the following bash function in my .bashrc file:

function myfind() {
    find $1 -not -path venv -not -path .tox -name "$2" | xargs grep -n "$3"
}

which is not doing what I expect. When I use that function to search for text in files, e.g.

myfind . *.py test

it does not return anything. But when I use the expression directly, i.e.

find . -not -path venv -not -path .tox -name "*.py" | xargs grep -n "test"

it returns some matches.

I already tried to use double quotes like

function myfind() {
    find $1 -not -path venv -not -path .tox -name '"$2"' | xargs grep -n '"$3"'
}

or

function myfind() {
    find $1 -not -path venv -not -path .tox -name "'$2'" | xargs grep -n "'$3'"
}

and tried to escape the quotes like

function myfind() {
    find $1 -not -path venv -not -path .tox -name \"$2\" | xargs grep -n \"$3\"
}

but none of these seem to be working.

How to fix this bash function so the quotes are being "used" around the arguments?

4
  • 2
    What about calling it by myfind . "*.py" test?
    – Philippos
    May 18, 2022 at 11:17
  • Yes that works as well...
    – Alex
    May 18, 2022 at 11:27
  • 1
    It's a good practice to always quote globbed names for findto keep the shell from expanding it. The same applies to your find-script. You can't solve that problem inside the script because the damage is already done before the script gets to see the parameter.
    – Philippos
    May 18, 2022 at 12:35
  • Btw.: See help function for correct syntax.
    – Cyrus
    May 18, 2022 at 21:50

1 Answer 1

6

ITYM:

myfind() {
  find "$1" '(' -name venv -o -name .tox ')' -prune -o \
            -name "$2" -type f -exec grep -Hne "$3" -- {} +
}

And then:

myfind . '*.py' test

See also Why is looping over find's output bad practice? for why calling xargs on the output of find like that is wrong.

Some other problems with your approach:

  • -path venv would never match anything as -path matches on the full path of the file being considered which will be something like <contents-of-$1>/subdir/file. Only at the top level of the invocation of myfind venv ... would it ever match. I assume you want to ignore files stored under directories called venv. While you could use ! -path '*/venv/*' for that, telling find not to descend into those directories in the first place is more efficient than filter out all the files in there after the fact (! -path '*/venv/*' is also problematic if there are file paths that are not valid text in the locale).

  • with myfind first *.py last, you're asking the shell to expand *.py into the list of matching files in the current directory to make up the second to second-last arguments to myfind, while it's a literal *.py argument you want to pass to myfind for myfind to pass it along to find after -name, hence the need to quote the * character so it's not interpreted as a globbing operator by the shell. Here, we're quoting the whole *.py with '*.py' though '*'.py or \*.py would be enough as the other 3 characters (., p and y) are not special to the shell.

    if using zsh instead of bash, you'd be able to do alias myfind='noglob myfind' for globbing to be disabled in arguments to myfind. Then you'd be able to do myfind . *.py test without that *.py being expanded by the shell.

  • parameter expansions must be quoted to prevent split+glob ("$1", not $1).

  • Without -H (a GNU extension) if find finds only one file, grep wouldn't print its path, so you wouldn't know where the matches are. With grep implementations that don't support -H, you can pass /dev/null as an extra argument: -exec grep -ne "$3" -- /dev/null {} +.

  • See ! for the standard (and shorter) equivalent to the non-standard -not.

  • with grep -n "$3", if $3 started with -, it would be taken as an option by grep. Hence the -e "$3" instead where the contents of $3 is passed as an argument to the -e option. There is the same problem with find "$1", however unfortunately, there is not standard workaround for that. find -- "$1" would stop the contents of $1 from being treated as an option by find it started with -, but it would still be treated as a predicate, so it's pointless. A directory called ! or ( for instance would also be a problem. BSD find have find -f "$1" for that, but GNU find and most other find implementations don't support it. Use myfind ./-dir-starting-with-data- '*.py' pattern to pass a directory starting with dash.

  • the only thing that is not standard sh syntax in your code is that function name() function definition syntax. The ksh syntax is function name {...;}, the Bourne and standard and bash syntax is name() {...;}. While bash also supports function name() { ...; } by accident, there's no really good reason to use it.

  • I've added a -type f to only considered regular files as you likely don't want grep to start search inside device files or fifos and it would report errors for directories or sockets. With GNU find, you can replace it with -xtype f so that symlinks eventually resolving to regular files be also considered.

3
  • While bash also supports function name() { ...; }. Could it be that you meant function name { ...; } without the parenthesis? May 18, 2022 at 14:59
  • 1
    @schrodigerscatcuriosity: Why would you think so?  (1) Like the March Hare, Stéphane usually says what he means and means what he says. (2) There are two standard-ish syntaxes for declaring shell functions: name + () and function + name.  The OP used the hybrid syntax function + name + () in the question, and Stéphane is pointing out that, while this works in bash, it’s non-standard and non-portable. May 18, 2022 at 20:50
  • @Scott You are right, I misunderstood the paragraph. I know Stéphane's excellence in every answer and comment, it's invaluable. May 18, 2022 at 21:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.