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?

Philippos
  • 13,453
Alex
  • 5,700

1 Answers1

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.

  • While bash also supports function name() { ...; }. Could it be that you meant function name { ...; } without the parenthesis? – schrodingerscatcuriosity May 18 '22 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. – Scott - Слава Україні May 18 '22 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. – schrodingerscatcuriosity May 18 '22 at 21:08