From 22b5140a69cad406f9853d1c8ffcba2a33f2f0f2 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:29:15 -0400 Subject: [PATCH 1/3] Test that every row of the glob page holds in dirsql and in bash --- docs/reference/glob.md | 139 ++++++++++++++++++ packages/rust/tests/glob_conformance.rs | 178 ++++++++++++++++++++++++ scripts/glob-conformance.sh | 81 +++++++++++ 3 files changed, 398 insertions(+) create mode 100644 docs/reference/glob.md create mode 100644 packages/rust/tests/glob_conformance.rs create mode 100755 scripts/glob-conformance.sh diff --git a/docs/reference/glob.md b/docs/reference/glob.md new file mode 100644 index 000000000..a038a8f10 --- /dev/null +++ b/docs/reference/glob.md @@ -0,0 +1,139 @@ +# Globs + +A glob is the string you write where a table name goes (`FROM './docs/*.md'`) +or in a config `[[table]]`'s `glob`. dirsql reads it the way `bash` does with +`globstar` on and `dotglob` off, and every place it does not is listed under +[Divergences from bash](#divergences-from-bash). + +## Why bash + +Bash with `globstar` is the most popular glob behavior. Bash, zsh, node-glob +and minimatch (npm, ESLint, VS Code, GitHub Actions) and Python's `glob` agree +on one core: `*`, `?` and `[...]`, recursive `**`, `{a,b}` braces, `*` hiding +dotfiles, and `**` not entering symlinked directories. Python has no braces +and follows symlinks; bash breaks that tie. POSIX has no `**` or braces. So +there is one standard nobody has to learn, the same string works in a +[path-table](./path-tables.md) and in a [config](./config.md) `glob`, and each +divergence is a written decision. + +## The fixture + +Every example below runs against this tree. A name ending in `/` is an empty +directory, `a -> b` is a symlink, and `.gitignore` holds `*.log`. + + +```text +top.md +a.md +b.md +c.md +1.md +2.md +3.md +4.md +x.txt +ignored.log +{q}.md +[a].md +[z.md +my file.md +.env +.git/ +.config/app.toml +.config/.secret +.config/sub/deep.md +docs/guide.md +docs/api.md +docs/.draft.md +docs/nested/deep.md +docs/nested/.cache/x.md +node_modules/pkg/index.js +real/r.md +linkdir -> real +link.md -> top.md +broken -> nowhere +``` + +## Examples + +Each row lists the files the pattern returns, sorted. Every row without a +divergence was produced by `bash -O globstar -O nullglob` with `dotglob` off, +run from the fixture's root, keeping only files. The `Divergence` column names +the rule from [below](#divergences-from-bash) that makes a row differ from +bash's raw output. `$ROOT` is the fixture's directory and `$BASE` its parent. + +A test runs every row against dirsql as a path-table and, with the leading +`./` dropped, as a config `glob`, so this table cannot drift from the code. +`scripts/glob-conformance.sh check` re-runs the bash side. + + +| Pattern | Rows | Divergence | +| --- | --- | --- | +| `./*.md` | `1.md`, `2.md`, `3.md`, `4.md`, `[a].md`, `[z.md`, `a.md`, `b.md`, `c.md`, `link.md`, `my file.md`, `top.md`, `{q}.md` | | +| `./**/*.md` | `1.md`, `2.md`, `3.md`, `4.md`, `[a].md`, `[z.md`, `a.md`, `b.md`, `c.md`, `docs/api.md`, `docs/guide.md`, `docs/nested/deep.md`, `link.md`, `linkdir/r.md`, `my file.md`, `real/r.md`, `top.md`, `{q}.md` | | +| `./**` | `1.md`, `2.md`, `3.md`, `4.md`, `[a].md`, `[z.md`, `a.md`, `b.md`, `c.md`, `docs/api.md`, `docs/guide.md`, `docs/nested/deep.md`, `link.md`, `my file.md`, `real/r.md`, `top.md`, `x.txt`, `{q}.md` | | +| `./?.md` | `1.md`, `2.md`, `3.md`, `4.md`, `a.md`, `b.md`, `c.md` | | +| `./[ab].md` | `a.md`, `b.md` | | +| `./[!a].md` | `1.md`, `2.md`, `3.md`, `4.md`, `b.md`, `c.md` | | +| `./\[a\].md` | `[a].md` | | +| `./[z.md` | `[z.md` | | +| `./{a,b}.md` | `a.md`, `b.md` | | +| `./{q}.md` | `{q}.md` | | +| `./{a.md` | none | | +| `./{1..3}.md` | `1.md`, `2.md`, `3.md` | | +| `./my file.md` | `my file.md` | | +| `./.*` | `.env`, `.gitignore` | | +| `./**/.*` | `.env`, `.gitignore`, `docs/.draft.md` | | +| `./.env` | `.env` | | +| `./.config/**` | `.config/app.toml`, `.config/sub/deep.md` | | +| `./docs/.*` | `docs/.draft.md` | | +| `./link.md` | `link.md` | | +| `./linkdir/*` | `linkdir/r.md` | | +| `./*/r.md` | `linkdir/r.md`, `real/r.md` | | +| `./**/r.md` | `linkdir/r.md`, `real/r.md` | | +| `./real/**` | `real/r.md` | | +| `./docs` | `docs/api.md`, `docs/guide.md` | directory-name | +| `./docs/` | `docs/api.md`, `docs/guide.md` | directory-name | +| `./*/` | `docs/api.md`, `docs/guide.md`, `linkdir/r.md`, `real/r.md` | directory-name | +| `./d*` | none | files-only | +| `./*.log` | none | gitignore | +| `./**/*.js` | none | node-modules | +| `./node_modules/*/index.js` | `node_modules/pkg/index.js` | | +| `./docs/**/*.md` | `docs/api.md`, `docs/guide.md`, `docs/nested/deep.md` | | +| `./docs/*.md` | `docs/api.md`, `docs/guide.md` | | +| `$ROOT/docs/*.md` | `$ROOT/docs/api.md`, `$ROOT/docs/guide.md` | absolute | +| `~/docs/*.md` | `$ROOT/docs/api.md`, `$ROOT/docs/guide.md` | absolute | +| `../proj/docs/*.md` | `$BASE/proj/docs/api.md`, `$BASE/proj/docs/guide.md` | absolute | + +## Divergences from bash + +| Tag | Rule | Why | +| --- | --- | --- | +| `gitignore` | Inside a git repo, `.gitignore` files hide matching files and directories. `--no-ignore` turns it off. | A query over a repo should not drown in build output. | +| `directory-name` | A pattern's last component that names a directory lists the files directly inside it, like `ls docs`. A trailing `/` is `*` appended. | A table is rows of files, and `ls` is the shell habit. | +| `files-only` | A pattern that matches a directory but no file returns nothing. | Every row is a file. | +| `node-modules` | A `node_modules` directory is skipped unless the pattern spells its name. | A zero-config `./**` should not walk dependencies. | +| `absolute` | `/`, `~/` and `../` patterns report absolute `path` values, with `..` folded out textually. | The scanned root is derived from the pattern, so there is no relative base. | + +`.git` needs no rule of its own: the dotfile rule hides it. + +## Where the glob is anchored + +A path-table pattern is relative to the index root. A config `[[table]] glob` +is relative to the directory holding its config file, whatever directory you +run `dirsql` from. A config table whose glob matches no files prints +`dirsql: table 'name': glob 'pattern' matched no files under ` to +stderr; the exit code is unchanged. Path-tables never warn. + +## Edge cases + +- `{` , `}` or `[` with no partner is a literal character. +- A brace group with no comma and no `..` is literal (`{q}`); `{1..3}` and + `{a..c}` expand. +- `\[` is a literal bracket. +- A dot-named file or directory is listed only where a component spells the + dot at that depth: `./**/.*` finds `docs/.draft.md` but does not enter + `.config/`. +- A symlinked file is a file. A symlinked directory is entered by any component + except `**`. +- A broken link lists nothing. diff --git a/packages/rust/tests/glob_conformance.rs b/packages/rust/tests/glob_conformance.rs new file mode 100644 index 000000000..13921f8e5 --- /dev/null +++ b/packages/rust/tests/glob_conformance.rs @@ -0,0 +1,178 @@ +//! The example table in `docs/reference/glob.md` is the specification: every +//! row runs against dirsql as a path-table and as a config `[[table]] glob`. +//! `scripts/glob-conformance.sh` checks the same rows against bash. +#![cfg(all(unix, feature = "cli"))] + +use std::collections::HashMap; +use std::fs; +use std::os::unix::fs::symlink; +use std::path::Path; + +use assert_cmd::prelude::*; +use dirsql::{DirSQL, Table, Value}; +use tempfile::TempDir; + +struct Row { + pattern: String, + expected: Vec, + divergence: String, +} + +fn page() -> String { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/reference/glob.md"); + fs::read_to_string(path).unwrap() +} + +fn after<'a>(page: &'a str, marker: &str) -> &'a str { + let at = page.find(marker).unwrap_or_else(|| panic!("no {marker}")); + &page[at + marker.len()..] +} + +fn tree_entries(page: &str) -> Vec { + after(page, "") + .lines() + .skip(2) + .take_while(|line| !line.starts_with("```")) + .map(str::to_string) + .collect() +} + +fn rows(page: &str) -> Vec { + after(page, "") + .lines() + .skip(1) + .take_while(|line| line.starts_with('|')) + .skip(2) + .map(|line| { + let cells: Vec<&str> = line.trim_matches('|').split(" | ").map(str::trim).collect(); + let backticked = |cell: &str| -> Vec { + cell.split('`') + .skip(1) + .step_by(2) + .map(str::to_string) + .collect() + }; + Row { + pattern: backticked(cells[0]).remove(0), + expected: backticked(cells[1]), + divergence: cells[2].to_string(), + } + }) + .collect() +} + +fn build(root: &Path, entries: &[String]) { + for entry in entries { + let at = root.join(entry.split(" -> ").next().unwrap()); + if let Some((_, target)) = entry.split_once(" -> ") { + symlink(target, at).unwrap(); + } else if entry.ends_with('/') { + fs::create_dir_all(at).unwrap(); + } else { + fs::create_dir_all(at.parent().unwrap()).unwrap(); + fs::write(at, "x\n").unwrap(); + } + } + fs::write(root.join(".gitignore"), "*.log\n").unwrap(); +} + +fn texts(db: &DirSQL, sql: &str, column: &str) -> Vec { + let mut out: Vec = db + .query(sql) + .unwrap() + .iter() + .map(|r| match r.get(column) { + Some(Value::Text(s)) => s.clone(), + other => panic!("{column} was not text: {other:?}"), + }) + .collect(); + out.sort(); + out +} + +fn path_table_paths(root: &Path, pattern: &str) -> Vec { + let output = std::process::Command::cargo_bin("dirsql") + .unwrap() + .args([ + "query", + &format!("SELECT path FROM '{pattern}'"), + "--format", + "json", + ]) + .current_dir(root) + .env("HOME", root) + .output() + .unwrap(); + if !output.status.success() { + return vec![String::from_utf8_lossy(&output.stderr).trim().to_string()]; + } + let rows: Vec = serde_json::from_slice(&output.stdout).unwrap(); + let mut out: Vec = rows + .iter() + .map(|row| row["path"].as_str().unwrap().to_string()) + .collect(); + out.sort(); + out +} + +fn config_paths(root: &Path, glob: &str) -> Vec { + let base = root.to_path_buf(); + let table = Table::new("t", "CREATE TABLE t (p TEXT)", glob, move |path| { + let rel = Path::new(path) + .strip_prefix(&base) + .unwrap_or(Path::new(path)); + vec![HashMap::from([( + "p".to_string(), + Value::Text(rel.to_string_lossy().into_owned()), + )])] + }); + let db = match DirSQL::builder().root(root).table(table).build() { + Ok(db) => db, + Err(err) => return vec![err.to_string()], + }; + texts(&db, "SELECT p FROM t", "p") +} + +#[test] +fn every_row_of_the_glob_page_holds() { + let page = page(); + let base = TempDir::new().unwrap(); + let root = base.path().join("proj"); + fs::create_dir(&root).unwrap(); + build(&root, &tree_entries(&page)); + let (root_text, base_text) = ( + root.to_string_lossy().into_owned(), + base.path().to_string_lossy().into_owned(), + ); + + let mut failures = Vec::new(); + for row in rows(&page) { + let pattern = row.pattern.replace("$ROOT", &root_text); + let mut expected: Vec = row + .expected + .iter() + .filter(|cell| *cell != "none") + .map(|cell| { + cell.replace("$ROOT", &root_text) + .replace("$BASE", &base_text) + }) + .collect(); + expected.sort(); + + let got = path_table_paths(&root, &pattern); + if got != expected { + failures.push(format!( + "path-table {}: {got:?} != {expected:?}", + row.pattern + )); + } + if row.divergence != "absolute" { + let glob = pattern.strip_prefix("./").unwrap(); + let got = config_paths(&root, glob); + if got != expected { + failures.push(format!("config {}: {got:?} != {expected:?}", row.pattern)); + } + } + } + assert!(failures.is_empty(), "{}", failures.join("\n")); +} diff --git a/scripts/glob-conformance.sh b/scripts/glob-conformance.sh new file mode 100755 index 000000000..1aefa33f8 --- /dev/null +++ b/scripts/glob-conformance.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Regenerates (write) or verifies (check) the bash-derived rows of the +# conformance table in docs/reference/glob.md. Needs bash >= 4.3. +set -euo pipefail + +mode="${1:-check}" +doc="$(cd "$(dirname "$0")/.." && pwd)/docs/reference/glob.md" + +base="$(mktemp -d /tmp/glob-conformance.XXXXXX)" +root="$base/proj" +mkdir "$root" + +awk '//{f=1;next} f&&/^```/{n++;next} f&&n==1{print} n==2{exit}' "$doc" | +while IFS= read -r entry; do + case "$entry" in + *' -> '*) ln -s "${entry#* -> }" "$root/${entry%% -> *}" ;; + */) mkdir -p "$root/$entry" ;; + *) mkdir -p "$root/$(dirname "$entry")"; printf 'x\n' > "$root/$entry" ;; + esac +done +printf '*.log\n' > "$root/.gitignore" + +expand() { + local pattern="${1//\$ROOT/$root}" + pattern="${pattern// /\\ }" + (cd "$root" && HOME="$root" bash -O globstar -O nullglob -c 'shopt -u dotglob; eval "printf \"%s\\n\" $1"' _ "$pattern") | + LC_ALL=C sort +} + +normalize() { + sed -e "s#^$root#\$ROOT#" -e 's#^\.\./#$BASE/#' -e 's#^\./##' +} + +files_only() { + local raw="$1" f + while IFS= read -r f; do + [ -n "$f" ] || continue + if (cd "$root" && [ -f "$f" ]); then echo "$f"; fi + done <<<"$raw" +} + +out="$(mktemp /tmp/glob-conformance-doc.XXXXXX)" +failed=0 +while IFS= read -r line; do + if [[ "$line" =~ ^\|\ \`([^\`]+)\`\ \|\ (.*)\ \|\ ([a-z.-]*)\ \|$ ]]; then + pattern="${BASH_REMATCH[1]}" + cell="${BASH_REMATCH[2]}" + tag="${BASH_REMATCH[3]}" + raw="$(expand "$pattern")" + if [ -n "$tag" ] && [ "$tag" != absolute ]; then + if [ "$(printf '%s' "$raw" | normalize | sed 's/.*/`&`/' | paste -sd, - | sed 's/,/, /g')" = "$cell" ] || { [ -z "$raw" ] && [ "$cell" = none ]; }; then + echo "stale divergence tag on $pattern: bash agrees" >&2 + failed=1 + fi + printf '%s\n' "$line" >>"$out" + continue + fi + kept="$(files_only "$raw")" + if [[ "$pattern" != *node_modules* ]]; then + kept="$(grep -v -E '(^|/)node_modules/' <<<"$kept" || true)" + fi + kept="$(grep -v -E '\.log$' <<<"$kept" || true)" + if [ -z "$kept" ]; then + fresh=none + else + fresh="$(printf '%s\n' "$kept" | normalize | LC_ALL=C sort | sed 's/.*/`&`/' | paste -sd, - | sed 's/,/, /g')" + fi + if [ "$fresh" != "$cell" ]; then + [ "$mode" = write ] || { echo "row differs from bash: $pattern" >&2; failed=1; } + fi + printf '| `%s` | %s | %s |\n' "$pattern" "$fresh" "$tag" >>"$out" + else + printf '%s\n' "$line" >>"$out" + fi +done <"$doc" + +if [ "$mode" = write ]; then + cp "$out" "$doc" +fi +find "$base" "$out" -delete +exit "$failed" From 0e5b9123980782ded5b79cedce03e2bdce8734d0 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:34:38 -0400 Subject: [PATCH 2/3] Document globs in one page and link it from config and path-tables --- docs/.vitepress/config.ts | 1 + docs/reference/config.md | 15 +++++++-------- docs/reference/glob.md | 10 ++++++---- docs/reference/path-tables.md | 10 ++-------- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 9d05f3c66..5450259e0 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -76,6 +76,7 @@ export default defineConfig({ { text: 'Command Hooks', link: '/reference/hooks' }, { text: 'Columns', link: '/reference/columns' }, { text: 'Path-tables', link: '/reference/path-tables' }, + { text: 'Globs', link: '/reference/glob' }, { text: 'HTTP API', link: '/reference/http-api' }, { text: 'SDK', link: '/reference/sdk' } ] diff --git a/docs/reference/config.md b/docs/reference/config.md index 415d13e8a..670021d24 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -45,13 +45,12 @@ ignore = ["node_modules/**", ".git/**"] ### Glob rule -Every glob in dirsql — `ignore`, a `[[table]]`'s `glob`, and a -[path-table](./path-tables.md#writing-the-path) — reads as the shell does: -`*` and `?` match within one path segment and never cross `/`; `**` matches -any depth. So `ignore = ["*"]` hides only the files directly inside the root, -`ignore = ["build/*"]` hides `build/a.o` but not `build/sub/b.o`, and -`glob = "*.json"` selects only the top-level `.json` files; write -`**/*.json` to select them at every depth. +Every glob in dirsql, whether `ignore`, a `[[table]]`'s `glob` or a +[path-table](./path-tables.md#writing-the-path), reads as bash does with +`globstar` on. The [glob page](./glob.md) has the rules, examples and every +divergence. In short, `*` and `?` stay within one path segment, `**` crosses +directories, and `glob = "*.json"` selects only top-level `.json` files; write +`**/*.json` for every depth. Persistence is not a config key. Keep the SQLite index on disk between runs with the [`--persist [PATH]` CLI flag](./cli.md#dirsql-server) — a machine-local @@ -230,7 +229,7 @@ what its required `on-file` command emits — dirsql injects nothing (see |---|---|---| | `name` | yes | The table's SQL name — the name you query it by. Declared, never derived from `ddl`: dirsql does not read the DDL text. The `ddl` must create a table by this name; if it doesn't, loading fails. | | `ddl` | yes | A SQL batch, run verbatim — any number of statements. It must create a table called `name`; that table holds the file rows, and only the columns it declares are kept (keys the `on-file` command emits that are not declared are dropped). The rest of the batch is yours: indexes, virtual tables, triggers. See [Batch `ddl`](#batch-ddl). | -| `glob` | yes | Glob pattern matched against root-relative paths, under the [one glob rule](#glob-rule): `*` matches one level, `**` any depth. Every table whose glob matches a file receives that file's rows — a file can populate multiple tables. A `{name}` segment is rewritten to `*` (it matches one path segment but captures nothing). | +| `glob` | yes | Glob pattern matched against root-relative paths, under the [one glob rule](#glob-rule): `*` matches one level, `**` any depth. Every table whose glob matches a file receives that file's rows — a file can populate multiple tables. A glob is anchored at the directory holding the config file. | | `on-file` | **yes** | A command run once per table, with every matched file's absolute path appended as a trailing argument; its stdout (one JSON array of row objects) is the table's rows. Must be non-empty. A `[[table]]` with no `on-file` is a load error (see [parse errors](#parse-errors)). See [Command hooks](./hooks.md#on-file). | | `strict` | no (default `false`) | When `true`, rows whose keys do not exactly match the declared columns are rejected with an error: extra keys error, and every declared column must be supplied by the `on-file` output. When `false`, extra keys are dropped and missing columns become `NULL`. | diff --git a/docs/reference/glob.md b/docs/reference/glob.md index a038a8f10..6a0c3750a 100644 --- a/docs/reference/glob.md +++ b/docs/reference/glob.md @@ -119,17 +119,19 @@ A test runs every row against dirsql as a path-table and, with the leading ## Where the glob is anchored -A path-table pattern is relative to the index root. A config `[[table]] glob` +A path-table pattern is relative to the index root and starts with `./`. A config `[[table]] glob` is relative to the directory holding its config file, whatever directory you -run `dirsql` from. A config table whose glob matches no files prints +run `dirsql` from, and is written without the `./`: `./docs/*.md` in a path-table +is `glob = "docs/*.md"` in a config. A config table whose glob matches no files prints `dirsql: table 'name': glob 'pattern' matched no files under ` to stderr; the exit code is unchanged. Path-tables never warn. ## Edge cases -- `{` , `}` or `[` with no partner is a literal character. +- A `{`, `}` or `[` with no partner is a literal character. - A brace group with no comma and no `..` is literal (`{q}`); `{1..3}` and - `{a..c}` expand. + `{a..c}` expand, as do zero-padded + `{01..10}` and stepped `{1..10..2}` ranges. Groups nest. - `\[` is a literal bracket. - A dot-named file or directory is listed only where a component spells the dot at that depth: `./**/.*` finds `docs/.draft.md` but does not enter diff --git a/docs/reference/path-tables.md b/docs/reference/path-tables.md index 52b830ef5..851efcb89 100644 --- a/docs/reference/path-tables.md +++ b/docs/reference/path-tables.md @@ -52,14 +52,8 @@ depth, as in the shell. A trailing `/` is `*` appended, so `'./*/'` is | `'./notes/today.md'` | exactly that one file — one file is one row | A path containing `*`, `?`, `[` or `{` is a glob and is used exactly as -written: `*` matches within a single directory, `**` crosses directories. - -Braces expand as bash expands them. A group holding a comma is an -alternation: `'./docs/{a,b}.md'` is `docs/a.md` and `docs/b.md`, and groups -nest. A group holding a `..` range counts: `'./log/{1..3}.txt'`, -`'./{a..c}.md'`, `'./{01..10}.md'` (zero-padded) and `'./{1..10..2}.md'` -(every second value). Any other brace is literal text, so `'./br/{q}.md'` -names the file `br/{q}.md`, and `\{` is always a literal brace. +written. The [glob page](./glob.md) has the syntax, examples and every +divergence from bash, including braces and unbalanced brackets. The scan starts at the last directory named outright before the first glob component, and descends only as deep as the pattern can match -- From 9d3c783083b70b0fbed4a55aa05e4008e0b3a638 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:34:10 -0400 Subject: [PATCH 3/3] docs: drop the bash conformance script --- docs/reference/glob.md | 1 - packages/rust/tests/glob_conformance.rs | 1 - scripts/glob-conformance.sh | 81 ------------------------- 3 files changed, 83 deletions(-) delete mode 100755 scripts/glob-conformance.sh diff --git a/docs/reference/glob.md b/docs/reference/glob.md index 6a0c3750a..1c001acf4 100644 --- a/docs/reference/glob.md +++ b/docs/reference/glob.md @@ -64,7 +64,6 @@ bash's raw output. `$ROOT` is the fixture's directory and `$BASE` its parent. A test runs every row against dirsql as a path-table and, with the leading `./` dropped, as a config `glob`, so this table cannot drift from the code. -`scripts/glob-conformance.sh check` re-runs the bash side. | Pattern | Rows | Divergence | diff --git a/packages/rust/tests/glob_conformance.rs b/packages/rust/tests/glob_conformance.rs index 13921f8e5..8fcc58807 100644 --- a/packages/rust/tests/glob_conformance.rs +++ b/packages/rust/tests/glob_conformance.rs @@ -1,6 +1,5 @@ //! The example table in `docs/reference/glob.md` is the specification: every //! row runs against dirsql as a path-table and as a config `[[table]] glob`. -//! `scripts/glob-conformance.sh` checks the same rows against bash. #![cfg(all(unix, feature = "cli"))] use std::collections::HashMap; diff --git a/scripts/glob-conformance.sh b/scripts/glob-conformance.sh deleted file mode 100755 index 1aefa33f8..000000000 --- a/scripts/glob-conformance.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env bash -# Regenerates (write) or verifies (check) the bash-derived rows of the -# conformance table in docs/reference/glob.md. Needs bash >= 4.3. -set -euo pipefail - -mode="${1:-check}" -doc="$(cd "$(dirname "$0")/.." && pwd)/docs/reference/glob.md" - -base="$(mktemp -d /tmp/glob-conformance.XXXXXX)" -root="$base/proj" -mkdir "$root" - -awk '//{f=1;next} f&&/^```/{n++;next} f&&n==1{print} n==2{exit}' "$doc" | -while IFS= read -r entry; do - case "$entry" in - *' -> '*) ln -s "${entry#* -> }" "$root/${entry%% -> *}" ;; - */) mkdir -p "$root/$entry" ;; - *) mkdir -p "$root/$(dirname "$entry")"; printf 'x\n' > "$root/$entry" ;; - esac -done -printf '*.log\n' > "$root/.gitignore" - -expand() { - local pattern="${1//\$ROOT/$root}" - pattern="${pattern// /\\ }" - (cd "$root" && HOME="$root" bash -O globstar -O nullglob -c 'shopt -u dotglob; eval "printf \"%s\\n\" $1"' _ "$pattern") | - LC_ALL=C sort -} - -normalize() { - sed -e "s#^$root#\$ROOT#" -e 's#^\.\./#$BASE/#' -e 's#^\./##' -} - -files_only() { - local raw="$1" f - while IFS= read -r f; do - [ -n "$f" ] || continue - if (cd "$root" && [ -f "$f" ]); then echo "$f"; fi - done <<<"$raw" -} - -out="$(mktemp /tmp/glob-conformance-doc.XXXXXX)" -failed=0 -while IFS= read -r line; do - if [[ "$line" =~ ^\|\ \`([^\`]+)\`\ \|\ (.*)\ \|\ ([a-z.-]*)\ \|$ ]]; then - pattern="${BASH_REMATCH[1]}" - cell="${BASH_REMATCH[2]}" - tag="${BASH_REMATCH[3]}" - raw="$(expand "$pattern")" - if [ -n "$tag" ] && [ "$tag" != absolute ]; then - if [ "$(printf '%s' "$raw" | normalize | sed 's/.*/`&`/' | paste -sd, - | sed 's/,/, /g')" = "$cell" ] || { [ -z "$raw" ] && [ "$cell" = none ]; }; then - echo "stale divergence tag on $pattern: bash agrees" >&2 - failed=1 - fi - printf '%s\n' "$line" >>"$out" - continue - fi - kept="$(files_only "$raw")" - if [[ "$pattern" != *node_modules* ]]; then - kept="$(grep -v -E '(^|/)node_modules/' <<<"$kept" || true)" - fi - kept="$(grep -v -E '\.log$' <<<"$kept" || true)" - if [ -z "$kept" ]; then - fresh=none - else - fresh="$(printf '%s\n' "$kept" | normalize | LC_ALL=C sort | sed 's/.*/`&`/' | paste -sd, - | sed 's/,/, /g')" - fi - if [ "$fresh" != "$cell" ]; then - [ "$mode" = write ] || { echo "row differs from bash: $pattern" >&2; failed=1; } - fi - printf '| `%s` | %s | %s |\n' "$pattern" "$fresh" "$tag" >>"$out" - else - printf '%s\n' "$line" >>"$out" - fi -done <"$doc" - -if [ "$mode" = write ]; then - cp "$out" "$doc" -fi -find "$base" "$out" -delete -exit "$failed"