dirsql 0.4.72__tar.gz → 0.4.73__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirsql-0.4.72 → dirsql-0.4.73}/Cargo.lock +2 -2
- {dirsql-0.4.72 → dirsql-0.4.73}/PKG-INFO +1 -1
- {dirsql-0.4.72/packages/rust → dirsql-0.4.73}/docs/reference/path-tables.md +7 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/Cargo.toml +1 -1
- dirsql-0.4.73/packages/python/changelog.d/2026-10-05-brace-groups-follow-bash.md +6 -0
- {dirsql-0.4.72 → dirsql-0.4.73/packages/python}/docs/reference/path-tables.md +7 -0
- dirsql-0.4.73/packages/python/e2e-attestations/claude-1262-brace-literal.json +7 -0
- dirsql-0.4.73/packages/python/e2e-attestations/claude-1286-nested-repo.json +7 -0
- dirsql-0.4.73/packages/python/migrations.d/2026-10-05-brace-groups-follow-bash.md +39 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/Cargo.toml +1 -1
- {dirsql-0.4.72/packages/python → dirsql-0.4.73/packages/rust}/docs/reference/path-tables.md +7 -0
- dirsql-0.4.73/packages/rust/src/brace.rs +425 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/lib.rs +1 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/scanner.rs +86 -33
- {dirsql-0.4.72 → dirsql-0.4.73}/Cargo.toml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/README.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/_async.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/discover_plugins/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/main.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/plan_entry_path.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/py.typed +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/read_config.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/resolve_configs_extension_specs.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/dirsql/resolve_package.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/AGENTS.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/context/.gitattributes +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/context/recipes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/context/troubleshooting.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/context/usage.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/explanation.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/getting-started.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/define-tables.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/embed.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/load-extension.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/parse-files-into-columns.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/persist.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/query-json.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/query-without-config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/search-indexes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/skip-files.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/index.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/package.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/plugins.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/reference/cli.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/reference/columns.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/reference/config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/reference/hooks.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/reference/http-api.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/docs/reference/sdk.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/CHANGELOG.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/MIGRATIONS.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/README.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-18-export-scan-failure.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-24-query-rows-in-projection-order.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-02-readme-docs-host.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-03-one-glob-rule-for-config-globs.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-03-path-table-directory-one-level.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-04-path-table-dotfiles-hidden.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-cli-context.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-gitignore-requires-repo.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-path-table-node-modules-named.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-path-table-symlinks.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-path-table-trailing-slash.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/changelog.d/README.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/conftest.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/context/.gitattributes +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/context/recipes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/context/troubleshooting.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/context/usage.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/explanation.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/define-tables.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/embed.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/load-extension.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/persist.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/query-json.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/query-without-config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/search-indexes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/skip-files.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/index.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/package.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/plugins.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/reference/cli.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/reference/columns.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/reference/config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/reference/hooks.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/reference/http-api.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/docs/reference/sdk.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1130-codec-crates.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1139-sse-flatten.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1140-scan-failure-export.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1148-clippy-all-targets.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1164-sigint-fatal.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1170-projection-order.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1172-on-file-path.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1173-py-projection-order.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1184-win-rel-paths.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1185-win-abs-table-names.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1186-win-test-harness.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1196-join-index.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1199-glob-prefix.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1201-single-walk.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1204-embed-batch.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1207-win-argv-quote.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1208-win-nonverbatim-paths.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1215-on-file-batch.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1216-on-file-cleanup.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1223-config-glob-rule.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1223-dir-one-level.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1226-stale-docs.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1238-parallel-walk.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1241-content-prefetch.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1243-ingest-perf.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1249-dotfiles.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1256-symlinks.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1257-trailing-slash.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1264-context.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1267-posix-classes.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1269-utf8-chars.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1271-node-modules-named.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1272-ancestor-gitignore.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1273-walk-prune.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1285-ingest-fast.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-09-24-query-rows-in-projection-order.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-02-readme-docs-host.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-03-one-glob-rule-for-config-globs.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-03-path-table-directory-one-level.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-04-path-table-dotfiles-hidden.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-cli-context.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-gitignore-requires-repo.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-path-table-node-modules-named.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-path-table-symlinks.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-path-table-trailing-slash.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/migrations.d/README.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/src/lib.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/testing-conventions.toml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/e2e/speed_of_light.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/integration/binding/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/python/tests/integration/hermetic/__init__.py +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/CHANGELOG.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/MIGRATIONS.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/README.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/context/.gitattributes +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/context/recipes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/context/troubleshooting.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/context/usage.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/explanation.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/define-tables.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/embed.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/load-extension.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/persist.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/query-json.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/query-without-config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/search-indexes.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/skip-files.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/index.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/plugins.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/reference/cli.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/reference/columns.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/reference/config.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/reference/hooks.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/reference/http-api.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/docs/reference/sdk.md +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/context.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/execute.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/repl.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/run.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/cli/table.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/command.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/config.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/db.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/default_config.toml +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/extension_resolution.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/functions.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/infer.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/launcher.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/on_file.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/parsed_vtab.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/path_table.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/posix_class.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/progress.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/row_event_flat.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/sql_literal.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/vtab.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/vtab_scaffold.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.4.72 → dirsql-0.4.73}/pyproject.toml +0 -0
|
@@ -552,7 +552,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
|
|
552
552
|
|
|
553
553
|
[[package]]
|
|
554
554
|
name = "dirsql"
|
|
555
|
-
version = "0.4.
|
|
555
|
+
version = "0.4.73"
|
|
556
556
|
dependencies = [
|
|
557
557
|
"assert_cmd",
|
|
558
558
|
"axum",
|
|
@@ -601,7 +601,7 @@ dependencies = [
|
|
|
601
601
|
|
|
602
602
|
[[package]]
|
|
603
603
|
name = "dirsql-py-ext"
|
|
604
|
-
version = "0.4.
|
|
604
|
+
version = "0.4.73"
|
|
605
605
|
dependencies = [
|
|
606
606
|
"dirsql",
|
|
607
607
|
"pyo3",
|
|
@@ -54,6 +54,13 @@ depth, as in the shell. A trailing `/` is `*` appended, so `'./*/'` is
|
|
|
54
54
|
A path containing `*`, `?`, `[` or `{` is a glob and is used exactly as
|
|
55
55
|
written: `*` matches within a single directory, `**` crosses directories.
|
|
56
56
|
|
|
57
|
+
Braces expand as bash expands them. A group holding a comma is an
|
|
58
|
+
alternation: `'./docs/{a,b}.md'` is `docs/a.md` and `docs/b.md`, and groups
|
|
59
|
+
nest. A group holding a `..` range counts: `'./log/{1..3}.txt'`,
|
|
60
|
+
`'./{a..c}.md'`, `'./{01..10}.md'` (zero-padded) and `'./{1..10..2}.md'`
|
|
61
|
+
(every second value). Any other brace is literal text, so `'./br/{q}.md'`
|
|
62
|
+
names the file `br/{q}.md`, and `\{` is always a literal brace.
|
|
63
|
+
|
|
57
64
|
The scan starts at the last directory named outright before the first glob
|
|
58
65
|
component, and descends only as deep as the pattern can match --
|
|
59
66
|
`'./small/*.md'` lists `small/` and nothing else -- so a query over one
|
|
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
|
|
|
4
4
|
# pypi/maturin handler can rewrite it via `write-version` before
|
|
5
5
|
# `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
|
|
6
6
|
# and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.73"
|
|
8
8
|
# abi3 note: the `abi3-py310` pyo3 feature (below) builds ONE stable-ABI
|
|
9
9
|
# `cp310-abi3` wheel per platform that loads on every CPython >= 3.10
|
|
10
10
|
# (matching `pyproject.toml`'s `requires-python = ">=3.10"`), instead of
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
**Changed** Braces in a path-table follow bash brace expansion. A group with
|
|
2
|
+
a top-level comma (`{a,b}`, nested allowed) is an alternation, and a `..`
|
|
3
|
+
range (`{1..3}`, `{a..c}`, `{01..10}`, `{1..10..2}`) expands to each value.
|
|
4
|
+
Any other brace is literal text: `'./br/{q}.md'` names the file `br/{q}.md`
|
|
5
|
+
where it used to match `br/q.md`, and `{` or `}` without a partner no longer
|
|
6
|
+
fails to compile. Config `[[table]] glob` is unchanged.
|
|
@@ -54,6 +54,13 @@ depth, as in the shell. A trailing `/` is `*` appended, so `'./*/'` is
|
|
|
54
54
|
A path containing `*`, `?`, `[` or `{` is a glob and is used exactly as
|
|
55
55
|
written: `*` matches within a single directory, `**` crosses directories.
|
|
56
56
|
|
|
57
|
+
Braces expand as bash expands them. A group holding a comma is an
|
|
58
|
+
alternation: `'./docs/{a,b}.md'` is `docs/a.md` and `docs/b.md`, and groups
|
|
59
|
+
nest. A group holding a `..` range counts: `'./log/{1..3}.txt'`,
|
|
60
|
+
`'./{a..c}.md'`, `'./{01..10}.md'` (zero-padded) and `'./{1..10..2}.md'`
|
|
61
|
+
(every second value). Any other brace is literal text, so `'./br/{q}.md'`
|
|
62
|
+
names the file `br/{q}.md`, and `\{` is always a literal brace.
|
|
63
|
+
|
|
57
64
|
The scan starts at the last directory named outright before the first glob
|
|
58
65
|
component, and descends only as deep as the pattern can match --
|
|
59
66
|
`'./small/*.md'` lists `small/` and nothing else -- so a query over one
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
### Core: path-table braces follow bash brace expansion
|
|
2
|
+
|
|
3
|
+
#### Summary
|
|
4
|
+
|
|
5
|
+
A path-table's braces now expand as bash expands them before globbing: a
|
|
6
|
+
group with a top-level comma is an alternation, a `..` range expands to each
|
|
7
|
+
value, and every other brace is literal text. `'./br/{q}.md'` used to match
|
|
8
|
+
`br/q.md` and now matches only a file named `br/{q}.md`; `'./a{1..3}.md'`
|
|
9
|
+
used to match nothing and now matches `a1.md`, `a2.md`, `a3.md`. Comma groups
|
|
10
|
+
behave as before. Every surface that resolves a path-table is affected (CLI,
|
|
11
|
+
REPL, HTTP server, SDKs, `--on-file`); config `[[table]] glob` is not.
|
|
12
|
+
|
|
13
|
+
#### Required changes
|
|
14
|
+
|
|
15
|
+
| Surface | Before | After |
|
|
16
|
+
| ------- | ------ | ----- |
|
|
17
|
+
| One-option group | `SELECT * FROM './br/{q}.md'` (matched `br/q.md`) | `SELECT * FROM './br/q.md'` |
|
|
18
|
+
|
|
19
|
+
#### Deprecations removed
|
|
20
|
+
|
|
21
|
+
_None._
|
|
22
|
+
|
|
23
|
+
#### Behavior changes without code changes
|
|
24
|
+
|
|
25
|
+
- A brace group with neither a top-level comma nor a valid `..` range is
|
|
26
|
+
literal text.
|
|
27
|
+
- `{1..3}`, `{a..c}`, `{01..10}` and `{1..10..2}` expand to their values
|
|
28
|
+
instead of matching the literal text between the braces.
|
|
29
|
+
- An unpaired `{` or `}` is literal text instead of a glob compile error.
|
|
30
|
+
|
|
31
|
+
#### Verification
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
# root holds q.md, {q}.md, a1.md and a2.md
|
|
35
|
+
rows = await DirSQL(root).query("SELECT path FROM './{q}.md'")
|
|
36
|
+
# expected: [{"path": "{q}.md"}]
|
|
37
|
+
rows = await DirSQL(root).query("SELECT path FROM './a{1..2}.md' ORDER BY path")
|
|
38
|
+
# expected: [{"path": "a1.md"}, {"path": "a2.md"}]
|
|
39
|
+
```
|
|
@@ -6,7 +6,7 @@ description = "Ephemeral SQL index over a local directory"
|
|
|
6
6
|
# version stays for the binding crates (publish = false). The literal
|
|
7
7
|
# intentionally lags the published version: putitoutthere rewrites it to
|
|
8
8
|
# the planned release version at build time and never commits it back.
|
|
9
|
-
version = "0.4.
|
|
9
|
+
version = "0.4.73"
|
|
10
10
|
edition.workspace = true
|
|
11
11
|
# Literal `license` (not `license.workspace = true`) because
|
|
12
12
|
# putitoutthere's preflight check reads `[package].license` directly
|
|
@@ -54,6 +54,13 @@ depth, as in the shell. A trailing `/` is `*` appended, so `'./*/'` is
|
|
|
54
54
|
A path containing `*`, `?`, `[` or `{` is a glob and is used exactly as
|
|
55
55
|
written: `*` matches within a single directory, `**` crosses directories.
|
|
56
56
|
|
|
57
|
+
Braces expand as bash expands them. A group holding a comma is an
|
|
58
|
+
alternation: `'./docs/{a,b}.md'` is `docs/a.md` and `docs/b.md`, and groups
|
|
59
|
+
nest. A group holding a `..` range counts: `'./log/{1..3}.txt'`,
|
|
60
|
+
`'./{a..c}.md'`, `'./{01..10}.md'` (zero-padded) and `'./{1..10..2}.md'`
|
|
61
|
+
(every second value). Any other brace is literal text, so `'./br/{q}.md'`
|
|
62
|
+
names the file `br/{q}.md`, and `\{` is always a literal brace.
|
|
63
|
+
|
|
57
64
|
The scan starts at the last directory named outright before the first glob
|
|
58
65
|
component, and descends only as deep as the pattern can match --
|
|
59
66
|
`'./small/*.md'` lists `small/` and nothing else -- so a query over one
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
//! Bash brace expansion for path globs.
|
|
2
|
+
//!
|
|
3
|
+
//! A pattern expands the way bash expands a word before globbing it: a brace
|
|
4
|
+
//! group holding a top-level comma is an alternation, one holding a `..`
|
|
5
|
+
//! sequence is a range, and any other brace is literal text. The expansions
|
|
6
|
+
//! come back as globset patterns with every remaining brace escaped, so
|
|
7
|
+
//! globset never sees an alternation of its own.
|
|
8
|
+
|
|
9
|
+
/// Expand `pattern`'s brace groups as bash does, one globset pattern per word.
|
|
10
|
+
pub(crate) fn expand(pattern: &str) -> Vec<String> {
|
|
11
|
+
let chars: Vec<char> = pattern.chars().collect();
|
|
12
|
+
expand_word(&chars)
|
|
13
|
+
.iter()
|
|
14
|
+
.map(|word| escape_braces(word))
|
|
15
|
+
.collect()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[derive(Clone, Copy, PartialEq)]
|
|
19
|
+
enum Stop {
|
|
20
|
+
Close,
|
|
21
|
+
Comma,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// The characters of `text` from `from` on that no backslash escapes, with
|
|
25
|
+
/// their indices; the backslashes themselves are dropped too.
|
|
26
|
+
fn unescaped(text: &[char], from: usize) -> impl Iterator<Item = (usize, char)> + '_ {
|
|
27
|
+
let mut escaped = false;
|
|
28
|
+
text.iter()
|
|
29
|
+
.copied()
|
|
30
|
+
.enumerate()
|
|
31
|
+
.skip(from)
|
|
32
|
+
.filter(move |&(_, c)| {
|
|
33
|
+
let keep = !escaped && c != '\\';
|
|
34
|
+
escaped = !escaped && c == '\\';
|
|
35
|
+
keep
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Index of the first unescaped `stop` at nesting level zero, scanning from
|
|
40
|
+
/// `from`; `text.len()` when there is none. A `}` only closes once a comma or
|
|
41
|
+
/// a `..` has been seen, which is why `{q}` never closes.
|
|
42
|
+
fn gobble(text: &[char], from: usize, stop: Stop) -> usize {
|
|
43
|
+
let target = match stop {
|
|
44
|
+
Stop::Close => '}',
|
|
45
|
+
Stop::Comma => ',',
|
|
46
|
+
};
|
|
47
|
+
let mut level = 0usize;
|
|
48
|
+
let mut closable = stop != Stop::Close;
|
|
49
|
+
for (i, c) in unescaped(text, from) {
|
|
50
|
+
if c == target && level == 0 && closable {
|
|
51
|
+
return i;
|
|
52
|
+
}
|
|
53
|
+
if c == '{' {
|
|
54
|
+
level += 1;
|
|
55
|
+
} else if c == '}' && level > 0 {
|
|
56
|
+
level -= 1;
|
|
57
|
+
} else if level == 0
|
|
58
|
+
&& (c == ',' || (text[i..].starts_with(&['.', '.']) && text.get(i + 2) != Some(&'}')))
|
|
59
|
+
{
|
|
60
|
+
closable = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
text.len()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Bash never opens a group at a `{` that starts a word-like token and is
|
|
67
|
+
/// followed by whitespace or `}`, so `{}` stays intact for `find -exec`.
|
|
68
|
+
fn is_shell_brace(text: &[char], i: usize) -> bool {
|
|
69
|
+
let after_space = i == 0 || text[i - 1].is_ascii_whitespace();
|
|
70
|
+
let before_space_or_close = text
|
|
71
|
+
.get(i + 1)
|
|
72
|
+
.is_some_and(|c| c.is_ascii_whitespace() || *c == '}');
|
|
73
|
+
after_space && before_space_or_close
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn expand_word(text: &[char]) -> Vec<String> {
|
|
77
|
+
let group = unescaped(text, 0)
|
|
78
|
+
.filter(|&(i, c)| c == '{' && !is_shell_brace(text, i))
|
|
79
|
+
.find_map(|(open, _)| {
|
|
80
|
+
let close = gobble(text, open + 1, Stop::Close);
|
|
81
|
+
(close < text.len()).then_some((open, close))
|
|
82
|
+
});
|
|
83
|
+
let Some((open, close)) = group else {
|
|
84
|
+
return vec![text.iter().collect()];
|
|
85
|
+
};
|
|
86
|
+
let preamble: String = text[..open].iter().collect();
|
|
87
|
+
let amble = &text[open + 1..close];
|
|
88
|
+
let middle = if unescaped(amble, 0).any(|(_, c)| c == ',') {
|
|
89
|
+
expand_alternatives(amble)
|
|
90
|
+
} else {
|
|
91
|
+
let amble: String = amble.iter().collect();
|
|
92
|
+
sequence(&amble).unwrap_or_else(|| vec![format!("{{{amble}}}")])
|
|
93
|
+
};
|
|
94
|
+
let rest = expand_word(&text[close + 1..]);
|
|
95
|
+
middle
|
|
96
|
+
.iter()
|
|
97
|
+
.flat_map(|m| {
|
|
98
|
+
rest.iter()
|
|
99
|
+
.map(|r| format!("{preamble}{m}{r}"))
|
|
100
|
+
.collect::<Vec<_>>()
|
|
101
|
+
})
|
|
102
|
+
.collect()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn expand_alternatives(amble: &[char]) -> Vec<String> {
|
|
106
|
+
let end = gobble(amble, 0, Stop::Comma);
|
|
107
|
+
let mut words = expand_word(&amble[..end]);
|
|
108
|
+
if let Some(tail) = amble.get(end + 1..) {
|
|
109
|
+
words.extend(expand_alternatives(tail));
|
|
110
|
+
}
|
|
111
|
+
words
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The `..` range bash expands `{lo..hi}` / `{lo..hi..step}` to, or `None`
|
|
115
|
+
/// when the text is not a range bash accepts.
|
|
116
|
+
fn sequence(amble: &str) -> Option<Vec<String>> {
|
|
117
|
+
let (lhs, rhs) = amble.split_once("..")?;
|
|
118
|
+
if lhs.is_empty() || rhs.is_empty() {
|
|
119
|
+
return None;
|
|
120
|
+
}
|
|
121
|
+
let digits = number_prefix_len(rhs);
|
|
122
|
+
let (end, rhs_len) = if let Ok(n) = rhs[..digits].parse() {
|
|
123
|
+
(Term::Int(n), digits)
|
|
124
|
+
} else {
|
|
125
|
+
let c = rhs.as_bytes()[0];
|
|
126
|
+
if !c.is_ascii_alphabetic() || !(rhs.len() == 1 || rhs.as_bytes()[1] == b'.') {
|
|
127
|
+
return None;
|
|
128
|
+
}
|
|
129
|
+
(Term::Char(c), 1)
|
|
130
|
+
};
|
|
131
|
+
let step = match &rhs[rhs_len..] {
|
|
132
|
+
"" => 1,
|
|
133
|
+
tail => tail
|
|
134
|
+
.strip_prefix("..")
|
|
135
|
+
.filter(|s| !s.is_empty())?
|
|
136
|
+
.parse()
|
|
137
|
+
.ok()?,
|
|
138
|
+
};
|
|
139
|
+
let start = if let Ok(n) = lhs.parse::<i64>() {
|
|
140
|
+
Term::Int(n)
|
|
141
|
+
} else if lhs.len() == 1 && lhs.as_bytes()[0].is_ascii_alphabetic() {
|
|
142
|
+
Term::Char(lhs.as_bytes()[0])
|
|
143
|
+
} else {
|
|
144
|
+
return None;
|
|
145
|
+
};
|
|
146
|
+
match (start, end) {
|
|
147
|
+
(Term::Int(lo), Term::Int(hi)) => {
|
|
148
|
+
let width = pad_width(lhs, &rhs[..rhs_len]);
|
|
149
|
+
Some(
|
|
150
|
+
steps(lo, hi, step)?
|
|
151
|
+
.map(|n| format!("{n:0width$}"))
|
|
152
|
+
.collect(),
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
(Term::Char(lo), Term::Char(hi)) => Some(
|
|
156
|
+
steps(i64::from(lo), i64::from(hi), step)?
|
|
157
|
+
.filter_map(|n| u8::try_from(n).ok())
|
|
158
|
+
.map(|b| char::from(b).to_string())
|
|
159
|
+
.collect(),
|
|
160
|
+
),
|
|
161
|
+
_ => None,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
enum Term {
|
|
166
|
+
Int(i64),
|
|
167
|
+
Char(u8),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn number_prefix_len(s: &str) -> usize {
|
|
171
|
+
let sign = usize::from(matches!(s.as_bytes()[0], b'+' | b'-'));
|
|
172
|
+
sign + s[sign..].bytes().take_while(u8::is_ascii_digit).count()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Bash zero-pads every term to the widest end written with a leading zero.
|
|
176
|
+
fn pad_width(lhs: &str, rhs: &str) -> usize {
|
|
177
|
+
let padded =
|
|
178
|
+
|s: &str| (s.len() > 1 && s.starts_with('0')) || (s.len() > 2 && s.starts_with("-0"));
|
|
179
|
+
if padded(lhs) || padded(rhs) {
|
|
180
|
+
lhs.len().max(rhs.len())
|
|
181
|
+
} else {
|
|
182
|
+
0
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// `lo` to `hi` inclusive in steps of `|step|` (0 counts as 1), walking down
|
|
187
|
+
/// when `hi < lo`. `None` past the element count bash refuses to generate.
|
|
188
|
+
fn steps(lo: i64, hi: i64, step: i64) -> Option<impl Iterator<Item = i128>> {
|
|
189
|
+
let stride = i128::from(step).abs().max(1);
|
|
190
|
+
let distance = i128::from(hi) - i128::from(lo);
|
|
191
|
+
if distance.abs() / stride > i128::from(i32::MAX) - 3 {
|
|
192
|
+
return None;
|
|
193
|
+
}
|
|
194
|
+
let count = distance.abs() / stride + 1;
|
|
195
|
+
let signed = stride * distance.signum();
|
|
196
|
+
Some((0..count).map(move |k| i128::from(lo) + k * signed))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Escape the braces bash left as literal text, so globset matches them
|
|
200
|
+
/// rather than reading an alternation. A complete `[...]` class is copied as
|
|
201
|
+
/// written; an unclosed one ends escaping so globset reports the error.
|
|
202
|
+
fn escape_braces(word: &str) -> String {
|
|
203
|
+
let chars: Vec<char> = word.chars().collect();
|
|
204
|
+
let mut out = String::with_capacity(word.len());
|
|
205
|
+
let mut resume = 0;
|
|
206
|
+
for (i, &c) in chars.iter().enumerate() {
|
|
207
|
+
if i < resume {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
match (c, chars.get(i + 1)) {
|
|
211
|
+
('\\', Some(&next)) => {
|
|
212
|
+
match next {
|
|
213
|
+
'{' => out.push_str("[{]"),
|
|
214
|
+
'}' => out.push_str("[}]"),
|
|
215
|
+
',' => out.push(','),
|
|
216
|
+
_ => out.extend(['\\', next]),
|
|
217
|
+
}
|
|
218
|
+
resume = i + 2;
|
|
219
|
+
}
|
|
220
|
+
('[', _) => match class_end(&chars, i) {
|
|
221
|
+
Some(end) => {
|
|
222
|
+
out.extend(&chars[i..=end]);
|
|
223
|
+
resume = end + 1;
|
|
224
|
+
}
|
|
225
|
+
None => {
|
|
226
|
+
out.extend(&chars[i..]);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
('{', _) => out.push_str("[{]"),
|
|
231
|
+
('}', _) => out.push_str("[}]"),
|
|
232
|
+
_ => out.push(c),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
out
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Index of the `]` closing the class globset opens at `open`, where a `]`
|
|
239
|
+
/// first in the class (after any `!` / `^`) is a member, not the close.
|
|
240
|
+
fn class_end(chars: &[char], open: usize) -> Option<usize> {
|
|
241
|
+
let mut i = open + 1;
|
|
242
|
+
if matches!(chars.get(i), Some('!' | '^')) {
|
|
243
|
+
i += 1;
|
|
244
|
+
}
|
|
245
|
+
if chars.get(i) == Some(&']') {
|
|
246
|
+
i += 1;
|
|
247
|
+
}
|
|
248
|
+
(i..chars.len()).find(|&j| chars[j] == ']')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[cfg(test)]
|
|
252
|
+
mod tests {
|
|
253
|
+
use super::*;
|
|
254
|
+
|
|
255
|
+
fn words(pattern: &str) -> Vec<String> {
|
|
256
|
+
expand_word(&pattern.chars().collect::<Vec<_>>())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn a_group_without_a_comma_or_sequence_is_literal() {
|
|
261
|
+
for pattern in [
|
|
262
|
+
"x{q}y", "x{}y", "x{a,b", "xa,b}y", "x{}}y", "x{{}}y", "{},a}",
|
|
263
|
+
] {
|
|
264
|
+
assert_eq!(words(pattern), vec![pattern], "{pattern}");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#[test]
|
|
269
|
+
fn a_comma_group_alternates_in_order() {
|
|
270
|
+
assert_eq!(words("x{a,b}y"), vec!["xay", "xby"]);
|
|
271
|
+
assert_eq!(words("{a,b}{c,d}"), vec!["ac", "ad", "bc", "bd"]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn empty_alternatives_are_kept() {
|
|
276
|
+
assert_eq!(words("x{,a}y"), vec!["xy", "xay"]);
|
|
277
|
+
assert_eq!(words("x{a,}y"), vec!["xay", "xy"]);
|
|
278
|
+
assert_eq!(words("x{,}y"), vec!["xy", "xy"]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#[test]
|
|
282
|
+
fn nested_groups_expand_inside_out_as_bash_does() {
|
|
283
|
+
assert_eq!(words("x{a,{b,c}}y"), vec!["xay", "xby", "xcy"]);
|
|
284
|
+
assert_eq!(words("x{a{b,c}}y"), vec!["x{ab}y", "x{ac}y"]);
|
|
285
|
+
assert_eq!(words("x{{a,b}}y"), vec!["x{a}y", "x{b}y"]);
|
|
286
|
+
assert_eq!(words("x{a,{b}}y"), vec!["xay", "x{b}y"]);
|
|
287
|
+
assert_eq!(words("x{{},a}y"), vec!["x{}y", "xay"]);
|
|
288
|
+
assert_eq!(words("x{}a,b}y"), vec!["x}ay", "xby"]);
|
|
289
|
+
assert_eq!(words("x{..{a,b}}y"), vec!["x..ay", "x..by"]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[test]
|
|
293
|
+
fn stray_braces_beside_a_group_are_literal() {
|
|
294
|
+
assert_eq!(words("x{{a,b}y"), vec!["x{ay", "x{by"]);
|
|
295
|
+
assert_eq!(words("x{a,b}}y"), vec!["xa}y", "xb}y"]);
|
|
296
|
+
assert_eq!(words("x{a,}}y"), vec!["xa}y", "x}y"]);
|
|
297
|
+
assert_eq!(words("xx{a..}y,b}"), vec!["xxa..}y", "xxb"]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn an_escaped_brace_or_comma_does_not_count() {
|
|
302
|
+
assert_eq!(words(r"x\{a,b}y"), vec![r"x\{a,b}y"]);
|
|
303
|
+
assert_eq!(words(r"x{a\,b}y"), vec![r"x{a\,b}y"]);
|
|
304
|
+
assert_eq!(words(r"x{a\,b,c}y"), vec![r"xa\,by", "xcy"]);
|
|
305
|
+
assert_eq!(words(r"x{\}a,b}y"), vec![r"x\}ay", "xby"]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[test]
|
|
309
|
+
fn a_brace_before_whitespace_or_a_close_after_whitespace_never_opens() {
|
|
310
|
+
assert_eq!(words("x {},a}y"), vec!["x {},a}y"]);
|
|
311
|
+
assert_eq!(words("{ a,b}"), vec!["{ a,b}"]);
|
|
312
|
+
assert_eq!(words("x{ a,b}"), vec!["x a", "xb"]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[test]
|
|
316
|
+
fn integer_sequences_count_either_way() {
|
|
317
|
+
assert_eq!(words("x{1..3}y"), vec!["x1y", "x2y", "x3y"]);
|
|
318
|
+
assert_eq!(words("x{3..1}"), vec!["x3", "x2", "x1"]);
|
|
319
|
+
assert_eq!(words("{-2..1}"), vec!["-2", "-1", "0", "1"]);
|
|
320
|
+
assert_eq!(words("{+1..3}"), vec!["1", "2", "3"]);
|
|
321
|
+
assert_eq!(words("{1..-1}"), vec!["1", "0", "-1"]);
|
|
322
|
+
assert_eq!(words("{1..1}"), vec!["1"]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn a_step_is_taken_by_magnitude() {
|
|
327
|
+
assert_eq!(words("{1..10..3}"), vec!["1", "4", "7", "10"]);
|
|
328
|
+
assert_eq!(words("{10..1..3}"), vec!["10", "7", "4", "1"]);
|
|
329
|
+
assert_eq!(words("{1..3..-1}"), vec!["1", "2", "3"]);
|
|
330
|
+
assert_eq!(words("{1..3..0}"), vec!["1", "2", "3"]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn a_leading_zero_pads_every_term() {
|
|
335
|
+
assert_eq!(words("{08..10}"), vec!["08", "09", "10"]);
|
|
336
|
+
assert_eq!(words("{1..03}"), vec!["01", "02", "03"]);
|
|
337
|
+
assert_eq!(words("{001..2}"), vec!["001", "002"]);
|
|
338
|
+
assert_eq!(words("{-01..1}"), vec!["-01", "000", "001"]);
|
|
339
|
+
assert_eq!(words("{0..10..5}"), vec!["0", "5", "10"]);
|
|
340
|
+
assert_eq!(words("{-0..10..5}"), vec!["0", "5", "10"]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
#[test]
|
|
344
|
+
fn letter_sequences_walk_the_alphabet() {
|
|
345
|
+
assert_eq!(words("{a..c}"), vec!["a", "b", "c"]);
|
|
346
|
+
assert_eq!(words("{e..a..2}"), vec!["e", "c", "a"]);
|
|
347
|
+
assert_eq!(words("{Y..[}"), vec!["{Y..[}"]);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[test]
|
|
351
|
+
fn a_malformed_sequence_is_literal() {
|
|
352
|
+
for pattern in [
|
|
353
|
+
"{1..a}",
|
|
354
|
+
"{ab..c}",
|
|
355
|
+
"{1...3}",
|
|
356
|
+
"{a..}",
|
|
357
|
+
"{..a}",
|
|
358
|
+
"{1..3..}",
|
|
359
|
+
"{ab..cd}",
|
|
360
|
+
"{1..3...}",
|
|
361
|
+
"{1..3.}",
|
|
362
|
+
"{1..3..x}",
|
|
363
|
+
"{é..f}",
|
|
364
|
+
"{9223372036854775807..9223372036854775808}",
|
|
365
|
+
] {
|
|
366
|
+
assert_eq!(words(pattern), vec![pattern], "{pattern}");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#[test]
|
|
371
|
+
fn a_malformed_sequence_leaves_later_groups_expanding() {
|
|
372
|
+
assert_eq!(words("x{1..z}{a,b}"), vec!["x{1..z}a", "x{1..z}b"]);
|
|
373
|
+
assert_eq!(words("x{q}{1..2}"), vec!["x{q}1", "x{q}2"]);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[test]
|
|
377
|
+
fn a_comma_beside_dots_makes_an_alternation_not_a_range() {
|
|
378
|
+
assert_eq!(words("{a..c,d}"), vec!["a..c", "d"]);
|
|
379
|
+
assert_eq!(words("{1..3,}"), vec!["1..3", ""]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn a_range_bash_refuses_to_generate_stays_literal() {
|
|
384
|
+
assert!(steps(0, i64::from(i32::MAX), 1).is_none());
|
|
385
|
+
assert!(steps(0, i64::from(i32::MAX) - 2, 1).is_none());
|
|
386
|
+
let widest = steps(0, i64::from(i32::MAX) - 3, 1).unwrap();
|
|
387
|
+
assert_eq!(widest.size_hint().0, i32::MAX as usize - 2);
|
|
388
|
+
assert!(steps(0, i64::from(i32::MAX), 2).is_some());
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#[test]
|
|
392
|
+
fn a_range_missing_either_end_is_not_a_sequence() {
|
|
393
|
+
assert!(sequence("a..").is_none());
|
|
394
|
+
assert!(sequence("..a").is_none());
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#[test]
|
|
398
|
+
fn literal_braces_are_escaped_for_globset() {
|
|
399
|
+
assert_eq!(escape_braces("x{q}y"), "x[{]q[}]y");
|
|
400
|
+
assert_eq!(escape_braces(r"x\{a,b\}"), "x[{]a,b[}]");
|
|
401
|
+
assert_eq!(escape_braces(r"a\,b"), "a,b");
|
|
402
|
+
assert_eq!(escape_braces(r"a\*b\"), r"a\*b\");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#[test]
|
|
406
|
+
fn a_class_is_copied_as_written() {
|
|
407
|
+
assert_eq!(escape_braces("[{}]x"), "[{}]x");
|
|
408
|
+
assert_eq!(escape_braces("[]{]{"), "[]{][{]");
|
|
409
|
+
assert_eq!(escape_braces("[!]{]{"), "[!]{][{]");
|
|
410
|
+
assert_eq!(escape_braces("[^}]}"), "[^}][}]");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[test]
|
|
414
|
+
fn an_unclosed_class_ends_escaping() {
|
|
415
|
+
assert_eq!(escape_braces("x[{q}"), "x[{q}");
|
|
416
|
+
assert_eq!(escape_braces("x[]{"), "x[]{");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn expand_escapes_every_word() {
|
|
421
|
+
assert_eq!(expand("{a,{b}}.md"), vec!["a.md", "[{]b[}].md"]);
|
|
422
|
+
assert_eq!(expand("{q}"), vec!["[{]q[}]"]);
|
|
423
|
+
assert_eq!(expand("plain/*.md"), vec!["plain/*.md"]);
|
|
424
|
+
}
|
|
425
|
+
}
|