@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,171 @@
1
+ # `sh/require-network-wrapper`
2
+
3
+ Require network commands (`curl`, `wget`, `ssh`, `git fetch`, …) to be run through your project's
4
+ configured wrapper, such as a retry or audit helper.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ----------- | --------- |
8
+ | `agent-authored` (inert until a wrapper is configured) | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ Network calls fail in ways local commands don't: timeouts, rate limits, flaky DNS. Many projects
13
+ route every network call through one wrapper that retries, logs, or proxies it. This rule checks
14
+ that every network command a script runs goes through that wrapper.
15
+
16
+ **The wrapper has no default.** Which wrapper your project uses is your policy, and a generic
17
+ linter must not invent it. With no wrapper configured, the rule is inert: it reports nothing.
18
+
19
+ The rule checks every command in the file: top level, in functions, in pipelines, in
20
+ conditionals, and inside command substitutions (`$(…)`). A network command is reported (message
21
+ `unwrappedCommand`, on the whole command) unless the configured wrapper runs it. It is wrapped
22
+ when the wrapper encloses it, directly (`retry curl x`) or through transparent wrappers such as
23
+ `sudo`, `env`, or `nohup` (`retry sudo curl x`, `sudo retry curl x`). `sudo curl x` is not
24
+ wrapped, and neither is the `curl` in `retry echo "$(curl x)"`: the command substitution is its
25
+ own command. See [Wrapper rules](../wrapper-rules.md) for the full matching rules.
26
+
27
+ A command run by path is matched by its basename, so `/usr/bin/curl x` and `./bin/curl x` report
28
+ as `curl`. A wrapper run by path is the wrapper it names: `/usr/local/bin/retry curl x` is wrapped.
29
+
30
+ `git` counts only for its network subcommands, `fetch`, `clone`, `pull`, and `push`. git's global
31
+ options before the subcommand are skipped, so `git -C dir fetch` is reported as `git fetch`, and
32
+ `git status` is not reported.
33
+
34
+ Invocations that only act locally are not reported: `curl --version`, `curl --help`,
35
+ `curl --help all`, `wget --version` and `wget --help`, and their short forms `-V` and `-h`
36
+ (`curl -h all`). The argument list must be exactly that, so `curl --version https://example.com`
37
+ still reports. `git clone` is not reported when its source is a literal local path (`./x`, `../x`,
38
+ `/x`) or a `file://` URL, after any of git clone's own options (`git clone --depth 1 ./x`). Only
39
+ the source decides: a dynamic destination (`git clone ./x "$dest"`) does not matter, but a dynamic
40
+ source (`git clone "$src"`), a remote URL, and a clone option the rule does not know leave the
41
+ clone reported. So do `--recurse-submodules`, `--recursive` and `--remote-submodules` (a local
42
+ repository's `.gitmodules` can point at a remote URL, which those options fetch) and
43
+ `--bundle-uri` (it fetches from its URL, `--bundle-uri=URL` or `--bundle-uri URL`, whatever the
44
+ source is). `--no-recurse-submodules` and an attached or separate option value such as `-j4` or
45
+ `-j 4` do not change that the source is local. Options are read wherever they appear before a literal `--`, so
46
+ `git clone ./x --recurse-submodules` and `git clone ./x d --bundle-uri URL` report too, while
47
+ `git clone ./x -- --recurse-submodules` (everything after `--` is an operand) does not. A rule pack binds
48
+ the same kind of exemptions in [`createWrapperRule`](../wrapper-rules.md) with
49
+ `localInvocations`; the factory itself names no command.
50
+
51
+ A call to a function the file defines is not reported, even when the function is defined after the
52
+ call, because it runs the function and not the network client. A real invocation inside the
53
+ function (`command curl x`) is reported.
54
+
55
+ The rule never guesses. A dynamic command word (`"$client" x`), an option your wrapper does not
56
+ declare (`retry -n 3 curl x` when `retry` is configured as a plain name), and a dynamic git
57
+ subcommand (`git "$op"`) leave the command unknown, and nothing is reported. Command strings run
58
+ by another shell (`bash -c 'curl x'`) are not checked.
59
+
60
+ It does not depend on `languageOptions`.
61
+
62
+ ## Options
63
+
64
+ - `wrapper` (`string` or `WrapperSpec` object, no default): the wrapper network commands must be
65
+ run through. When it is unset, the rule uses `settings.sh.networkWrapper` (below); with
66
+ neither, the rule reports nothing. A string names a wrapper that takes no options
67
+ (or one of the default transparent wrappers, such as `timeout`, whose options are known). An
68
+ object declares the wrapper's options so the command after them is found:
69
+ `{ names: ['retry'], argFlags: ['-n'] }` makes `retry -n 3 curl x` wrapped. Its fields are
70
+ `names` (required; any of them counts as the wrapper), `noArgFlags`, `argFlags`,
71
+ `stopsChainFlags`, `unresolvableFlags`, `positionalOperandsBeforeCommand`, and
72
+ `assignmentOperands`, as in `sh-ast/analyze`'s `WrapperSpec`.
73
+ - `commands` (`string[]`, default `["curl", "wget", "nc", "ssh", "scp", "rsync", "git"]`): the
74
+ network commands. Setting it replaces the default list. `git` keeps its subcommand restriction
75
+ wherever it is listed.
76
+ - `alsoTreatAsNetwork` (`string[]`, default `[]`): more commands to check, added to `commands` (or
77
+ to its default).
78
+
79
+ ```js
80
+ // eslint.config.js
81
+ {
82
+ rules: {
83
+ 'sh/require-network-wrapper': ['error', { wrapper: 'retry', alsoTreatAsNetwork: ['aws'] }],
84
+ },
85
+ }
86
+ ```
87
+
88
+ ### Declaring the wrapper once: `settings.sh.networkWrapper`
89
+
90
+ `sh/no-undeclared-commands` and `sh/no-unused-declarations` also need to know about your wrapper:
91
+ in `with-retry curl x` the script runs `curl`, and only a rule that looks through `with-retry` sees
92
+ that. ESLint doesn't let one rule read another rule's options, so a wrapper set only as this rule's
93
+ `wrapper` option is invisible to them, and an undeclared `curl` behind it goes unreported.
94
+
95
+ Set the wrapper in shared settings instead, and all three rules read it:
96
+
97
+ ```js
98
+ // eslint.config.js
99
+ {
100
+ settings: { sh: { declarationConvention: commentPragma, networkWrapper: 'with-retry' } },
101
+ rules: { 'sh/require-network-wrapper': 'error' }, // or extend sh.configs['agent-authored']
102
+ }
103
+ ```
104
+
105
+ `networkWrapper` takes the same values as the `wrapper` option, a name or a `WrapperSpec` object,
106
+ and the declaration rules follow a spec's options too (`with-retry -n 3 curl`). This rule's
107
+ `wrapper` option, when set, takes precedence for this rule only. A malformed value stops linting
108
+ with an error starting `Invalid settings.sh.networkWrapper:`.
109
+
110
+ The setting names your **network** wrapper, a policy. Other wrappers that run a command, such as
111
+ a `with-lock` helper, aren't network policy: list those in the declaration rules'
112
+ `transparentWrappers` option.
113
+
114
+ ## Examples
115
+
116
+ ### Incorrect
117
+
118
+ With `{ wrapper: 'retry' }`:
119
+
120
+ <!-- example: {"options": {"wrapper": "retry"}, "findings": 6} -->
121
+
122
+ ```sh
123
+ curl -fsSL https://example.com/install.sh # curl is not wrapped
124
+ retry echo start; curl x # retry wraps echo, not curl
125
+ sudo curl x # sudo is not the configured wrapper
126
+ git -C repo fetch # reported as `git fetch`
127
+ /usr/bin/curl x # a path to curl is still curl
128
+ body="$(wget -qO- https://example.com)" # commands inside $(…) are checked
129
+ ```
130
+
131
+ An in-file function does not hide the real call inside it:
132
+
133
+ <!-- example: {"options": {"wrapper": "retry"}, "findings": 1} -->
134
+
135
+ ```sh
136
+ curl() { command curl "$@"; } # `command curl` runs the external curl
137
+ ```
138
+
139
+ ### Correct
140
+
141
+ With `{ wrapper: 'retry' }`:
142
+
143
+ <!-- example: {"options": {"wrapper": "retry"}} -->
144
+
145
+ ```sh
146
+ retry curl -fsSL https://example.com/install.sh
147
+ retry sudo rsync -a src/ host:dst/ # retry encloses sudo, which runs rsync
148
+ /usr/local/bin/retry curl x # a path to the wrapper is the wrapper
149
+ git status # not a network subcommand
150
+ body="$(retry wget -qO- https://example.com)"
151
+ echo curl # an argument, not a command
152
+ curl --version # prints a version, touches no network
153
+ git clone ./vendor/lib copy # a local source
154
+ wget() { :; }; wget x # a function the file defines, not the client
155
+ ```
156
+
157
+ ## When not to use it
158
+
159
+ Leave the rule off if your project has no single wrapper for network calls, or if network
160
+ resilience is handled outside your scripts (for example by a proxy or by the CI runner). For a
161
+ policy about commands other than network clients, define your own rule with
162
+ [`createWrapperRule`](../wrapper-rules.md) rather than overloading `alsoTreatAsNetwork`.
163
+
164
+ ## Relationship to shellcheck
165
+
166
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
167
+ layer. Run shellcheck beside it.
168
+
169
+ shellcheck has no equivalent. It checks how commands are written, not which wrapper a project
170
+ requires around them, and has no way to configure a project-specific wrapper. This rule exists
171
+ because that requirement is a project contract, not a shell gotcha.
@@ -0,0 +1,157 @@
1
+ # `sh/require-shebang`
2
+
3
+ Require line 1 of every shell file to be a shebang that runs a recognized shell.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ------- | ------- | --------- |
7
+ | `recommended`, `agent-authored` | autofix (missing shebang only) | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ A script without a shebang runs under whichever shell happens to invoke it. A bash script
12
+ started as `sh deploy.sh`, or by a tool that defaults to `/bin/sh`, then fails in confusing
13
+ ways far from the real cause. A shebang makes the file's interpreter explicit.
14
+
15
+ This rule reads line 1 of the file and reports two problems:
16
+
17
+ - **`missingShebang`**: line 1 does not start with `#!`. A comment, a blank line, or
18
+ whitespace before `#!` all count as missing. An empty file is missing a shebang too.
19
+ - **`unrecognizedInterpreter`**: line 1 is a shebang, but it does not run one of the
20
+ recognized shells: `bash`, `sh`, `dash`, `ksh`, `mksh`, `zsh` or `bats`. Each one is
21
+ accepted two ways:
22
+ - by absolute path: `#!/bin/bash`, `#!/usr/local/bin/bash`, with any arguments after it;
23
+ - through `env` named by absolute path: `#!/usr/bin/env bash`,
24
+ `#!/usr/bin/env -S bash -e`. The rule skips `env` options, their arguments and
25
+ `NAME=value` assignments to find the command `env` runs.
26
+
27
+ A relative interpreter such as `#!bash` or `#!env bash` is reported, because the kernel
28
+ resolves it against the caller's working directory, not `PATH`.
29
+
30
+ A UTF-8 byte order mark before `#!` does not stop the shebang from counting.
31
+
32
+ The rule does **not** check that the shebang agrees with `languageOptions.dialect`.
33
+ `#!/bin/sh` in a file linted with `dialect: 'bash'` is accepted here. That mismatch belongs
34
+ to [`sh/shebang-matches-dialect`](./shebang-matches-dialect.md), so one problem is never reported under two rule names.
35
+
36
+ ### Autofix
37
+
38
+ For `missingShebang`, the fix inserts one line above line 1. The interpreter comes from
39
+ `languageOptions.dialect`:
40
+
41
+ | `dialect` | Inserted with `style: 'env'` | Inserted with `style: 'absolute'` |
42
+ | ------------------ | ---------------------------- | --------------------------------- |
43
+ | `bash` (default) | `#!/usr/bin/env bash` | `#!/bin/bash` |
44
+ | `posix` | `#!/usr/bin/env sh` | `#!/bin/sh` |
45
+ | `mksh` | `#!/usr/bin/env mksh` | `#!/bin/mksh` |
46
+ | `zsh` | `#!/usr/bin/env zsh` | `#!/bin/zsh` |
47
+ | `bats` | `#!/usr/bin/env bats` | `#!/bin/bats` |
48
+
49
+ The inserted line ends with the file's own line break (`\r\n` in a CRLF file). For an empty
50
+ file the fix produces exactly the shebang line and a newline.
51
+
52
+ `unrecognizedInterpreter` has no fix. The file names some other interpreter, and choosing a
53
+ shell for it is a decision for a person.
54
+
55
+ ## Options
56
+
57
+ This rule takes one options object:
58
+
59
+ - `style` (`'env' | 'absolute'`, default `'env'`): how the autofix spells the shebang it
60
+ inserts. `'env'` writes `#!/usr/bin/env <shell>`, which finds the shell on `PATH`.
61
+ `'absolute'` writes `#!/bin/<shell>`. The option affects only the fix. Both spellings
62
+ are accepted when checking a file.
63
+
64
+ Any other key, or any other `style` value, is a configuration error.
65
+
66
+ ```js
67
+ // eslint.config.js
68
+ {
69
+ rules: {
70
+ 'sh/require-shebang': ['error', { style: 'absolute' }],
71
+ },
72
+ }
73
+ ```
74
+
75
+ ## Examples
76
+
77
+ ### Incorrect
78
+
79
+ <!-- example: {"findings": 1, "messageIds": ["missingShebang"]} -->
80
+ ```sh
81
+ echo "hello" # line 1 is a command: missingShebang
82
+ ```
83
+
84
+ <!-- example: {"findings": 1, "messageIds": ["missingShebang"]} -->
85
+ ```sh
86
+ # Deploys the site. # line 1 is an ordinary comment: missingShebang
87
+ echo "hello"
88
+ ```
89
+
90
+ <!-- example: {"findings": 1, "messageIds": ["unrecognizedInterpreter"]} -->
91
+ ```sh
92
+ #!/usr/bin/python3 # not a shell: unrecognizedInterpreter
93
+ ```
94
+
95
+ <!-- example: {"findings": 1, "messageIds": ["unrecognizedInterpreter"]} -->
96
+ ```sh
97
+ #!/usr/bin/env node # env runs a non-shell: unrecognizedInterpreter
98
+ ```
99
+
100
+ <!-- example: {"findings": 1, "messageIds": ["unrecognizedInterpreter"]} -->
101
+ ```sh
102
+ #!bash # relative interpreter path: unrecognizedInterpreter
103
+ ```
104
+
105
+ ### Correct
106
+
107
+ ```sh
108
+ #!/usr/bin/env bash
109
+ echo "hello"
110
+ ```
111
+
112
+ ```sh
113
+ #!/usr/local/bin/bash -euo pipefail
114
+ echo "hello"
115
+ ```
116
+
117
+ ```sh
118
+ #!/usr/bin/env -S bash -e
119
+ echo "hello"
120
+ ```
121
+
122
+ ```sh
123
+ #!/bin/sh
124
+ # Accepted even under dialect: 'bash'; see shebang-matches-dialect.
125
+ echo "hello"
126
+ ```
127
+
128
+ ## When not to use it
129
+
130
+ Files that are only ever sourced (`. ./lib.sh`, `source lib.sh`) are never executed, so their
131
+ shebang is never read. If your project keeps such libraries without a shebang, turn the rule
132
+ off for them with `files` or `ignores` in your config:
133
+
134
+ ```js
135
+ // eslint.config.js
136
+ export default [
137
+ {
138
+ files: ['**/*.sh'],
139
+ ignores: ['lib/**/*.sh'],
140
+ rules: { 'sh/require-shebang': 'error' },
141
+ },
142
+ ];
143
+ ```
144
+
145
+ The rule does not detect sourced libraries on its own.
146
+
147
+ ## Relationship to shellcheck
148
+
149
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
150
+ layer. Run shellcheck beside it.
151
+
152
+ shellcheck's SC2148 ("Tips depend on target shell and yours is unknown") fires when a file has
153
+ no shebang and no `shell=` directive. shellcheck's aim is to know which shell to analyze for,
154
+ so a `# shellcheck shell=bash` directive or a `--shell` flag silences it. This rule enforces
155
+ the policy that the script itself names its interpreter, which a directive or a command-line
156
+ flag does not satisfy. It also autofixes the missing line from your configured dialect.
157
+ shellcheck's SC1008 ("This shebang was unrecognized") overlaps `unrecognizedInterpreter`.
@@ -0,0 +1,253 @@
1
+ # `sh/require-strict-mode`
2
+
3
+ Require a script to turn on its strict-mode shell options (`set -euo pipefail` by default, or
4
+ `set -eu` for POSIX `sh`) within its first few statements.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ------- | --------- |
8
+ | `recommended`, `agent-authored`, `posix-portable` (with set -eu) | autofix | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ A script without strict mode keeps going after things fail. A command that fails, a variable that
13
+ was never set, or a failed stage in the middle of a pipeline does not stop the script, so one
14
+ failed step turns into wrong output or a damaged state further down. This rule enforces the policy
15
+ once, at the top of the file: by default `errexit` (`-e`), `nounset` (`-u`) and `pipefail` must be
16
+ on within the first 5 statements. Under the `posix` dialect, the default leaves out `pipefail` (see
17
+ [Dialects](#dialects)).
18
+
19
+ The rule checks the **set of options turned on**, not the literal text. All of these satisfy the
20
+ Bash default:
21
+
22
+ ```sh
23
+ set -euo pipefail
24
+ set -o errexit -o nounset -o pipefail
25
+ set -e; set -u; set -o pipefail
26
+ set -ue -o pipefail
27
+ ```
28
+
29
+ How the rule reads the file:
30
+
31
+ - **Statements, not lines.** The window counts top-level statements from the top of the file.
32
+ Comments and blank lines are not statements, and `set -e; set -u` on one line is two
33
+ statements.
34
+ - **Order matters.** Statements in the window apply in order, and the rule checks which options
35
+ are on after the last one. A `set +e` (or `set +o errexit`) in the window turns `errexit` off
36
+ again, so the file is reported unless a later statement in the window turns it back on. A
37
+ `set +e` after the window is not checked. Turning `errexit` off around one command later in the
38
+ script is a common, deliberate pattern.
39
+ - **Only plain top-level `set` commands count.** A `set` in a subshell, a pipeline, or a background
40
+ job (`set -e &`) runs in a child process and never changes the script's options. A `set` inside a
41
+ function, an `if`, a loop, a `{ …; }` group or an `&&`/`||` list does not count either, even
42
+ though the shell may apply it there. The rule reads only plain top-level statements, so put the
43
+ prologue on its own line.
44
+ - **Arguments must be static.** The rule reads `set`'s arguments as the shell does: flag clusters
45
+ (`-eu`, `+e`), `-o name` / `+o name`, and an `o` inside a cluster (`-euo pipefail`) that takes the
46
+ next word as an option name. `--` and the first argument that is not a flag end the options. When
47
+ an argument's value is only known at run time (`set -e "$flags"`), the rule stops reading that
48
+ `set` command there. Options turned on before that argument still count.
49
+
50
+ The report is placed at line 1, column 1 and lists the required options that are not on.
51
+
52
+ **Autofix.** The fix inserts the prologue on its own line right after the shebang, or at line 1 when
53
+ the file has no shebang:
54
+
55
+ <!-- transform: {"kind": "fix"} -->
56
+
57
+ ```sh
58
+ #!/bin/bash
59
+ cd "$1"
60
+ # becomes
61
+ #!/bin/bash
62
+ set -euo pipefail
63
+ cd "$1"
64
+ ```
65
+
66
+ It uses the file's own line ending, so a CRLF file stays CRLF. The fix is
67
+ offered only when inserting the prologue would satisfy the rule. For example, `set -e` followed by
68
+ `set +e` in the window is reported with no fix, because the `set +e` would turn `errexit` off
69
+ again. Decide what that `set +e` is for, then edit the file by hand.
70
+
71
+ ### Dialects
72
+
73
+ When `mode` is not configured, the required prologue depends on `languageOptions.dialect`:
74
+
75
+ | `dialect` | Default prologue |
76
+ | -------------------------------------------- | ------------------- |
77
+ | `bash` (the default), `bats`, `mksh`, `zsh` | `set -euo pipefail` |
78
+ | `posix` | `set -eu` |
79
+
80
+ `pipefail` became part of POSIX only in POSIX.1-2024, and older `dash` and BusyBox `ash` builds
81
+ still reject `set -o pipefail`. An autofix that inserted it would break a working `#!/bin/sh` script
82
+ on those shells, so under `posix` the default requires and inserts only `set -eu`. A `posix` file
83
+ may still turn `pipefail` on; the rule just doesn't require it.
84
+
85
+ A configured `mode` always wins, whatever the dialect. To require `pipefail` in POSIX scripts too,
86
+ configure it (see below).
87
+
88
+ The dialect comes from `languageOptions`, not from the shebang. A `#!/bin/sh` file linted under the
89
+ default `bash` dialect gets the `set -euo pipefail` default. That mismatch is reported at the
90
+ shebang by [`sh/shebang-matches-dialect`](./shebang-matches-dialect.md); set
91
+ `languageOptions.dialect` to `posix` for such files so the `pipefail` note above does not apply.
92
+
93
+ ## Options
94
+
95
+ This rule takes one options object:
96
+
97
+ - `mode` (`string | string[]`, default `"set -euo pipefail"`, or `"set -eu"` under the `posix`
98
+ dialect): the required prologue.
99
+ - A **string** must be a `set` command that only turns options on, such as `"set -eu"` or
100
+ `"set -o errexit -o nounset"`. The options it turns on are what the rule requires, and the
101
+ string is the exact text the autofix inserts. A string that is not such a command (for example
102
+ `"set +e"` or `"echo hi"`) is a configuration error.
103
+ - A **list** names the required options by their long names (`errexit`, `nounset`, `pipefail`,
104
+ `noclobber`, …, any name `set -o` accepts). The autofix inserts a `set` command built from the
105
+ list. Options with a one-letter flag go into one cluster in list order. The first option with no
106
+ letter is attached to that cluster with `o`, and any others follow as `-o <name>`. So
107
+ `["errexit", "nounset", "pipefail"]` inserts `set -euo pipefail` and `["errexit", "nounset"]`
108
+ inserts `set -eu`.
109
+
110
+ <!-- transform: {"kind": "fix", "options": {"mode": ["errexit", "nounset", "pipefail"]}} -->
111
+
112
+ ```sh
113
+ echo hi
114
+ # becomes
115
+ set -euo pipefail
116
+ echo hi
117
+ ```
118
+
119
+ <!-- transform: {"kind": "fix", "options": {"mode": ["errexit", "nounset"]}} -->
120
+
121
+ ```sh
122
+ echo hi
123
+ # becomes
124
+ set -eu
125
+ echo hi
126
+ ```
127
+ - `withinStatements` (integer ≥ 1, default `5`): how many top-level statements from the top of the
128
+ file the prologue may take to appear.
129
+ - `allowShebangOnly` (`boolean`, default `false`): when `true`, options passed to the shell on the
130
+ shebang line count as turned on, so a shebang alone can satisfy the rule. Shebang options combine
131
+ with `set` statements in the window. Only spellings the operating system passes to the shell as
132
+ written are read:
133
+ - `#!/usr/bin/env -S bash -euo pipefail`: `env -S` splits the rest of the line into words, so
134
+ every word after the shell name is read.
135
+ - `#!/bin/bash -eu`: the kernel passes everything after the interpreter as **one** argument, so
136
+ exactly one flag word is read. `#!/bin/bash -e -u` arrives as the single argument `-e -u`,
137
+ which the shell rejects, so it counts as nothing.
138
+ - `#!/usr/bin/env bash -e` counts as nothing. Without `-S`, Linux passes `bash -e` to `env` as
139
+ one command name.
140
+
141
+ It is off by default because shebang options are ignored when the script is run as
142
+ `bash script.sh`, so a `set` line is the only prologue that applies however the script is
143
+ started.
144
+
145
+ ```js
146
+ // eslint.config.js
147
+ {
148
+ rules: {
149
+ 'sh/require-strict-mode': ['error', {
150
+ mode: 'set -euo pipefail',
151
+ withinStatements: 5,
152
+ allowShebangOnly: false,
153
+ }],
154
+ },
155
+ }
156
+ ```
157
+
158
+ For POSIX `sh` scripts that only run on shells with `pipefail` support, and should require it:
159
+
160
+ ```js
161
+ // eslint.config.js
162
+ {
163
+ files: ['**/*.sh'],
164
+ languageOptions: { dialect: 'posix' },
165
+ rules: {
166
+ 'sh/require-strict-mode': ['error', { mode: ['errexit', 'nounset', 'pipefail'] }],
167
+ },
168
+ }
169
+ ```
170
+
171
+ ## Examples
172
+
173
+ ### Incorrect
174
+
175
+ <!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
176
+ ```sh
177
+ #!/bin/bash
178
+ # reported: no strict-mode options are turned on
179
+ cd "$1"
180
+ rm -rf build
181
+ ```
182
+
183
+ <!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
184
+ ```sh
185
+ #!/bin/bash
186
+ # reported: pipefail is not turned on
187
+ set -eu
188
+ ```
189
+
190
+ <!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
191
+ ```sh
192
+ #!/bin/bash
193
+ # reported: the set +e in the window turns errexit off again (no autofix)
194
+ set -euo pipefail
195
+ set +e
196
+ ```
197
+
198
+ <!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
199
+ ```sh
200
+ #!/bin/bash
201
+ # reported: a set inside a function doesn't apply to the script's top level
202
+ main() { set -euo pipefail; }
203
+ main "$@"
204
+ ```
205
+
206
+ ### Correct
207
+
208
+ ```sh
209
+ #!/bin/bash
210
+ set -euo pipefail
211
+ cd "$1"
212
+ ```
213
+
214
+ ```sh
215
+ #!/bin/bash
216
+ # the options may be spread over statements and spelled long-form
217
+ set -e
218
+ set -o nounset -o pipefail
219
+ ```
220
+
221
+ ```sh
222
+ #!/bin/bash
223
+ # errexit is off for one command and back on inside the window
224
+ set -euo pipefail
225
+ set +e
226
+ grep -q pattern file
227
+ set -e
228
+ ```
229
+
230
+ <!-- example: {"dialect": "posix"} -->
231
+ ```sh
232
+ #!/bin/sh
233
+ # with languageOptions: { dialect: 'posix' } and no configured mode
234
+ set -eu
235
+ ```
236
+
237
+ ## When not to use it
238
+
239
+ Turn this rule off if your project does not hold the strict-mode policy. For example, you may have
240
+ scripts that handle every error explicitly, or files that are sourced into another shell rather
241
+ than run, where a `set -e` would change the caller's shell. For a few files that are meant to be
242
+ lenient, disable it with a directive comment or a `files`-scoped config entry instead.
243
+
244
+ ## Relationship to shellcheck
245
+
246
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
247
+ layer. Run shellcheck beside it.
248
+
249
+ shellcheck has no check that requires strict mode. It warns about the _consequences_ of not
250
+ having it, case by case. For example, SC2164 flags a `cd` whose failure isn't handled, and SC2155
251
+ flags a `local x=$(cmd)` that hides the command's exit status. This rule enforces the _policy_
252
+ that prevents that whole class of problem, once, at the top of the file, which shellcheck's
253
+ per-construct checks cannot express.