@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
@@ -0,0 +1,85 @@
1
+ # `sh/compat/no-pipe-all`
2
+
3
+ Disallow the `|&` pipe, shorthand for `2>&1 |`, in scripts whose target shell lacks it: Bash older
4
+ 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
+ `|&` arrived in Bash 4.0 (Bash NEWS for 4.0). Bash 3.2 rejects it as a syntax error (``syntax error near unexpected token `&'``), so the whole script fails to run.
13
+
14
+ The rule reports a pipeline whose operator is `|&`; the report is on the operator. A plain `|`, `||`, `&&`, a background `&` followed by a command, a `>&` redirection, and any of these in quotes or comments are other syntax and are not reported.
15
+
16
+ The portable spelling is `cmd 2>&1 | other`. The Bash manual defines `|&` as an implicit `2>&1` applied after any redirections the left-hand command writes, so writing `2>&1` just before the `|` is exactly equivalent, also after a redirection such as `2>/dev/null`. The rule offers that rewrite as a suggestion. It edits only the operator, so a here-document on the left-hand command keeps its body where it is.
17
+
18
+ Under `dialect: "posix"` the parser itself rejects `|&`, so a POSIX file that uses it fails to parse and this rule does not run on it.
19
+
20
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
21
+ described in [version compatibility](../../compat.md):
22
+
23
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
24
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
25
+ - `dialect: "mksh"` or `"zsh"`: never reports (in zsh, `|&` is a different operator).
26
+
27
+ ### Suggestion
28
+
29
+ A suggestion rewrites the operator:
30
+
31
+ <!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
32
+ ```sh
33
+ make |& tee build.log
34
+ # becomes
35
+ make 2>&1 | tee build.log
36
+ ```
37
+
38
+ ## Options
39
+
40
+ This rule has no options. Set the target through `languageOptions`:
41
+
42
+ ```js
43
+ // eslint.config.js
44
+ {
45
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
46
+ rules: {
47
+ 'sh/compat/no-pipe-all': 'error',
48
+ },
49
+ }
50
+ ```
51
+
52
+ ## Examples
53
+
54
+ ### Incorrect
55
+
56
+ With `shellVersion: "3.2"`:
57
+
58
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
59
+ ```sh
60
+ make |& tee build.log
61
+ ```
62
+
63
+ ### Correct
64
+
65
+ With `shellVersion: "3.2"`:
66
+
67
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
68
+ ```sh
69
+ make 2>&1 | tee build.log
70
+ make || echo "failed |& retried"
71
+ ```
72
+
73
+ With `shellVersion: "4.0"` or newer, or with no `shellVersion`, `|&` is fine.
74
+
75
+ ## When not to use it
76
+
77
+ Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
78
+ unset, which makes the rule report nothing.
79
+
80
+ ## Relationship to shellcheck
81
+
82
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
83
+ layer. Run shellcheck beside it.
84
+
85
+ shellcheck reports `|&` 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 `|&` for a Bash 3.2 target. This rule exists for that target.
@@ -0,0 +1,75 @@
1
+ # `sh/compat/no-printf-time-format`
2
+
3
+ Disallow `printf`'s `%(fmt)T` date format in scripts whose target shell lacks it: Bash older than
4
+ 4.2, 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 `%(fmt)T` directive arrived in Bash 4.2 (Bash NEWS for 4.2). Bash 4.1 rejects it with ``printf: `(': invalid format character``, so the timestamp is never printed.
13
+
14
+ The rule reads `printf` commands (also through `builtin` or `command`). The format is the first argument after an optional `-v name` and `--`; when it is a static word, the rule scans it as `printf` does: `%%` is a literal percent sign, and a directive may carry flags, a width and a precision before the `(`. A format with a `%(…)T` directive reports, on the format word. A format that is not static (`printf "$fmt"`) cannot be read, and the same text as another argument or in another command (`echo '%(%Y)T'`) is not a format, so neither reports.
15
+
16
+ The portable alternative for the current time is `date '+fmt'`, which takes the same `strftime` format. `printf '%(fmt)T' N` formats an arbitrary epoch time `N`, which has no portable `date` spelling (GNU `date -d @N` and BSD `date -r N` differ), so convert those by hand.
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`; dash's `printf` has no `%(fmt)T`.
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-printf-time-format': '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
+ printf '%(%Y-%m-%d)T\n' -1
49
+ printf -v stamp '[%(%H:%M:%S)T]' -1
50
+ ```
51
+
52
+ ### Correct
53
+
54
+ With `shellVersion: "3.2"`:
55
+
56
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
57
+ ```sh
58
+ date '+%Y-%m-%d'
59
+ stamp="[$(date '+%H:%M:%S')]"
60
+ printf '100%%(of it)T\n'
61
+ ```
62
+
63
+ With `shellVersion: "4.2"` or newer, or with no `shellVersion`, `%(fmt)T` is fine.
64
+
65
+ ## When not to use it
66
+
67
+ Turn the rule off when your scripts never run on Bash older than 4.2, or leave `shellVersion`
68
+ unset, which makes the rule report nothing.
69
+
70
+ ## Relationship to shellcheck
71
+
72
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
73
+ layer. Run shellcheck beside it.
74
+
75
+ shellcheck 0.11.0 reports nothing for `printf "%(%s)T\n" -1` under either `-s sh` or `-s bash`. So shellcheck gives no finding for `%(fmt)T` in a POSIX sh script or in a Bash 3.2 target. This rule exists for both.
@@ -0,0 +1,98 @@
1
+ # `sh/compat/no-source`
2
+
3
+ Disallow the `source` builtin in scripts that target POSIX sh, where the portable name is `.`.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ------- | ------- | --------- |
7
+ | `posix-portable` | suggestions | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ POSIX lists `source` 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: `source ./lib.sh` fails with `source: not found` and status 127, and the file is never read. Under `set -e` that stops the script.
12
+
13
+ `bash --posix` and BusyBox ash both accept `source`, so a script can pass on them and still fail on dash.
14
+
15
+ Under `dialect: "posix"` the parser reads `source` as an ordinary command, and the rule reports a command whose effective name is `source`, after following wrappers such as `command`. It reports the `source` word. `command -v source` 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 `source` anywhere in the file exempts a call that names it directly, since dash can then run that function. `bash --posix` refuses to define a function with that name, which this rule does not judge. 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 `source` call still fails on dash with `source: 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 source`.
23
+
24
+ A call through a wrapper gets no exemption and is reported. `command source ./lib.sh` skips the function and fails with status 127, as does `env source ./lib.sh`, and dash has no `builtin` at all.
25
+
26
+ The portable alternative is the `.` special built-in. The rule offers it as a suggestion only where the two read the same file with the same effect, which needs all of these:
27
+
28
+ - The command is written directly, with no wrapper.
29
+ - It has no leading assignment. `FOO=1 . ./lib.sh` leaves `FOO` set afterwards on a POSIX shell, while Bash outside POSIX mode scopes `FOO=1 source ./lib.sh` to that command.
30
+ - It has exactly one operand. dash ignores further operands of `.`, where `source` would set the positional parameters.
31
+ - The operand has a literal `/`, outside any expansion. With no slash, `.` searches `PATH` and does not look in the current directory.
32
+
33
+ <!-- transform: {"kind": "suggestion", "dialect": "posix"} -->
34
+ ```sh
35
+ source ./lib.sh
36
+ # becomes
37
+ . ./lib.sh
38
+ ```
39
+
40
+ Other forms get the report without a suggestion.
41
+
42
+ Those conditions do not cover a file that cannot be read, where the rewrite behaves differently. POSIX says a non-interactive shell aborts when [`.`](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#dot) finds no readable file. `. ./missing || echo fallback` ends the script with status 2 on dash and BusyBox ash and status 1 under `bash --posix`, and the fallback never runs. Bash outside POSIX mode returns status 1 from `source` and runs the fallback, and on dash `source ./missing || echo fallback` also runs it, because `source` is not found. To keep a fallback, test first:
43
+
44
+ ```sh
45
+ if [ -r ./lib.sh ]; then . ./lib.sh; else echo fallback; fi
46
+ ```
47
+
48
+ This is a POSIX-only gate. Every Bash has `source`, so whether the rule reports depends only on `dialect`, as described in [version compatibility](../../compat.md):
49
+
50
+ - `dialect: "posix"`: reports at any `shellVersion`.
51
+ - `dialect: "bash"` (the default), `"bats"`, `"mksh"` or `"zsh"`: never reports.
52
+
53
+ ## Options
54
+
55
+ This rule has no options. Set the target through `languageOptions`:
56
+
57
+ ```js
58
+ // eslint.config.js
59
+ {
60
+ languageOptions: { dialect: 'posix' },
61
+ rules: {
62
+ 'sh/compat/no-source': 'error',
63
+ },
64
+ }
65
+ ```
66
+
67
+ ## Examples
68
+
69
+ ### Incorrect
70
+
71
+ With `dialect: "posix"`:
72
+
73
+ <!-- example: {"dialect": "posix", "findings": 2} -->
74
+ ```sh
75
+ source ./lib.sh
76
+ source "$HOME/.profile"
77
+ ```
78
+
79
+ ### Correct
80
+
81
+ With `dialect: "posix"`:
82
+
83
+ <!-- example: {"dialect": "posix"} -->
84
+ ```sh
85
+ . ./lib.sh
86
+ . "$HOME/.profile"
87
+ ```
88
+
89
+ ## When not to use it
90
+
91
+ Turn the rule off when your scripts only ever run where `source` exists, such as under Bash or BusyBox ash, and set `dialect` to match if you can.
92
+
93
+ ## Relationship to shellcheck
94
+
95
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
96
+ layer. Run shellcheck beside it.
97
+
98
+ shellcheck reports this construct as SC3046 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. shellcheck keeps whether it can follow the sourced path (SC1090, SC1091).
@@ -0,0 +1,82 @@
1
+ # `sh/compat/no-test-v`
2
+
3
+ Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`, `[ -v x ]`) in scripts whose target shell
4
+ lacks it: Bash older than 4.2, including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX
5
+ sh.
6
+
7
+ | Configs | Fixable | Languages |
8
+ | ------- | ------- | --------- |
9
+ | `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
10
+
11
+ ## Rule details
12
+
13
+ The `-v` test arrived in Bash 4.2 (Bash NEWS for 4.2). In Bash 4.1 and older, `[[ -v x ]]` is a syntax error (`conditional binary operator expected`), so the whole script fails to parse, not only the test. `test -v x` fails with `unary operator expected`.
14
+
15
+ The rule reports the `-v` test in two places:
16
+
17
+ - inside `[[ … ]]`, where the parser gives the test its own node;
18
+ - in a `test` or `[` command (also through `builtin` or `command`), where `-v` is an ordinary argument. The rule reads the arguments the way Bash's `test` does: with two arguments a leading `-v` is the operator; with three, the middle one is a binary operator if it can be one, and `! -v name` negates the test; with four, a leading `!` or surrounding parentheses peel off; anything longer goes through `test`'s own precedence rules, where `-v` is the operator only where a primary is expected and another argument follows it.
19
+
20
+ So `-v` as an operand does not report: `[ "$a" = -v ]`, `[ -n -v ]`, a lone `[ -v ]`, and `[[ $a == -v ]]` are clean, and so is `-v` of another command (`grep -v`) and any `-v` in quotes. A double-quoted expansion (`"$name"`) always makes exactly one argument, so it counts as an operand; an unquoted one may split into several arguments or none, so reading stops there. The report is on the `[[` test or on the `-v` word.
21
+
22
+ The portable alternative is `[ -n "${name+set}" ]`. `${name+set}` expands to `set` when the variable is set, even to the empty string, and to nothing when it is unset, and it is safe under `set -u`. Under `dialect: "posix"` the parser does not read `[[` as a test, so only `test` and `[` report there; `[[` itself in a POSIX script is outside this rule.
23
+
24
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
25
+ described in [version compatibility](../../compat.md):
26
+
27
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
28
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.2`.
29
+ - `dialect: "posix"`: reports `test -v` and `[ -v ]` at any `shellVersion`; dash has no `-v` test.
30
+ - `dialect: "mksh"` or `"zsh"`: never reports.
31
+
32
+ ## Options
33
+
34
+ This rule has no options. Set the target through `languageOptions`:
35
+
36
+ ```js
37
+ // eslint.config.js
38
+ {
39
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
40
+ rules: {
41
+ 'sh/compat/no-test-v': 'error',
42
+ },
43
+ }
44
+ ```
45
+
46
+ ## Examples
47
+
48
+ ### Incorrect
49
+
50
+ With `shellVersion: "3.2"`:
51
+
52
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 3} -->
53
+ ```sh
54
+ if [[ -v CI ]]; then echo ci; fi
55
+ if test -v HOME; then echo home; fi
56
+ [ ! -v TOKEN ] && exit 1
57
+ ```
58
+
59
+ ### Correct
60
+
61
+ With `shellVersion: "3.2"`:
62
+
63
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
64
+ ```sh
65
+ if [ -n "${CI+set}" ]; then echo ci; fi
66
+ [ "$flag" = -v ] && verbose=1
67
+ grep -v '^#' config
68
+ ```
69
+
70
+ With `shellVersion: "4.2"` or newer, or with no `shellVersion`, the `-v` test is fine.
71
+
72
+ ## When not to use it
73
+
74
+ Turn the rule off when your scripts never run on Bash older than 4.2, or leave `shellVersion`
75
+ unset, which makes the rule report nothing.
76
+
77
+ ## Relationship to shellcheck
78
+
79
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
80
+ layer. Run shellcheck beside it.
81
+
82
+ shellcheck reports `[[` and `test -v` 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 `[[ -v x ]]` for a Bash 3.2 target. This rule exists for that target.
@@ -0,0 +1,91 @@
1
+ # `sh/compat/no-unicode-escape`
2
+
3
+ Disallow the `\uXXXX` and `\UXXXXXXXX` escapes in `$'…'` strings in scripts whose target shell does
4
+ not decode them to UTF-8: Bash older than 4.2 for most code points, and older than 4.3 for U+0080
5
+ to U+00FF. This includes the Bash 3.2 that macOS ships as `/bin/bash`.
6
+
7
+ | Configs | Fixable | Languages |
8
+ | ------- | ------- | --------- |
9
+ | `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
10
+
11
+ ## Rule details
12
+
13
+ Bash 4.2 added `\u` and `\U` escapes to `$'…'` strings (Bash NEWS for 4.2). The release tarballs behave in three ways, observed on real shells (see [Evidence](../../compat.md#evidence)):
14
+
15
+ - **Bash 4.1 and older** leave the escape as literal text (`$'\u00e9'` is the six characters `\u00e9`), with no error.
16
+ - **Bash 4.2** decodes it, but writes a code point from U+0080 to U+00FF as a single Latin-1 byte even in a UTF-8 locale (`e9` for U+00E9, where UTF-8 is `c3 a9`). Every other code point comes out as UTF-8, both `\u2713` and `\U0001F389`.
17
+ - **Bash 4.3 and newer** write every code point as UTF-8.
18
+
19
+ So the minimum version depends on the code point: 4.3 for U+0080 to U+00FF, 4.2 for the rest. A distribution's patched 4.2 may differ from the release tarball.
20
+
21
+ The rule scans each `$'…'` string as Bash decodes it: `\\` is a backslash, `\cX` is one control character (so in `\cu0041` the `u` belongs to `\c`), and `\u` takes one to four hex digits, `\U` one to eight. A `\u` with no hex digit is literal text. It reports once per string, naming the escape that needs the newest release; the report is on the whole string. Plain `'…'` and `"…"` strings do not decode escapes and are not read, and neither are `printf` formats.
22
+
23
+ The portable alternative is to write the character's UTF-8 bytes as octal escapes, which every Bash decodes in `$'…'`: `$'\303\251'` for U+00E9. The message gives the bytes for the escape it names.
24
+
25
+ Under `dialect: "posix"` the parser reads `$'…'` as a literal `$` followed by an ordinary single-quoted string, as dash does, so there is no `$'…'` string for this rule to report.
26
+
27
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
28
+ described in [version compatibility](../../compat.md):
29
+
30
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
31
+ - `dialect: "bash"` (the default) or `"bats"`: reports an escape when `shellVersion` is older than its release (4.2, or 4.3 for U+0080 to U+00FF).
32
+ - `dialect: "mksh"` or `"zsh"`: never reports.
33
+
34
+ ## Options
35
+
36
+ This rule has no options. Set the target through `languageOptions`:
37
+
38
+ ```js
39
+ // eslint.config.js
40
+ {
41
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
42
+ rules: {
43
+ 'sh/compat/no-unicode-escape': 'error',
44
+ },
45
+ }
46
+ ```
47
+
48
+ ## Examples
49
+
50
+ ### Incorrect
51
+
52
+ With `shellVersion: "3.2"`:
53
+
54
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
55
+ ```sh
56
+ ok=$'\u2713'
57
+ printf '%s\n' $'caf\u00e9'
58
+ ```
59
+
60
+ With `shellVersion: "4.2"`, only the Latin-1 range reports:
61
+
62
+ <!-- example: {"dialect": "bash", "shellVersion": "4.2", "findings": 1} -->
63
+ ```sh
64
+ ok=$'\u2713'
65
+ printf '%s\n' $'caf\u00e9'
66
+ ```
67
+
68
+ ### Correct
69
+
70
+ With `shellVersion: "3.2"`:
71
+
72
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
73
+ ```sh
74
+ ok=$'\342\234\223'
75
+ printf '%s\n' $'caf\303\251'
76
+ echo '\u00e9 is written literally here'
77
+ ```
78
+
79
+ With `shellVersion: "4.3"` or newer, or with no `shellVersion`, every escape is fine.
80
+
81
+ ## When not to use it
82
+
83
+ Turn the rule off when your scripts never run on Bash older than 4.3, or leave `shellVersion`
84
+ unset, which makes the rule report nothing.
85
+
86
+ ## Relationship to shellcheck
87
+
88
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
89
+ layer. Run shellcheck beside it.
90
+
91
+ shellcheck reports `$'…'` 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 a `\u` escape for a Bash 3.2 or 4.2 target. This rule exists for that target.
@@ -0,0 +1,85 @@
1
+ # `sh/compat/no-wait-n`
2
+
3
+ Disallow `wait -n` in scripts whose target shell lacks it: Bash older than 4.3, including the
4
+ 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
+ `wait -n`, which waits for the next job to finish, arrived in Bash 4.3 (Bash NEWS for 4.3). Bash 4.2 and older reject the option with `wait: -n: invalid option` and return 2 at once. A job pool that throttles itself with `wait -n` therefore stops waiting, and every job runs at the same time, with no error that stops the script.
13
+
14
+ The rule reads `wait` commands (also through `builtin` or `command`). It reports an option word, up to the first operand or `--`, that is a cluster containing `n` (`-n`, `-fn`); the report is on that word. The value after `-p` (Bash 5.1's `wait -p var`) is that option's operand, not an option. `-n` of another command (`echo -n`) and an `-n` after an operand are not read.
15
+
16
+ A script that defines its own `wait` function still reports: the rule reads the name `wait` and does not follow shell function definitions.
17
+
18
+ The portable alternative is to wait for one job at a time by its PID: keep each job's `$!`, and run `wait "$pid"`, which returns that job's exit status. This waits for the oldest job rather than the first to finish, so a pool that uses it can sit idle behind one slow job, but it never runs more jobs than its limit.
19
+
20
+ This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
21
+ described in [version compatibility](../../compat.md):
22
+
23
+ - `shellVersion` unset: never reports. An unset version means the newest Bash.
24
+ - `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.3`.
25
+ - `dialect: "posix"`: reports at any `shellVersion`; POSIX `wait` has no options.
26
+ - `dialect: "mksh"` or `"zsh"`: never reports.
27
+
28
+ ## Options
29
+
30
+ This rule has no options. Set the target through `languageOptions`:
31
+
32
+ ```js
33
+ // eslint.config.js
34
+ {
35
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
36
+ rules: {
37
+ 'sh/compat/no-wait-n': 'error',
38
+ },
39
+ }
40
+ ```
41
+
42
+ ## Examples
43
+
44
+ ### Incorrect
45
+
46
+ With `shellVersion: "3.2"`:
47
+
48
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
49
+ ```sh
50
+ for job in a b c d; do
51
+ run "$job" &
52
+ if [ "$(jobs -r | wc -l)" -ge 2 ]; then wait -n; fi
53
+ done
54
+ wait
55
+ ```
56
+
57
+ ### Correct
58
+
59
+ With `shellVersion: "3.2"`:
60
+
61
+ <!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
62
+ ```sh
63
+ pids=
64
+ for job in a b c d; do
65
+ run "$job" &
66
+ pids="$pids $!"
67
+ done
68
+ for pid in $pids; do
69
+ wait "$pid" || echo "job $pid failed"
70
+ done
71
+ ```
72
+
73
+ With `shellVersion: "4.3"` or newer, or with no `shellVersion`, `wait -n` is fine.
74
+
75
+ ## When not to use it
76
+
77
+ Turn the rule off when your scripts never run on Bash older than 4.3, or leave `shellVersion`
78
+ unset, which makes the rule report nothing.
79
+
80
+ ## Relationship to shellcheck
81
+
82
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
83
+ layer. Run shellcheck beside it.
84
+
85
+ shellcheck reports `wait` options 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 `wait -n` for a Bash 4.2 target. This rule exists for that target.
@@ -0,0 +1,109 @@
1
+ # `sh/max-script-complexity`
2
+
3
+ Cap how many functions, how many levels of nesting and how many statements a single script may
4
+ contain, so that a machine-written script stays reviewable in one sitting.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ---------------- | ----------- | --------- |
8
+ | `agent-authored` | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ A script that grows past what a reviewer can hold in their head cannot be vouched for, however
13
+ clean each line is. This rule enforces three independent limits. Each limit is reported **at most
14
+ once per file**, under its own message, and is skipped when set to `null`. A limit of `0` is a real limit: it allows none of that construct, so the first one reports.
15
+
16
+ - **Function count** (`maxFunctions`, default `20`): the number of function declarations in the
17
+ file, at any depth. Reported at the file level (line 1), because no one declaration is to blame.
18
+ - **Nesting depth** (`maxNesting`, default `4`): every `if`, `for` (including `select`), `while`,
19
+ `until`, `case`, subshell `( … )` and brace group `{ …; }` adds one level. Reported at the first
20
+ construct whose depth is past the limit. An `elif` or `else` arm continues its `if` and adds no
21
+ depth. A function's own body braces are the function's syntax, not a brace group, and functions,
22
+ command substitutions and process substitutions add no depth.
23
+ - **Statement count** (`maxStatements`, default `300`): every statement anywhere in the file,
24
+ because the limit bounds the script's overall size. That includes top-level statements, those in
25
+ function bodies and in nested compound commands, and the statements in condition and header
26
+ lists: in `if a; then :; fi` the `if`, the `a` and the `:` are three statements, and likewise the
27
+ `a` in `while a; do …`. Statements inside command substitutions `$(…)` count too: in
28
+ `x=$(echo a; echo b)` the assignment and the two `echo` statements are three statements. The operands of `&&`, `||` and `|` are not counted separately, so one
29
+ pipeline or and-or list is one statement, and a function declaration is one statement plus those
30
+ in its body. Reported at the first statement past the limit.
31
+
32
+ The rule inspects syntax only and has no autofix or suggestion: splitting a script is a human
33
+ decision.
34
+
35
+ ## Options
36
+
37
+ An optional object; each field is a non-negative integer limit, `null` (switches that one limit off) or omitted (takes the default). `0` is a real limit, as in ESLint's `max-depth`, `max-params` and `max-statements`: `maxFunctions: 0` forbids function declarations.
38
+
39
+ - `maxFunctions` (default `20`): most function declarations allowed.
40
+ - `maxNesting` (default `4`): deepest control-structure nesting allowed.
41
+ - `maxStatements` (default `300`): most statements allowed.
42
+
43
+ ```js
44
+ // eslint.config.js
45
+ {
46
+ rules: {
47
+ 'sh/max-script-complexity': ['error', { maxFunctions: 10, maxNesting: 3, maxStatements: null }],
48
+ },
49
+ }
50
+ ```
51
+
52
+ ## Examples
53
+
54
+ ### Incorrect
55
+
56
+ With `{ maxNesting: 2 }`:
57
+
58
+ <!-- example: {"options": {"maxNesting": 2}, "findings": 1} -->
59
+ ```sh
60
+ if [ -f a ]; then
61
+ for f in *; do
62
+ while read -r line; do # reported: nested 3 levels deep
63
+ echo "$line"
64
+ done < "$f"
65
+ done
66
+ fi
67
+ ```
68
+
69
+ With `{ maxFunctions: 1 }`:
70
+
71
+ <!-- example: {"options": {"maxFunctions": 1}, "findings": 1} -->
72
+ ```sh
73
+ a() { :; }
74
+ b() { :; } # the file is reported on line 1: 2 functions
75
+ ```
76
+
77
+ ### Correct
78
+
79
+ With `{ maxNesting: 2 }`:
80
+
81
+ <!-- example: {"options": {"maxNesting": 2}} -->
82
+ ```sh
83
+ if [ -f a ]; then
84
+ echo one
85
+ elif [ -f b ]; then # an elif adds no depth
86
+ for f in *; do
87
+ echo "$f"
88
+ done
89
+ fi
90
+
91
+ run() { # a function's own braces add no depth
92
+ if true; then
93
+ echo ok
94
+ fi
95
+ }
96
+ ```
97
+
98
+ ## When not to use it
99
+
100
+ When your project does not hold a size policy for scripts, or when a generated script is
101
+ legitimately large. Disable one check by setting its limit to `null` rather than the whole rule.
102
+
103
+ ## Relationship to shellcheck
104
+
105
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
106
+ layer. Run shellcheck beside it.
107
+
108
+ shellcheck reports defects in individual commands and has no check that limits a script's size,
109
+ function count or nesting depth, so this rule overlaps none of its `SCxxxx` checks.