BASH extglob negate not working as I expect
Let's start with these files:
$ ls
a.b b.c testdir2 t.tar
This matches all files that have a period but do not have tar
following the period:
$ echo *.!(tar)
a.b b.c
This matches all files that do not end with tar
:
$ echo !(*tar)
a.b b.c testdir2
This is the extglob that surprises but shouldn't:
$ echo *!(tar)
a.b b.c testdir2 t.tar
The *
itself can match anything, such as t.tar
. Since it is not necessary to add tar
after t.tar
, this is a match.
How does negative matching work in extglob in parameter expansion
Parameter expansion of the form ${parameter/pattern/string}
(where pattern
doesn't start with a /
) works by finding the leftmost longest substring in the value of the variable parameter
that matches the pattern pattern
and replacing it with string
. In other words, $parameter
is decomposed into three parts prefix
,match
, and suffix
such that
$parameter == "${prefix}${match}${suffix}"
$prefix
is the shortest possible string enabling the other requirements to be fulfilled (i.e. the match, if at all possible, occurs in the leftmost position)$match
matchespattern
and is as long as possible- any of
$prefix
,$match
and/or$suffix
can be empty
and the result of ${parameter/pattern/string}
is "${prefix}string${suffix}"
.
For the global replacement form (${parameter//pattern/string}
) of this type of parameter expansion, the same process is recursively performed for the suffix
part, however a zero-length match is handled as a special case (in order to prevent infinite recursion):
if
"${prefix}${match}" != ""
"${parameter//pattern/string}" = "${prefix}string${suffix//pattern/string}"
else
suffix=${parameter:1}
and"${parameter//pattern/string}" = "string${parameter:0:1}${suffix}//pattern/string}"
Now let's analyze the cases individually:
"${a/!([0-9])/}" --> prefix='' match='1 2 3 4 5 6 7 8 9 10' suffix=''
. Indeed, '1 2 3 4 5 6 7 8 9 10' is not a string consisting of a single digit, and therefore it matches the pattern!([0-9])
. Hence the empty result of expansion."${a/!(2)/}" --> prefix='' match='1 2 3 4 5 6 7 8 9 10' suffix=''
. Similar to the above, '1 2 3 4 5 6 7 8 9 10' is not a string consisting of the single character '2', and therefore it matches the pattern!(2)
. Hence the empty result of expansion."${a/!(*2*)/}" --> prefix='' match='1 ' suffix='2 3 4 5 6 7 8 9 10'
. The substring '1 ' doesn't match the pattern*2*
, and therefore it matches the pattern!(*2*)
."${a/!(*2*)/,}"
. There were no surprises here, so no need to elaborate."${a//!(*2*)/}"
. There were no surprises here, so no need to elaborate."${a//!(*2*)/,}" --> prefix='' match='1 ' suffix='2 3 4 5 6 7 8 9 10'
. Then${suffix//!(*2*)/,}
expands to",2,"
as follows. The empty string in the beginning ofsuffix
matches the pattern!(*2*)
, producing an extra comma in the result. Since the zero-length match special case (described above) was triggered, the first character ofsuffix
is forcibly consumed, leaving us with' 3 4 5 6 7 8 9 10'
, which matches the!(*2*)
pattern in its entirety and is replaced with the last comma that we see in the final result of the expansion.
shopt -s extglob not working
The correct syntax for extended globs is
!(pattern-list)
The exclamation mark should go outside the parentheses.
Why doesn't this Bash globbing pattern work?
Because the *
matches IMG_1236-renamed
, !(-renamed)
matches an empty string, and .jpg
matches itself.
Extended glob does not expand as expected
The problem is that $*
is in double quotes, which means that its contents will not be treated as a pattern, just like echo "*"
does not expand the asterisk. Combining the outer pattern with the inner quoted portion automatically escapes the latter, so !("b|c")
is treated like !(b\|c)
. Negation of the nonexistent b|c
file naturally expands to all files in the directory.
An additional problem is that extended globbing is messed up by IFS
being set to |
, so you must reset it before expanding the pattern. Therefore, you must do the expansion in two steps: first, calculate the pattern, then reset IFS
and expand it:
#!/usr/bin/env bash
# enable extended globbing
shopt -s extglob
# temporarily set IFS to | so that $* expands to part an extended pattern
old_ifs=$IFS
IFS='|'
pattern="!($*)"
IFS=$old_ifs
printf '%s' $pattern
Negate in bash extended globs does not work
*
is matching everything, so *!(8)*
is always going to match everything - first !(8)
will not match anything (match empty), then *
will match everything.
atop_20210 @(3|4) * [0-4] * !(8) *
atop_20210 4 2 8
Why all the *
? Remove them. You want to just match what you want to match, not to match anything in between. Just:
atop_20210@(3|4)[0-4]!(8)
How does extglob work with shell parameter expansion?
If you use @
instead of ?
then it works as expected:
$> echo "${V#@(35|88)}"
xAB
$> echo "${V%@(xAB|Bzh)}"
35
Similarly behavior of +
instead of *
:
$> echo "${V#*(35|88)}"
35xAB
$>echo "${V#+(35|88)}"
xAB
It is because:
?(pattern-list)
# Matches zero or one occurrence of the given patterns@(pattern-list)
# Matches one of the given patterns
And:
*(pattern-list)
# Matches zero or more occurrences of the given patterns+(pattern-list)
# Matches one or more occurrences of the given patterns
Bash negative wildcard using sub shell with `bash -c`
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.
Unable to exclude a single file from getting deleted after enabling bash extglob
Assuming save.tar
is inside the test
folder, you don't need to use a separate path to exclude the file from getting deleted. The negate operator already globs all the files except the one you provide inside !()
.
You can just do
rm -rvf "$outpath"/!(save.tar)
Or if you multiple .tar
files which need to be excluded, you can move the glob inside the negation as !(*.tar)
which means delete everything except the files ending with .tar
A simple reproducible example of your problem is below. Here all the files except file6
are commanded to be deleted and works as expected.
$ mkdir -p /tmp/foo/bar/test
$ touch /tmp/foo/bar/test/file{1..7}
ls /tmp/foo/bar/test/
file1 file2 file3 file4 file5 file6 file7
$ outpath=/tmp/foo/bar/test
$ rm -rvf "$outpath"/!(file6)
removed ‘/tmp/foo/bar/test/file1’
removed ‘/tmp/foo/bar/test/file2’
removed ‘/tmp/foo/bar/test/file3’
removed ‘/tmp/foo/bar/test/file4’
removed ‘/tmp/foo/bar/test/file5’
removed ‘/tmp/foo/bar/test/file7’
See more on bash extended globbing.
Can I do a negated wildcard on the command-line?
It depends on your shell.
In ksh, you can use this:
!(*tmp*)
In bash, the same thing works if you first enable the feature with shopt -s extglob
.
In zsh, you can enable the same syntax with setopt ksh_glob
, but there's a conflict with another zsh feature that you have to disable with setopt no_bare_glob_qual
before the above will actually work. Alternatively, you can just use zsh's native version via setopt extended_glob
; the equivalent of the above expression then looks like this:
^*tmp*
Related Topics
Replace Text Between Two Lines with Contents of a File Stored in a Variable in Sed
How Can a Program Detect If It Is Running as a Systemd Daemon
Print Differences of File1 to File2 Without Deleting Anything from File2
How to Get the Exit Status of the First Command in a Pipe
Update Specific Field in Text File in Specific Line
Why Would the Elf Header of a Shared Library Specify Linux as the Osabi
How to Add Text After Last Pattern Match Using Ed
Making Proprietary Elf Binaries Portable on Linux
Script to Get User That Has Process with Most Memory Usage
How to to Delete a Line Given with a Variable in Sed
How to Merge Two Rows in a Same Row from a Text File in Linux Shell Script
How to Disable Hardware Prefetcher in Core I7
When Installing Rust Toolchain in Docker, Bash 'Source' Command Doesn't Work
Install Binaries into /Bin, /Sbin, /Usr/Bin and /Usr/Sbin, Interactions with --Prefix and Destdir
Shell Script to Delete Files When Disk Is Full
How to Route Webcam Video to Virtual Video Device on Linux (Via Opencv)
A Way to Prevent Bash from Parsing Command Line W/Out Using Escape Symbols