Bash negative wildcard using sub shell with `bash -c` - bash

In bash, I can use a negative wildcard to glob all files in a directory that don't match some pattern, for example:
echo src/main/webapp/!(WEB-INF)
This works fine.
However, if I try to use exactly the same wildcard with bash -c to pass the command as an argument to a new bash shell, I get a syntax error:
$ bash -c 'echo src/main/webapp/!(WEB-INF)'
bash: -c: line 0: syntax error near unexpected token `('
bash: -c: line 0: `echo src/main/webapp/!(WEB-INF)'
Note that if I use a different glob, like bash -c 'echo src/main/webapp/*' it works as expected.
Why doesn't bash accept the same negative glob with -c as it does when run normally, and how can I get it to accept this negative glob?

That's because !(..) is a extended glob pattern that is turned on by default in your interactive bash shell, but in an explicit sub-shell launched with -c, the option is turned off. You can see that
$ shopt | grep extglob
extglob on
$ bash -c 'shopt | grep extglob'
extglob off
One way to turn on the option explicitly in command line would be to use the -O flag followed by the option to be turned on
$ bash -O extglob -c 'shopt | grep extglob'
extglob on
See extglob on Greg's Wiki for the list of extended glob patterns supported and The Shopt Builtin for a list of the extended shell options and which ones are enabled by default.

It happens the feature at stake is only enabled by default in an interactive shell. In bash, this is controlled by the extglob option:
extglob
If set, the extended pattern matching features described above (see Pattern Matching) are enabled.
To confirm this, you can run for example:
$ bash -c 'shopt -p | grep extglob'
shopt -u extglob
$ bash -i -c 'shopt -p | grep extglob'
shopt -s extglob

Related

ssh: bash: line 5: syntax error near unexpected token `(' [duplicate]

for example:
ssh localhost echo *([^.])
requires me to pass -O extglob to the remote shell bash like so:
ssh localhost -O extglob echo *([^.])
however, ssh then thinks the -O is for itself, so I try again:
ssh localhost -- -O extglob echo *([^.])
but then bash thinks -- is for itself.
how can I pass -O extglob to bash through ssh?
thanks.
update: i would prefer not to ask ssh to ask bash to launch another bash:
ssh yourserver bash -O extglob -c "'echo *([^.])'"
Update: As mentioned in the comments the extglob will lead to a syntax error. I've managed it by piping the command to stdin:
ssh yourserver -- bash -O extglob <<'EOF'
echo *([^.])
EOF
I would start an additonal shell remotely. Like this:
ssh yourserver bash -O extglob -c 'ls -al'
Although it is not required in this example I would advice you to use -- after ssh arguments (as you mentioned):
ssh yourserver -- bash -O extglob -c 'ls -al'
This will prevent ssh from parsing the command to execute remotely as arguments.
But you can also pass the option to the shell ssh itself starts for you using the shopt bash builtin. Note that ; separates the two commands.
ssh yourserver -- 'shopt -s extglob; ls -al'

pass arguments to remote shell in ssh

for example:
ssh localhost echo *([^.])
requires me to pass -O extglob to the remote shell bash like so:
ssh localhost -O extglob echo *([^.])
however, ssh then thinks the -O is for itself, so I try again:
ssh localhost -- -O extglob echo *([^.])
but then bash thinks -- is for itself.
how can I pass -O extglob to bash through ssh?
thanks.
update: i would prefer not to ask ssh to ask bash to launch another bash:
ssh yourserver bash -O extglob -c "'echo *([^.])'"
Update: As mentioned in the comments the extglob will lead to a syntax error. I've managed it by piping the command to stdin:
ssh yourserver -- bash -O extglob <<'EOF'
echo *([^.])
EOF
I would start an additonal shell remotely. Like this:
ssh yourserver bash -O extglob -c 'ls -al'
Although it is not required in this example I would advice you to use -- after ssh arguments (as you mentioned):
ssh yourserver -- bash -O extglob -c 'ls -al'
This will prevent ssh from parsing the command to execute remotely as arguments.
But you can also pass the option to the shell ssh itself starts for you using the shopt bash builtin. Note that ; separates the two commands.
ssh yourserver -- 'shopt -s extglob; ls -al'

Using bash -c and Globbing

I'm running gnu-parallel on a command that works fine when run from a bash shell but returns an error when parallel executes it with bash using the -c flag. I assume this has to do with the special globbing expression I'm using.
ls !(*site*).mol2
This returns successfully.
With the flag enabled the command fails
/bin/bash -c 'ls !(*site*).mol2'
/bin/bash: -c: line 0: syntax error near unexpected token `('
The manual only specifies that -c calls for bash to read the arguments for a string, am I missing something?
Edit:
I should add I need this to run from a gnu-parallel string, so the end resultant command must be runnable by /bin/bash -c "Some Command"
You should try the following code :
bash <<EOF
shopt -s extglob
ls !(*site*).mol2
EOF
Explanation :
when you run bash -c, you create a subshell, and shopt settings are not inherited.
EDIT
If you really need a one liner :
bash -O extglob -c 'ls !(*site*).mol2'
See this thread

Escape backquote in a double-quoted string in shell

For the command: /usr/bin/sh -c "ls 1`" (a backquote after 1).
How to make it run successfully? Adding a backslash before "`" does not work.
` is a special char as we know, and I tried surrounding it with single quote too (/usr/bin/sh -c "ls 1'`'"), but that doesn't work either.
The error always are:
% /usr/bin/sh -c "ls 1\`"
Unmatched `
You need to escape the backtick, but also escape the backslash:
$ touch 1\`
$ /bin/sh -c "ls 1\\\`"
1`
The reason you have to escape it "twice" is because you're entering this command in an environment (such as a shell script) that interprets the double-quoted string once. It then gets interpreted again by the subshell.
You could also avoid the double-quotes, and thus avoid the first interpretation:
$ /bin/sh -c 'ls 1\`'
1`
Another way is to store the filename in a variable, and use that value:
$ export F='1`'
$ printenv F
1`
$ /bin/sh -c 'ls $F' # note that /bin/sh interprets $F, not my current shell
1`
And finally, what you tried will work on some shells (I'm using bash, as for the above examples), just apparently not with your shell:
$ /bin/sh -c "ls 1'\`'"
1`
$ csh # enter csh, the next line is executed in that environment
% /bin/sh -c "ls 1'\`'"
Unmatched `.
I strongly suggest you avoid such filenames in the first place.
Use single quotes instead:
/usr/bin/sh -c 'ls 1\`'
/usr/bin/sh -c "ls '1\`'"

How to escape extended pathname expansion patterns in quoted expressions?

In addition to the basic *, ? and [...] patterns, the Bash shell provides extended pattern matching operators like !(pattern-list) ("match all except one of the given patterns"). The extglob shell option needs to be set to use them. An example:
~$ mkdir test ; cd test ; touch file1 file2 file3
~/test$ echo *
file1 file2 file3
~/test$ shopt -s extglob # make sure extglob is set
~/test$ echo !(file2)
file1 file3
If I pass a shell expression to a program which executes it in a sub-shell, the operator causes an error. Here's a test which runs a sub-shell directly (here I'm executing from another directory to make sure expansion doesn't happen prematurely):
~/test$ cd ..
~$ bash -c "cd test ; echo *"
file1 file2 file3
~$ bash -c "cd test ; echo !(file2)" # expected output: file1 file3
bash: -c: line 0: syntax error near unexpected token `('
bash: -c: line 0: `cd test ; echo !(file2)'
I've tried all kinds of escaping, but nothing I've come up with has worked correctly. I also suspected extglob is not set in a sub-shell, but that's not the case:
~$ bash -c "shopt -s extglob ; cd test ; echo !(file2)"
bash: -c: line 0: syntax error near unexpected token `('
bash: -c: line 0: `cd test ; echo !(file2)'
Any solution appreciated!
bash parses each line before executing it, so "shopt -s extglob" won't have taken effect when bash is validating the globbing pattern syntax. The option can't be enabled on the same line. That's why the "bash -O extglob -c 'xyz'" solution (from Randy Proctor) works and is required.
$ bash -O extglob -c 'echo !(file2)'
file1 file3
Here's another way, if you want to avoid eval and you need to be able to turn extglob on and off within the subshell. Just put your pattern in a variable:
bash -c 'shopt -s extglob; cd test; patt="!(file2)"; echo $patt; shopt -u extglob; echo $patt'
gives this output:
file1 file3
!(file2)
demonstrating that extglob was set and unset. If the first echo had quotes around the $patt, it would just spit out the pattern like the second echo (which probably should have quotes).
Well, I don't have any real experince with extglob, but I can get it to work by wrapping the echo in an eval:
$ bash -c 'shopt -s extglob ; cd test ; eval "echo !(file2)"'
file1 file3

Resources