104

I know **/*.ext expands to all files in all subdirectories matching *.ext, but what is a similar expansion that includes all such files in the current directory as well?

codeforester
  • 39,467
  • 16
  • 112
  • 140
Ramon
  • 8,202
  • 4
  • 33
  • 41

5 Answers5

124

This will work in Bash 4:

ls -l {,**/}*.ext

In order for the double-asterisk glob to work, the globstar option needs to be set (default: on):

shopt -s globstar

From man bash:

    globstar
                  If set, the pattern ** used in a filename expansion con‐
                  text will match a files and zero or more directories and
                  subdirectories.  If the pattern is followed by a /, only
                  directories and subdirectories match.

Now I'm wondering if there might have once been a bug in globstar processing, because now using simply ls **/*.ext I'm getting correct results.

Regardless, I looked at the analysis kenorb did using the VLC repository and found some problems with that analysis and in my answer immediately above:

The comparisons to the output of the find command are invalid since specifying -type f doesn't include other file types (directories in particular) and the ls commands listed likely do. Also, one of the commands listed, ls -1 {,**/}*.* - which would seem to be based on mine above, only outputs names that include a dot for those files that are in subdirectories. The OP's question and my answer include a dot since what is being sought is files with a specific extension.

Most importantly, however, is that there is a special issue using the ls command with the globstar pattern **. Many duplicates arise since the pattern is expanded by Bash to all file names (and directory names) in the tree being examined. Subsequent to the expansion the ls command lists each of them and their contents if they are directories.

Example:

In our current directory is the subdirectory A and its contents:

A
└── AB
    └── ABC
        ├── ABC1
        ├── ABC2
        └── ABCD
            └── ABCD1

In that tree, ** expands to "A A/AB A/AB/ABC A/AB/ABC/ABC1 A/AB/ABC/ABC2 A/AB/ABC/ABCD A/AB/ABC/ABCD/ABCD1" (7 entries). If you do echo ** that's the exact output you'd get and each entry is represented once. However, if you do ls ** it's going to output a listing of each of those entries. So essentially it does ls A followed by ls A/AB, etc., so A/AB gets shown twice. Also, ls is going to set each subdirectory's output apart:

...
<blank line>
directory name:
content-item
content-item

So using wc -l counts all those blank lines and directory name section headings which throws off the count even farther.

This a yet another reason why you should not parse ls.

As a result of this further analysis, I recommend not using the globstar pattern in any circumstance other than iterating over a tree of files in this manner:

for entry in **
do
    something "$entry"
done

As a final comparison, I used a Bash source repository I had handy and did this:

shopt -s globstar dotglob
diff <(echo ** | tr ' ' '\n') <(find . | sed 's|\./||' | sort)
0a1
> .

I used tr to change spaces to newlines which is only valid here since no names include spaces. I used sed to remove the leading ./ from each line of output from find. I sorted the output of find since it is normally unsorted and Bash's expansion of globs is already sorted. As you can see, the only output from diff was the current directory . output by find. When I did ls ** | wc -l the output had almost twice as many lines.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 6
    I tested Ubuntu and Cygwin, and `globstar` is defaulted `off` – Zombo Jul 01 '13 at 08:17
  • 13
    The best answer! but I think `**/*.ext` should be enough though. Also, you won't have the hidden files unless you `shopt -s dotglob`. – gniourf_gniourf Jul 01 '13 at 20:49
  • My Centos 6 Bash 4.1.2 also defaults to off for globstar option. – toxalot Mar 10 '14 at 09:29
  • 2
    To disable `globstar`: `shopt -u globstar`. – kenorb Apr 18 '15 at 21:48
  • 5
    @gniourf_gniourf The question actually asks to include the current directory specifically so no, `**/*.ext` won't be enough – msciwoj Aug 27 '15 at 14:17
  • 1
    @msciwoj But `**/*.ext` *does* include files in the current directory. The quoted manual snippet even says "zero or more directories and subdirectories". – Benjamin W. Nov 04 '17 at 17:41
  • @BenjaminW. I guess it depends on your config. On my current macOS machine it does not include files in the current directory. – dotnetCarpenter Sep 04 '18 at 11:27
  • However the following works `rename 's/\.js$/\.mjs/' {*,**/*}` – dotnetCarpenter Sep 04 '18 at 11:29
  • @dotnetCarpenter It looks like `**` not matching the current directory was a bug that exists in a few versions of Bash: http://lists.gnu.org/archive/html/bug-bash/2009-05/msg00062.html – Benjamin W. Sep 04 '18 at 13:42
  • 2
    It also seems like the bash version that ships with macOS is handicapped. `{*,**/*}` only matches current directory plus child directories but not children of these child directories. and the `shopt` list doesn't have `globstar`. I've tried the bash shell that comes with homebrew (4.4.23(1)) with globstar on and it does the same with my glob pattern. – dotnetCarpenter Sep 04 '18 at 18:27
  • 2
    @dotnetCarpenter: The version of Bash that ships with MacOS is 3.2 which doesn't support globstar, as you found out. A double asterisk is treated the same as a single one. Globstar was introduced in Bash 4.0. – Dennis Williamson Sep 04 '18 at 18:34
  • For some reason for me, bash 5 on a MacBook Pro wouldn't let me set globstar, and wouldn't work with `*/**/*`, but did work with `**/*`. More evidence of a bug? – combinatorist May 25 '23 at 18:23
  • 1
    @combinatorist: How are you setting it? What error message do you receive? What is the output of `shopt globstar` (no switches) afterwards? To set `globstar` the command is `shopt -s globstar`. You may know this but `*/**/*` says "within the subdirectories in the current directory, output those subdirectory names and all filenames below them recursively without any lower bare subdirectory names" while `**/*` is the same as `**` which expands to ... – Dennis Williamson May 26 '23 at 00:46
  • 1
    ... all the files and directories in the current directory recursively. The former is a pretty unusual and the latter, simplified to (`**`), is pretty common. What is it that you're trying to do? – Dennis Williamson May 26 '23 at 00:46
  • @DennisWilliamson: My output for both commands (`shopt -s globstar` and shopt globstar`) is `-bash: shopt: globstar: invalid shell option name`, but I just realized I'm used a brew installed bash, so maybe that's the reason. – combinatorist Jun 01 '23 at 02:30
  • @DennisWilliamson: Meanwhile, I lied about running `*/**/*`. In reality I tried `s*/**/cache/*` because I had `cache` directories I wanted to look at in two directories that both happened to start with `s`. But I said it "didn't" work because it only grabbed one at a certain depth and ignored the other at another depth. I didn't have `cache` directories anywhere else, so I dropped the `s` specification and `**/cache/*` worked for me, grabbed both directories at their different depths. – combinatorist Jun 01 '23 at 02:33
17

You can use: **/*.* to include all files recursively (enable by: shopt -s globstar).

Here is the behavior of other variations:


Testing folder with 3472 files in the sample VLC repository folder:

(Total files of 3472 counted as per: find . -type f | wc -l)

  • ls -1 **/*.* - returns 3338
  • ls -1 {,**/}*.* - returns 3341 (as proposed by Dennis)
  • ls -1 {,**/}* - returns 8265
  • ls -1 **/* - returns 7817, except hidden files (as proposed by Dennis)
  • ls -1 **/{.[^.],}* - returns 7869 (as proposed by Dennis)
  • ls -1 {,**/}.?* - returns 15855
  • ls -1 {,**/}.* - returns 20321

So I think the most closest method to list all files recursively is the first example (**/*.*) as per gniourf-gniourf comment (assuming the files have the proper extensions, or use the specific one), as the second example gives few more duplicates like below:

$ diff -u <(ls -1 {,**/}*.*) <(ls -1 **/*.*)
--- /dev/fd/63  2015-04-19 15:25:07.000000000 +0100
+++ /dev/fd/62  2015-04-19 15:25:07.000000000 +0100
@@ -1,6 +1,4 @@
 COPYING.LIB
-COPYING.LIB
-Makefile.am
 Makefile.am
@@ -45,7 +43,6 @@
 compat/tdestroy.c
 compat/vasprintf.c
 configure.ac
-configure.ac

and the other generate even further duplicates.


To include hidden files, use: shopt -s dotglob (disable by shopt -u dotglob). It's not recommended, because it can affect commands such as mv or rm and you can remove accidentally the wrong files.

Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
kenorb
  • 155,785
  • 88
  • 678
  • 743
  • 1
    On Mac terminal and bash with globstar enabled, I found the above solution (```**/*.*```) informative and worked best. The accepted answer caused duplicates of items in the top directory. My working pattern was: ```"${path}"**/*.*``` – mummybot Jan 16 '17 at 10:16
  • It would be interesting to try this with other options like nullglob and dotglob – Wilf Jul 08 '18 at 22:24
14

This wil print all files in the current directory and its subdirectories which end in '.ext'.

find . -name '*.ext' -print
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • While this answer doesn't meet the OP's requested "expansion" in the strictest sense, it is most likely to produce the desired outcome. – Dennis Williamson Feb 21 '19 at 02:34
9

Why not just use brace expansion to include the current directory as well?

./{*,**/*}.ext

Brace expansion happens before glob expansion, so you can effectively do what you want with older versions of bash, and can forego monkeying with globstar in newer versions.

Also, it's considered good practice in bash to include the leading ./ in your glob patterns.

clone206
  • 89
  • 1
  • 1
4
$ find . -type f

That will list all of the files in the current directory. You can then do some other command on the output using -exec

$find . -type f -exec grep "foo" {} \;

That will grep each file from the find for the string "foo".

Amir Afghani
  • 37,814
  • 16
  • 84
  • 124
  • Now that it's 11 years later, might be time that someone points out that `find . -type f` applies recursively with the root at current directory, not only to the current directory. – Roger Dahl May 04 '20 at 00:24