1

I have a script that calls find internally and id like to do something like this:

FIND_CRITERIA="-name \"*.[ch]\" -o -name \"*.cpp\" -o -name \"*.hpp\""
if [ "$1" != "" ]; then
  FIND_CRITERIA=$1
fi

echo "Find command: find / ${FIND_CRITERIA} > /tmp/output.txt"
find / ${FIND_CRITERIA} > /tmp/output.txt

It prints exactly the find command I'd like to execute. However it doesn't execute it correctly.

Does anyone knows why?

Thanks very much.

2
  • you have different variables FIND_CRITERIA and CRITERIA in your example. Typo? Commented Apr 25, 2013 at 15:52
  • It was just a typo in the stack overflow post. Sorry for that. Commented Apr 25, 2013 at 16:04

2 Answers 2

4

Short answer: see BashFAQ #50:I'm trying to put a command in a variable, but the complex cases always fail!.

Long answer: when the shell parses a command like find / ${CRITERIA}, it first parses quotes, escapes, etc; then it replaced variables like ${CRITERIA} with their values, but does not go back through them to look for quotes etc -- it's already done that. As a result, putting quotes, escapes, etc in variables doesn't do anything useful.

The best way to do this sort of thing in bash is to store the parameters in an array:

#!/bin/bash
CRITERIA=(-name "*.[ch]" -o -name "*.cpp" -o -name "*.hpp")

echo "Find command: find / ${CRITERIA} > /tmp/output.txt"
find / "${CRITERIA[@]}" > /tmp/output.txt

...but this won't work with the way you're treating $1 as an optional set of criteria. Does the script need any other arguments? If not, I'd recommend having it take the search criteria as separate arguments rather than a single one (i.e. ./findscript -name "*.c" instead of ./findscript "-name \"*.c\""), like this:

#!/bin/bash
if [ $# -gt 0 ]; then
    CRITERIA=("$@")
else
    CRITERIA=(-name "*.[ch]" -o -name "*.cpp" -o -name "*.hpp")
fi

echo "Find command: find / ${CRITERIA} > /tmp/output.txt"
find / "${CRITERIA[@]}" > /tmp/output.txt

EDIT: Based on your comment about needing to allow other options/arguments to the script, I see three options:

  1. Use eval to parse the getopt argument into an array:

    case "$opt" in
        f) eval "CRITERIA=($OPTARG)" ;;
    

    This has all of the usual problems associated with eval, mainly having to do with failing in weird (and possibly dangerous) ways depending on what's passed as the OPTARG. Ick.

  2. If the script doesn't need to take any "regular" (non-option) arguments, pass the criteria as regular arguments (./script -a -b -c -d -- -name ".cpp" -o -name ".hpp"). The problem with this is that IIRC getopts doesn't read -- as a delimiter between options and regular arguments, so you'd have to do at least some of the parsing "by hand". Correction: at least in bash version 3.2.48, getopts handles -- automatically.

  3. If the only criteria you need to pass are -name's, you can use multiple -f options to specify name patterns to build the criteria, e.g. ./script -a -b -c -d -f "*.cpp" -f "*.hpp"

    CRITERIA=()
    ...
    case "$opt" in
        f) if [[ ${#CRITERIA[@]} -eq 0 ]]; then
                CRITERIA=(-name "$OPTARG")
            else
                CRITERIA+=(-o -name "$OPTARG")
            fi ;;
    ...
    
    if [[ ${#CRITERIA[@]} -eq 0 ]]; then
        CRITERIA=(-name "*.[ch]" -o -name "*.cpp" -o -name "*.hpp")
    fi
    
Sign up to request clarification or add additional context in comments.

5 Comments

I found this array solution and it worked. The problem is I use getopts to extract a lot of other parameters relevant to the rest of the script. My intention was to use this script this way: ./script -a -b -c -d -f "-name \".cpp\" -o -name \".hpp\"". This way I can't actually create the CRITERIA array from $0. Do you know some other way to do that? Thakns for your help.
You can build the array incrementally. You can certainly add a new entry to array a with a[${#a}]="new value"; there maybe a shorthand way to do it (but it isn't a+="new value"; that adds the new value to the zeroth element of the array).
Gordon, your answer was absolutely complete. Thanks very much. The iamauser solution is a little simpler than the ones you proposed, but your explanation was perfect. Thanks so much.
...continuing... the a+="new value" is wrong for an array, but a+=("new value") with the parentheses is correct.
Gordon, a good way of doing the parsing by hand is by using a while to fill an array of all regular options from the script parameters, using shift to consume them. You do it until you find a "--". After that, you have the regular options in the array and the other options in the regular parameter list. Then you can use getopts using the array as input and use the approach you suggested to parse the find arguments. Nifty.! :D
1

You are calling $CRITERIA whereas the variable name is $FIND_CRITERIA . I assume that's a typo.

Try this instead :

 #!/bin/bash

 FIND_CRITERIA="-name \"*.\[ch\]\" -o -name \"*.cpp\" -o -name \"*.hpp\""
 if [ "$1" != "" ]; then
   FIND_CRITERIA=$1
 fi

 echo "Find command: find / ${FIND_CRITERIA} > /tmp/output.txt"
 echo "${FIND_CRITERIA}" | xargs find / > /tmp/output.txt

1 Comment

great solution! It helped a lot. Thanks very much.

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.