quarto-dev / quarto-dev/quarto-cli
Listing `contents` glob silently matches files in sibling subdirectories (smart-glob `**/` prefix leaks across the project)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 6k
- Forks
- 458
- Avg merge
- 1d 9h
- Merged PRs (30d)
- 41
Description
I have:
- searched the issue tracker for similar issues
- installed the latest version of Quarto CLI (1.9.37)
- formatted my issue following the Bug Reports guide
Bug description
A listing configured with contents: "posts/*.qmd" (or any non-/-anchored path/glob) silently matches files in any directory named posts/ anywhere in the project, not just the posts/ directory adjacent to the listing page.
This breaks the natural use case of a multi-section / bilingual website where two listings live at different depths and each owns its own posts/ subdirectory.
Likely related (but a different symptom of the same path-handling area): #13677.
Steps to reproduce
Minimal project:
project/
├── _quarto.yml # project.type: website
├── index.qmd # listing.contents: "posts/*.qmd"
├── posts/foo.qmd
└── ko/
├── index.qmd # listing.contents: "posts/*.qmd"
└── posts/foo.qmd
_quarto.yml:
project:
type: website
index.qmd:
---
title: EN
listing:
contents: "posts/*.qmd"
---
ko/index.qmd:
---
title: KO
listing:
contents: "posts/*.qmd"
---
Each posts/foo.qmd and ko/posts/foo.qmd just contains a title + date.
Render and inspect _site/listings.json (or open the rendered index.html). The English listing in index.html includes both /posts/foo.html and /ko/posts/foo.html.
quarto render index.qmd --log-level debug confirms:
[listing] Contents: posts/*.qmd
[listing] matches 2 files:
[listing] Reading file .../project/posts/foo.qmd
[listing] Reading file .../project/ko/posts/foo.qmd ← unexpected
Expected behavior
contents: "posts/*.qmd" from a listing in index.qmd should match only dirname(index.qmd)/posts/*.qmd — i.e., posts under the listing's own directory. Cross-section matching is surprising for the multi-section / i18n use case.
Actual behavior
The glob posts/*.qmd is silently rewritten to **/posts/*.qmd, so it also matches ko/posts/foo.qmd.
Root cause
In src/core/path.ts, resolveGlobs.asFullGlob:
if (!glob.startsWith("/")) {
if (smartGlob && (!options || !options.explicitSubfolderSearch)) {
return "**/" + glob; // prepends `**/` to relative globs
}
...
}
useSmartGlobs() returns true by default when no options is passed.
In src/project/types/website/listing/website-listing-read.ts, filterListingFiles calls resolvePathGlobs(...) (and filterPaths(...)) without GlobOptions, so smartGlob is true and every relative entry in contents is silently widened to **/<glob>. This is the design that breaks parallel-section listings.
GlobOptions.explicitSubfolderSearch already exists in path.ts precisely for this case (per its inline comment: "set this to true to never prepend **/ to globs to create nested directory searching") — it's just never wired through from the YAML.
Workaround
Project-rooted leading-/ paths work for a listing whose directory equals the project root:
listing:
contents: "/posts/*.qmd"
asFullGlob strips the leading / and skips the **/ prefix. But this is asymmetric: from a non-root listing (e.g. ko/index.qmd) the /-prefix is resolved against project.dir in expandGlob and against dirname(source) in resolvePathGlobs, so the path doubles up and matches nothing. The remaining workarounds are:
- rename one of the colliding directories (e.g.
ko/articles/instead ofko/posts/), - enumerate files explicitly in
contents(brittle, and also fails when basenames collide across sections — same**/-prefix bug).
Suggested fix
Expose the existing internal explicitSubfolderSearch knob as a per-listing YAML option, preserving today's default (smart-glob true). E.g.:
listing:
contents: "posts/*.qmd"
recursive: false # anchored to dirname(listing.qmd); does not match sibling subdirs
Happy to send a PR (small: ~25 lines across website-listing-shared.ts, website-listing-read.ts, and the website-listing schema in definitions.yml). Naming open to maintainer preference (recursive / anchored / match-subfolders / …).
Your environment
- Quarto CLI 1.9.37
- Linux (Ubuntu 22.04, kernel 5.15)
- Project type:
website
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/core/path.ts with resolveGlobs.asFullGlob and trace how src/project/types/website/listing/website-listing-read.ts calls resolvePathGlobs and filterPaths. Review website-listing-shared.ts and the website-listing schema in definitions.yml for the YAML option wiring. Re-run the minimal bilingual project and confirm relative contents globs stay within the listing directory while the existing default behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100