3

I need to use "find" command to find several different sets of files in my Bash function, depending on my script input.

So, I have thing like:

DAYS=30
case $1 in
A1) ARGLINE="-name 'FOO*.xml' -or -name 'BAR*.xml' -or -name 'BTT*.txt'"
    ;;
A2) ARGLINE="-name 'PO*xml' -or -name 'PR*xml'"
    ;;
...
esac
find . -maxdepth 1 -type f -mtime +${DAYS} `${ARGLINE}`

This works.

However, as soon as I want to use variable for number of days to search back to, like this:

DAYS=30
case $1 in
A1) ARGLINE="-name 'FOO*.xml' -or -name 'BAR*.xml' -or -name 'BTT*.txt'"
    ;;
A2) ARGLINE="-name 'PO*xml' -or -name 'PR*xml'"
    ;;
...
esac
if [[ $# -gt 1 ]]; then
    DAYS=$2
fi
find . -maxdepth 1 -type f -mtime +${DAYS} `${ARGLINE}`

The function fails when find doesn't find any files matching, with the following error:

No command '-name' found, did you mean: Command 'uname' from package 'coreutils' (main) -name: command not found

It however works correctly, when the number of days is such that find finds some files. It also fails when I try to pipe the output of succesful run into another command.

How should I correctly build the argument line for "find"?

Gnudiff
  • 975

2 Answers2

8

In bash, use an array:

args=( '(' -name 'FOO*.xml' -or -name 'BAR*.xml' -or -name 'BTT*.txt' ')' )

The extra parentheses are there for creating the correct boolean logical grouping since you use -or).

Then, in the find command:

find ...some arguments... "${args[@]}"

You have an additional issue in that you use

`$ARGLINE`

This is a command substitution, similar to $( $ARGLINE ) and the shell will try to execute $ARGLINE (its value) as a command. This is why you get "No command '-name' found". The command substitution fails, but the find runs, which is why you think it "works".

Kusalananda
  • 333,661
  • Thank you, but if I do this, then no errors, but find appears to ignore DAYS parameter and always gives all files in the directory matching params set in $ARGLINE – Gnudiff May 16 '18 at 08:41
  • 1
    @Gnudiff Ah. I see why. Your arguments regarding -name has to be in parentheses: args=( '(' -name ... -or -name ... etc. ')' ) – Kusalananda May 16 '18 at 08:45
  • @Gnudiff See updated answer. – Kusalananda May 16 '18 at 08:46
  • Thank you; I solved this by passing the DAYS argument to the ARGLINE via ARGLINE+=(-mtime +${DAYS}) . It is not clear to me though, why the original line didn't work and why would I need to have extra parenthesis. – Gnudiff May 16 '18 at 08:47
  • Actually, scratch that, putting DAYS into ARGLINE didn't help. Going to try with parenthesis as you suggested. – Gnudiff May 16 '18 at 08:56
  • @Gnudiff, you want the -mtime to apply to all the -names, but -mtime +2 -name foo -or -name bar is parsed as (-mtime +2 -name foo ) -or -name bar. There's an implicit and between adjacent conditions, and and has higher priority than or. So you need to put the parenthesis there manually: -mtime +2 ( -name foo -or -name bar ) – ilkkachu May 16 '18 at 08:57
  • 1
    @Gnudiff If you want to put the -mtime argument into the array, you will have to use args=( -mtime +"$DAYS" '(' ... ')' ) where ... are the -name tests. – Kusalananda May 16 '18 at 08:58
  • @ilkkachu Unless they put the -mtime in the array... – Kusalananda May 16 '18 at 08:59
  • 1
    @Kusalananda Yeah, the main thing is that the resulting command line has them in the right place. – ilkkachu May 16 '18 at 09:02
5

The main issue here, is that quotes don't work inside quotes: after expanding a variable, the quotes inside it are just ordinary characters, e.g.:

$ foo='foo "bar doo"'
$ printf "<%s>\n" $foo
<foo>
<"bar>
<doo">

You should use an array to store command arguments like that:

ARGS=(-name 'FOO*.xml' -or -name 'BAR*.xml' -or -name 'BTT*.txt')

The shell will process quoting at this stage, and store distinct shell words as distinct array elements. Use the array like this:

find . "${ARGLINE[@]}"

If you want or need to, you can build the array piece by piece, this should result in the same array:

ARGS=(-name 'FOO*.xml')
ARGS+=(-or -name 'BAR*.xml')
ARGS+=(-or -name 'BTT*.txt')

However, note that you also used backticks to "quote" ${ARGLINE}. That will start a command substitution and run the contents of ARGLINE as a command. That's where your error comes from, the shell tries to run a program called -name.


Actually, in your example, you don't need an array, since you don't have any whitespace within the arguments. The main issue here usually is that the difference between whitespace between the arguments, and the whitespace within the arguments gets lost when the command line is stored to a string. But in your case, this might work, not that I'd recommend it:

set -f       # disable globbing
ARGLINE="-name FOO*.xml -or -name BAR*.xml -or -name BTT*.txt"
find . $ARGLINE
ilkkachu
  • 138,973
  • These are both very good answers, Kusalalanda was first, but I feel that the reference to array addition and building argument line one by one is a bit more complete. – Gnudiff May 16 '18 at 08:49
  • Strictly speaking, the problem is not with whitespace, but with characters of $IFS (which so happen to be SPC, TAB and NL, 3 whitespace characters by default). If the argument contained whitespace, you could still use split+glob, but using a different separator in $IFS (again, not recommended where you can use arrays instead). – Stéphane Chazelas May 16 '18 at 08:51