@eslint-sh/eslint-plugin 0.5.0 → 0.7.0

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 (260) hide show
  1. package/README.md +228 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +51 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +53 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
  66. package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
  67. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  68. package/dist/rules/compat/no-case-attributes.js +63 -0
  69. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  70. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  71. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  72. package/dist/rules/compat/no-case-modification.js +86 -0
  73. package/dist/rules/compat/no-coproc.d.ts +12 -0
  74. package/dist/rules/compat/no-coproc.js +47 -0
  75. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  76. package/dist/rules/compat/no-declare-g.js +59 -0
  77. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  78. package/dist/rules/compat/no-double-bracket.js +52 -0
  79. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  80. package/dist/rules/compat/no-epoch-variables.js +57 -0
  81. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  82. package/dist/rules/compat/no-fd-variable.js +48 -0
  83. package/dist/rules/compat/no-globstar.d.ts +12 -0
  84. package/dist/rules/compat/no-globstar.js +39 -0
  85. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  86. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  87. package/dist/rules/compat/no-let.d.ts +12 -0
  88. package/dist/rules/compat/no-let.js +62 -0
  89. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  90. package/dist/rules/compat/no-mapfile.js +122 -0
  91. package/dist/rules/compat/no-nameref.d.ts +12 -0
  92. package/dist/rules/compat/no-nameref.js +44 -0
  93. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  94. package/dist/rules/compat/no-negative-array-index.js +123 -0
  95. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  96. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  97. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  98. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  99. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  100. package/dist/rules/compat/no-pipe-all.js +68 -0
  101. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  102. package/dist/rules/compat/no-printf-time-format.js +99 -0
  103. package/dist/rules/compat/no-source.d.ts +13 -0
  104. package/dist/rules/compat/no-source.js +103 -0
  105. package/dist/rules/compat/no-test-v.d.ts +12 -0
  106. package/dist/rules/compat/no-test-v.js +280 -0
  107. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  108. package/dist/rules/compat/no-unicode-escape.js +120 -0
  109. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  110. package/dist/rules/compat/no-wait-n.js +73 -0
  111. package/dist/rules/compat/operator-range.d.ts +10 -0
  112. package/dist/rules/compat/operator-range.js +15 -0
  113. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  114. package/dist/rules/compat/posix-absent-command.js +134 -0
  115. package/dist/rules/compat/shopt-options.d.ts +9 -0
  116. package/dist/rules/compat/shopt-options.js +63 -0
  117. package/dist/rules/compat/version-gates.d.ts +76 -0
  118. package/dist/rules/compat/version-gates.js +205 -0
  119. package/dist/rules/create-rule.d.ts +91 -0
  120. package/dist/rules/create-rule.js +51 -0
  121. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  122. package/dist/rules/create-wrapper-rule.js +407 -0
  123. package/dist/rules/define-shell-rule.d.ts +184 -0
  124. package/dist/rules/define-shell-rule.js +56 -0
  125. package/dist/rules/index.d.ts +26 -0
  126. package/dist/rules/index.js +90 -0
  127. package/dist/rules/max-script-complexity.d.ts +28 -0
  128. package/dist/rules/max-script-complexity.js +130 -0
  129. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  130. package/dist/rules/no-dynamic-invocation.js +117 -0
  131. package/dist/rules/no-eval.d.ts +13 -0
  132. package/dist/rules/no-eval.js +89 -0
  133. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  134. package/dist/rules/no-masked-exit-status.js +667 -0
  135. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  136. package/dist/rules/no-secret-env-reads.js +133 -0
  137. package/dist/rules/no-secret-flags.d.ts +32 -0
  138. package/dist/rules/no-secret-flags.js +162 -0
  139. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  140. package/dist/rules/no-unchecked-cd.js +250 -0
  141. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  142. package/dist/rules/no-undeclared-commands.js +115 -0
  143. package/dist/rules/no-unused-declarations.d.ts +23 -0
  144. package/dist/rules/no-unused-declarations.js +259 -0
  145. package/dist/rules/prefer-local.d.ts +16 -0
  146. package/dist/rules/prefer-local.js +249 -0
  147. package/dist/rules/quote-expansions.d.ts +13 -0
  148. package/dist/rules/quote-expansions.js +229 -0
  149. package/dist/rules/require-network-wrapper.d.ts +40 -0
  150. package/dist/rules/require-network-wrapper.js +244 -0
  151. package/dist/rules/require-shebang.d.ts +29 -0
  152. package/dist/rules/require-shebang.js +101 -0
  153. package/dist/rules/require-strict-mode.d.ts +52 -0
  154. package/dist/rules/require-strict-mode.js +177 -0
  155. package/dist/rules/require-tty-guard.d.ts +13 -0
  156. package/dist/rules/require-tty-guard.js +318 -0
  157. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  158. package/dist/rules/resolve-declaration-convention.js +109 -0
  159. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  160. package/dist/rules/resolve-network-wrapper.js +116 -0
  161. package/dist/rules/set-options.d.ts +109 -0
  162. package/dist/rules/set-options.js +201 -0
  163. package/dist/rules/shebang-interpreter.d.ts +18 -0
  164. package/dist/rules/shebang-interpreter.js +81 -0
  165. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  166. package/dist/rules/shebang-matches-dialect.js +69 -0
  167. package/dist/rules/transparent-wrappers.d.ts +52 -0
  168. package/dist/rules/transparent-wrappers.js +99 -0
  169. package/dist/rules/user-pattern.d.ts +63 -0
  170. package/dist/rules/user-pattern.js +184 -0
  171. package/dist/syntax/analyze.d.ts +19 -0
  172. package/dist/syntax/analyze.js +21 -0
  173. package/dist/syntax/index.d.ts +23 -0
  174. package/dist/syntax/index.js +25 -0
  175. package/dist/version.d.ts +0 -1
  176. package/dist/version.js +0 -1
  177. package/docs/compat.md +354 -0
  178. package/docs/custom-rule-tutorial.md +780 -34
  179. package/docs/declaration-conventions.md +478 -0
  180. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  181. package/docs/examples/dep-line-convention.mjs +22 -0
  182. package/docs/examples/eslint.config.mjs +3 -2
  183. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  184. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  185. package/docs/examples/no-backticks-rule.mjs +60 -0
  186. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  187. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  188. package/docs/examples/no-listed-commands-rule.ts +63 -0
  189. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  190. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  191. package/docs/examples/sidecar-convention.mjs +38 -0
  192. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  193. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  194. package/docs/reading-nodes.md +159 -0
  195. package/docs/rules/README.md +79 -0
  196. package/docs/rules/_template.md +83 -0
  197. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  198. package/docs/rules/compat/no-associative-arrays.md +95 -0
  199. package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
  200. package/docs/rules/compat/no-case-attributes.md +82 -0
  201. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  202. package/docs/rules/compat/no-case-modification.md +74 -0
  203. package/docs/rules/compat/no-coproc.md +76 -0
  204. package/docs/rules/compat/no-declare-g.md +80 -0
  205. package/docs/rules/compat/no-double-bracket.md +72 -0
  206. package/docs/rules/compat/no-epoch-variables.md +79 -0
  207. package/docs/rules/compat/no-fd-variable.md +78 -0
  208. package/docs/rules/compat/no-globstar.md +74 -0
  209. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  210. package/docs/rules/compat/no-let.md +78 -0
  211. package/docs/rules/compat/no-mapfile.md +118 -0
  212. package/docs/rules/compat/no-nameref.md +81 -0
  213. package/docs/rules/compat/no-negative-array-index.md +87 -0
  214. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  215. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  216. package/docs/rules/compat/no-pipe-all.md +85 -0
  217. package/docs/rules/compat/no-printf-time-format.md +75 -0
  218. package/docs/rules/compat/no-source.md +98 -0
  219. package/docs/rules/compat/no-test-v.md +82 -0
  220. package/docs/rules/compat/no-unicode-escape.md +91 -0
  221. package/docs/rules/compat/no-wait-n.md +85 -0
  222. package/docs/rules/max-script-complexity.md +109 -0
  223. package/docs/rules/no-dynamic-invocation.md +139 -0
  224. package/docs/rules/no-eval.md +106 -0
  225. package/docs/rules/no-masked-exit-status.md +239 -0
  226. package/docs/rules/no-secret-env-reads.md +173 -0
  227. package/docs/rules/no-secret-flags.md +169 -0
  228. package/docs/rules/no-unchecked-cd.md +146 -0
  229. package/docs/rules/no-undeclared-commands.md +141 -0
  230. package/docs/rules/no-unused-declarations.md +216 -0
  231. package/docs/rules/prefer-local.md +221 -0
  232. package/docs/rules/quote-expansions.md +157 -0
  233. package/docs/rules/require-network-wrapper.md +171 -0
  234. package/docs/rules/require-shebang.md +157 -0
  235. package/docs/rules/require-strict-mode.md +253 -0
  236. package/docs/rules/require-tty-guard.md +131 -0
  237. package/docs/rules/shebang-matches-dialect.md +120 -0
  238. package/docs/selectors.md +2 -2
  239. package/docs/shellcheck.md +87 -0
  240. package/docs/untrusted-code.md +440 -0
  241. package/docs/wrapper-rules.md +206 -0
  242. package/package.json +26 -20
  243. package/dist/index.d.ts.map +0 -1
  244. package/dist/index.js.map +0 -1
  245. package/dist/language/selector-classes.d.ts.map +0 -1
  246. package/dist/language/selector-classes.js.map +0 -1
  247. package/dist/language/shell-language-options.d.ts.map +0 -1
  248. package/dist/language/shell-language-options.js.map +0 -1
  249. package/dist/language/shell-language.d.ts.map +0 -1
  250. package/dist/language/shell-language.js.map +0 -1
  251. package/dist/language/shell-source-code.d.ts.map +0 -1
  252. package/dist/language/shell-source-code.js.map +0 -1
  253. package/dist/language/source-coordinates.d.ts.map +0 -1
  254. package/dist/language/source-coordinates.js.map +0 -1
  255. package/dist/language/syntax-view.d.ts.map +0 -1
  256. package/dist/language/syntax-view.js.map +0 -1
  257. package/dist/language/word-resolution.d.ts.map +0 -1
  258. package/dist/language/word-resolution.js.map +0 -1
  259. package/dist/version.d.ts.map +0 -1
  260. package/dist/version.js.map +0 -1
package/docs/compat.md ADDED
@@ -0,0 +1,354 @@
1
+ # Version compatibility (`compat/*` rules)
2
+
3
+ The `compat/*` rules report shell features that the shell you deploy to does not have. They
4
+ are for scripts that must run on an older Bash, such as the Bash 3.2 that macOS ships as
5
+ `/bin/bash`, or on a plain POSIX `sh`.
6
+
7
+ `compat/*` is reserved for version gates: rules that read the gate table below and your
8
+ `shellVersion`/`dialect`. A rule that checks the file against your config without reading the
9
+ target, such as [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md), lives outside
10
+ the namespace.
11
+
12
+ The parser does not do this for you. It accepts everything its grammar knows, whatever target
13
+ you configure: Bash mode parses `declare -A` and `mapfile` even with `shellVersion: "3.2"`, and
14
+ POSIX mode parses `declare -A` as an ordinary command. Only the `compat/*` rules enforce the
15
+ target.
16
+
17
+ A `compat/*` finding stays reported when the code is behind a runtime version guard
18
+ (`if (( BASH_VERSINFO[0] >= 4 )); then …`): the rules do not follow control flow. Silence a guarded
19
+ use with a disable comment on the line before it:
20
+
21
+ ```sh
22
+ if (( BASH_VERSINFO[0] >= 4 )); then
23
+ # eslint-disable-next-line sh/compat/no-mapfile
24
+ mapfile -t lines < file
25
+ fi
26
+ ```
27
+
28
+ ## Rules
29
+
30
+ | Rule | Feature | Bash | POSIX sh |
31
+ | ---- | ------- | ---- | -------- |
32
+ | [`sh/compat/no-append-all-redirect`](./rules/compat/no-append-all-redirect.md) | `&>>` | 4.0 | no |
33
+ | [`sh/compat/no-associative-arrays`](./rules/compat/no-associative-arrays.md) | `declare -A`, `typeset -A` | 4.0 | no |
34
+ | [`sh/compat/no-brace-sequence-padding-step`](./rules/compat/no-brace-sequence-padding-step.md) | `{01..10}`, `{1..10..2}`, `{a..e..2}` | 4.0 | no |
35
+ | [`sh/compat/no-case-attributes`](./rules/compat/no-case-attributes.md) | `declare -l`, `declare -u` (and `typeset`, `local`) | 4.0 | no |
36
+ | [`sh/compat/no-case-fallthrough`](./rules/compat/no-case-fallthrough.md) | `;&`, `;;&` in `case` | 4.0 | no |
37
+ | [`sh/compat/no-case-modification`](./rules/compat/no-case-modification.md) | `${v^}`, `${v^^}`, `${v,}`, `${v,,}` | 4.0 | no |
38
+ | [`sh/compat/no-coproc`](./rules/compat/no-coproc.md) | `coproc` | 4.0 | no |
39
+ | [`sh/compat/no-declare-g`](./rules/compat/no-declare-g.md) | `declare -g`, `typeset -g` | 4.2 | no |
40
+ | [`sh/compat/no-double-bracket`](./rules/compat/no-double-bracket.md) | `[[ … ]]` | every | no |
41
+ | [`sh/compat/no-epoch-variables`](./rules/compat/no-epoch-variables.md) | `$EPOCHSECONDS`, `$EPOCHREALTIME` | 5.0 | no |
42
+ | [`sh/compat/no-fd-variable`](./rules/compat/no-fd-variable.md) | `exec {fd}>file` | 4.1 | no |
43
+ | [`sh/compat/no-globstar`](./rules/compat/no-globstar.md) | `shopt -s globstar` | 4.0 | no |
44
+ | [`sh/compat/no-lastpipe-semantics`](./rules/compat/no-lastpipe-semantics.md) | `shopt -s lastpipe` | 4.2 | no |
45
+ | [`sh/compat/no-let`](./rules/compat/no-let.md) | `let` | every | no |
46
+ | [`sh/compat/no-mapfile`](./rules/compat/no-mapfile.md) | `mapfile`, `readarray` | 4.0 | no |
47
+ | [`sh/compat/no-nameref`](./rules/compat/no-nameref.md) | `declare -n`, `local -n` | 4.3 | no |
48
+ | [`sh/compat/no-negative-array-index`](./rules/compat/no-negative-array-index.md) | `${arr[-1]}`, `arr[-1]=x` | 4.3 | no |
49
+ | [`sh/compat/no-nounset-empty-array`](./rules/compat/no-nounset-empty-array.md) | `"${a[@]}"` of an empty array under `set -u` | 4.4 | no |
50
+ | [`sh/compat/no-parameter-transformation`](./rules/compat/no-parameter-transformation.md) | `${v@Q}`, `@E`, `@P`, `@A`, `@a`; `@U`, `@u`, `@L`, `@K`; `@k` | 4.4; 5.1; 5.2 | no |
51
+ | [`sh/compat/no-pipe-all`](./rules/compat/no-pipe-all.md) | `\|&` | 4.0 | no |
52
+ | [`sh/compat/no-printf-time-format`](./rules/compat/no-printf-time-format.md) | `printf '%(fmt)T'` | 4.2 | no |
53
+ | [`sh/compat/no-source`](./rules/compat/no-source.md) | `source` | every | no |
54
+ | [`sh/compat/no-test-v`](./rules/compat/no-test-v.md) | `[[ -v x ]]`, `test -v x` | 4.2 | no |
55
+ | [`sh/compat/no-unicode-escape`](./rules/compat/no-unicode-escape.md) | `\u`, `\U` in `$'…'`; U+0080 to U+00FF | 4.2; 4.3 | no |
56
+ | [`sh/compat/no-wait-n`](./rules/compat/no-wait-n.md) | `wait -n` | 4.3 | no |
57
+
58
+ The Bash column is the oldest Bash release that has the feature, taken from the Bash release notes
59
+ ([NEWS](https://tiswww.case.edu/php/chet/bash/NEWS)) and checked against real shells built from
60
+ source; see [Evidence](#evidence). Bash 4.2 already reads a negative array subscript (`${arr[-1]}`), but
61
+ it rejects the element length `${#arr[-1]}`, assigning and unsetting; the table uses 4.3, the first
62
+ release where every use works. Two rules gate more than one release. `sh/compat/no-parameter-transformation`
63
+ reports each operator against its own release. `sh/compat/no-unicode-escape` needs 4.2 for most code
64
+ points but 4.3 for U+0080 to U+00FF, because the Bash 4.2 release writes those as a single Latin-1 byte
65
+ instead of UTF-8. `sh/compat/no-nounset-empty-array` gates a behavior, not syntax: every Bash has
66
+ `"${a[@]}"`, but through 4.3 an empty array aborts the script under `set -u`. `every` means every Bash release has the feature, so only
67
+ `dialect: "posix"` can lack it: those three are the POSIX-only gates, and the other twenty-two are
68
+ the version gates. The `bash-3.2` preset enables the version gates and the `posix-portable` preset
69
+ enables all twenty-five; see
70
+ [Presets](#presets).
71
+
72
+ ## Setting the target
73
+
74
+ Every `compat/*` rule reads the same two `languageOptions`, and nothing else. They have no rule
75
+ options of their own:
76
+
77
+ ```js
78
+ // eslint.config.js
79
+ import { defineConfig } from 'eslint/config';
80
+ import sh from '@eslint-sh/eslint-plugin';
81
+
82
+ export default defineConfig({
83
+ files: ['**/*.sh'],
84
+ extends: [sh.configs.recommended],
85
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
86
+ rules: {
87
+ 'sh/compat/no-associative-arrays': 'error',
88
+ 'sh/compat/no-mapfile': 'error',
89
+ // …and the other rules in the table above.
90
+ },
91
+ });
92
+ ```
93
+
94
+ A gated feature is available, and its rule reports nothing, according to these rules:
95
+
96
+ - **`shellVersion` unset means the newest version.** With no `shellVersion`, every Bash feature
97
+ is available and no `compat/*` rule reports under `dialect: "bash"`. Set a version to opt in.
98
+ - **Versions compare numerically, major first, then minor.** `shellVersion` has the shape
99
+ `MAJOR[.MINOR]`. A missing minor is `0`, so `"4"` is the same as `"4.0"`. Each part is a
100
+ number, not text, so `"3.10"` is newer than `"3.2"` and still older than `"4.0"`. A feature
101
+ is available when `shellVersion` is its minimum version or newer.
102
+ - **`dialect: "posix"` makes Bash-only features unavailable at any `shellVersion`.** A POSIX sh
103
+ script cannot rely on a feature that only Bash has, whatever version number is configured.
104
+ - **`dialect: "bats"` is gated as Bash**, because Bats test files run under Bash.
105
+ `shellVersion` is the version of that Bash.
106
+ - **`dialect: "mksh"` and `dialect: "zsh"` are not gated.** These shells number their own
107
+ releases, so a `shellVersion` there is not a Bash version, and the rules have no data on
108
+ which of their releases added which feature. The `compat/*` rules report nothing for them.
109
+
110
+ ## Presets
111
+
112
+ Two presets name a deployment target and enable every rule that enforces it, so you do not list
113
+ the rules yourself:
114
+
115
+ - **`sh.configs['bash-3.2']`** sets `languageOptions: { dialect: 'bash', shellVersion: '3.2' }`
116
+ and enables all twenty-two version-gated `compat/*` rules at `error`. Use it for scripts that must run on the Bash
117
+ 3.2 that macOS ships as `/bin/bash`.
118
+ - **`sh.configs['posix-portable']`** reports the Bash-only constructs in the table above, which
119
+ fail or read as unset on dash, but not `local`, `echo -e` or the others listed under
120
+ [What `posix-portable` does not report](#what-posix-portable-does-not-report), so a clean run does
121
+ not prove a script runs under every POSIX `sh`. It sets `languageOptions: { dialect: 'posix' }` and
122
+ enables all twenty-five `compat/*` rules, `sh/shebang-matches-dialect`, and
123
+ `sh/require-strict-mode` configured as `set -eu`, all at `error`. Strict mode is `set -eu` and not
124
+ `set -euo pipefail` because `pipefail` joined POSIX only in POSIX.1-2024, and older `dash` and
125
+ BusyBox `ash` reject it, so a `pipefail` requirement would make the autofix break a working
126
+ `#!/bin/sh` script.
127
+
128
+ Both presets are single objects with no `files`; extend them from an entry that lists your
129
+ scripts, after `recommended` if you want its rules too:
130
+
131
+ ```js
132
+ // eslint.config.js
133
+ import { defineConfig } from 'eslint/config';
134
+ import sh from '@eslint-sh/eslint-plugin';
135
+
136
+ export default defineConfig([
137
+ { files: ['scripts/**/*.sh'], extends: [sh.configs.recommended, sh.configs['bash-3.2']] },
138
+ { files: ['install/?*'], extends: [sh.configs.recommended, sh.configs['posix-portable']] },
139
+ ]);
140
+ ```
141
+
142
+ Because the presets carry `languageOptions`, a `languageOptions` of your own in the same entry
143
+ overrides them key by key. To run these rules beside shellcheck, see [shellcheck](./shellcheck.md).
144
+
145
+ ## A branch for one shell
146
+
147
+ `[[`, `let` and `source` are commands to dash, so it only fails when it runs one. A script meant for
148
+ `sh` can hold a branch for one shell, and `sh/compat/no-double-bracket`, `sh/compat/no-let` and
149
+ `sh/compat/no-source` do not report inside it. `V` below is `$BASH_VERSION`, `$KSH_VERSION` or
150
+ `$ZSH_VERSION`, written `$X`, `${X}`, `${X-}` or `${X:-}`, quoted or not. dash sets none of them.
151
+
152
+ ```sh
153
+ if [ -n "${BASH_VERSION-}" ]; then source ~/.bashrc; fi
154
+ [ -n "${BASH_VERSION-}" ] && source ~/.bashrc
155
+ [ -n "${ZSH_VERSION-}" ] || exit 0
156
+ [[ -o nomatch ]]
157
+ ```
158
+
159
+ The shapes recognized are the `then` body of `if [ -n V ]` and the `else` body of `if [ -z V ]`, the
160
+ right side of `[ -n V ] && …` and `[ -z V ] || …`, and the statements after `[ -n V ] || exit` or
161
+ `[ -z V ] && exit` (`return` too) in the same file or `{ … }` block. `test -n V` and `test -z V`
162
+ count as well. The recognition is syntactic, so a version captured in another variable is not
163
+ understood and still reports. A construct dash cannot parse is a syntax error whichever branch it
164
+ is in, and these exemptions do not apply to it.
165
+
166
+ ## What `posix-portable` does not report
167
+
168
+ These constructs pass `posix-portable` with no finding. shellcheck reports each of them for a
169
+ script whose shebang says `sh`, so run it beside eslint-sh; see [shellcheck](./shellcheck.md).
170
+
171
+ | Construct | On dash | On BusyBox ash | shellcheck |
172
+ | --------- | ------- | -------------- | ---------- |
173
+ | `local`, other than `-n`, `-l` and `-u` | works | works | SC3043 |
174
+ | `echo -e` | exits 0, prints the `-e` as text | exits 0, reads it as an option | SC3037 |
175
+ | `declare`, `typeset`, other than `-A`, `-g`, `-n`, `-l` and `-u` | `not found` | `not found` | SC3044 |
176
+ | `shopt`, any option but `lastpipe` and `globstar` | `not found` | `not found` | SC3044 |
177
+ | `(( … ))` | `not found` | `not found` | SC3006 |
178
+ | `x+=1` | `not found` | `not found` | SC3024 |
179
+ | `[ a == b ]` | status 2 | works | SC3014 |
180
+ | `$'…'` | prints the `$` as text | works | SC3003 |
181
+ | `$"…"` | prints the `$` as text | prints the `$` as text | SC3004 |
182
+ | `{a,b}` | prints it as text | prints it as text | SC3009 |
183
+
184
+ `local` is left out because it works on dash and BusyBox ash, although POSIX does not define it and
185
+ ksh93 has no such command. `echo -e` is left out because it never fails: only its output differs.
186
+ `declare`, `typeset`, `shopt`, `(( … ))` and `x+=1` do fail on dash. They are left out because
187
+ shellcheck reports each one and scripts meant for `sh` rarely use them.
188
+
189
+ A `let` or `source` call written directly also passes when the file defines a function of that name
190
+ anywhere, even if that function cannot run at the call. The check reads the name only, and dash
191
+ still fails on such a call; see [`no-let`](./rules/compat/no-let.md) and
192
+ [`no-source`](./rules/compat/no-source.md).
193
+
194
+ Some other Bash-only constructs are parse errors under `dialect: "posix"`, so they are reported one
195
+ at a time: `function f {}`, `${v//a/b}`, `${v:1:1}`, `${!v}`, `<<<`, `<(…)`, `&>`, `|&`, `select`,
196
+ arrays, C-style `for`, and a `[[` containing a parenthesis.
197
+
198
+ `bash --posix` is not a way to check any of this. It accepts `[[`, `let`, `source`, `local` and the
199
+ rest, because POSIX mode does not turn off Bash's extensions.
200
+
201
+ ## Fixes
202
+
203
+ The `compat/*` rules never autofix, because the portable alternative can change how a script
204
+ behaves. Where a rewrite with the same behavior exists, the rule offers it as an editor
205
+ suggestion that you apply by choice. Each rule's page says when it offers one.
206
+
207
+ ## Not covered
208
+
209
+ - **Bash syntax that the POSIX parser rejects.** Under `dialect: "posix"`, `|&`, `&>>`, `;&`, `;;&`,
210
+ `{fd}` redirections, `${v@op}` and arrays are parse errors, so a POSIX file that uses them is
211
+ reported as unparsable, and the rules for those features never run on it. `$'…'` is read as a
212
+ literal `$` and a single-quoted string, as dash reads it.
213
+
214
+ ## Evidence
215
+
216
+ Every row of the table above, and the alternative each rule's message recommends, has been run on
217
+ real shells. [`scripts/compat-matrix/run.sh`](https://github.com/mike-north/eslint-sh/blob/main/scripts/compat-matrix/run.sh) downloads the
218
+ GNU Bash release tarballs and dash, builds each one under the gitignored `scratch/` folder (nothing
219
+ is installed system-wide), and runs every probe in
220
+ [`scripts/compat-matrix/probes/`](https://github.com/mike-north/eslint-sh/tree/main/scripts/compat-matrix/probes) under each shell:
221
+
222
+ ```sh
223
+ scripts/compat-matrix/run.sh
224
+ ```
225
+
226
+ A probe is a short script that uses one feature, or one recommended alternative, and prints a known
227
+ value. "yes" means the shell ran it, exited 0, and printed exactly that value; "no" means it did not.
228
+ Each Bash column is the x.y release tarball with no upstream patches applied, pinned by SHA-256 in
229
+ the script. The `dash` column is the evidence for the POSIX sh column.
230
+
231
+ | Probe | Checks | bash-3.2 | bash-4.0 | bash-4.1 | bash-4.2 | bash-4.3 | bash-4.4 | bash-5.0 | bash-5.1 | bash-5.2 | bash-5.3 | dash-0.5.12 |
232
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
233
+ | `associative-arrays-alt-jq` | the message's `jq -r --arg k "$key" '.[$k]' map.json` lookup (needs `jq` on PATH) | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
234
+ | `associative-arrays-typeset` | `typeset -A m` keeps two string keys apart | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
235
+ | `associative-arrays` | `declare -A m` keeps two string keys apart | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
236
+ | `brace-pad-step` | zero-padded `{01..03}` and stepped `{1..9..4}` brace expansion | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
237
+ | `brace-sequence-alt-loop` | the messages' counting loops, the padded one with `printf '%02d\n'` and the stepped one by `i + 4`, each written exactly as printed | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
238
+ | `case-attributes-alt-tr` | the message's `$(printf '%s' "$v" \| tr '[:upper:]' '[:lower:]')` and its upper-case twin | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
239
+ | `case-fallthrough-alt` | the message's rewrites: the next branch's commands repeated for `;&`, and a second `case` for `;;&` | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
240
+ | `case-fallthrough` | `;&` runs the next branch without testing its pattern | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
241
+ | `case-modification-alt-pattern-tr` | the pattern message's `tr a A` advice, spelling out the matched characters | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
242
+ | `case-modification-alt-tr` | the four `tr` examples the message prints, written exactly as it prints them | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
243
+ | `case-modification-pattern` | the pattern forms `${v^^l}` and `${v,,L}` | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
244
+ | `case-modification` | `${v^}`, `${v^^}`, `${v,}`, `${v,,}` | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
245
+ | `case-resume` | `;;&` goes on to test the next patterns | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
246
+ | `coproc-alt-mkfifo` | the message's named-pipe pattern: `mkfifo`, a background reader/writer, the script opening both pipes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
247
+ | `coproc` | `coproc cat`, writing to and reading back from `${COPROC[@]}` | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
248
+ | `declare-g-alt-assign` | the message's plain `name=value` and `export name=value` inside a function | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
249
+ | `declare-g-typeset` | `typeset -g` inside a function sets a global | no | no | no | yes | yes | yes | yes | yes | yes | yes | no |
250
+ | `declare-g` | `declare -g` inside a function sets a global | no | no | no | yes | yes | yes | yes | yes | yes | yes | no |
251
+ | `declare-lu-local` | `local -l` and `typeset -u` | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
252
+ | `declare-lu` | `declare -l` / `declare -u` case-converting attributes | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
253
+ | `double-bracket-alt-test-case` | the message's `[ … ]` for tests and `case` for pattern matching | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
254
+ | `double-bracket` | `[[ abc == a* ]]` matches a pattern, and an `if` on it takes its `then` branch | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
255
+ | `epoch-alt-date` | the message's `$(date +%s)` (GNU, BSD and BusyBox `date`, not POSIX) is a number | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
256
+ | `epochrealtime` | `$EPOCHREALTIME` is set to seconds with a fraction | no | no | no | no | no | no | yes | yes | yes | yes | no |
257
+ | `epochseconds` | `$EPOCHSECONDS` is set to a number | no | no | no | no | no | no | yes | yes | yes | yes | no |
258
+ | `fd-variable-alt-fixed` | the message's fixed descriptor number, `exec 3>file` … `exec 3>&-` | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
259
+ | `fd-variable` | `exec {fd}>file` allocates a descriptor into a variable | no | no | yes | yes | yes | yes | yes | yes | yes | yes | no |
260
+ | `globstar-alt-find` | the message's `find a -name 'f.txt'` recursive search | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
261
+ | `globstar` | `shopt -s globstar` makes `**` recurse | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
262
+ | `lastpipe-alt-posix-heredoc` | the POSIX message's here-document feeding the loop | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
263
+ | `lastpipe-alt-posix-tmpfile` | the POSIX message's `cmd > "$tmp"` then `while read -r line; do …; done < "$tmp"` | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
264
+ | `lastpipe-alt-procsub` | the message's `while read -r line; do …; done < <(cmd)` | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
265
+ | `lastpipe` | `shopt -s lastpipe` runs the last pipeline element in the current shell | no | no | no | yes | yes | yes | yes | yes | yes | yes | no |
266
+ | `let-alt-arithmetic` | the message's arithmetic expansion, `n=$((1 + 2))` | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
267
+ | `let` | `let n=1+2` assigns the sum | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
268
+ | `mapfile-alt-posix-read-loop` | the message's `while IFS= read -r` loop, with a named variable as POSIX requires | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
269
+ | `mapfile-alt-suggestion` | the suggestion's exact rewrite of `mapfile -t a < in.txt`, from a pipeline too | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
270
+ | `mapfile-readarray` | the `readarray -t a < file` synonym | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
271
+ | `mapfile` | `mapfile -t a < file`, including a last line with no newline | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
272
+ | `nameref-alt-posix-eval` | the POSIX message's `eval "$name=\$value"` to assign and `eval "value=\$$name"` to read by name | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
273
+ | `nameref-alt-printf-v` | the message's `printf -v "$name" '%s' value` to assign and `${!name}` to read by name | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
274
+ | `nameref-declare` | `declare -n` and `typeset -n` read through a nameref | no | no | no | no | yes | yes | yes | yes | yes | yes | no |
275
+ | `nameref` | `local -n` makes a nameref that assigns the caller's variable | no | no | no | no | yes | yes | yes | yes | yes | yes | no |
276
+ | `negative-index-alt-length` | the message's `${arr[$((${#arr[@]}-1))]}`, the suggestion's `-2` form, and the docs' assignment form | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
277
+ | `negative-index-assign` | assigning `a[-1]=Z` | no | no | no | no | yes | yes | yes | yes | yes | yes | no |
278
+ | `negative-index-length` | the element length `${#a[-1]}` | no | no | no | no | yes | yes | yes | yes | yes | yes | no |
279
+ | `negative-index-read` | reading `${a[-1]}` and `${a[-2]}` | no | no | no | yes | yes | yes | yes | yes | yes | yes | no |
280
+ | `negative-index-unset` | `unset 'a[-1]'` | no | no | no | no | yes | yes | yes | yes | yes | yes | no |
281
+ | `nounset-empty-array-alt` | the message's and suggestion's `${a[@]+"${a[@]}"}` under `set -u`, empty and with words that hold spaces | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
282
+ | `nounset-empty-array-star-alt` | the suggestion's `"${a[*]-}"` under `set -u` gives one empty word for an empty array, and the joined words otherwise | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
283
+ | `nounset-empty-array-star` | `"${a[*]}"` and unquoted `${a[@]}` of an empty array under `set -u` | no | no | no | no | no | yes | yes | yes | yes | yes | no |
284
+ | `nounset-empty-array` | `"${a[@]}"` of an empty array under `set -u` | no | no | no | no | no | yes | yes | yes | yes | yes | no |
285
+ | `param-transform-alt-case-operators` | the case messages' Bash 4.0 spellings for a target from 4.0 to 5.0: `"${v^^}"`, `"${v^}"`, `"${V,,}"` | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
286
+ | `param-transform-alt-declare-p` | the `@A` message's `declare -p`, whose output re-creates the variable | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
287
+ | `param-transform-alt-printf-b` | the `@E` message's `printf '%b'` expanding backslash escapes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
288
+ | `param-transform-alt-printf-q` | the `@Q` message's `printf '%q'`, whose output `eval` reads back as the same value | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
289
+ | `param-transform-alt-tr` | the `tr` examples the `@U`, `@u` and `@L` messages print for a target older than 4.0, written exactly as printed | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
290
+ | `param-transform-eapa` | the other Bash 4.4 operators `${v@E}`, `${v@P}`, `${v@A}`, `${v@a}` | no | no | no | no | no | yes | yes | yes | yes | yes | no |
291
+ | `param-transform-k-lower` | `${a[@]@k}` key-value pairs as separate words | no | no | no | no | no | no | no | no | yes | yes | no |
292
+ | `param-transform-k-upper` | `${a[@]@K}` quoted key-value pairs | no | no | no | no | no | no | no | yes | yes | yes | no |
293
+ | `param-transform-q` | `${v@Q}` quoting transformation | no | no | no | no | no | yes | yes | yes | yes | yes | no |
294
+ | `param-transform-ul` | `${v@U}` / `${v@L}` / `${v@u}` case transformations | no | no | no | no | no | no | no | yes | yes | yes | no |
295
+ | `pipe-amp-alt` | the message's and suggestion's `2>&1 \|`, also after a redirection of the left side, as `\|&` orders it | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
296
+ | `pipe-amp` | `\|&` pipes stderr too | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
297
+ | `printf-time-alt-date` | the message's `date '+FORMAT'` for the current time, here `date '+%Y'` | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
298
+ | `printf-time` | `printf '%(%Y)T' 0` builtin date formatting | no | no | no | yes | yes | yes | yes | yes | yes | yes | no |
299
+ | `redirect-amp-append-alt` | the message's and suggestion's `>> log 2>&1` appends stdout and stderr | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
300
+ | `redirect-amp-append` | `&>>` appends stdout and stderr | no | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
301
+ | `source-alt-dot` | the message's `. ./lib.sh`, the `.` special built-in with a path that contains a slash | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
302
+ | `source` | `source ./lib.sh` reads the file into the current shell | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
303
+ | `test-v-alt-plus-set` | the message's `[ -n "${name+set}" ]`, under `set -u`, for a set, an empty and an unset variable | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
304
+ | `test-v` | `[[ -v name ]]` and `test -v name` | no | no | no | yes | yes | yes | yes | yes | yes | yes | no |
305
+ | `unicode-escape-alt-octal` | the message's UTF-8 bytes as octal escapes, `$'\303\251'`, with no locale set | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | no |
306
+ | `unicode-escape-beyond-latin1` | `$'\u0041'`, `$'\u2713'` and `$'\U0001F389'`, outside U+0080–U+00FF, are decoded to UTF-8 | no | no | no | yes | yes | yes | yes | yes | yes | yes | no |
307
+ | `unicode-escape` | `$'\u00e9'` (U+00E9, inside U+0080–U+00FF) is decoded to UTF-8 e-acute | no | no | no | no | yes | yes | yes | yes | yes | yes | no |
308
+ | `wait-n-alt-pid` | the message's `wait "$pid"` on a job's saved `$!`, which returns that job's status | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes |
309
+ | `wait-n` | `wait -n` waits for the next job to finish | no | no | no | no | yes | yes | yes | yes | yes | yes | no |
310
+
311
+ How to read it:
312
+
313
+ - **Gates.** Each feature probe turns from "no" to "yes" at exactly the release in the Bash column
314
+ of the rule table, and is "no" under dash, matching every `POSIX sh: no`. Bash 4.1 is in the
315
+ matrix because it is the release just before the 4.2 boundary of `declare -g` and `lastpipe`.
316
+ Bash 4.0 is the release just before the 4.1 boundary of `{fd}` redirections.
317
+ - **POSIX-only gates.** The rows for `[[`, `let` and `source`, the rule table's `every`, are "yes" on
318
+ every Bash in the matrix and "no" under dash, so no `shellVersion` can change whether they report.
319
+ dash runs a script past each one without a syntax error, which is why a shell's own version check
320
+ can guard them; see [A branch for one shell](#a-branch-for-one-shell).
321
+ - **Parameter transformations.** `@Q`, `@E`, `@P`, `@A` and `@a` start working in 4.4; `@U`, `@u`,
322
+ `@L` and `@K` in 5.1; `@k` in 5.2.
323
+ - **Unicode escapes.** `unicode-escape-beyond-latin1` (`\u0041`, `\u2713`, `\U0001F389`) works from
324
+ 4.2, but `unicode-escape` (`\u00e9`) only from 4.3: the 4.2 release prints `e9`, the Latin-1 byte,
325
+ for U+00E9. Bash 3.2 to 4.1 print the escape as literal text, with no error.
326
+ - **Empty arrays under `set -u`.** `"${a[@]}"`, `"${a[*]}"` and an unquoted `${a[@]}` abort with
327
+ `unbound variable` through 4.3. `${#a[@]}`, `${!a[@]}` and `${a[@]-}` work in every Bash, so the
328
+ rule does not report them. The slice `${a[@]:0}` and the replacement `${a[@]/x/y}` were also
329
+ probed outside this table: they work on 3.2 to 4.1, fail on 4.2 and 4.3, and work again from 4.4,
330
+ which a minimum-version gate cannot express, so the rule does not report them.
331
+ - **Negative subscripts.** Bash 4.2 reads `${a[-1]}`, but rejects `${#a[-1]}`, `a[-1]=Z` and
332
+ `unset 'a[-1]'` with `bad array subscript`. 4.3 is the first release where every form works.
333
+ - **Brace sequences.** `brace-pad-step` is "no" on Bash 3.2, which prints `1 2 3 {1..9..4}` with
334
+ no error, and on dash, which prints `{01..03} {1..9..4}`; it is "yes" from 4.0. That row records
335
+ the `run.sh` result reported in issue #237; the probe was also re-run on `/bin/bash` (3.2.57),
336
+ Homebrew's Bash 5.3.9 and the system `dash`, with the same outcomes. The
337
+ `brace-sequence-alt-loop` row has not been run through `run.sh`: its probe was run only on macOS's
338
+ `/bin/bash` (3.2.57), Homebrew's Bash 5.3.9 and the system `dash`, which are distribution builds,
339
+ not the tarball releases in the other columns, and each printed the expected line. Fill the row by
340
+ running `scripts/compat-matrix/run.sh`.
341
+ - **Alternatives.** Every alternative a message recommends for a Bash target works on Bash 3.2,
342
+ except `param-transform-alt-case-operators` (`"${v^^}"` and its twins), which the
343
+ `no-parameter-transformation` message offers only for a target from 4.0 to 5.0, where it works.
344
+ The ones that are "no" under dash (`< <(cmd)`, the `mapfile` suggestion's array loop, the
345
+ `${arr[$((${#arr[@]}-1))]}` count, `printf -v`, `printf '%q'`, `declare -p`, the `${a[@]+…}` and
346
+ `${a[*]-}` guards, `$'\303\251'`) use Bash syntax and appear only in Bash-target messages; the
347
+ alternatives the POSIX-target messages give (`tr`, a named pipe, a temporary file or here-document,
348
+ a `while IFS= read -r line` loop, a plain assignment, `eval`, `[ -n "${x+set}" ]`, `date`, `find`,
349
+ `wait "$pid"`, `[ … ]` and `case`, `$(( … ))`, `. ./lib.sh`) all work under dash.
350
+ - **`date +%s`.** `epoch-alt-date` passes under every shell here because the probes run with GNU
351
+ `date`. `%s` is not in POSIX `date`; the message says so.
352
+ - **`jq`.** The `no-associative-arrays` advice runs an external program, so its row shows that the
353
+ command as written works wherever `jq` is installed, not that a shell provides it.
354
+