GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Glob behavior is inconsistent across libraries · Issue #106747 · python/cpython

Glob behavior is inconsistent across libraries #106747

New issue
Copy link
New issue
Copy link
Closed
Closed
Glob behavior is inconsistent across libraries#106747
Copy link
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@jaraco
jaraco
opened on Jul 14, 2023
Issue body actions

the [Python] docs for pathlib.glob are unclear on the meaning of **

You are right, and it gets even more confusing:

  • glob.glob('**') returns dirs and files, non-recursively, not including .
  • glob.glob('**', recursive=True) returns dirs and files, recursively, not including .
  • list(pathlib.Path('.').glob('**')) returns only dirs, recursively, and also includes .

I think there should be a Python upstream issue to document all of that, including the differences between glob and pathlib.

Then zipp could implement what's documented for pathlib.

Repro

Python 3.10.11

mkdir -p dir/subdir ; touch toplevel.txt dir/indir.txt

creates:

toplevel.txt
dir/
  indir.txt
  subdir/
>>> import glob, pathlib

>>> glob.glob('**')
['dir', 'toplevel.txt']

>>> glob.glob('**', recursive=True)
['dir', 'dir/subdir', 'dir/indir.txt', 'toplevel.txt']

>>> list(pathlib.Path('.').glob('**'))
[PosixPath('.'), PosixPath('dir'), PosixPath('dir/subdir')]

Originally posted by @nh2 in jaraco/zipp#102 (comment)

Linked PRs

  • GH-106747: Improve Path.glob() expectations in pathlib tests #112365
  • GH-106747: Make pathlib ABC globbing more consistent with glob.glob() #115056
  • GH-106747: Prepare pathlib globbing for dir_fd support. #116319
  • GH-106747: Document another difference between glob and pathlib. #116518

Activity

Next
  1. jaraco
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    topic-pathlib
    on Jul 14, 2023
  2. jaraco commented on Jul 14, 2023

    @jaraco
    jaraco
    on Jul 14, 2023
    MemberAuthor
    More actions

    Without having thought about it too much, I'd argue pathlib.Path.glob('**') should return dirs and files recursively, not including ., same as glob.glob(recursive=True). I'd also argue that glob.glob('**') should imply recursive=True. After all, what's the meaning of ** if not recursive?

    Reacted by Rosie Wood
  3. jaraco commented on Jul 14, 2023

    @jaraco
    jaraco
    on Jul 14, 2023
    MemberAuthor
    More actions

    @barneygale Do you have any insight on the concern? Any opinions?

  4. jaraco commented on Jul 14, 2023

    @jaraco
    jaraco
    on Jul 14, 2023
    MemberAuthor
    More actions

    Another point of reference, ls on Unix also treats ** as recursive without the dot:

    # ls **
    dir/indir.txt  toplevel.txt
    
    dir:
    indir.txt  subdir
    
    dir/subdir:
    
  5. barneygale commented on Jul 14, 2023

    @barneygale
    barneygale
    on Jul 14, 2023
    Contributor
    More actions

    Yeah, it's a mess

    the [Python] docs for pathlib.glob are unclear on the meaning of **

    Logged as #101112, stale-ish PR up at #101223

    • glob.glob('**', recursive=True) returns dirs and files, recursively, not including .

    Try glob.glob('tests/**', recursive=True) - you'll find that tests is included in the results, and so ** is capable of consuming zero path segments. The fact that it doesn't emit . seems like the bug to me! Also conflicts with the docs.

    • list(pathlib.Path('.').glob('**')) returns only dirs, recursively, and also includes .

    Logged as #70303, PR up as #105413.

  6. jaraco commented on Jul 15, 2023

    @jaraco
    jaraco
    on Jul 15, 2023
    MemberAuthor
    More actions

    I'm going to be nitpicky because this issue demands it. Understand my matter of fact tone below may come off as hostile, but it's not intended that way. I'm merely trying to lay out the facts and disentangle the concerns.

    you'll find that tests is included in the results

    More precisely, tests/ is included in the results:

     cpython main @ glob.glob('Lib/test/**', recursive=True)[0]
    'Lib/test/'
    

    Note I'm using xonsh shell, which is why you see python interleaved with a shell prompt in the cpython repo.

    Interestingly, tests/ is not included in the results if recursive=False.

    The fact that it doesn't emit . seems like the bug to me! Also conflicts with the docs.

    That's the opposite of my intuition. It's almost never the case that a user wants . in their globs (any more than they want ..). When I do glob('mydir/*'), I want to find all of the contents of "mydir", and "mydir" itself is not contained in "mydir".

    Even pathlib only emits the . when a recursive glob is indicated:

     cpython main @ next(pathlib.Path().glob('*'))
    PosixPath('PC')
     cpython main @ next(pathlib.Path().glob('**'))
    PosixPath('.')
    

    I'm now confused. These two statements seem contradictory:

    tests is included in the results

    it doesn't emit .

    In the case of glob.glob('tests/**', recursive=True), tests/ is emitted and that's equivalent to tests/.. Are you saying you think it's a bug that tests/ is emitted without a dot?

    Also, I'm unsure how it conflicts with the docs. Can you elaborate on where the conflict lies?

  7. barneygale commented on Jul 15, 2023

    @barneygale
    barneygale
    on Jul 15, 2023
    Contributor
    More actions

    That's the opposite of my intuition. It's almost never the case that a user wants . in their globs (any more than they want ..). When I do glob('mydir/*'), I want to find all of the contents of "mydir", and "mydir" itself is not contained in "mydir".

    Even pathlib only emits the . when a recursive glob is indicated:

     cpython main @ next(pathlib.Path().glob('*'))
    PosixPath('PC')
     cpython main @ next(pathlib.Path().glob('**'))
    PosixPath('.')
    

    I was talking about **, not *. The behaviour of standalone * pattern segments is well-defined: it always consumes one path segment.

    In the case of glob.glob('tests/**', recursive=True), tests/ is emitted and that's equivalent to tests/.. Are you saying you think it's a bug that tests/ is emitted without a dot?

    I'm saying that these two function calls should return the same result, as they do in pathlib:

    >>> def myglob(pattern, root_dir=''):
    ...     for path in glob.glob(pattern, root_dir=root_dir, recursive=True):
    ...         yield os.path.join(root_dir, path)
    ... 
    >>> list(myglob('testdir/**', ''))
    ['testdir/', 'testdir/a', 'testdir/b']
    >>> list(myglob('**', 'testdir'))
    ['testdir/a', 'testdir/b']

    Also, I'm unsure how it conflicts with the docs. Can you elaborate on where the conflict lies?

    ** is described as matching "any files and zero or more directories". You can see that, in the testdir/** example above, it can consume zero files and zero directories. But in the ** example, it doesn't consume the current directory.

  8. barneygale commented on Jul 15, 2023

    @barneygale
    barneygale
    on Jul 15, 2023
    Contributor
    More actions

    FWIW, there is no single standard for how ** should work. Different shells and programming languages implement it differently. See https://stackoverflow.com/questions/70613723/recursive-globs-or-globstar-different-on-zsh-bash-python-and-ruby

  9. serhiy-storchaka
    added this to Fnmatch and glob issueson Jul 18, 2023
  10. barneygale commented on Jul 26, 2023

    @barneygale
    barneygale
    on Jul 26, 2023
    Contributor
    More actions

    FYI, I'm looking to add a new sep argument to fnmatch.translate() which would unify its pattern-matching with pathlib.PurePath.match() and pathlib.Path.glob(). In doing so it would make fnmatch.translate() more compatible with glob.glob(), but there would still be differences, such as the above-noted difference in how ** works, but also the default treatment of hidden files/directories. PR here:

    • GH-72904: Add glob.translate() function #106703
  11. jaraco
    assigned
    jaraco
    and unassigned
    jaraco
    on Aug 4, 2023
  12. jaraco commented on Aug 4, 2023

    @jaraco
    jaraco
    on Aug 4, 2023
    MemberAuthor
    More actions

    I'm very interested in reviewing the proposed change. I'm particularly interested to see if it obviates or conflicts with zipp.glob.

  13. iritkatriel
    added
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    on Nov 23, 2023
  14. 15 remaining items

  15. barneygale
    added a commit that references this issue on Mar 4, 2024

    pythonGH-106747: Prepare pathlib globbing for dir_fd support.

    6932522
  16. serhiy-storchaka commented on Mar 5, 2024

    @serhiy-storchaka
    serhiy-storchaka
    on Mar 5, 2024
    Member
    More actions

    Let resolve some misunderstandings.

    • glob.glob('**') returns dirs and files, non-recursively, not including .

    ** without recursive=True is interpreted as * (two subsequent *s, but this is the same). This is not an example for **.

    Another point of reference, ls on Unix also treats ** as recursive without the dot:

    ls does not interpolate patterns. The shell does this. By default ** is equivalent to *, which is expanded to dir toplevel.txt. ls dir produces indir.txt subdir, and ls toplevel.txt produces toplevel.txt. I do not know how did you get your result, except if you use very non-standard shell and ls.

    In these two cases but no others, ** effectively means **/*, i.e. the returned paths must have at least one segment. I'm increasingly convinced that this is wrong.

    The patch cannot have less than one component.

    Also different: glob.glob("", root_dir=path) returns nothing, but path.glob("") raises ValueError. I think they're both wrong, and instead the path should be yielded if it's a directory.

    No file or directory name matches pattern "", therefore the glob.glob() result is correct. As for path.glob("") it is a matter of question how much we want it to differ from glob.glob(). They cannot be completely equivalent for objective reasons, so we should decide what differences are deliberate. Raising an error is safer than producing a different result.

    Reacted by Barney Gale and Kerim Kabirov
  17. barneygale commented on Mar 5, 2024

    @barneygale
    barneygale
    on Mar 5, 2024
    Contributor
    More actions

    Thanks for the clarifications

    The patch cannot have less than one component.

    pathlib.Path('.') is a zero-component path in pathlib. Its .parts is an empty tuple, whereas Path('foo').parts is a 1-element tuple.

    os.path.relpath() needs to count components in order to generate the correct number of ../ components, and counts empty/dot paths as having zero components:

    >>> os.path.relpath('foo', start='.')
    'foo'
    >>> os.path.relpath('foo', start='bar')
    '../foo'
  18. barneygale commented on Mar 5, 2024

    @barneygale
    barneygale
    on Mar 5, 2024
    Contributor
    More actions

    FWIW, most of these differences highlighted in this thread are now either resolved or documented.

    I'll open a PR to document that glob.glob(root_dir=root_dir) never returns an empty path (that could represent the root_dir), whereas pathlib.Path(root_dir).glob() can return the root_dir. I think that'll cover the remaining differences.

    Reacted by Jason R. Coombs and Maciej Olko
  19. barneygale
    added 2 commits that reference this issue on Mar 8, 2024

    pythonGH-106747: Document another difference between `glob` and `path…

    c897890

    Merge branch 'main' into pythongh-106747-empty-path

    d33f087
  20. barneygale
    added a commit that references this issue on Mar 22, 2024

    GH-106747: Document another difference between `glob` and `pathlib`. (#…

    72eea51
  21. adorilson
    added a commit that references this issue on Mar 25, 2024

    pythonGH-106747: Document another difference between `glob` and `path…

    735ed97
  22. NGRsoftlab
    added 2 commits that reference this issue on Apr 2, 2024

    Squashed commit of the following:

    1b5fe4c

    Squashed commit of the following:

    a016a54
  23. diegorusso
    added a commit that references this issue on Apr 17, 2024

    pythonGH-106747: Document another difference between `glob` and `path…

    af55900
  24. Glyphack
    added a commit that references this issue on Sep 2, 2024

    pythonGH-106747: Improve `Path.glob()` expectations in pathlib tests (p…

    acdaca7
  25. hugovk
    closed this as completedon Oct 23, 2026
  26. github-project-automation
    moved this from Todo to Done in pathlib issueson Oct 23, 2026
  27. github-project-automation
    moved this to Done in Fnmatch and glob issueson Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    • Fnmatch and glob issues
      Status
      Done
      Show more project fields
    • pathlib issues
      Status
      Done
      Show more project fields

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.