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