4
\$\begingroup\$

This is an example of my filesystem:

  • /code/
    • internal/
      • dev/
      • main/
    • public/
      • dev/
      • main/
      • release/
    • tools/

/code/internal/dev/, /code/public/dev/ and /code/tools/ contain subdirectories for multiple projects. I work almost exclusively in the dev branches of /code/internal/ and /code/public/, and often I want to search for a text string in those directories along with /code/tools/ (which has no branches). In these instances I run a command like this:

grep -I -r FooBar /code/internal/dev/ /code/public/dev/ /code/tools/

Additionally, sometimes I am only interested in certain file types. Then the command becomes:

grep -I -r FooBar /code/internal/dev/ /code/public/dev/ /code/tools/ | grep .c:\|.h:

I usually forget this command between usages and end up having to relearn it. To alleviate that problem, I created a script - which I would appreciate feedback on :)

search() {
    local t
    local OPTIND
    local pattern
    local files
    local types

    if [ $1 = --help ]; then
        echo "Usage: search [OPTION] ... PATTERN [FILE] ..."
        echo "Search for PATTERN in each FILE."
        echo "Example: search -t c -t h 'hello world' /code/internal/dev/ /code/public/dev/"
        echo
        echo "Output control:"
        echo "  -t    limit results to files of type"
        return
    fi

    while getopts ":t:" opt; do
        case $opt in
            t) t=(${t[@]} $OPTARG);; # create an array
        esac
    done
    shift $((OPTIND-1))

    pattern=$1
    files=${@:2}

    if [ -n "$t" ]; then
        # cast the array to a string
        types=${t[@]}

        # convert the string to a pattern usable by grep
        # example: "c h" becomes ".c:\|.h:"
        types=.${types// /':\|.'}:

        grep -I -r $pattern $files | grep $types
    else
        grep -I -r $pattern $files
    fi
}

With this and a couple more shortcut scripts, I can (relatively) quickly find anything I'm looking for:

search-all-code() {
    search $@ /code/internal/dev/ /code/public/dev/ /code/tools/
}

Aside: I realize some versions of grep support --include and --exlude options, but the version of grep I'm stuck with doesn't.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I'd lean towards find. I'll provide some code review type comments at the bottom, but first a rewrite:

search() {
    local extensions
    local pattern
    local find_cmd
    local OPTIND opt

    local usage=$( cat - << END
Usage: $FUNCNAME [OPTION] ... PATTERN [FILE] ...
Search for PATTERN in each FILE.
Example: $FUNCNAME -t c -t h 'hello world' /code/internal/dev/ /code/public/dev/

Output control:
  -t    limit results to files of type
END
)
    if [[ $1 == --help ]]; then
        echo "$usage"
        return
    fi

    extensions=()
    while getopts ":ht:" opt; do
        case $opt in
            h) echo "$usage"; return;;
            t) extensions+=("$OPTARG");;
            ?) echo "invalid option: -$OPTARG";;
        esac
    done
    shift $((OPTIND-1))

    if (( $# == 0 )); then
        echo "no search term provided"
        return
    fi

    pattern=$1
    shift

    if (( $# == 0 )); then
        echo "no directories provided"
        return
    fi
    # your directories to search are now the positional parameters

    find_cmd=(find "$@" '(')
    or=""
    for type in "${extensions[@]}"; do
        find_cmd+=($or -name "*.$type")
        or="-o"
    done
    find_cmd+=(')' -exec grep -I "$pattern" '{}' +)

    "${find_cmd[@]}"
}

Notes

  • [ $1 = --help ] will give you syntax error if $1 is blank -- you get [ = --help ] and the = operator requires 2 operands. bash's [[ ... ]] is smarter about not dropping operands just because they're empty
  • You can build up arrays bit-by-bit with arr+=("$element")
  • you want to use more double quotes for your variables. If the pattern is "hello world", then grep $pattern file1 will look for the pattern "hello" in files "world" and "file1" -- grep "$pattern" file1 will work as you expect.
  • I use (( ... )) arithmetic expression for numeric comparisons.
  • I build up the find command piece by piece and execute it with "${arr[@]}" -- that specific syntax, indexing the array with [@] and surrounding it with double quotes, is the way you'll want to expand arrays most of the time. That specific syntax will expand the array into elements, but it will keep elements containing whitespace as a single argument.

    • This includes using "$@" over $@ -- the former gives you the actual arguments given by the user, the latter gives you all the words in the arguments. A demonstration:

      set -- "foo bar" "hello world"    # set the positional parameters
      printf "%s\n" "$@" | wc -l        # print the parameters one per line 
                                        # and count the lines -- 2
      printf "%s\n" $@ | wc -l          # here you get 4
      
  • if t is an array, $t gives you the first element only.
\$\endgroup\$
16
  • \$\begingroup\$ For the invalid option message, don't you want $opt instead of $OPTARG? \$\endgroup\$ Commented May 7, 2014 at 1:54
  • \$\begingroup\$ Can you please explain or link to why you prefer parens for numeric tests vs. brackets? I've never seen that syntax, though I'm a Bash noob. \$\endgroup\$ Commented May 7, 2014 at 1:55
  • 1
    \$\begingroup\$ @DavidHarkness, nope. see gnu.org/software/bash/manual/bashref.html#index-getopts -- "If an invalid option is seen, getopts places ‘?’ into name and, [...] the option character found is placed in OPTARG" \$\endgroup\$ Commented May 7, 2014 at 1:57
  • 1
    \$\begingroup\$ I like the look of (( $x == 0 )) versus [[ $x -eq 0 ]] -- documentation: gnu.org/software/bash/manual/…, scroll down for ((...)) \$\endgroup\$ Commented May 7, 2014 at 1:58
  • \$\begingroup\$ Why use == instead of =? According to this it is the same, but with some quirky behavior. \$\endgroup\$ Commented May 7, 2014 at 19:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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