Why does glob negation require a space after `!`

I think that the new glob negation feature is great (#13663), but I got burnt because I forgot the space after the !. (I was in the process of porting a site to the new sites.matrix and thought that the feature was broken.)

For example, I used:

files: ['!specs/**']  # Doesn't work - silently ignored

When I needed:

files: ['! specs/**']  # Works

@bep, I’m curious why there’s a requirement for a space following the !. You’re very explicit about this requirement in #13663, but don’t explain why. Other systems, including .gitignore, Prettier, ESLint, etc., just need a leading ! without a space.

Otherwise, would it make sense for Hugo to emit a warning when a pattern starts with ! but doesn’t match the "! " prefix? That would help users discover the correct syntax instead of failing silently.

Ref:

I think this is a valid Glob pattern: !a. So to, Hugo’s "! " negates the match result, not the pattern, and short circuits on match, so:

  • !a is very different from ! a
  • And without the space, we could never write ! !a

So, to avoid the above ambigouty (I can never spell that word), we needed some kind of separator, and a "! " is used in some other internal tooling in Hugo, so that was it. With the latest Hugo we added more prefixes, which makes this decision even “smarter”.

1 Like

If I’m not mistaken, Hugo uses GitHub - gobwas/glob: Go glob, which does not interpret a leading ! in any special way. A ! only has meaning inside a character class, like [!abc]. That is, !abc matches the literal string !abc. (How likely is it that there’s a file name starting with !? Very unlikely IMHO.)

So, as far as I can tell, the space after the ! in Hugo’s negation syntax isn’t necessary. Could it be dropped? Or could Hugo, at a minimum, warn about !abc probably not meaning what the user expects?

We currently use glob slices for other things too (e.g., image Meta fields, content dimensions in a sites matrix), and I suspect we’ll use them more in the future. I have no idea if the literal string !a could appear in anything we might want to match against in the future; it might seem unlikely now, but you and I have both been bitten by stuff like this in the past.

With v0.155.0 we now have “range matchers” that use these operators, separated from the value by a… space:

<= v1.2.3
< v1.2.3
> v1.2.3
>= v1.2.3
== v1.2.3
!= v1.2.3
1 Like

Not sure where this discussion comes from, but EVEN IF Glob doesn’t interpret ! in a special way:

Assuming there’s an language starting with “!”, the n this:

languages = "!weirdlanguasestartingwithexplamationmark"

Is fundamentaly different from

languages = "! !weirdlanguasestartingwithexplamationmark"

Curious, @chalin how does the examples libraries in your intial post handle the above?

We currently use glob slices for other things too

@jmooring - thanks for that clarification. I haven’t used version dimensions yet, and wasn’t aware of the other operators.

The source of my confusion was the following. The files entry reads:

files

(string or []string) A glob slice defining the files to include or exclude.

Glob slice reads:

A glob slice is a slice of strings where each string is a glob pattern.

I originally looked quickly at the Wikipedia Glob page (that “glob pattern” links to) and assumed that ! was a part of the Glob syntax because the “glob slice” definition I quoted above says it should be.

But based on our discussion, it would be more accurate to define the syntax of a glob slice in Hugo as:

  • An optional (Hugo glob-slice?) operator followed by a space; followed by
  • A glob pattern (non-empty)

Right?


Not sure where this discussion comes from …

@bep - hopefully my comments above clarify the source of my confusion.

As I mentioned in opentelemetry.io/pull/9070, my original quibble is that for someone new to the sites.matrix and mounts files features, it’s easy to mistakingly write

files: ['!specs/**']

and be confused that (1) it doesn’t work as expected (that is, no files get included at all), and (2) there’s no indication that this is erroneous.

That being said, I understand the feature’s syntax better now, thanks!

Edit: I’d be glad to submit an update to the docs if that can help. Let me know.