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
4
  • 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. Commented May 7, 2014 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. Commented May 7, 2014 at 22:49
  • Stop playing with quotes, while using bash arrays. See my answer! Commented May 8, 2014 at 0:53
  • @steeldriver I think the problem is reverse: There too much quotes. Commented May 8, 2014 at 1:00

2 Answers 2

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).

1
  • Very clear explanation that doesn't require rewrite. Thank you. Commented May 8, 2014 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'
4
  • What does this mean? I updated my question btw. Commented May 8, 2014 at 0:02
  • 1
    echo is not well for this, use declare -p VARNAME for looking into a variable. Commented May 8, 2014 at 0:44
  • My running version work same as your, but permit the use of more than one path to browse into. Commented May 8, 2014 at 1:01
  • New version with some grep arguments binded to function, simplified command and whole command as array. Commented Apr 7, 2019 at 7:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .