Reputation: 25791
What I'm trying to do is exclude all submodules of a git repository in a find command. I know that I can exclude a single directory like this:
find . -not -path './$submodule/*'
So I built a command that generates a lot of these statements and stores them:
EXCLUDES=$(for submodule in $(git submodule status | awk '{print $2}'); do
echo -n "-not -path './$submodule/*' ";
done)
But when I run find . $EXCLUDES
, this does not work. I suspect this is because of a bash quoting strategy that I do not understand. For example, lets assume (#
marks output):
tree .
# .
# ├── bar
# │ └── baz.scala
# └── foo.scala
set -x
EXCLUDES="-not -path './bar/*'"
find . -type f $EXCLUDES
# + find . -not -path ''\''./bar/*'\''' <---- weird stuff
# foo.scala
# baz.scala
find . -type f -not -path './bar/*'
# + find . -type f -not -path './bar/*'
# foo.scala
How do I tell bash not to to the weird quoting stuff its doing (see marked line above)?
Edit: @eddiem suggested using git ls-files
, which I will do in this concrete case. But I'm still interested in how I'd do this in the general case where I have a variable with quotes and would like to use it as arguments to a command.
Upvotes: 2
Views: 119
Reputation: 17051
The "weird stuff" you note is because bash only expands $EXCLUDES
once, by substituting in the value you stored in EXCLUDES
. It does not recursively process the contents of EXCLUDES
to remove single-quotes like it does when you specify the quoted string on the command line. Instead, bash escapes special characters in $EXCLUDES
, assuming that you want them there:
-not -path './bar/*'
becomes
-not -path ''\''./bar/*'\'''
^^ ^^ escaped single quotes
^^ ^^ random empty strings I'm actually not sure about
^ ^ single quotes around the rest of your text.
So, as @Jean-FrançoisFabre said, if you leave off the single quotes in EXCLUDES=...
, you won't get the weird stuff.
So why isn't the first find
working as expected? Because bash expands $EXCLUDES
into a single word, i.e., a single element of argv
that gets passed to find
.* However, find
expects its arguments to be separate words. As a result, find
does not do what you expect.
The most reliable way I know of to do this sort of thing is to use an array:
declare -a EXCLUDES #make a new array
EXCLUDES+=("-not" "-path" './bar/*')
# single-quotes ^ ^ so we don't glob when creating the array
and you can repeat the += line any number of times for exclusions that you want. Then, to use these:
find . -type f "${EXCLUDES[@]}"
The "${name[@]}"
form, with all that punctuation, expands each element of the array to a separate word, but does not further expand those words. So ./bar/*
will stay as that and not be globbed. (If you do want globbing, find . -type f ${EXCLUDES[@]}
(without the ""
) will expand each element of the array.)
Edit By the way, to see what's in your array, do set|grep EXCLUDES
. You will each each element listed separately. You can also do echo "${EXCLUDES[@]}"
, but I find that less useful for debugging since it doesn't show the indices.
* see the "expansion" section of the man page. "Parameter expansion," expanding things that start with $
, cannot change the number of words on the command line — except for "$@"
and "${name[@]}"
.
Upvotes: 2