| 1 | # Extended globs are an OPTION in bash, but not mksh (because the feature |
| 2 | # originated in ksh). |
| 3 | # |
| 4 | # However all extended globs are syntax errors if shopt -s extglob isn't set. |
| 5 | # In Oil, they are not PARSE TIME errors, but the syntax won't be respected at |
| 6 | # RUNTIME, i.e. when passed to fnmatch(). |
| 7 | # |
| 8 | # GNU libc has the FNM_EXTMATCH extension to fnmatch(). (I don't think musl |
| 9 | # libc has it.) However, this came after all popular shells were implemented! |
| 10 | # I don't think any shell uses it, but we're taking advantage of it. |
| 11 | # |
| 12 | # Extended glob syntax is ugly, but I guess it's handy because it's similar to |
| 13 | # *.[ch]... but the extensions can be different length: *.@(cc|h) |
| 14 | # It's also used for negation like |
| 15 | # |
| 16 | # cp !(_*) /tmp |
| 17 | # |
| 18 | # I tend to use 'find', but this is a shorter syntax. |
| 19 | |
| 20 | # From the bash manual: |
| 21 | |
| 22 | # "In addition to the traditional globs (supported by all Bourne-family shells) |
| 23 | # that we've seen so far, Bash (and Korn Shell) offers extended globs, which |
| 24 | # have the expressive power of regular expressions. Korn shell enables these by |
| 25 | # default; in Bash, you must run the command " |
| 26 | |
| 27 | # ?(pattern-list): Matches empty or one of the patterns |
| 28 | # *(pattern-list): Matches empty or any number of occurrences of the patterns |
| 29 | # +(pattern-list): Matches at least one occurrences of the patterns |
| 30 | # @(pattern-list): Matches exactly one of the patterns |
| 31 | # !(pattern-list): Matches anything EXCEPT any of the patterns |
| 32 | |
| 33 | #### @() matches exactly one of the patterns |
| 34 | shopt -s extglob |
| 35 | mkdir -p 0 |
| 36 | cd 0 |
| 37 | touch {foo,bar}.cc {foo,bar,baz}.h |
| 38 | echo @(*.cc|*.h) |
| 39 | ## stdout: bar.cc bar.h baz.h foo.cc foo.h |
| 40 | |
| 41 | #### ?() matches 0 or 1 |
| 42 | shopt -s extglob |
| 43 | mkdir -p 1 |
| 44 | cd 1 |
| 45 | touch {foo,bar}.cc {foo,bar,baz}.h foo. foo.hh |
| 46 | echo foo.?(cc|h) |
| 47 | ## stdout: foo. foo.cc foo.h |
| 48 | |
| 49 | #### *() matches 0 or more |
| 50 | shopt -s extglob |
| 51 | mkdir -p eg1 |
| 52 | touch eg1/_ eg1/_One eg1/_OneOne eg1/_TwoTwo eg1/_OneTwo |
| 53 | echo eg1/_*(One|Two) |
| 54 | ## stdout: eg1/_ eg1/_One eg1/_OneOne eg1/_OneTwo eg1/_TwoTwo |
| 55 | |
| 56 | #### +() matches 1 or more |
| 57 | shopt -s extglob |
| 58 | mkdir -p eg2 |
| 59 | touch eg2/_ eg2/_One eg2/_OneOne eg2/_TwoTwo eg2/_OneTwo |
| 60 | echo eg2/_+(One|Two) |
| 61 | ## stdout: eg2/_One eg2/_OneOne eg2/_OneTwo eg2/_TwoTwo |
| 62 | |
| 63 | #### !(*.h|*.cc) to match everything except C++ |
| 64 | shopt -s extglob |
| 65 | mkdir -p extglob2 |
| 66 | touch extglob2/{foo,bar}.cc extglob2/{foo,bar,baz}.h \ |
| 67 | extglob2/{foo,bar,baz}.py |
| 68 | echo extglob2/!(*.h|*.cc) |
| 69 | ## stdout: extglob2/bar.py extglob2/baz.py extglob2/foo.py |
| 70 | |
| 71 | #### Two adjacent alternations |
| 72 | shopt -s extglob |
| 73 | mkdir -p 2 |
| 74 | touch 2/{aa,ab,ac,ba,bb,bc,ca,cb,cc} |
| 75 | echo 2/!(b)@(b|c) |
| 76 | echo 2/!(b)?@(b|c) # wildcard in between |
| 77 | echo 2/!(b)a@(b|c) # constant in between |
| 78 | ## STDOUT: |
| 79 | 2/ab 2/ac 2/cb 2/cc |
| 80 | 2/ab 2/ac 2/bb 2/bc 2/cb 2/cc |
| 81 | 2/ab 2/ac |
| 82 | ## END |
| 83 | |
| 84 | #### Nested extended glob pattern |
| 85 | shopt -s extglob |
| 86 | mkdir -p eg6 |
| 87 | touch eg6/{ab,ac,ad,az,bc,bd} |
| 88 | echo eg6/a@(!(c|d)) |
| 89 | echo eg6/a!(@(ab|b*)) |
| 90 | ## STDOUT: |
| 91 | eg6/ab eg6/az |
| 92 | eg6/ac eg6/ad eg6/az |
| 93 | ## END |
| 94 | |
| 95 | #### Extended glob patterns with spaces |
| 96 | shopt -s extglob |
| 97 | mkdir -p eg4 |
| 98 | touch eg4/a 'eg4/a b' eg4/foo |
| 99 | argv.py eg4/@(a b|foo) |
| 100 | ## STDOUT: |
| 101 | ['eg4/a b', 'eg4/foo'] |
| 102 | ## END |
| 103 | |
| 104 | #### Filenames with spaces |
| 105 | shopt -s extglob |
| 106 | mkdir -p eg5 |
| 107 | touch eg5/'a b'{cd,de,ef} |
| 108 | argv.py eg5/'a '@(bcd|bde|zzz) |
| 109 | ## STDOUT: |
| 110 | ['eg5/a bcd', 'eg5/a bde'] |
| 111 | ## END |
| 112 | |
| 113 | #### nullglob with extended glob |
| 114 | shopt -s extglob |
| 115 | shopt -s nullglob # test this too |
| 116 | mkdir eg6 |
| 117 | argv.py eg6/@(no|matches) # no matches |
| 118 | ## STDOUT: |
| 119 | [] |
| 120 | ## END |
| 121 | ## BUG mksh STDOUT: |
| 122 | ['eg6/@(no|matches)'] |
| 123 | ## END |
| 124 | |
| 125 | |
| 126 | #### glob other punctuation chars (lexer mode) |
| 127 | # mksh sorts them differently |
| 128 | shopt -s extglob |
| 129 | mkdir -p eg5 |
| 130 | cd eg5 |
| 131 | touch __{'<>','{}','|','#','&&'} |
| 132 | argv.py @('__<>'|__{}|__\||__#|__&&) |
| 133 | ## stdout: ['__<>', '__|', '__{}', '__&&', '__#'] |
| 134 | ## OK mksh stdout: ['__#', '__&&', '__<>', '__{}', '__|'] |
| 135 | |
| 136 | #### dynamic extglob from variable |
| 137 | |
| 138 | # mksh does static parsing so it doesn't like this? |
| 139 | shopt -s extglob |
| 140 | mkdir -p eg3 |
| 141 | touch eg3/{foo,bar} |
| 142 | g=eg3/@(foo|bar) |
| 143 | echo $g "$g" # quoting inhibits globbing |
| 144 | ## stdout: eg3/bar eg3/foo eg3/@(foo|bar) |
| 145 | ## N-I mksh stdout: eg3/@(foo|bar) eg3/@(foo|bar) |
| 146 | |
| 147 | #### Extended glob syntax in bad redirect context |
| 148 | shopt -s extglob |
| 149 | rm bad_* |
| 150 | |
| 151 | # They actually write this literal file! This is what EvalWordToString() does, |
| 152 | # as opposed to _EvalWordToParts. |
| 153 | echo foo > bad_@(*.cc|*.h) |
| 154 | echo bad_* |
| 155 | ## STDOUT: |
| 156 | bad_@(*.cc|*.h) |
| 157 | ## END |
| 158 | |
| 159 | #### Extended glob as argument to ${undef:-} (dynamic globbing) |
| 160 | |
| 161 | # This case popped into my mind after inspecting osh/word_eval.py for calls to |
| 162 | # _EvalWordToParts() |
| 163 | |
| 164 | shopt -s extglob |
| 165 | |
| 166 | mkdir -p eg8 |
| 167 | cd eg8 |
| 168 | touch {foo,bar,spam}.py |
| 169 | |
| 170 | # regular glob |
| 171 | echo ${undef:-*.py} |
| 172 | |
| 173 | # extended glob |
| 174 | echo ${undef:-@(foo|bar).py} |
| 175 | |
| 176 | ## STDOUT: |
| 177 | bar.py foo.py spam.py |
| 178 | bar.py foo.py |
| 179 | ## END |
| 180 | ## OK mksh STDOUT: |
| 181 | bar.py foo.py spam.py |
| 182 | @(foo|bar).py |
| 183 | ## END |
| 184 | |
| 185 | #### Extended glob in assignment builtin |
| 186 | |
| 187 | # Another invocation of _EvalWordToParts() that OSH should handle |
| 188 | |
| 189 | shopt -s extglob |
| 190 | mkdir -p eg9 |
| 191 | cd eg9 |
| 192 | touch {foo,bar}.py |
| 193 | typeset -@(*.py) myvar |
| 194 | echo status=$? |
| 195 | ## STDOUT: |
| 196 | status=2 |
| 197 | ## END |
| 198 | ## OK mksh STDOUT: |
| 199 | status=1 |
| 200 | ## END |
| 201 | |
| 202 | #### Extended glob in same word as array |
| 203 | shopt -s extglob |
| 204 | mkdir -p eg10 |
| 205 | cd eg10 |
| 206 | |
| 207 | touch {'a b c',bee,cee}.{py,cc} |
| 208 | set -- 'a b' 'c' |
| 209 | |
| 210 | argv.py "$@" |
| 211 | |
| 212 | # This works! |
| 213 | argv.py star glob "$*"*.py |
| 214 | argv.py star extglob "$*"*@(.py|cc) |
| 215 | |
| 216 | # Hm this actually still works! the first two parts are literal. And then |
| 217 | # there's something like the simple_word_eval algorithm on the rest. Gah. |
| 218 | argv.py at extglob "$@"*@(.py|cc) |
| 219 | |
| 220 | ## STDOUT: |
| 221 | ['a b', 'c'] |
| 222 | ['star', 'glob', 'a b c.py'] |
| 223 | ['star', 'extglob', 'a b c.cc', 'a b c.py'] |
| 224 | ['at', 'extglob', 'a b', 'cee.cc', 'cee.py'] |
| 225 | ## END |
| 226 | |
| 227 | #### In Array Literal and for loop |
| 228 | shopt -s extglob |
| 229 | mkdir -p eg11 |
| 230 | cd eg11 |
| 231 | touch {foo,bar,spam}.py |
| 232 | for x in @(fo*|bar).py; do |
| 233 | echo $x |
| 234 | done |
| 235 | |
| 236 | echo --- |
| 237 | declare -a A |
| 238 | A=(zzz @(fo*|bar).py) |
| 239 | echo "${A[@]}" |
| 240 | ## STDOUT: |
| 241 | bar.py |
| 242 | foo.py |
| 243 | --- |
| 244 | zzz bar.py foo.py |
| 245 | ## END |
| 246 | |
| 247 | # TODO: Also test with shopt --set simple_word_eval |