1

Here is a command that works perfectly for me on the command-line:

find . -type f -exec grep -Hin --include=*.h --include=*.c 'somestring' {} \;

When I run the above command substituting the search path . with any path, the command still shows me only the list of files with .c or .h extension.

Now, I want to write a simple bash script with the same command as the value of a variable, just so that I could execute the script with minor modifications to do a similar search, rather than having to type the command all over again. But that is where I run into the escape rules nightmare (or lack of proper understanding of it!).

I wrote a script as shown below:

#!/bin/bash

path="/home/vinod" string="somestring"

command="find ${path} -type f -exec grep -Hin --include=.h --include=.c '${string}' {} ;"

echo $command

$command

When I run the above script, I get the command echoed two times instead of once, as shown below

find . -type f -exec grep -Hin --include=*.h --include=*.c 'somestring' {} \;
find . -type f -exec grep -Hin --include=*.h --include=*.c 'somestring' {} \;

and the following run-time error:

find: missing argument to -exec

As you can see from the echo, the command is exactly the same as when I ran it on the command-line, for which I got the expected result.

Any thoughts on what could be wrong with my script?

TIA

Vinod
  • 165
  • 1
  • 4
  • 1
    How can we run a command stored in a variable? But here you don't need the variable. Keep it simple and just put find … command in the script. And quote. And use the right quotes. – Kamil Maciorowski Jul 13 '23 at 06:46
  • @KamilMaciorowski I tried running the command without the variable, but this time I don't see any output. I still use $path and $string in my command. But if I were to run the same command on command-line without these variables, I get immediate result. Would you mind editing my script and showing me what change I should make to get this working? – Vinod Jul 13 '23 at 07:13
  • You should always verify scripts with shellcheck.net. In this case, it throws SC2090 and refers to SC2089 -- view at https://github.com/koalaman/shellcheck/wiki/SC2089. Basically, the \; is unescaped by the variable expansion, and ; is taken by shell as end of command. Also note the *.c and *.h should be single-quoted. Yes, the text is passed on, but the shell first attempts an expansion of every file in your directory before it finds it cannot return anything from it. – Paul_Pedant Jul 13 '23 at 09:56
  • From the script there, I can't see why the command would get echoed twice, and I can't replicate that. But the reason the find command fails is that variables contain data, not code, and their expansions are not parsed for shell syntax (like quotes or backslashes). – ilkkachu Jul 13 '23 at 12:41

1 Answers1

2

Don't use a variable to store a command . Instead, use variables to store data and use functions to store (define) commands.

We can create a command that takes two parameters: a starting search path and a search string.

You could trivially modify it to take just a search pattern if your starting search path was also fixed. In fact, let's turn the order of the parameters around and say that if the search path is omitted it defaults to your $HOME directory:

#!/bin/sh
# Search in *.c and *.h files for a matching pattern
#
pattern=$1
path=${2:-$HOME}

find "${path:-.}" -type f ( -name '.c' -o -name '.h' ) -exec grep -Hin -- "$pattern" {} +

Save this as the file chfind, make it executable (chmod a+rx chfind), and put it somewhere in your $PATH. You can now use it just like any other utility:

chfind 'main'
chfind 'main' /some/other/tree/of/files

Because we didn't use grep -F the search string is actually a Regular Expression rather than a plain string, so searching for a declaration such as FILE *fp will not work.

Finally, if you want path names to be relative to your search path, you could change directory to the $path and then search from there:

cd "$path" &&
    find -- * -type f \( -name '*.c' -o -name '*.h' \) -exec grep -Hin -- "$pattern" {} +

It's possibly worth explaining how this could be achieved with a function rather than in-line in the script. I've addressed your underlying requirement ("I want to write a simple bash script […] so that I could execute the script […] to do a similar search, rather than having to type the command all over again"). But if you want to use a function, it's almost exactly the same:

#!/bin/bash
# Search in *.c and *.h files for a matching pattern
#
chFind() {
    local pattern=$1 path=$2
find "${path:-.}" -type f \( -name '*.c' -o -name '*.h' \) -exec grep -Hin -- "$pattern" {} +

}

chFind "$1" "${2:-$HOME}"

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