@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,131 @@
1
+ # `sh/require-tty-guard`
2
+
3
+ Require interactive commands such as `read` and `select` to be guarded by a `[ -t 0 ]` test, or to
4
+ have their input supplied explicitly.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ---------------- | ---------------------------- | --------- |
8
+ | `agent-authored` | suggestions (wrap in a guard) | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ A prompt that waits on the terminal hangs forever when a script runs without one: in CI, from
13
+ cron, or from an agent's tool call. In automation it also reads whatever happens to be on stdin.
14
+ This rule reports `read` and `select` (and any command you add with `commands`) that are not
15
+ guarded.
16
+
17
+ The check is **purely syntactic**. The rule never follows data or control flow, so what it treats
18
+ as guarded is exactly the following.
19
+
20
+ A command is guarded by a test when it is:
21
+
22
+ - lexically inside the `then` or `else` body of an `if` whose condition contains a TTY test, or
23
+ - on the right of an `&&` whose left side contains a TTY test (`[ -t 0 ] && [ -n "$X" ] && read x`).
24
+
25
+ A TTY test is `[ -t 0 ]`, `test -t 0` or `[[ -t 0 ]]`. Only file descriptor `0` counts, and the
26
+ test may sit anywhere in the condition: `[ ! -t 0 ]` is also recognized as a TTY test, because
27
+ the rule does not interpret negation.
28
+
29
+ A command has its input supplied explicitly, and is not reported, when:
30
+
31
+ - it, or any statement that encloses it, redirects fd 0 from a file, here-document or
32
+ here-string (`read x < file`, `read x <<< "$v"`, `while read l; do …; done < file`), or from
33
+ another descriptor (`read x <&3`);
34
+ - it is on the right of a pipe (`echo a | read x`, `cat f | while read l; do …; done`);
35
+ - it is inside a `coproc`, which reads its own input pipe;
36
+ - it is `read -u N` for a descriptor other than `0` (`read -u 3 x`, `read -ru3 x`). A descriptor
37
+ that is not a static value is also treated as not stdin.
38
+
39
+ A `read` with `-p` is still reported: the prompt does not make it safe without a terminal.
40
+
41
+ ### Limits
42
+
43
+ The rule is honest about what it cannot see:
44
+
45
+ - **A guard in a calling function is not seen.** If `ask() { read x; }` is only ever called as
46
+ `[ -t 0 ] && ask`, the `read` is still reported.
47
+ - **Early exits are not seen.** `[ -t 0 ] || exit 1` followed by `read x` does not guard the
48
+ `read`; neither does a guard in an earlier statement, a `||` list, or a `while` condition.
49
+ - **Negation is not interpreted.** `if [ ! -t 0 ]; then read x; fi` counts as guarded.
50
+ - **Variables are not tracked.** `is_tty=$([ -t 0 ] && echo 1)` is not a guard.
51
+ - **Command names are read as written.** A command reached through a wrapper (`command read x`)
52
+ or with a dynamic name is not matched.
53
+
54
+ ## Options
55
+
56
+ - `commands` (`string[]`, default `['read', 'select']`): the interactive commands to check. A
57
+ configured list **replaces** the default, as with `sh/require-network-wrapper`; include `read`
58
+ and `select` to keep them. `select` is parsed as a loop, not a command, and is checked whenever
59
+ the list contains `select`.
60
+
61
+ ```js
62
+ // eslint.config.js
63
+ {
64
+ rules: {
65
+ 'sh/require-tty-guard': ['error', { commands: ['read', 'select', 'confirm'] }],
66
+ },
67
+ }
68
+ ```
69
+
70
+ ## Suggestion
71
+
72
+ The rule offers one suggestion per report: wrap the statement in `if [ -t 0 ]; then … fi`:
73
+
74
+ <!-- transform: {"kind": "suggestion"} -->
75
+
76
+ ```sh
77
+ read name
78
+ # becomes
79
+ if [ -t 0 ]; then read name; fi
80
+ ```
81
+
82
+ A multi-line statement that starts its own line is wrapped on lines of its own. Anywhere else the
83
+ wrapper goes around it in place, because an earlier command on the line may have opened a
84
+ here-document whose body begins at the next line break; a line break goes before `fi` only when the
85
+ statement ends in a here-document of its own. A statement whose here-document opener shares its
86
+ line with another command (`read x 3<<EOF; echo hi`, or a trailing comment) gets no suggestion: the
87
+ statement's source runs on to the terminator, so wrapping it would pull that command into the guard
88
+ and it would run only on a terminal. A statement run in the background (`read x &`)
89
+ gets no suggestion, because wrapping it would change what is backgrounded. A statement whose exit
90
+ status steers control flow (an `if`/`while`/`until` condition, or an operand of `&&` or `||`) is
91
+ reported without a suggestion too: the wrapper would exit 0 when there is no terminal, which
92
+ changes which way the construct goes. The suggestion is
93
+ never applied by `--fix`; whether the script should skip the prompt or fail when there is no
94
+ terminal is your decision.
95
+
96
+ ## Examples
97
+
98
+ ### Incorrect
99
+
100
+ <!-- example: {"findings": 4, "messageIds": ["unguardedInteractive", "unguardedInteractive", "unguardedInteractive", "unguardedInteractive"]} -->
101
+ ```sh
102
+ read name # waits on stdin with no terminal check
103
+ read -p "Name: " name # a prompt does not make it safe
104
+ select opt in a b; do :; done # select reads the terminal too
105
+ [ -t 0 ] || read x # `||` is not a guard
106
+ ```
107
+
108
+ ### Correct
109
+
110
+ ```sh
111
+ if [ -t 0 ]; then read name; fi
112
+ [ -t 0 ] && read -p "Name: " name
113
+ read name < answers.txt
114
+ read name <<< "$default"
115
+ cat answers.txt | while read -r line; do :; done
116
+ read -u 3 name # from descriptor 3, not stdin
117
+ ```
118
+
119
+ ## When not to use it
120
+
121
+ Turn the rule off for scripts that are interactive by definition and never run unattended, such
122
+ as a local setup wizard.
123
+
124
+ ## Relationship to shellcheck
125
+
126
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
127
+ layer. Run shellcheck beside it.
128
+
129
+ shellcheck has no equivalent check: it does not report a `read` that can run without a terminal.
130
+ Its `read` diagnostics (for example SC2162, `read` without `-r`) concern how input is parsed, not
131
+ where it comes from.
@@ -0,0 +1,120 @@
1
+ # `sh/shebang-matches-dialect`
2
+
3
+ Require the shebang to name the same kind of shell, bash or POSIX, as `languageOptions.dialect`.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ------------------------------- | ----------- | --------- |
7
+ | `recommended`, `agent-authored`, `posix-portable` | not fixable | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ Dialect-sensitive rules read `languageOptions.dialect` from your config, not the file. When the
12
+ two disagree they advise for the wrong shell. A `#!/bin/sh` script linted under the default
13
+ `bash` dialect is told, by [`sh/require-strict-mode`](./require-strict-mode.md), to turn on
14
+ `pipefail`, which dash and older BusyBox `ash` reject. This rule reports the disagreement once,
15
+ at the shebang (line 1, in full), so the fix is one config entry or one line.
16
+
17
+ It reports two cases:
18
+
19
+ - **`posixShebangBashDialect`**: the shebang runs a POSIX shell (`sh`, `dash` or `ash`) but the
20
+ dialect is `bash` (the default).
21
+ - **`bashShebangPosixDialect`**: the shebang runs `bash` but the dialect is `posix`.
22
+
23
+ The shell is found the way [`sh/require-shebang`](./require-shebang.md) finds it: an absolute
24
+ interpreter path (`#!/bin/sh`, `#!/usr/local/bin/bash`), or an absolute-path `env` and the command
25
+ it runs (`#!/usr/bin/env sh`, `#!/usr/bin/env -S bash -e`). A relative interpreter such as
26
+ `#!sh` is not read; `sh/require-shebang` reports it.
27
+
28
+ Nothing else is reported. A file with no shebang is clean here (that is `sh/require-shebang`'s
29
+ concern), as is a shebang that names another interpreter, and any file whose dialect is `mksh`,
30
+ `zsh` or `bats`. A shebang and dialect that already agree are clean: `#!/usr/bin/env bash` under
31
+ `bash`, `#!/bin/sh` under `posix`.
32
+
33
+ The message tells you to set the dialect for these files, or to change the shebang. To set it,
34
+ scope `languageOptions.dialect` to the files that use that shell:
35
+
36
+ ```js
37
+ // eslint.config.js
38
+ export default [
39
+ {
40
+ files: ['scripts/posix/**/*.sh'],
41
+ languageOptions: { dialect: 'posix' },
42
+ },
43
+ ];
44
+ ```
45
+
46
+ ## Options
47
+
48
+ This rule has no options.
49
+
50
+ ```js
51
+ // eslint.config.js
52
+ {
53
+ rules: {
54
+ 'sh/shebang-matches-dialect': 'error',
55
+ },
56
+ }
57
+ ```
58
+
59
+ ## Examples
60
+
61
+ ### Incorrect
62
+
63
+ <!-- example: {"dialect": "bash", "messageIds": ["posixShebangBashDialect"], "findings": 1} -->
64
+ ```sh
65
+ #!/bin/sh
66
+ # dialect: 'bash' (the default): posixShebangBashDialect
67
+ echo "hello"
68
+ ```
69
+
70
+ <!-- example: {"dialect": "bash", "messageIds": ["posixShebangBashDialect"], "findings": 1} -->
71
+ ```sh
72
+ #!/usr/bin/env dash
73
+ # dialect: 'bash': posixShebangBashDialect
74
+ echo "hello"
75
+ ```
76
+
77
+ <!-- example: {"dialect": "posix", "messageIds": ["bashShebangPosixDialect"], "findings": 1} -->
78
+ ```sh
79
+ #!/usr/bin/env bash
80
+ # dialect: 'posix': bashShebangPosixDialect
81
+ echo "hello"
82
+ ```
83
+
84
+ ### Correct
85
+
86
+ <!-- example: {"dialect": "bash"} -->
87
+ ```sh
88
+ #!/usr/bin/env bash
89
+ # dialect: 'bash'
90
+ echo "hello"
91
+ ```
92
+
93
+ <!-- example: {"dialect": "posix"} -->
94
+ ```sh
95
+ #!/bin/sh
96
+ # dialect: 'posix'
97
+ echo "hello"
98
+ ```
99
+
100
+ ```sh
101
+ echo "no shebang: sh/require-shebang's concern, not this rule's"
102
+ ```
103
+
104
+ ## When not to use it
105
+
106
+ Turn it off if your project deliberately lints POSIX-shebang scripts as bash, for example
107
+ because every `sh` on your targets is bash. Also turn it off for files you only source, whose
108
+ shebang is never read.
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 chooses its dialect from the shebang (or a `shell=` directive) and then checks the file
116
+ against it, so it has no notion of a mismatch between the shebang and a separately configured
117
+ dialect. Its SC3xxx checks report bash-only features in a `#!/bin/sh` script, which is
118
+ a different question: that the syntax is wrong for the shebang, not that your lint configuration
119
+ disagrees with it. This rule keeps eslint-sh's own dialect-sensitive rules honest about the
120
+ shell they are advising for.
package/docs/selectors.md CHANGED
@@ -5,8 +5,8 @@
5
5
  classes** against shell ASTs, in addition to matching bare node type names
6
6
  (`CallExpr`, `IfClause`, ...). Selector-first rule authoring — writing a rule as a selector string
7
7
  rather than a `create()` full of manual type checks — is a headline feature of this plugin; this
8
- page is a copy-paste cookbook of real selectors, each backed by a passing test (see
9
- `test/selectors-cookbook.test.ts`) so the examples can't silently rot as the plugin evolves.
8
+ page is a copy-paste cookbook of real selectors, each backed by a passing test in this package's
9
+ test suite so the examples can't silently rot as the plugin evolves.
10
10
 
11
11
  Looking for a complete, runnable `create()` that uses one of these selectors — the full
12
12
  `{ meta, create(context) }` rule module, `context.report`, and the `eslint.config.mjs`
@@ -0,0 +1,87 @@
1
+ # Running eslint-sh beside shellcheck
2
+
3
+ Run both. They answer different questions, and each is blind to what the other checks.
4
+
5
+ - **shellcheck** is the general-correctness tool. It finds shell gotchas: quoting and
6
+ word-splitting mistakes, constructs that behave differently from how they read, and
7
+ portability problems for the shell named in the shebang. It works from a fixed catalog of
8
+ checks and has no plugin system, so it cannot learn your project's rules.
9
+ - **eslint-sh** is the policy tool. It enforces conventions and contracts that shellcheck
10
+ treats as style or cannot express: a required strict-mode prologue, declared commands,
11
+ network wrappers, and the shell version or dialect you deploy to.
12
+
13
+ eslint-sh does not aim to reproduce shellcheck's diagnostic catalog. If a check is only about
14
+ whether a construct is a bug, shellcheck owns it.
15
+
16
+ ## Running both
17
+
18
+ They are independent commands with independent exit codes. Run them side by side in CI and in
19
+ your editor:
20
+
21
+ ```sh
22
+ git ls-files -z '*.sh' '*.bash' 'bin/*' | xargs -0 sh -c '[ "$#" -eq 0 ] || exec shellcheck -- "$@"' sh
23
+ npx eslint . --max-warnings 0
24
+ ```
25
+
26
+ The first command checks every tracked shell script, including extensionless ones under `bin/`,
27
+ and does nothing in a project with no such files. Adapt the pathspecs to where your scripts live.
28
+
29
+ `eslint .` lints the files your config's `files` patterns select, including extensionless
30
+ scripts when the config lists `bin/?*`. A CLI glob such as `'scripts/**/*.sh'` would skip them.
31
+ For a complete GitHub Actions job, exit codes, a pre-commit hook and editor setup, see the
32
+ [`eslint-sh` README](https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-sh/README.md#continuous-integration).
33
+
34
+ Give them the same target shell, since each reads its own configuration. shellcheck takes it
35
+ from the shebang or a `# shellcheck shell=…` directive; eslint-sh takes it from
36
+ `languageOptions.dialect` and `languageOptions.shellVersion`. The `bash-3.2` and
37
+ `posix-portable` presets set the latter for you, and
38
+ [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md) reports a
39
+ shebang that disagrees with the configured dialect, so the two tools do not end up advising for
40
+ different shells.
41
+
42
+ Silence each tool with its own directive: `# shellcheck disable=SC2086` for shellcheck and
43
+ `# eslint-disable-next-line sh/quote-expansions -- reason` for eslint-sh. One does not
44
+ silence the other.
45
+
46
+ ## Who owns what
47
+
48
+ | Concern | Owner | Notes |
49
+ | ------- | ----- | ----- |
50
+ | Quoting and word splitting in general (SC2086, SC2068, …) | shellcheck | eslint-sh's [`quote-expansions`](./rules/quote-expansions.md) covers only the list contexts it documents, deliberately. |
51
+ | Unreachable code, useless commands, misuse of `[` / `[[` / `test`, `$?` mistakes | shellcheck | Correctness gotchas with no policy dimension. |
52
+ | Bugs caused by subshells, pipelines, and `read` without `-r` | shellcheck | Parsing and scoping behavior. |
53
+ | `source` of a path shellcheck cannot follow (SC1090, SC1091) | shellcheck | Configure its `source-path`. eslint-sh only objects to a non-literal path as a policy. |
54
+ | Bash-only syntax in a script whose shebang says `sh` (SC3xxx) | shellcheck | It judges the file against its own shebang. eslint-sh reports only the few constructs [`posix-portable`](./compat.md#presets) names, and leaves `local`, `echo -e` and [the rest](./compat.md#what-posix-portable-does-not-report) here. |
55
+ | Bash-only features on a specific Bash version, such as the 3.2 macOS ships | eslint-sh | shellcheck cannot be pointed at a Bash version. See [version compatibility](./compat.md). |
56
+ | Whether the shebang agrees with the dialect you lint for | eslint-sh | [`shebang-matches-dialect`](./rules/shebang-matches-dialect.md). |
57
+ | A strict-mode prologue every script must start with | eslint-sh | shellcheck only warns about the consequences of lacking one. |
58
+ | Which commands a script may run, and that each is declared | eslint-sh | [`no-dynamic-invocation`](./rules/no-dynamic-invocation.md), [`no-undeclared-commands`](./rules/no-undeclared-commands.md), [`no-unused-declarations`](./rules/no-unused-declarations.md). Needs your declaration convention. |
59
+ | Network commands going through your wrapper | eslint-sh | [`require-network-wrapper`](./rules/require-network-wrapper.md). Needs your wrapper name. |
60
+ | Secrets passed as flags or read from environment variables | eslint-sh | [`no-secret-flags`](./rules/no-secret-flags.md), [`no-secret-env-reads`](./rules/no-secret-env-reads.md). |
61
+ | Size and structure caps on a script | eslint-sh | [`max-script-complexity`](./rules/max-script-complexity.md). |
62
+ | Guarding interactive `read` with a TTY check | eslint-sh | [`require-tty-guard`](./rules/require-tty-guard.md). shellcheck's `read` checks concern how input is parsed. |
63
+
64
+ ## Deliberate overlaps
65
+
66
+ Some eslint-sh rules report the same code as a shellcheck check. They exist anyway for the
67
+ reason given, and each rule's page has a "Relationship to shellcheck" section. The shellcheck findings for the version-compat rules below were observed with shellcheck 0.11.0 under `-s sh` and `-s bash`.
68
+
69
+ | eslint-sh rule | Overlapping shellcheck code | Why the rule exists anyway |
70
+ | -------------- | --------------------------- | -------------------------- |
71
+ | [`sh/quote-expansions`](./rules/quote-expansions.md) | SC2086, SC2068 | Makes unquoted expansions a project policy with a severity you choose, and offers a quoting suggestion. It is `warn` in `recommended`; disable it if you already enforce SC2086. |
72
+ | [`sh/no-eval`](./rules/no-eval.md) | SC2294 (`eval` on arrays), SC1090 (non-literal `source`) | shellcheck flags `eval` only in specific misuse patterns. The rule treats every `eval` and every non-literal `source` as a reviewable exception. |
73
+ | [`sh/no-unchecked-cd`](./rules/no-unchecked-cd.md) | SC2164 | Kept for files that run without `set -e`; the two agree on the common cases. |
74
+ | [`sh/require-shebang`](./rules/require-shebang.md) | SC2148, SC1008 | A required, recognized shebang is a rule of the repository, not a hint about the target shell. |
75
+ | [`sh/no-masked-exit-status`](./rules/no-masked-exit-status.md) | SC2155 | `require-strict-mode` makes `set -euo pipefail` a contract, and `local x=$(cmd)` is the most common way a script that honours it still swallows a failure. shellcheck offers no fix; the rule offers the split as an editor suggestion and can be limited to files where errexit is on. It is not in `recommended`, because that preset runs beside shellcheck. |
76
+ | [`sh/prefer-local`](./rules/prefer-local.md) | SC2155 (a related check) | Requires `local` declarations, and its fix declares the variable on a separate line, which avoids SC2155. |
77
+ | [`sh/compat/no-associative-arrays`](./rules/compat/no-associative-arrays.md), [`no-case-modification`](./rules/compat/no-case-modification.md), [`no-negative-array-index`](./rules/compat/no-negative-array-index.md), [`no-mapfile`](./rules/compat/no-mapfile.md) | SC3044 (the `declare` or `mapfile` command, not the `-A` option), SC3043 (`local -A`), SC3054 (`${a[-1]}`), SC3059 (case modification) | shellcheck reports these only for scripts it treats as POSIX `sh`. These rules report them for a Bash version you name. |
78
+ | [`sh/compat/no-double-bracket`](./rules/compat/no-double-bracket.md), [`no-let`](./rules/compat/no-let.md), [`no-source`](./rules/compat/no-source.md) | SC3010 (`[[`), SC3039 (`let`), SC3046 (`source`), under `-s sh` only | Under `-s bash` shellcheck reports nothing for `[[` or `source`, and for `let` only SC2219, a style note that prefers `(( ))`. These rules report them for the dialect you configure, because they fail on dash and would otherwise defeat a preset named for portability. |
79
+ | [`sh/compat/no-test-v`](./rules/compat/no-test-v.md), [`no-wait-n`](./rules/compat/no-wait-n.md), [`no-globstar`](./rules/compat/no-globstar.md), [`no-pipe-all`](./rules/compat/no-pipe-all.md), [`no-fd-variable`](./rules/compat/no-fd-variable.md), [`no-unicode-escape`](./rules/compat/no-unicode-escape.md) | SC3016 (`test -v`, with SC3010 for `[[`), SC3045 (`wait -n`), SC3044 (`shopt`), SC3029 (`\|&`), SC3022 (`{fd}`), SC3003 (`$'…'`) | shellcheck reports these Bash features only for scripts it treats as POSIX `sh`. These rules report them for a Bash version you name. |
80
+ | [`sh/compat/no-nameref`](./rules/compat/no-nameref.md), [`no-case-attributes`](./rules/compat/no-case-attributes.md) | SC3043 (`local`), SC3044 (`declare`, `typeset`): the command, not the `-n`, `-l` or `-u` option | Under `-s sh` shellcheck reports the command that carries the option; under `-s bash` it reports nothing for either. These rules report the option for a Bash version you name. |
81
+ | [`sh/compat/no-brace-sequence-padding-step`](./rules/compat/no-brace-sequence-padding-step.md) | SC3009 (brace expansion, under `-s sh` only) | Under `-s sh` shellcheck reports every brace expansion, a plain `{1..3}` and `{a,b}` too, as undefined in POSIX sh. Under `-s bash` it reports nothing, so a Bash older than 4.0 gets no finding for the padded and stepped forms. The rule reports only those two forms, for a Bash version you name. |
82
+ | [`sh/compat/no-case-fallthrough`](./rules/compat/no-case-fallthrough.md) | SC2127 (an error, not an SC3xxx check) | Under `-s sh` shellcheck asks for a bash or ksh shebang; under `-s bash` it reports nothing. The rule reports `;&` and `;;&` for a Bash version you name. |
83
+ | [`sh/compat/no-epoch-variables`](./rules/compat/no-epoch-variables.md) | SC3028 (`$EPOCHSECONDS`, `$EPOCHREALTIME`, under `-s sh` only) | Under `-s bash` shellcheck reports nothing, so a Bash older than 5.0 gets no finding. The rule reports them for a Bash version you name. |
84
+ | [`sh/compat/no-append-all-redirect`](./rules/compat/no-append-all-redirect.md), [`no-parameter-transformation`](./rules/compat/no-parameter-transformation.md), [`no-printf-time-format`](./rules/compat/no-printf-time-format.md), [`no-nounset-empty-array`](./rules/compat/no-nounset-empty-array.md) | none | shellcheck reports nothing for `&>>`, `${v@op}`, `printf '%(…)T'` or an empty array under `set -u`, under `-s sh` or `-s bash` (it reports only the array syntax, SC3030 and SC3054, under `-s sh`). |
85
+ | [`sh/compat/no-lastpipe-semantics`](./rules/compat/no-lastpipe-semantics.md) | SC2030, SC2031 | shellcheck explains the subshell effect of lastpipe; the rule reports `shopt -s lastpipe` on a Bash that lacks it. |
86
+ | [`sh/shebang-matches-dialect`](./rules/shebang-matches-dialect.md) | SC3xxx | shellcheck judges the syntax against the shebang. This rule compares the shebang with your configured dialect, a different question. |
87
+ | [`sh/require-strict-mode`](./rules/require-strict-mode.md) | none directly (SC2164, an unchecked `cd`, is one consequence) | shellcheck has no check that requires strict mode. |