quarto-dev / quarto-dev/quarto-cli

Listing `contents` glob silently matches files in sibling subdirectories (smart-glob `**/` prefix leaks across the project)

Open
#14,512 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug listings
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 of ko/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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.