Apply the POSIX dot rule to each glob pattern component - #7481
Open
robsyme wants to merge 1 commit into
Open
Conversation
Output file globs and `channel.fromPath` descend into hidden directories
and decide hidden-ness from the matched file name alone, so a file with
an ordinary name below a hidden directory is collected even when the
pattern does not name that directory. Fusion writes its metadata store
to `<taskWorkDir>/.fusion/v1/<absolute entry path>/md.json`, so a module
declaring an unanchored pattern such as `path("**/outs/**")` collects
those internal files as task outputs and publishes them.
POSIX Issue 8, section 2.14.3: "If a filename begins with a <period>
( '.' ), the <period> shall be explicitly matched by using a <period> as
the first character of the pattern or immediately following a <slash>
character." bash and zsh reproduce that wording. Nextflow implements the
first clause and not the second, which the `FileOutParam.hidden` javadoc
already claims to follow ("coherently with linux bash rule").
Given `.foo/bar.txt`, `.secret.txt`, `pub/bar.txt` and `visible.txt`,
bash 3.2, bash 5.3 and zsh 5.9 return:
*.txt -> visible.txt
*/bar.txt -> pub/bar.txt
.*/bar.txt -> .foo/bar.txt
Nextflow returned `.foo/bar.txt` as well for the second pattern, from
both a process output declaration and `channel.fromPath`. It now returns
the three rows above.
Collect the dot-prefixed components of the pattern up-front, compile
each one to a single-component matcher, and visit a hidden name only
when it matches one of them. The predicate replaces the tree-wide
`includeHidden` flag at the three places hidden-ness is decided i.e. the
sub-tree pruning, the directory match and the file match. Hidden
directories are pruned from the walk, which also avoids listing them on
an object store.
The authorisation is checked against the name rather than the position
of the component within the pattern, because `**` matches an arbitrary
number of components and cannot be aligned with a path component. A
dotted component therefore authorises that name at any depth. A literal
component such as `.fusion` is exact; a glob component such as the `.*`
in `**/.*/outs/*` over-authorises nested hidden directories relative to
bash.
`TaskFileCollector` no longer derives the `hidden` option from the
pattern. Doing so turned a single dotted component into a tree-wide
authorisation, which defeated the rule for a pattern starting with a
dot e.g. `.config/**/*.txt` also collected `.config/a/.secret/x.txt`.
The non-glob `syntax` keeps the previous behaviour, since a leading dot
in a regular expression is the any-char meta-character and a slash is
not a component separator. A start folder that is itself hidden stays
walkable, so a work directory such as `/scratch/.tmp/ab/cdef` is
unaffected.
Two behaviour changes for users. A pattern with a glob component above a
hidden directory no longer reaches into it, so `channel.fromPath('/data/**')`
stops returning files under `.git` or `.snapshot`. A pattern that leads
with a dot no longer switches off hidden filtering for the rest of the
pattern. Both are opt-out with `hidden: true` or by naming the component.
Tested on the S3 file system, which does not implement `getPathMatcher`
and therefore falls back to `getDefaultPathMatcher`, matching the path
`toString()`. `S3Path.getFileName()` returns a bucket-less relative
path, so a component matcher sees the plain name.
Closes #7480
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Rob Syme <rob.syme@gmail.com>
✅ Deploy Preview for nextflow-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
pditommaso
force-pushed
the
master
branch
2 times, most recently
from
August 20, 2026 12:03
5f935c2 to
d1eae20
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7480
The problem
Output file globs and
channel.fromPathwalk into hidden directories, and decide hidden-ness from the matched file's own name. A file with an ordinary name below a hidden directory is therefore collected even when the pattern never names that directory.This surfaced through Fusion, which writes its metadata store to
<taskWorkDir>/.fusion/v1/<absolute entry path>/md.json. nf-core/scrnaseq'sCELLRANGER_COUNTdeclarespath("**/outs/**"), so those internal files are collected as task outputs and published into the user's results as<outdir>/cellranger/count/.fusion/v1/s3/<bucket>/.... Reported by a user copying published outputs into a data catalogue.The rule
POSIX Issue 8, §2.14.3:
bash and zsh reproduce that wording, and
FileOutParam.hidden's javadoc already claims to follow it ("coherently with linux bash rule"). Nextflow implements the first clause and not the second. Note the authorisation is positional: a dotted component authorises a hidden name where it appears, not for the whole pattern.Given
.foo/bar.txt,.secret.txt,pub/bar.txt,visible.txt, bash 3.2, bash 5.3 and zsh 5.9 return:Before this PR Nextflow also returned
.foo/bar.txtfor the second pattern, from both a process output declaration andchannel.fromPath. It now returns the three rows above.The change
The dot-prefixed components of the pattern are collected up-front and compiled to single-component matchers. A hidden name is visited only when it matches one of them. That predicate replaces the tree-wide
includeHiddenflag at the three places hidden-ness is decided: the sub-tree pruning, the directory match and the file match. Hidden directories are pruned from the walk, which also avoids listing them on an object store.TaskFileCollectorno longer deriveshiddenfrom the pattern. Doing so turned a single dotted component into a tree-wide authorisation and defeated the rule for any pattern starting with a dot.Unchanged:
hidden: true, the non-globsyntax(a leading dot in a regex is the any-char meta-character and a slash is not a component separator), and a start folder that is itself hidden, so a work dir such as/scratch/.tmp/ab/cdefstays walkable.Reproducer
Self-contained, no Fusion, no cloud, no containers.
Notes for reviewers
Two user-visible behaviour changes. A pattern with a glob component above a hidden directory no longer reaches into it, so
channel.fromPath('/data/**')stops returning files under.gitor.snapshot. And a pattern that leads with a dot no longer switches off hidden filtering for the rest of the pattern, so.config/**/*.txtno longer returns.config/a/.secret/x.txt. Both match bash and zsh, and both are opt-out withhidden: trueor by naming the component.One existing test expectation was inverted.
TaskFileCollectorTest > should create the map of path visit optionsassertedvisitOptions('.hidden_file')yieldshidden: true. That is the behaviour being removed, so it now expectsfalse, with a comment pointing here.A known approximation. The authorisation is checked against the name rather than the position of the component within the pattern, because
**matches an arbitrary number of components and cannot be aligned with a path component. A literal component such as.fusionis exact. A glob component such as the.*in**/.*/outs/*over-authorises nested hidden directories relative to bash. Making this exact needs real position alignment, which I did not attempt.Unrelated, but adjacent.
**/.hiddenmid/outs/*does not match a.hiddenmidat the root the way bash does, because Java's**/requires a leading component. That is #5948, not this change. The test fixtures nest their hidden directories one level down to avoid conflating the two.S3 was verified specifically, since it is the backend the issue was reported on and
S3FileSystem.getPathMatcher()throws, so matching falls back togetDefaultPathMatcheragainst the pathtoString().S3Path.getFileName()returns a bucket-less relative path, so a component matcher sees the plain name. Pinned by tests inFileHelperS3Test, built offline with no credentials, including a dotted bucket case.Testing
New tests in
FileHelperTest(including a file system that refusesgetPathMatcher, to exercise the fallback),FileHelperS3Test, andTaskFileCollectorTestat the layer the user sees. Docs updated: thehiddenoption reference claimed the pattern has to start with a dot and said nothing about directory traversal.