1

I can run the following command in bash without any errors:

$ find /d/Code/Web/Development/Source/ \( -name '*.cs' -o -name '*.cshtml' \) -exec grep -IH UserProfileModel {} \;

I wrote a function in .bash_aliases to quickly run this command:

search() {
    local file_type file_types find_cmd opt OPTARG OPTIND or pattern usage

    usage="Usage: search [OPTION] ... PATTERN [FILE] ...
Search for PATTERN in each FILE.
Example: search -t c -t h 'hello world' /code/internal/dev/ /code/public/dev/

Output control:
  -t    limit results to files of type"

    if [[ $1 == --help ]]; then
        echo "$usage"
        return
    fi

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

    if (( $# == 0 )); then
        echo "$usage"
        return
    fi

    pattern=$1
    shift

    if (( $# == 0 )); then
        echo "$usage"
        return
    fi

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

    "${find_cmd[@]}"
}

However, the function throws an error:

find: paths must precede expression

If I change the last line to echo "${find_cmd[@]}", it prints the exact same command as above:

$ search -t cs -t cshtml UserProfileModel /d/Code/Web/Development/Source/
find /d/Code/Web/Development/Source/ \( -name '*.cs' -o -name '*.cshtml' \) -exec grep -IH UserProfileModel {} \;

I don't understand why it would work when run in the console but fail when run inside a function.

Also, if I simplify the function to just the command it works:

search() {
    find /d/Code/Web/Development/Source/ \( -name '*.cs' -o -name '*.cshtml' \) -exec grep -IH UserProfileModel {} \;
}

I am editing .bash_aliases in Notepad++, but I have made sure the line endings are Unix format.

Edit

Per F. Hauri's advice below, I enabled debugging. Apparently this is the command that's actually being executed:

find /d/Code/Web/Development/Source/ '\(' -name ''\''*.cs'\''' -o -name ''\''*.cshtml'\''' '\)' -exec grep -IH UserProfileModel '{}' '\;'

I'm not sure what to make of this information. Removing the escape character before the parens cause it to throw a different error:

find: missing argument to -exec
  • 1
    I don't know if it's related to your problem specifically, but you should quote the -name patterns e.g. -name '*.cs' and similar arguments when you run find. Otherwise the shell may expand the glob to a list of matching files from the current directory. Quoting ensures that *.cs is passed unexpanded to find. – steeldriver May 7 '14 at 22:44
  • I meant to add that. Does it matter if I use single quotes or double quotes? Either way it doesn't change the error. – Big McLargeHuge May 7 '14 at 22:49
  • Stop playing with quotes, while using bash arrays. See my answer! – F. Hauri May 8 '14 at 0:53
  • @steeldriver I think the problem is reverse: There too much quotes. – F. Hauri May 8 '14 at 1:00
2

Tip: run set -x to enable tracing mode. Bash prints each command before executing it. Run set +x to turn off tracing mode.

+ find . '\(' '\)' -exec grep -IH needle '{}' '\;'

Notice how the last argument to find is \; instead of ;. You have the same problem with the opening and closing parentheses. In your source, you've quoted the semicolon twice. Change

    find_cmd=(find "$@" '\(')
    …
    find_cmd+=('\)' -exec grep -IH "$pattern" {} '\;')

to

    find_cmd=(find "$@" '(')
    …
    find_cmd+=(')' -exec grep -IH "$pattern" {} ';')

or

    find_cmd=(find "$@" \()
    …
    find_cmd+=(\) -exec grep -IH "$pattern" {} \;)

Additionally, -name \'*.$file_type\' has bad quotes — you're looking for files whose name starts and ends with a single quote. Make this -name "*.$file_type" (the * needs to be quoted in case there are matching files in the current directory, and variable expansions should be in double quotes unless you know why you need to leave out the double quotes).

|improve this answer|||||
  • Very clear explanation that doesn't require rewrite. Thank you. – Big McLargeHuge May 8 '14 at 15:08
1

Running command using arrays

Let try:

find /tmp \( -type f -o -type d \) -ls 

Wow, there is a lot of output...

Well, now:

cmd_list=(find /tmp \()
cmd_list+=(-type f)
cmd_list+=(-o -type d)
cmd_list+=(\) -ls)
"${cmd_list[@]}"

Hmm... There seem identical!

find /tmp \( -type f -o -type d \) -ls 2>/dev/null | md5sum
eb49dfe4f05a90797e444db119e0d9bd  -
"${cmd_list[@]}" 2>/dev/null| md5sum
eb49dfe4f05a90797e444db119e0d9bd  -

Ok, finally:

printf "%q " "${cmd_list[@]}";echo
find /tmp \( -type f -o -type d \) -ls 

while

printf "%s " "${cmd_list[@]}";echo
find /tmp ( -type f -o -type d ) -ls

My full running version:

Sample of using array as command

search() {
    local OPTIND=0 _o _usage _debug=false
    local -a _t _g _cmd=(find)
    read -rN9999 _usage <<-EOUsage
    Usage: $FUNCNAME [-a] [-d] [-i] [-I] [-H] [-l] [-t type [-t type]] \\ 
            /path/ [path2/ ../path3/] pattern
        -t .ext specifying an extension to search for
        -d      debug, will dump command variable before execution
        -[aiIHl] are 'grep' argument, see: man grep.
        many type and many path could be given but only one pattern.
    EOUsage
    while getopts 't:aiIHld' _o ;do
        case $_o in
        d) _debug=true ;;
        t) _t+=(${_t+-o} -name \*.${OPTARG}) ;;
        [aiIHl]) _g+=(-$_o) ;;
        *) echo "${_usage%$'\n'}" ; return 1 ;;
        esac
    done
    _cmd+=(${@:OPTIND:$#-$OPTIND} -type f)
    ((${#_t[@]})) && _cmd+=(\( "${_t[@]}" \))
    _cmd+=(-exec grep ${_g[@]} ${@:$#} {} +)
    $_debug && declare -p _cmd
    "${_cmd[@]}"
}

Care first characters in portion from <<-EOUsage to EOUsage must be a tabulation! You could download this script there or as .txt: there.

Note: some grep arguments could (or must) be given to search function:

search -t txt -Il /tmp/path /home/user 'invoice\|bill'
|improve this answer|||||
  • What does this mean? I updated my question btw. – Big McLargeHuge May 8 '14 at 0:02
  • 1
    echo is not well for this, use declare -p VARNAME for looking into a variable. – F. Hauri May 8 '14 at 0:44
  • My running version work same as your, but permit the use of more than one path to browse into. – F. Hauri May 8 '14 at 1:01
  • New version with some grep arguments binded to function, simplified command and whole command as array. – F. Hauri Apr 7 '19 at 7:18

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.