dirsql 0.4.71__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.
Files changed (314) hide show
  1. {dirsql-0.4.71 → dirsql-0.4.73}/Cargo.lock +2 -2
  2. {dirsql-0.4.71 → dirsql-0.4.73}/PKG-INFO +1 -1
  3. {dirsql-0.4.71/packages/rust → dirsql-0.4.73}/docs/reference/path-tables.md +7 -0
  4. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/Cargo.toml +1 -1
  5. dirsql-0.4.73/packages/python/changelog.d/2026-10-05-brace-groups-follow-bash.md +6 -0
  6. {dirsql-0.4.71 → dirsql-0.4.73/packages/python}/docs/reference/path-tables.md +7 -0
  7. dirsql-0.4.73/packages/python/e2e-attestations/claude-1262-brace-literal.json +7 -0
  8. dirsql-0.4.73/packages/python/e2e-attestations/claude-1286-nested-repo.json +7 -0
  9. dirsql-0.4.73/packages/python/migrations.d/2026-10-05-brace-groups-follow-bash.md +39 -0
  10. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/Cargo.toml +1 -1
  11. {dirsql-0.4.71/packages/python → dirsql-0.4.73/packages/rust}/docs/reference/path-tables.md +7 -0
  12. dirsql-0.4.73/packages/rust/src/brace.rs +425 -0
  13. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/lib.rs +1 -0
  14. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/scanner.rs +86 -33
  15. {dirsql-0.4.71 → dirsql-0.4.73}/Cargo.toml +0 -0
  16. {dirsql-0.4.71 → dirsql-0.4.73}/README.md +0 -0
  17. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/__init__.py +0 -0
  18. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/_async.py +0 -0
  19. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/_dirsql.pyi +0 -0
  20. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/__init__.py +0 -0
  21. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/discover_plugins/__init__.py +0 -0
  22. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  23. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  24. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  25. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  26. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  27. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/main.py +0 -0
  28. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/cli/resolve_config_extensions.py +0 -0
  29. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/plan_entry_path.py +0 -0
  30. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/py.typed +0 -0
  31. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/read_config.py +0 -0
  32. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/resolve_configs_extension_specs.py +0 -0
  33. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/resolve_extension.py +0 -0
  34. {dirsql-0.4.71 → dirsql-0.4.73}/dirsql/resolve_package.py +0 -0
  35. {dirsql-0.4.71 → dirsql-0.4.73}/docs/.claude/CLAUDE.md +0 -0
  36. {dirsql-0.4.71 → dirsql-0.4.73}/docs/.vitepress/config.ts +0 -0
  37. {dirsql-0.4.71 → dirsql-0.4.73}/docs/.vitepress/theme/index.ts +0 -0
  38. {dirsql-0.4.71 → dirsql-0.4.73}/docs/.vitepress/theme/lang.ts +0 -0
  39. {dirsql-0.4.71 → dirsql-0.4.73}/docs/AGENTS.md +0 -0
  40. {dirsql-0.4.71 → dirsql-0.4.73}/docs/context/.gitattributes +0 -0
  41. {dirsql-0.4.71 → dirsql-0.4.73}/docs/context/recipes.md +0 -0
  42. {dirsql-0.4.71 → dirsql-0.4.73}/docs/context/troubleshooting.md +0 -0
  43. {dirsql-0.4.71 → dirsql-0.4.73}/docs/context/usage.md +0 -0
  44. {dirsql-0.4.71 → dirsql-0.4.73}/docs/explanation.md +0 -0
  45. {dirsql-0.4.71 → dirsql-0.4.73}/docs/getting-started.md +0 -0
  46. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/columns-from-paths.md +0 -0
  47. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/define-tables.md +0 -0
  48. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/embed.md +0 -0
  49. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/extract-from-contents.md +0 -0
  50. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/load-extension.md +0 -0
  51. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/parse-files-into-columns.md +0 -0
  52. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/persist.md +0 -0
  53. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/query-json.md +0 -0
  54. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/query-without-config.md +0 -0
  55. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/react-to-changes.md +0 -0
  56. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/search-by-meaning.md +0 -0
  57. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/search-indexes.md +0 -0
  58. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/skip-files.md +0 -0
  59. {dirsql-0.4.71 → dirsql-0.4.73}/docs/howto/write-a-plugin.md +0 -0
  60. {dirsql-0.4.71 → dirsql-0.4.73}/docs/index.md +0 -0
  61. {dirsql-0.4.71 → dirsql-0.4.73}/docs/package.json +0 -0
  62. {dirsql-0.4.71 → dirsql-0.4.73}/docs/plugins.md +0 -0
  63. {dirsql-0.4.71 → dirsql-0.4.73}/docs/pnpm-lock.yaml +0 -0
  64. {dirsql-0.4.71 → dirsql-0.4.73}/docs/pnpm-workspace.yaml +0 -0
  65. {dirsql-0.4.71 → dirsql-0.4.73}/docs/reference/cli.md +0 -0
  66. {dirsql-0.4.71 → dirsql-0.4.73}/docs/reference/columns.md +0 -0
  67. {dirsql-0.4.71 → dirsql-0.4.73}/docs/reference/config.md +0 -0
  68. {dirsql-0.4.71 → dirsql-0.4.73}/docs/reference/hooks.md +0 -0
  69. {dirsql-0.4.71 → dirsql-0.4.73}/docs/reference/http-api.md +0 -0
  70. {dirsql-0.4.71 → dirsql-0.4.73}/docs/reference/sdk.md +0 -0
  71. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/CHANGELOG.md +0 -0
  72. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/MIGRATIONS.md +0 -0
  73. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/README.md +0 -0
  74. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  75. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  76. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  77. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  78. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  79. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  80. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  81. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  82. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  83. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  84. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  85. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  86. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  87. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  88. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  89. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  90. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  91. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  92. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  93. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  94. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  95. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  96. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  97. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
  98. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
  99. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-18-export-scan-failure.md +0 -0
  100. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +0 -0
  101. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
  102. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  103. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-09-24-query-rows-in-projection-order.md +0 -0
  104. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-02-readme-docs-host.md +0 -0
  105. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-03-one-glob-rule-for-config-globs.md +0 -0
  106. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-03-path-table-directory-one-level.md +0 -0
  107. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-04-path-table-dotfiles-hidden.md +0 -0
  108. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-cli-context.md +0 -0
  109. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-gitignore-requires-repo.md +0 -0
  110. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-path-table-node-modules-named.md +0 -0
  111. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-path-table-symlinks.md +0 -0
  112. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/2026-10-05-path-table-trailing-slash.md +0 -0
  113. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/changelog.d/README.md +0 -0
  114. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/conftest.py +0 -0
  115. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/.claude/CLAUDE.md +0 -0
  116. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/.vitepress/config.ts +0 -0
  117. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  118. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  119. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/AGENTS.md +0 -0
  120. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/context/.gitattributes +0 -0
  121. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/context/recipes.md +0 -0
  122. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/context/troubleshooting.md +0 -0
  123. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/context/usage.md +0 -0
  124. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/explanation.md +0 -0
  125. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/getting-started.md +0 -0
  126. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/columns-from-paths.md +0 -0
  127. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/define-tables.md +0 -0
  128. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/embed.md +0 -0
  129. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/extract-from-contents.md +0 -0
  130. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/load-extension.md +0 -0
  131. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  132. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/persist.md +0 -0
  133. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/query-json.md +0 -0
  134. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/query-without-config.md +0 -0
  135. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/react-to-changes.md +0 -0
  136. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/search-by-meaning.md +0 -0
  137. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/search-indexes.md +0 -0
  138. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/skip-files.md +0 -0
  139. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/howto/write-a-plugin.md +0 -0
  140. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/index.md +0 -0
  141. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/package.json +0 -0
  142. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/plugins.md +0 -0
  143. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/pnpm-lock.yaml +0 -0
  144. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/pnpm-workspace.yaml +0 -0
  145. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/reference/cli.md +0 -0
  146. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/reference/columns.md +0 -0
  147. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/reference/config.md +0 -0
  148. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/reference/hooks.md +0 -0
  149. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/reference/http-api.md +0 -0
  150. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/docs/reference/sdk.md +0 -0
  151. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  152. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  153. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  154. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
  155. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  156. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
  157. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
  158. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
  159. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
  160. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
  161. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
  162. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
  163. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1130-codec-crates.json +0 -0
  164. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +0 -0
  165. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1139-sse-flatten.json +0 -0
  166. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1140-scan-failure-export.json +0 -0
  167. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1148-clippy-all-targets.json +0 -0
  168. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1164-sigint-fatal.json +0 -0
  169. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1170-projection-order.json +0 -0
  170. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1172-on-file-path.json +0 -0
  171. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1173-py-projection-order.json +0 -0
  172. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1184-win-rel-paths.json +0 -0
  173. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1185-win-abs-table-names.json +0 -0
  174. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1186-win-test-harness.json +0 -0
  175. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1196-join-index.json +0 -0
  176. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1199-glob-prefix.json +0 -0
  177. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1201-single-walk.json +0 -0
  178. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1204-embed-batch.json +0 -0
  179. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1207-win-argv-quote.json +0 -0
  180. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1208-win-nonverbatim-paths.json +0 -0
  181. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1215-on-file-batch.json +0 -0
  182. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1216-on-file-cleanup.json +0 -0
  183. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1223-config-glob-rule.json +0 -0
  184. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1223-dir-one-level.json +0 -0
  185. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1226-stale-docs.json +0 -0
  186. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1238-parallel-walk.json +0 -0
  187. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1241-content-prefetch.json +0 -0
  188. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1243-ingest-perf.json +0 -0
  189. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1249-dotfiles.json +0 -0
  190. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1256-symlinks.json +0 -0
  191. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1257-trailing-slash.json +0 -0
  192. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1264-context.json +0 -0
  193. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1267-posix-classes.json +0 -0
  194. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1269-utf8-chars.json +0 -0
  195. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1271-node-modules-named.json +0 -0
  196. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1272-ancestor-gitignore.json +0 -0
  197. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1273-walk-prune.json +0 -0
  198. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-1285-ingest-fast.json +0 -0
  199. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  200. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  201. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  202. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  203. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  204. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  205. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  206. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  207. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  208. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  209. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  210. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  211. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  212. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  213. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  214. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  215. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  216. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  217. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  218. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  219. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  220. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  221. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  222. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  223. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  224. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  225. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-09-24-query-rows-in-projection-order.md +0 -0
  226. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-02-readme-docs-host.md +0 -0
  227. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-03-one-glob-rule-for-config-globs.md +0 -0
  228. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-03-path-table-directory-one-level.md +0 -0
  229. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-04-path-table-dotfiles-hidden.md +0 -0
  230. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-cli-context.md +0 -0
  231. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-gitignore-requires-repo.md +0 -0
  232. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-path-table-node-modules-named.md +0 -0
  233. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-path-table-symlinks.md +0 -0
  234. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/2026-10-05-path-table-trailing-slash.md +0 -0
  235. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/migrations.d/README.md +0 -0
  236. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/src/lib.rs +0 -0
  237. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/testing-conventions.toml +0 -0
  238. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/__init__.py +0 -0
  239. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/conftest.py +0 -0
  240. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/e2e/__init__.py +0 -0
  241. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  242. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  243. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/e2e/speed_of_light.py +0 -0
  244. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/integration/__init__.py +0 -0
  245. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/integration/binding/__init__.py +0 -0
  246. {dirsql-0.4.71 → dirsql-0.4.73}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  247. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/CHANGELOG.md +0 -0
  248. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/MIGRATIONS.md +0 -0
  249. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/README.md +0 -0
  250. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/benches/db_bench.rs +0 -0
  251. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/benches/differ_bench.rs +0 -0
  252. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/benches/matcher_bench.rs +0 -0
  253. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/benches/scanner_bench.rs +0 -0
  254. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/context/.gitattributes +0 -0
  255. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/context/recipes.md +0 -0
  256. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/context/troubleshooting.md +0 -0
  257. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/context/usage.md +0 -0
  258. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/explanation.md +0 -0
  259. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/getting-started.md +0 -0
  260. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  261. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/define-tables.md +0 -0
  262. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/embed.md +0 -0
  263. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  264. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/load-extension.md +0 -0
  265. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  266. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/persist.md +0 -0
  267. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/query-json.md +0 -0
  268. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/query-without-config.md +0 -0
  269. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/react-to-changes.md +0 -0
  270. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  271. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/search-indexes.md +0 -0
  272. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/skip-files.md +0 -0
  273. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  274. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/index.md +0 -0
  275. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/plugins.md +0 -0
  276. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/reference/cli.md +0 -0
  277. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/reference/columns.md +0 -0
  278. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/reference/config.md +0 -0
  279. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/reference/hooks.md +0 -0
  280. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/reference/http-api.md +0 -0
  281. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/docs/reference/sdk.md +0 -0
  282. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/bin/dirsql.rs +0 -0
  283. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/context.rs +0 -0
  284. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/execute.rs +0 -0
  285. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/init.rs +0 -0
  286. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/mod.rs +0 -0
  287. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/repl.rs +0 -0
  288. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/router.rs +0 -0
  289. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/run.rs +0 -0
  290. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/serialize.rs +0 -0
  291. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/server.rs +0 -0
  292. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/cli/table.rs +0 -0
  293. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/command.rs +0 -0
  294. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/config.rs +0 -0
  295. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/db.rs +0 -0
  296. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/default_config.toml +0 -0
  297. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/differ.rs +0 -0
  298. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/extension_resolution.rs +0 -0
  299. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/functions.rs +0 -0
  300. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/infer.rs +0 -0
  301. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/launcher.rs +0 -0
  302. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/matcher.rs +0 -0
  303. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/on_file.rs +0 -0
  304. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/parsed_vtab.rs +0 -0
  305. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/path_table.rs +0 -0
  306. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/persist.rs +0 -0
  307. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/posix_class.rs +0 -0
  308. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/progress.rs +0 -0
  309. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/row_event_flat.rs +0 -0
  310. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/sql_literal.rs +0 -0
  311. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/vtab.rs +0 -0
  312. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/vtab_scaffold.rs +0 -0
  313. {dirsql-0.4.71 → dirsql-0.4.73}/packages/rust/src/watcher.rs +0 -0
  314. {dirsql-0.4.71 → 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.71"
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.71"
604
+ version = "0.4.73"
605
605
  dependencies = [
606
606
  "dirsql",
607
607
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.71
3
+ Version: 0.4.73
4
4
  Requires-Dist: bin-shim>=0.1
5
5
  Summary: Ephemeral SQL index over a local directory
6
6
  Keywords: sql,filesystem,directory,sqlite,index
@@ -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.71"
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,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1791240509,
4
+ "exit_code": 0,
5
+ "commit": "42886c665ab14434f259593bf7868455cf206eda",
6
+ "branch": "claude/1262-brace-literal"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1791238629,
4
+ "exit_code": 0,
5
+ "commit": "68858295a0d429345af616a28e2cee3946f186cc",
6
+ "branch": "claude/1286-nested-repo"
7
+ }
@@ -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.71"
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
+ }
@@ -7,6 +7,7 @@
7
7
  //! bindings in this workspace can reach them, but they are not part of the
8
8
  //! stable public API.
9
9
 
10
+ mod brace;
10
11
  /// Reusable command runner backing the command-backed events.
11
12
  pub mod command;
12
13
  #[doc(hidden)]