@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
@@ -0,0 +1,74 @@
1
+ # `sh/compat/no-globstar`
2
+
3
+ Disallow `shopt -s globstar` in scripts whose target shell lacks the option: Bash older than 4.0,
4
+ including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ------- | --------- |
8
+ | `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ The `globstar` option, which makes `**` match across directories, arrived in Bash 4.0 (Bash NEWS for 4.0). Bash 3.2 has no such option: `shopt` prints `globstar: invalid shell option name`, the script carries on, and `**` then matches like `*`. A recursive glob therefore matches fewer files, often none, with no failure.
13
+
14
+ The rule reads `shopt` commands (also through `builtin` or `command`) whose options, up to the first operand, include `-s`, and reports a `globstar` operand; the report is on that word. `shopt -u globstar` turns the option off, a `-u` or `-o` beside the `-s` changes what the command does, a query (`shopt globstar`, `shopt -q globstar`) turns nothing on, and an operand that is not a static word cannot be read, so none of those reports.
15
+
16
+ The portable alternative is `find`, for example `find dir -name '*.txt'`.
17
+
18
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
19
+ described in [version compatibility](../../compat.md):
20
+
21
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
22
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
23
+ - `dialect: "posix"`: reports at any `shellVersion`, because POSIX sh has no `shopt`.
24
+ - `dialect: "mksh"` or `"zsh"`: never reports.
25
+
26
+ ## Options
27
+
28
+ This rule has no options. Set the target through `languageOptions`:
29
+
30
+ ```js
31
+ // eslint.config.js
32
+ {
33
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
34
+ rules: {
35
+ 'sh/compat/no-globstar': 'error',
36
+ },
37
+ }
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ ### Incorrect
43
+
44
+ With `shellVersion: "3.2"`:
45
+
46
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
47
+ ```sh
48
+ shopt -s nullglob globstar
49
+ for f in src/**/*.sh; do lint "$f"; done
50
+ ```
51
+
52
+ ### Correct
53
+
54
+ With `shellVersion: "3.2"`:
55
+
56
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
57
+ ```sh
58
+ find src -name '*.sh' -exec lint {} \;
59
+ shopt -s nullglob
60
+ ```
61
+
62
+ With `shellVersion: "4.0"` or newer, or with no `shellVersion`, `globstar` is fine.
63
+
64
+ ## When not to use it
65
+
66
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
67
+ unset, which makes the rule report nothing.
68
+
69
+ ## Relationship to shellcheck
70
+
71
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
72
+ layer. Run shellcheck beside it.
73
+
74
+ shellcheck reports `shopt` only for a script it treats as POSIX `sh` (its SC3xxx portability checks). It does not know which Bash release a script must run on, so it cannot report `globstar` for a Bash 3.2 target. This rule exists for that target.
@@ -0,0 +1,73 @@
1
+ # `sh/compat/no-lastpipe-semantics`
2
+
3
+ Disallow `shopt -s lastpipe` in scripts whose target shell lacks the option: Bash older than 4.2,
4
+ including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ------- | --------- |
8
+ | `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ `lastpipe` arrived in Bash 4.2 (Bash NEWS for 4.2: "a new `lastpipe` shell option"). It runs the last command of a pipeline in the current shell. Bash 3.2 has no such option: `shopt` fails with `invalid shell option name`, and the last command keeps running in a subshell, so a `cmd | while read …; do total=…; done` loop silently loses its assignments.
13
+
14
+ The rule reports a `shopt` command (also through `builtin` or `command`) whose options, up to the first operand, include `-s` and which names `lastpipe`; the report is on the `lastpipe` word. `shopt -u lastpipe`, a query (`shopt lastpipe`), `shopt -o` (which names `set -o` options) and a name that is not static are not reported.
15
+
16
+ The portable alternative is process substitution: `while read -r line; do …; done < <(cmd)` keeps the loop in the current shell on every Bash that has process substitution. Process substitution is not POSIX, so for `dialect: "posix"` the message suggests a temporary file (`cmd > "$tmp"`, then `done < "$tmp"`) or a here-document instead. It hides `cmd`'s exit status, though: the pipeline's status was the loop's (or, with `pipefail`, the first failure), and with `< <(cmd)` a failing `cmd` is invisible to the script, so check it another way (for example, write `cmd`'s output to a temporary file and test `cmd`'s status before reading it).
17
+
18
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
19
+ described in [version compatibility](../../compat.md):
20
+
21
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
22
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.2`.
23
+ - `dialect: "posix"`: reports at any `shellVersion`.
24
+ - `dialect: "mksh"` or `"zsh"`: never reports.
25
+
26
+ ## Options
27
+
28
+ This rule has no options. Set the target through `languageOptions`:
29
+
30
+ ```js
31
+ // eslint.config.js
32
+ {
33
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
34
+ rules: {
35
+ 'sh/compat/no-lastpipe-semantics': 'error',
36
+ },
37
+ }
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ ### Incorrect
43
+
44
+ With `shellVersion: "3.2"`:
45
+
46
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
47
+ ```sh
48
+ shopt -s lastpipe
49
+ cmd | while read -r line; do total=$line; done
50
+ ```
51
+
52
+ ### Correct
53
+
54
+ With `shellVersion: "3.2"`:
55
+
56
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
57
+ ```sh
58
+ while read -r line; do total=$line; done < <(cmd)
59
+ ```
60
+
61
+ With `shellVersion: "4.2"` or newer, or with no `shellVersion`, the original forms are fine.
62
+
63
+ ## When not to use it
64
+
65
+ Turn the rule off when your scripts never run on Bash older than 4.2, or leave
66
+ `shellVersion` unset, which makes the rule report nothing.
67
+
68
+ ## Relationship to shellcheck
69
+
70
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
71
+ layer. Run shellcheck beside it.
72
+
73
+ shellcheck warns that variables assigned in the last pipeline element are lost (SC2030/SC2031), and understands `shopt -s lastpipe` for that purpose, but it cannot target a Bash version, so it cannot report the option for a Bash 3.2 target. This rule exists for that target.
@@ -0,0 +1,78 @@
1
+ # `sh/compat/no-let`
2
+
3
+ Disallow the `let` builtin in scripts that target POSIX sh.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ------- | ------- | --------- |
7
+ | `posix-portable` | not fixable | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ POSIX lists `let` among the command names whose results are unspecified ([Shell Command Language, section 2.9.1.4](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_09_01_04)). dash, the `/bin/sh` of Debian and Ubuntu, has no such command: `let n=1+2` fails with `let: not found` and status 127, and `n` stays unset. Under `set -e` that stops the script.
12
+
13
+ `bash --posix` and BusyBox ash both accept `let`, so a script can pass on them and still fail on dash.
14
+
15
+ Under `dialect: "posix"` the parser reads `let` as an ordinary command, and the rule reports a command whose effective name is `let`, after following wrappers such as `command`. It reports the `let` word. `command -v let` only asks about the word and is not reported. A use only a shell with a version variable can reach is not reported either, such as the body of `if [ -n "${BASH_VERSION-}" ]`, because dash never runs it; [version compatibility](../../compat.md#a-branch-for-one-shell) lists the shapes recognized.
16
+
17
+ A function named `let` anywhere in the file exempts a call that names it directly, since dash can then run that function. This is only a name check across the whole file: it ignores execution order and does not prove the function exists at the call. Each of these stays silent, and the `let` call still fails on dash with `let: not found` and status 127:
18
+
19
+ - A call that runs before the definition.
20
+ - A definition in a branch that is not taken.
21
+ - A definition in a subshell, or in a function that is never called.
22
+ - A definition removed by a later `unset -f let`.
23
+
24
+ A call through a wrapper gets no exemption and is reported. `command let n=1` skips the function and fails with status 127, as does `env let n=1`, and dash has no `builtin` at all.
25
+
26
+ The portable alternative is arithmetic expansion, `n=$((1 + 2))`. The rule offers no suggestion, because `let` sets its exit status from the value of its last expression and an assignment does not, so the rewrite can change what `set -e` or an `if` sees.
27
+
28
+ This is a POSIX-only gate. Every Bash has `let`, so whether the rule reports depends only on `dialect`, as described in [version compatibility](../../compat.md):
29
+
30
+ - `dialect: "posix"`: reports at any `shellVersion`.
31
+ - `dialect: "bash"` (the default), `"bats"`, `"mksh"` or `"zsh"`: never reports.
32
+
33
+ ## Options
34
+
35
+ This rule has no options. Set the target through `languageOptions`:
36
+
37
+ ```js
38
+ // eslint.config.js
39
+ {
40
+ languageOptions: { dialect: 'posix' },
41
+ rules: {
42
+ 'sh/compat/no-let': 'error',
43
+ },
44
+ }
45
+ ```
46
+
47
+ ## Examples
48
+
49
+ ### Incorrect
50
+
51
+ With `dialect: "posix"`:
52
+
53
+ <!-- example: {"dialect": "posix", "findings": 2} -->
54
+ ```sh
55
+ let n=1+2
56
+ let "m = n * 2"
57
+ ```
58
+
59
+ ### Correct
60
+
61
+ With `dialect: "posix"`:
62
+
63
+ <!-- example: {"dialect": "posix"} -->
64
+ ```sh
65
+ n=$((1 + 2))
66
+ m=$((n * 2))
67
+ ```
68
+
69
+ ## When not to use it
70
+
71
+ Turn the rule off when your scripts only ever run where `let` exists, such as under Bash or BusyBox ash, and set `dialect` to match if you can.
72
+
73
+ ## Relationship to shellcheck
74
+
75
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
76
+ layer. Run shellcheck beside it.
77
+
78
+ shellcheck reports this construct as SC3039 for a script whose shebang says `sh`. This rule reports it for the dialect you configure, whatever the shebang says, and at the severity you choose.
@@ -0,0 +1,118 @@
1
+ # `sh/compat/no-mapfile`
2
+
3
+ Disallow the `mapfile` builtin and its synonym `readarray` in scripts whose target shell lacks
4
+ them: Bash older than 4.0, including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ----------- | --------- |
8
+ | `bash-3.2`, `posix-portable` | suggestions | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ `mapfile` and `readarray` arrived in Bash 4.0. On Bash 3.2 neither is a builtin, so the shell
13
+ looks for an external command of that name, fails with `command not found`, and the array is
14
+ never filled.
15
+
16
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
17
+ described in [version compatibility](../../compat.md):
18
+
19
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
20
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
21
+ - `dialect: "posix"`: reports at any `shellVersion`.
22
+ - `dialect: "mksh"` or `"zsh"`: never reports.
23
+
24
+ The rule reports every command whose effective command is `mapfile` or `readarray`, after
25
+ following transparent wrappers such as `command` and `builtin`. The report is on the whole
26
+ command.
27
+
28
+ ### Suggestion
29
+
30
+ For `mapfile -t NAME` or `readarray -t NAME`, written directly (no wrapper and no leading
31
+ `VAR=value`) in a Bash or Bats file, the rule suggests a `while IFS= read -r` loop that fills
32
+ the same array the same way:
33
+
34
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
35
+
36
+ ```sh
37
+ mapfile -t lines < in.txt
38
+ # becomes
39
+ { lines=(); while IFS= read -r || [ -n "$REPLY" ]; do lines+=("$REPLY"); done; } < in.txt
40
+ ```
41
+
42
+ - `|| [ -n "$REPLY" ]` keeps a last line that has no trailing newline, as `mapfile -t` does.
43
+ - Reading with no variable name keeps each line unchanged, including leading and trailing
44
+ spaces and backslashes. The tradeoff is that `read` stores each line in `REPLY`, which
45
+ `mapfile` does not touch. After the loop, `REPLY` is the empty string, whatever it held
46
+ before. `REPLY` is `read`'s own default variable, so this overwrites less than a named loop
47
+ variable such as `line` would. If your script uses `REPLY` across this command, save it
48
+ first or edit the loop.
49
+ - The brace group keeps the replacement a single command, so a redirection (`< in.txt`,
50
+ `<<< "$text"`, `< <(cmd)`) or a pipeline (`cmd | mapfile -t lines`) still feeds the loop.
51
+ - With no `NAME`, `mapfile` fills `MAPFILE`, and so does the loop.
52
+
53
+ Other forms are reported without a suggestion, because no loop reproduces them exactly or the
54
+ target has no arrays at all: `mapfile` without `-t` (each element keeps its newline), any other
55
+ option (`-d`, `-n`, `-O`, `-s`, `-u`, `-C`), an array name that is not static, a leading
56
+ assignment, a wrapper, and every file under `dialect: "posix"`.
57
+
58
+ ## Options
59
+
60
+ This rule has no options. Set the target through `languageOptions`:
61
+
62
+ ```js
63
+ // eslint.config.js
64
+ {
65
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
66
+ rules: {
67
+ 'sh/compat/no-mapfile': 'error',
68
+ },
69
+ }
70
+ ```
71
+
72
+ ## Examples
73
+
74
+ ### Incorrect
75
+
76
+ With `shellVersion: "3.2"`:
77
+
78
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 3} -->
79
+ ```sh
80
+ mapfile -t lines < in.txt # mapfile needs Bash 4.0
81
+ git ls-files | readarray -t files # readarray is the same builtin
82
+ command mapfile -t lines < in.txt # reached through the command wrapper
83
+ ```
84
+
85
+ ### Correct
86
+
87
+ With `shellVersion: "3.2"`:
88
+
89
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
90
+ ```sh
91
+ { lines=(); while IFS= read -r || [ -n "$REPLY" ]; do lines+=("$REPLY"); done; } < in.txt
92
+
93
+ while IFS= read -r line; do
94
+ printf '%s\n' "$line"
95
+ done < in.txt
96
+ ```
97
+
98
+ With `shellVersion: "4.0"` or newer, or with no `shellVersion`:
99
+
100
+ <!-- example: {"dialect": "bash", "shellVersion": "4.0"} -->
101
+ ```sh
102
+ mapfile -t lines < in.txt
103
+ ```
104
+
105
+ ## When not to use it
106
+
107
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
108
+ unset, which makes the rule report nothing.
109
+
110
+ ## Relationship to shellcheck
111
+
112
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
113
+ layer. Run shellcheck beside it.
114
+
115
+ shellcheck reports Bash-only features in scripts it treats as POSIX sh (the SC3xxx checks), but
116
+ it cannot target a Bash version, so it cannot report `mapfile` in a script that must run on
117
+ Bash 3.2. shellcheck 0.11.0 does recommend `mapfile` or `read -a` over `a=($(cmd))` splitting (SC2207) under `-s bash`, which is
118
+ the opposite advice for a Bash 3.2 target. This rule exists for that target.
@@ -0,0 +1,81 @@
1
+ # `sh/compat/no-nameref`
2
+
3
+ Disallow namerefs (`declare -n`, `typeset -n`, `local -n`) in scripts whose target shell lacks
4
+ them: Bash older than 4.3, including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ------- | --------- |
8
+ | `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ Namerefs arrived in Bash 4.3 (Bash NEWS for 4.3). Bash 4.2 and older reject `-n` with `local: -n: invalid option`, so the reference is never made, and an assignment through it sets a variable of its own name instead of the caller's.
13
+
14
+ The rule reads the options of every `declare`, `typeset` and `local` command, up to the first operand, as the builtin does. An option word reports when it is a `-` cluster containing `n` (`-n`, `-rn`); the report is on that word. `export -n`, which removes the export attribute and is in every Bash, is not read, and neither is an `-n` after the first operand or after `--`.
15
+
16
+ The portable alternative is to pass the variable's name and use it directly. On Bash, assign through the name with `printf -v "$name" '%s' "$value"` and read it with `${!name}`; both are in Bash 3.2. A POSIX target has neither, so its message suggests `eval "$name=\$value"` to assign and `eval "value=\$$name"` to read, after checking that the name is a valid identifier, since `eval` runs whatever the name contains.
17
+
18
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
19
+ described in [version compatibility](../../compat.md):
20
+
21
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
22
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.3`.
23
+ - `dialect: "posix"`: reports at any `shellVersion`, with the `eval` advice.
24
+ - `dialect: "mksh"` or `"zsh"`: never reports.
25
+
26
+ ## Options
27
+
28
+ This rule has no options. Set the target through `languageOptions`:
29
+
30
+ ```js
31
+ // eslint.config.js
32
+ {
33
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
34
+ rules: {
35
+ 'sh/compat/no-nameref': 'error',
36
+ },
37
+ }
38
+ ```
39
+
40
+ ## Examples
41
+
42
+ ### Incorrect
43
+
44
+ With `shellVersion: "3.2"`:
45
+
46
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
47
+ ```sh
48
+ set_result() {
49
+ local -n out=$1
50
+ out=done
51
+ }
52
+ declare -n alias=target
53
+ ```
54
+
55
+ ### Correct
56
+
57
+ With `shellVersion: "3.2"`:
58
+
59
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
60
+ ```sh
61
+ set_result() {
62
+ printf -v "$1" '%s' done
63
+ }
64
+ read_by_name() {
65
+ printf '%s\n' "${!1}"
66
+ }
67
+ ```
68
+
69
+ With `shellVersion: "4.3"` or newer, or with no `shellVersion`, namerefs are fine.
70
+
71
+ ## When not to use it
72
+
73
+ Turn the rule off when your scripts never run on Bash older than 4.3, or leave `shellVersion`
74
+ unset, which makes the rule report nothing.
75
+
76
+ ## Relationship to shellcheck
77
+
78
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
79
+ layer. Run shellcheck beside it.
80
+
81
+ Under `-s sh`, shellcheck 0.11.0 reports the command, not the option: `local -n` draws SC3043 (`local` is undefined in POSIX sh), and `declare -n` and `typeset -n` draw SC3044. Under `-s bash` it reports nothing for a nameref. It does not know which Bash release a script must run on, so it reports nothing for a nameref in a Bash 4.2 or 3.2 target. This rule exists for that target.
@@ -0,0 +1,87 @@
1
+ # `sh/compat/no-negative-array-index`
2
+
3
+ Disallow negative indexed-array subscripts written as a literal (`${arr[-1]}`, `arr[-1]=x`) in scripts
4
+ whose target shell lacks them: Bash older than 4.3, including the Bash 3.2 that macOS ships as
5
+ `/bin/bash`, and POSIX sh.
6
+
7
+ | Configs | Fixable | Languages |
8
+ | ------- | ------- | --------- |
9
+ | `bash-3.2`, `posix-portable` | suggestions | `sh/sh` |
10
+
11
+ ## Rule details
12
+
13
+ Negative subscripts count back from the end of an indexed array. Bash NEWS for 4.3 introduces assigning, referencing and unsetting elements this way; Bash 4.2 already reads `${arr[-1]}`, but rejects the element length `${#arr[-1]}`, assigning and unsetting with `bad array subscript` (see the [evidence table](../../compat.md#evidence)). The rule uses 4.3, the first release where every use works, so a plain `${arr[-1]}` read at `shellVersion: "4.2"` is reported although 4.2 accepts it. Bash 3.2 fails with `bad array subscript`.
14
+
15
+ The rule reports a subscript that is a minus sign followed by a static decimal integer, on an expansion (`${arr[-1]}`, `${#arr[-1]}`) or on an assignment (`arr[-1]=x`). It also reports unsetting an element, `unset arr[-1]` (quoted or not, also after options such as `-v` and after `--`), which Bash NEWS for 4.3 covers; that form has no suggestion. It does not report `-0` (zero, which Bash 3.2 accepts), `${v: -1}` (a substring offset, not a subscript), a computed `-$n`, or a parenthesized `-(1)`. An associative array's `-1` key is a plain string; the rule cannot tell and reports it, but associative arrays need Bash 4.0 anyway.
16
+
17
+ ### Suggestion
18
+
19
+ For a bare `${NAME[-N]}` read, the rule suggests counting from the length:
20
+
21
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
22
+
23
+ ```sh
24
+ echo "${arr[-1]}"
25
+ # becomes
26
+ echo "${arr[$((${#arr[@]}-1))]}"
27
+ ```
28
+
29
+ Negative subscripts count back from the highest assigned index plus one, while `${#arr[@]}` counts set elements, so the two agree only for an array without holes. Other forms are reported without a suggestion.
30
+
31
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
32
+ described in [version compatibility](../../compat.md):
33
+
34
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
35
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.3`.
36
+ - `dialect: "posix"`: reports nothing in practice, because POSIX sh has no arrays, because the POSIX parser rejects it with a parse error before any rule runs.
37
+ - `dialect: "mksh"` or `"zsh"`: never reports.
38
+
39
+ ## Options
40
+
41
+ This rule has no options. Set the target through `languageOptions`:
42
+
43
+ ```js
44
+ // eslint.config.js
45
+ {
46
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
47
+ rules: {
48
+ 'sh/compat/no-negative-array-index': 'error',
49
+ },
50
+ }
51
+ ```
52
+
53
+ ## Examples
54
+
55
+ ### Incorrect
56
+
57
+ With `shellVersion: "3.2"`:
58
+
59
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
60
+ ```sh
61
+ last=${items[-1]}
62
+ items[-1]=done
63
+ ```
64
+
65
+ ### Correct
66
+
67
+ With `shellVersion: "3.2"`:
68
+
69
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
70
+ ```sh
71
+ last=${items[$((${#items[@]}-1))]}
72
+ items[$((${#items[@]}-1))]=done
73
+ ```
74
+
75
+ With `shellVersion: "4.3"` or newer, or with no `shellVersion`, the original forms are fine.
76
+
77
+ ## When not to use it
78
+
79
+ Turn the rule off when your scripts never run on Bash older than 4.3, or leave
80
+ `shellVersion` unset, which makes the rule report nothing.
81
+
82
+ ## Relationship to shellcheck
83
+
84
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
85
+ layer. Run shellcheck beside it.
86
+
87
+ shellcheck reports negative array subscripts in scripts it treats as POSIX sh (SC3054), but it cannot target a Bash version, so it cannot report `${arr[-1]}` for a Bash 3.2 target. This rule exists for that target.
@@ -0,0 +1,103 @@
1
+ # `sh/compat/no-nounset-empty-array`
2
+
3
+ Disallow whole-array expansions (`"${a[@]}"`, `${a[*]}`) in a script that turns on `set -u`, when
4
+ its target shell aborts on an empty array: Bash older than 4.4, including the Bash 3.2 that macOS
5
+ ships as `/bin/bash`.
6
+
7
+ | Configs | Fixable | Languages |
8
+ | ------- | ------- | --------- |
9
+ | `bash-3.2`, `posix-portable` | suggestions | `sh/sh` |
10
+
11
+ ## Rule details
12
+
13
+ The syntax is in every Bash, but through Bash 4.3, expanding an empty array while `nounset` is on aborts the script with `a[@]: unbound variable`. Bash 4.4 is the first release where it expands to nothing. This is the classic "works on Linux CI, breaks on a Mac" failure: a script that runs under `set -euo pipefail` and passes an optional, possibly empty, argument array dies on macOS `/bin/bash`.
14
+
15
+ The rule reports when both of these hold:
16
+
17
+ - the file turns `nounset` on somewhere: a `set` command that enables it (`set -u`, `set -eu`, `set -o nounset`), or a shebang that passes the flag (`#!/bin/bash -eu`, or `#!/usr/bin/env -S bash -eu`). Where in the file that happens is not considered, because a function defined before `set -u` can run after it;
18
+ - an expansion is `${name[@]}` or `${name[*]}` with no operator, in quotes or not.
19
+
20
+ A linter cannot know whether an array is empty when the script runs, so every such expansion reports. The check is file-wide: it ignores `set +u`, so an expansion after the file turned `nounset` back off still reports. It also reports arrays that are visibly non-empty (`a=(x); "${a[@]}"`) and arrays already guarded by a length test (`[[ ${#a[@]} -eq 0 ]] && return`). Its message therefore says "If this array is empty", not that it is. These forms work on an empty array under `set -u` in every Bash, and are not reported: `${#a[@]}`, `${!a[@]}`, `${a[@]-default}`, and the guarded forms below, including an expansion inside `${a[@]+…}` or `${a[@]:+…}` of the same array. The slice `${a[@]:1}` and the replacement `${a[@]/x/y}` were observed to work on Bash 3.2 to 4.1 and fail only on 4.2 and 4.3, which a minimum-version gate cannot express, so they are not reported either.
21
+
22
+ The rule offers a guarded spelling as a suggestion when the word is exactly the quoted expansion:
23
+
24
+ - `"${a[@]}"` becomes `${a[@]+"${a[@]}"}`, which gives no words for an empty array and the same words otherwise;
25
+ - `"${a[*]}"` becomes `"${a[*]-}"`, which gives the one empty word that `"${a[*]}"` gives for an empty array on Bash 4.4, and the same joined word otherwise.
26
+
27
+ An unquoted expansion, or one inside a longer word, reports with no suggestion. POSIX sh has no arrays; under `dialect: "posix"` the parser rejects array syntax, so this rule never sees one there.
28
+
29
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
30
+ described in [version compatibility](../../compat.md):
31
+
32
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
33
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.4`.
34
+ - `dialect: "mksh"` or `"zsh"`: never reports.
35
+
36
+ ### Suggestion
37
+
38
+ A suggestion rewrites the quoted `[@]` form:
39
+
40
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
41
+ ```sh
42
+ set -u
43
+ run_tool "${extra[@]}"
44
+ # becomes
45
+ set -u
46
+ run_tool ${extra[@]+"${extra[@]}"}
47
+ ```
48
+
49
+ ## Options
50
+
51
+ This rule has no options. Set the target through `languageOptions`:
52
+
53
+ ```js
54
+ // eslint.config.js
55
+ {
56
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
57
+ rules: {
58
+ 'sh/compat/no-nounset-empty-array': 'error',
59
+ },
60
+ }
61
+ ```
62
+
63
+ ## Examples
64
+
65
+ ### Incorrect
66
+
67
+ With `shellVersion: "3.2"`:
68
+
69
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
70
+ ```sh
71
+ set -euo pipefail
72
+ extra=()
73
+ run_tool "${extra[@]}"
74
+ echo "args: ${extra[*]}"
75
+ ```
76
+
77
+ ### Correct
78
+
79
+ With `shellVersion: "3.2"`:
80
+
81
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
82
+ ```sh
83
+ set -euo pipefail
84
+ extra=()
85
+ run_tool ${extra[@]+"${extra[@]}"}
86
+ echo "args: ${extra[*]-}"
87
+ echo "count: ${#extra[@]}"
88
+ ```
89
+
90
+ With `shellVersion: "4.4"` or newer, with no `shellVersion`, or in a file that never turns on
91
+ `set -u`, whole-array expansions are fine.
92
+
93
+ ## When not to use it
94
+
95
+ Turn the rule off when your scripts never run on Bash older than 4.4, or leave `shellVersion`
96
+ unset, which makes the rule report nothing.
97
+
98
+ ## Relationship to shellcheck
99
+
100
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
101
+ layer. Run shellcheck beside it.
102
+
103
+ shellcheck 0.11.0 reports nothing for an empty array expanded under `set -u` (`set -u; a=(); echo "${a[@]}"`) under `-s bash`. Under `-s sh` it reports only the array syntax (SC3030, SC3054), not the abort. An empty array under `set -u` is valid Bash from 4.4 on, and shellcheck does not know which Bash release a script must run on. This rule exists for an older target.