@eslint-sh/eslint-plugin 0.5.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,169 @@
1
+ # `sh/no-secret-flags`
2
+
3
+ Disallow secret-shaped command-line flags, such as `--password=` or `--token`, with a literal value.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ----------------------------------------- | ------------ | --------- |
7
+ | `recommended` (warn), `agent-authored` (error) | not fixable | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ A command's arguments are visible to every user on the machine: `ps` and `/proc/<pid>/cmdline`
12
+ show them while the command runs. A secret written as a literal flag value is also committed with
13
+ the script. This rule reports a flag whose name looks like it carries a secret when its value is
14
+ written literally.
15
+
16
+ The rule inspects the arguments of every simple command (`CallExpr`), after the command word. It
17
+ recognizes two forms:
18
+
19
+ - One word: `--token=abc`. The flag name is the text before the first `=`, and the value is the
20
+ rest. An empty value (`--token=`) passes nothing and is not reported.
21
+ - Two words: `--token abc`. A flag word without `=` takes the next word as its value when that word
22
+ is static, non-empty, and doesn't start with `-`. So a flag followed by another flag
23
+ (`--auth --verbose`), by `-` (the usual "read from stdin" marker), or by nothing is valueless and
24
+ not reported.
25
+
26
+ A value is literal only when its whole word is static. Any parameter expansion, command
27
+ substitution, or other dynamic part makes the value fine: `--token="$TOKEN"` and
28
+ `--password=$(cat f)` are the correct pattern. Static-ness is decided by the plugin's shared word
29
+ resolution (`ShellSourceCode#resolveWordText`), so quoting that resolves to fixed text
30
+ (`"--password"='hunter2'`) is still literal.
31
+
32
+ Only command arguments are matched. Assignment values (`X="--token=abc"`, `export`, `local`),
33
+ heredoc bodies, here-strings, `[[ … ]]` operands, and `case` patterns are never reported. Commands
34
+ behind wrappers are still checked, because their flags are arguments of the same command:
35
+ `sudo mysql --password=hunter2` reports.
36
+
37
+ This is a heuristic screen on flag names, not secret detection. The default pattern matches a
38
+ flag whose name, after its dashes, is a secret word: `password`, `passwd`, `token`, `secret`,
39
+ `api-key` (or `apikey`), `auth`, `authkey`, or `authorization`. The word may follow an `access-`
40
+ or `auth` qualifier (`--access-token`, `--auth-token`, `--authtoken`). It may be followed by a
41
+ digit (`--password2`, `--token1`) or by another `-segment` (`--secret-key`,
42
+ `--authorization-token`). The pattern leaves two kinds of name alone:
43
+
44
+ - Names ending in `-stdin` or `-file`, such as `--password-stdin`, `--password-file`, and
45
+ `--token-file`. These flags read the secret from stdin or from a file, which are the alternatives
46
+ this rule recommends. In `docker login --password-stdin registry.example.com`, the next word is a
47
+ positional argument, not the flag's value.
48
+ - Names with a letter directly after the secret word, such as `--author` (`auth` + `or`),
49
+ `--tokens`, and `--tokenizer` (`token` + a letter). These are longer words that only start
50
+ with a secret word.
51
+
52
+ Other qualifiers aren't matched, so `--my-token` is not reported. A general prefix would also
53
+ match negations such as `--no-password`, which take no value. Add a project's own flag names
54
+ with the `patterns` option.
55
+
56
+ Expect some false positives: a name such as `--token-type` still matches. Suppress a single false
57
+ positive with a disable comment that says why, or narrow the `patterns` option for your project.
58
+
59
+ ## Options
60
+
61
+ - `patterns` (`string[]`, at least one entry). Regular-expression sources tested against each flag's
62
+ name, without `=` or the value. They **replace** the default list. Each source is compiled
63
+ case-insensitively, like the default. An invalid source fails config validation with an error that
64
+ names the rule and the source.
65
+
66
+ Default: `["^(?!.*-(?:stdin|file)$)--?(?:access-|auth-?)?(?:password|passwd|token|secret|api-?key|auth(?:key|orization)?)(?![a-z])"]`.
67
+
68
+ ```js
69
+ // eslint.config.js
70
+ {
71
+ rules: {
72
+ 'sh/no-secret-flags': [
73
+ 'error',
74
+ // Only these exact names, plus a project-specific flag.
75
+ { patterns: ['^--?(password|token|api-?key)$', '^--vault-pin$'] },
76
+ ],
77
+ },
78
+ }
79
+ ```
80
+
81
+ ### Patterns run against script-controlled text
82
+
83
+ `patterns` is tested against every flag name the linted script writes, and the script's author
84
+ chooses that text. When you lint scripts you do not control, a pattern with nested or overlapping
85
+ quantifiers can make the regular expression engine backtrack for a time that grows exponentially
86
+ with the name's length. Write anchored patterns with one level of repetition over a character class.
87
+
88
+ The rule rejects the most common accident: a pattern that repeats a group which itself repeats
89
+ (`*`, `+`, `{n,}` or `{n,m}` with `m > 1` applied to such a group). It is not a proof that an
90
+ accepted pattern is safe. Review every pattern you accept as if the guard were not there.
91
+
92
+ **What it does not catch.** These pass the check and can still be slow on a long name. Timings are
93
+ from Node 24 on a busy laptop and vary by machine; the shape of the growth is what matters.
94
+
95
+ - Alternation branches that overlap under a repetition grow exponentially. `^([A-Z]|[A-Z0-9])+_KEY$`
96
+ took about 0.4 s on a 24-character name, 1.2 s at 26 and 4.7 s at 28. `(a|a)+$` took 1.6 s at
97
+ 24 characters and 6.4 s at 26. `^(A|AA)*$` is the same shape.
98
+ - Adjacent quantifiers that can match the same text grow polynomially. `.*.*.*x` took 0.5 s on a
99
+ 255-character name and 0.9 s at 300; each extra `.*` raises the exponent. Nothing bounds the length
100
+ of a name or flag in a script, so a long enough one is slow.
101
+
102
+ **What it rejects that is in fact linear.** The check does not look at what the repeated group can
103
+ match, so it also refuses some patterns that are safe, such as `(a*b)*`,
104
+ `^(?:[a-z]+-)*[a-z]+$` and `^(?:SECRET|TOKEN|PASSWORD)(?:_[A-Z]+)*$`. A rewrite without the nesting
105
+ exists for some of these and not for others, so if you cannot rewrite a pattern exactly, loosen it to
106
+ the character class you mean. The last one can be rewritten exactly with a lookahead
107
+ (it accepts the same names; compared on every suffix of up to seven characters over `A`, `_`, `a`
108
+ and `1` after `SECRET`, `TOKEN` and `PASSWORD`):
109
+
110
+ ```js
111
+ { patterns: ['^(?:SECRET|TOKEN|PASSWORD)(?!.*_(?:_|$))(?:_[A-Z][A-Z_]*)?$'] }
112
+ ```
113
+
114
+
115
+ **When you see the error.** The error is thrown from the rule's `create()`, which ESLint calls for
116
+ each file it lints, so the check does not run when the config is loaded. ESLint stops the run at the
117
+ first file it lints with the rule enabled and prints "Error while loading rule" followed by
118
+ "Occurred while linting <that file>" and a stack trace, and exits with code 2. A run that lints no
119
+ file with the rule enabled never sees it.
120
+
121
+ The accident the check is for, and its fix:
122
+
123
+ ```js
124
+ // Rejected: `([A-Z]+_?)*` repeats a group that already repeats, so a long
125
+ // name that does not match can take seconds.
126
+ { patterns: ['^--([A-Z]+_?)*_KEY$'] }
127
+
128
+ // Safe rewrite: one repetition over a character class.
129
+ { patterns: ['^--[A-Z0-9_]*_KEY$'] }
130
+ ```
131
+
132
+ ## Examples
133
+
134
+ ### Incorrect
135
+
136
+ <!-- example: {"findings": 3} -->
137
+ ```sh
138
+ curl --token=abc123 https://api.example.com # literal value in the same word
139
+ mysql --user=app --password hunter2 # literal value in the next word
140
+ sudo deploy --api-key='k-123' # quoting doesn't make it dynamic
141
+ ```
142
+
143
+ ### Correct
144
+
145
+ ```sh
146
+ curl --token="$API_TOKEN" https://api.example.com # dynamic value
147
+ mysql --password="$(pass show db/app)" # command substitution
148
+ printf '%s' "$DB_PASSWORD" | db-cli --password - # read from stdin
149
+ printf '%s' "$REG_TOKEN" | docker login --password-stdin reg.example.com # a -stdin flag
150
+ db-cli --password-file=/run/secrets/db # a -file flag names a file
151
+ git commit --author="Jane <jane@example.com>" # --author is not --auth
152
+ mytool --auth --verbose # valueless flag
153
+ DB_PASSWORD="$DB_PASSWORD" db-cli # passed through the environment
154
+ X="--token=abc" # not a command argument
155
+ ```
156
+
157
+ ## When not to use it
158
+
159
+ Turn it off when literal flag values in your scripts are never real secrets, for example
160
+ throwaway fixtures for a local test service, or when another secret scanner already covers the same
161
+ files and you don't want duplicate reports.
162
+
163
+ ## Relationship to shellcheck
164
+
165
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
166
+ layer. Run shellcheck beside it.
167
+
168
+ shellcheck has no equivalent check. It reports shell mistakes, not credential handling, and it has
169
+ no way to configure which flag names your project treats as secrets. This rule enforces that policy.
@@ -0,0 +1,146 @@
1
+ # `sh/no-unchecked-cd`
2
+
3
+ Require the failure of `cd`, `pushd` and `popd` to be checked, so a script never carries on in the wrong directory.
4
+
5
+ | Configs | Fixable | Languages |
6
+ | ------------------------------- | ---------------------------- | --------- |
7
+ | `recommended`, `agent-authored` | suggestions (append `\|\| exit`) | `sh/sh` |
8
+
9
+ ## Rule details
10
+
11
+ When `cd` fails, the script keeps running in the directory it was already in, and every relative
12
+ path after it silently means something else. `cd "$build"; rm -rf *` is the classic disaster.
13
+ `pushd` and `popd` change the directory (and the directory stack) and fail the same way.
14
+
15
+ A directory-changing command is **checked** when its exit status decides what runs next:
16
+
17
+ - it is the left side of `&&` or `||` (`cd x || exit`, `cd x || return`, `cd x && make`);
18
+ - it is the last command of an `if`, `elif`, `while` or `until` condition;
19
+ - it is the right side of a list, or the last command of a `{ …; }` group or `( … )` subshell,
20
+ whose own status is checked in one of these ways (`a && cd x || exit`, `{ cd x; } || exit`).
21
+
22
+ Everything else reports: a bare `cd`, `cd` in a function, loop or `case` body, a pipeline stage, a
23
+ background job, and `cd` inside a subshell (`( cd x; make )`). The subshell keeps the script's own
24
+ directory intact, but the commands after a failed `cd` inside it still run in the wrong place, so
25
+ it must be checked there too. A negated `! cd x` is only as checked as the position it sits in.
26
+
27
+ The command is recognized after transparent wrappers, so `builtin cd x` and `command cd x` are
28
+ read as `cd`.
29
+
30
+ **Files running under `set -e`.** A file whose prologue enables `errexit` stops at a failed `cd`
31
+ by itself, so the rule is quiet there. "Prologue" is read exactly as
32
+ [`require-strict-mode`](./require-strict-mode.md) reads it: the first five top-level statements,
33
+ tracked in order, so a later `set +e` undoes an earlier `set -e`, and a `set -e` outside that
34
+ window does not count. Set `ignoreWhenErrexit` to `false` to report anyway.
35
+
36
+ **Files run by bats.** With `languageOptions.dialect` set to `"bats"`, a bare `cd` (or `pushd` or
37
+ `popd`) written directly in the body of a `@test` block, or of a top-level `setup`, `teardown`,
38
+ `setup_file` or `teardown_file` function, is not reported: bats runs those bodies with `errexit`
39
+ on, so a failed `cd` fails the test. The same holds for a `cd` directly in a `( … )` subshell that
40
+ is itself a statement of such a body (`( cd "$dir"; make )`), because the subshell inherits
41
+ `errexit` and its failing status fails the test. A `$( … )` command substitution does not count: it
42
+ is reported as before.
43
+
44
+ The rule reads the body in order. After a plain `set +e` (or `set +o errexit`) in the same body,
45
+ `errexit` is off, and a later `cd` is reported again, because the test carries on after the failed
46
+ `cd`. A later plain `set -e` turns it back on, so `set +e; set -e; cd x` is not reported while
47
+ `set +e; set -e; set +e; cd x` is. A `cd` written before the `set +e` is still covered.
48
+
49
+ What the rule cannot tell: a `set +e` (or `shopt`, `eval 'set +e'`, `set "$flags"`) that is nested
50
+ in an `if`, loop or function, that lives in a file the test `load`s or `source`s or in a helper it
51
+ calls, or that ran in `setup` before the test starts. It reads each body on its own, assuming
52
+ `errexit` starts on, so none of these are seen, and a `cd` after one of them is not reported.
53
+ For example, bats runs `setup` in the same shell as the test, so after
54
+ `setup() { set +e; }` a bare `cd` in a `@test` has no `errexit` either, yet it is not reported.
55
+ Likewise a `cd` nested in an `if`, loop or `case` inside the body, a negated `! cd x`, a `cd` in
56
+ any other function of the same file, and a `cd` in a file read under another dialect (a `.sh` file)
57
+ are reported as usual. `run cd x` is not reported either, as before: `run` is not one of the
58
+ wrappers this rule follows, so the command it sees is `run`.
59
+
60
+ **Suggestion.** The editor suggestion appends ` || exit` after the command and its redirections.
61
+
62
+ <!-- transform: {"kind": "suggestion"} -->
63
+
64
+ ```sh
65
+ cd "$build" > /dev/null
66
+ # becomes
67
+ cd "$build" > /dev/null || exit
68
+ ```
69
+
70
+ It is not offered where appending would change the meaning: when the `cd` is already part of an
71
+ `&&`/`||` list or a pipeline, negated, in the background, or followed by a here-document.
72
+
73
+ The rule reads `languageOptions.dialect` only for the bats exemption above; no other option affects it.
74
+
75
+ ## Options
76
+
77
+ | Option | Type | Default | Effect |
78
+ | ------------------- | --------- | ------- | --------------------------------------------------------------------------- |
79
+ | `ignoreWhenErrexit` | `boolean` | `true` | Skip files whose prologue enables `errexit` (for example `set -euo pipefail`). |
80
+
81
+ ```js
82
+ // eslint.config.js
83
+ {
84
+ rules: {
85
+ 'sh/no-unchecked-cd': ['error', { ignoreWhenErrexit: false }],
86
+ },
87
+ }
88
+ ```
89
+
90
+ ## Examples
91
+
92
+ ### Incorrect
93
+
94
+ <!-- example: {"findings": 5} -->
95
+
96
+ ```sh
97
+ cd "$build" # a failure goes unnoticed
98
+ rm -rf ./* # ...and this runs in the wrong directory
99
+
100
+ pushd /tmp # pushd and popd fail the same way
101
+ popd
102
+
103
+ ( cd "$dir"; make ) # inside a subshell, still unchecked
104
+
105
+ f() {
106
+ cd "$1" # a function body does not check it
107
+ }
108
+ ```
109
+
110
+ ### Correct
111
+
112
+ ```sh
113
+ cd "$build" || exit 1
114
+ cd "$build" || return
115
+ cd "$build" && make
116
+
117
+ if cd "$build"; then make; fi
118
+ while ! cd "$build"; do sleep 1; done
119
+
120
+ ( cd "$dir" || exit; make )
121
+ ```
122
+
123
+ With `errexit` in the prologue (the first five top-level statements), a bare `cd` is fine:
124
+
125
+ ```sh
126
+ set -euo pipefail
127
+ cd "$build"
128
+ ```
129
+
130
+ ## When not to use it
131
+
132
+ Turn the rule off when every script already starts with `set -e` and you would rather not think
133
+ about the distinction (the default `ignoreWhenErrexit` already covers that), or when a project
134
+ deliberately tolerates a failed `cd`, such as `cd "$dir" 2>/dev/null; true`. Prefer writing the
135
+ `|| true` so the intent is explicit.
136
+
137
+ ## Relationship to shellcheck
138
+
139
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
140
+ layer. Run shellcheck beside it.
141
+
142
+ shellcheck overlaps with SC2164 ("Use `cd ... || exit` in case `cd` fails"), and this rule
143
+ enforces the same practice. It exists anyway because it offers a suggestion, covers `pushd` and
144
+ `popd`, can be turned off per file by `set -e` detection that follows the same prologue reading as
145
+ `require-strict-mode`, and is part of the shipped presets, so the policy applies without running
146
+ shellcheck. If you already run shellcheck with SC2164 enabled, the two agree on the common cases.
@@ -0,0 +1,141 @@
1
+ # `sh/no-undeclared-commands`
2
+
3
+ Require every external command a script uses to be declared, so that a script's dependencies
4
+ are stated up front instead of discovered when a command is missing.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ----------- | --------- |
8
+ | `agent-authored` (inert until a declaration convention is bound) | not fixable | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ A script depends on every external command it runs, and any of them can be missing on the
13
+ machine it lands on. When your project states each script's dependencies, this rule keeps that
14
+ statement complete: it reports each command the script uses but does not declare.
15
+
16
+ The rule needs a **declaration convention**, which says how your project declares commands. Bind
17
+ one with `settings.sh.declarationConvention`. The plugin ships one, `commentPragma`, which reads
18
+ `# uses: jq curl` comments from a script's header. See
19
+ [Declaration conventions](../declaration-conventions.md) to write your own. **With no convention
20
+ bound, the rule reports nothing**, so turning it on is safe until your project picks a convention.
21
+
22
+ A command is **used** when the script runs it by a name known from the source. The rule looks
23
+ at every simple command in the file, wherever it is: the top level, function bodies, loops,
24
+ conditionals, pipelines, and command substitutions (`$(…)` and backquotes) in any position,
25
+ including arguments, assignment values, redirect targets, `for` lists, and `case` subjects.
26
+
27
+ - **Wrappers are followed to the command they run.** `sudo`, `env`, `nohup`, `nice`, `timeout`,
28
+ `time`, `command`, and `exec` run the command named after them, so that command is used. A
29
+ wrapper is also used itself when it is an external command: `sudo rsync -a . host:` uses both
30
+ `sudo` and `rsync`. `command` and `exec` are builtins, so `exec rsync …` uses only `rsync`.
31
+ `time` is a keyword in bash, so `time rsync …` uses only `rsync` there; in a `posix` file it
32
+ is the `time` utility and is used too. `command -v jq` only looks `jq` up, so it does not use
33
+ `jq`. In `bats` files, bats' `run` is a wrapper too, with its options (`-N`,
34
+ `!`, `--keep-empty-lines`, `--separate-stderr`, `--`), so `run ! curl x` uses `curl`.
35
+ - **Builtins don't count.** The builtin list depends on `languageOptions.dialect`. `bash` and
36
+ `bats` files use the bash builtins (`cd`, `echo`, `local`, `mapfile`, `source`, …). `bats`
37
+ files also leave out the helpers bats-core defines for every test file: `run`, `load`, `skip`,
38
+ `bats_pipe`, `bats_load_safe`, `bats_load_library`, `bats_require_minimum_version`, and
39
+ `bats_test_function`. Helpers from separate libraries, such as bats-assert's
40
+ `assert_success`, are not bats-core's and are still reported. `posix`
41
+ files use the POSIX special and regular built-ins plus `echo`, `printf`, `test` and `[`. So
42
+ `mapfile` is a command in a `posix` file. `mksh` and `zsh` files use the POSIX list for now,
43
+ so their own builtins (`print`, `whence`, `setopt`, …) are reported until they get lists of
44
+ their own.
45
+ - **Functions defined in the file don't count**, wherever they are defined, including after the
46
+ call or inside another function. A function named like a command (`curl() { … }`) shadows it.
47
+ - **Dynamic command words don't count.** `"$cmd" args`, `${tool} x`, and `$(pick) x` name no
48
+ command the rule can know. `sh/no-dynamic-invocation` reports them. A static wrapper in front
49
+ of one is still used: `sudo "$prog"` uses `sudo`.
50
+ - **Commands inside strings don't count.** `bash -c 'jq .'` uses `bash`, not `jq`.
51
+
52
+ Each undeclared command is reported once (message `undeclaredCommand`), at the word that names
53
+ it in its first use. The message says how to declare it, in the convention's own syntax when
54
+ the convention has a `declarationHint`. With `commentPragma`:
55
+
56
+ ```text
57
+ 'curl' is used but not declared. Add `# uses: curl` on a line below the shebang.
58
+ ```
59
+
60
+ In a file without a shebang, the message says to add the line as the first line of the file
61
+ instead. A convention without a hint ends the message with the generic sentence
62
+ "Declare it with the project's declaration convention." A command run by path (`./build.sh`) is reported by that path.
63
+
64
+ ## Options
65
+
66
+ - `transparentWrappers` (`string[]`, default `[]`): names of your project's own wrapper
67
+ commands, added to the built-in wrappers above. The word after a listed wrapper is the command
68
+ it runs, so with `['with-retry']`, `with-retry curl x` uses both `with-retry` and `curl`. A
69
+ listed wrapper takes no flags: in `with-retry -n 3 curl`, the rule stops at `-n` and does not
70
+ count `curl`. If the wrapper is a function defined in the same file, only the wrapped command
71
+ needs declaring. Your network wrapper doesn't need listing here when it is set in
72
+ `settings.sh.networkWrapper`: this rule looks through it, with the options its spec declares
73
+ (see [`sh/require-network-wrapper`](./require-network-wrapper.md#declaring-the-wrapper-once-settingsshnetworkwrapper)).
74
+
75
+ The convention is not a rule option. Bind it in `settings`, because ESLint copies rule options
76
+ with `structuredClone`, which cannot copy a convention's methods.
77
+
78
+ ```js
79
+ // eslint.config.js
80
+ import sh, { commentPragma } from '@eslint-sh/eslint-plugin';
81
+
82
+ export default [
83
+ {
84
+ files: ['**/*.sh'],
85
+ plugins: { sh },
86
+ language: 'sh/sh',
87
+ settings: { sh: { declarationConvention: commentPragma } },
88
+ rules: {
89
+ 'sh/no-undeclared-commands': ['error', { transparentWrappers: ['with-retry'] }],
90
+ },
91
+ },
92
+ ];
93
+ ```
94
+
95
+ ## Examples
96
+
97
+ With `commentPragma` bound and `{ transparentWrappers: ['with-retry'] }`:
98
+
99
+ ### Incorrect
100
+
101
+ <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}, "findings": 6} -->
102
+ ```sh
103
+ #!/usr/bin/env bash
104
+ # uses: jq
105
+ curl -s "$URL" | jq . # curl is not declared
106
+ echo "built $(date +%s)" # date, inside $(…), is not declared
107
+ sudo systemctl restart app # neither sudo nor systemctl is declared
108
+ with-retry git pull # git, run by the wrapper, is not declared (nor is with-retry)
109
+ ```
110
+
111
+ ### Correct
112
+
113
+ <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}} -->
114
+ ```sh
115
+ #!/usr/bin/env bash
116
+ # uses: curl jq date
117
+ log() { printf '%s\n' "$*"; } # a function defined here, using only builtins
118
+ log "fetching"
119
+ curl -s "$URL" | jq .
120
+ echo "built $(date +%s)"
121
+ cd /tmp && read -r answer # builtins need no declaration
122
+ "$cmd" --version # a dynamic command word: see sh/no-dynamic-invocation
123
+ command -v docker # looks docker up without running it
124
+ ```
125
+
126
+ ## When not to use it
127
+
128
+ Turn the rule off when your project doesn't track script dependencies, or tracks them somewhere
129
+ this rule can't read, such as a container image or a system package list. It is also noisy for
130
+ scripts that run many repository-local programs by path, since each path is reported until it is
131
+ declared.
132
+
133
+ ## Relationship to shellcheck
134
+
135
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
136
+ layer. Run shellcheck beside it.
137
+
138
+ shellcheck has no equivalent. It checks how a script uses commands, not which commands the
139
+ script may depend on, and it has no notion of a project's declaration convention to compare
140
+ against. A dependency manifest is project policy, which is the kind of rule shellcheck cannot be
141
+ configured to express.
@@ -0,0 +1,216 @@
1
+ # `sh/no-unused-declarations`
2
+
3
+ Disallow declaring external commands a script never uses, so that a script's declared
4
+ dependencies match what it actually runs.
5
+
6
+ | Configs | Fixable | Languages |
7
+ | ------- | ----------------------------- | --------- |
8
+ | `agent-authored` (inert until a declaration convention is bound) | autofix (via the convention) | `sh/sh` |
9
+
10
+ ## Rule details
11
+
12
+ A declaration that outlives the command it names makes a script's stated dependencies lie. An
13
+ installer provisions a tool nothing runs, and a reviewer trusts a list that is no longer the
14
+ script's real surface. This rule reports each declared command the script never uses. It is the
15
+ converse of [`sh/no-undeclared-commands`](./no-undeclared-commands.md).
16
+
17
+ The rule needs a **declaration convention**, which says how your project declares commands. Bind
18
+ one with `settings.sh.declarationConvention`. The plugin ships one, `commentPragma`, which reads
19
+ `# uses: jq curl` comments from a script's header. See
20
+ [Declaration conventions](../declaration-conventions.md) to write your own. **With no convention
21
+ bound, the rule reports nothing**, so turning it on is safe until your project picks a convention.
22
+
23
+ The fix deletes declarations, so the rule only reports a name it is sure the script does not
24
+ run. That makes it more cautious than `sh/no-undeclared-commands`, which reports in the other
25
+ direction.
26
+
27
+ **Incomplete usage: the rule reports nothing in a file that may run a command it cannot name.**
28
+ If any command in the file is only known at run time, that command could be any declared name,
29
+ so no declaration in the file is reported. These are the commands
30
+ [`sh/no-dynamic-invocation`](./no-dynamic-invocation.md) reports. Enable it to find them and
31
+ replace them with literal commands:
32
+
33
+ - a command word that is an expansion: `"$cmd" x`, `"$@"`, a loop variable
34
+ (`for t in jq curl; do "$t" --version; done`), `"${tools[@]}"`, or `$(pick-tool) x`. This
35
+ also applies behind a wrapper (`sudo "$prog"`) or behind a wrapper flag the rule doesn't
36
+ recognize;
37
+ - a command string run by a shell: `bash -c '…'`, `sh -c '…'`, or `env -S '…'`, also when the
38
+ shell or `env` is run by path (`/bin/bash -c '…'`, `/usr/bin/env -S '…'`);
39
+ - a shell given a command string as the argument of another command: a word after the command
40
+ name that is a shell (`bash`, `sh`, `dash`, `ksh`, `mksh` or `zsh`, by name or by path),
41
+ followed later by an option cluster containing `c` (`-c`, `-ec`, `-xc`). This covers
42
+ `xargs sh -c '…'`, `find . -exec sh -c '…' \;`, `setsid -f bash -c '…'` and
43
+ `busybox sh -c '…'`. A shell name with no `-c` after it, as in `xargs echo sh`, is fine;
44
+ - `eval`;
45
+ - sourcing another file: `. ./lib.sh` or `source ./lib.sh`;
46
+ - a shell (`bash`, `sh`, `dash`, `ksh`, `mksh` or `zsh`, by name or by path) run with no script
47
+ operand and no `-c`, which reads its code from standard input: `curl -fsSL x | bash`,
48
+ `bash < script`, `bash -s`. A shell given a script, as in `bash script.sh`, is fine;
49
+ - a `trap` whose action is a code string: `trap 'rm -f "$tmp"' EXIT`. A trap whose action is a
50
+ plain name, such as `trap cleanup EXIT`, is fine. So are `trap '' INT` (ignore) and
51
+ `trap - EXIT` (reset).
52
+
53
+ **Used.** Otherwise, a declared name is used when any of the following holds:
54
+
55
+ - **The script runs it as a command.** The rule counts commands exactly as
56
+ `sh/no-undeclared-commands` does, so a command that rule counts as used is never reported
57
+ here. That covers commands anywhere in the file: the top level, loops, conditionals,
58
+ pipelines, and command substitutions (`$(…)` and backquotes) in any position. It also covers
59
+ function bodies, including a function that is never called. That is deliberately cautious: the
60
+ rule doesn't decide which functions run.
61
+ - **The script runs it through a wrapper.** `sudo rsync -a . host:` uses both `sudo` and
62
+ `rsync`, and in `bats` files `run curl x` uses `curl`. Your own wrappers count once you list
63
+ them in `transparentWrappers` (below).
64
+ - **It is a whole argument word of any command.** Many commands run their arguments, so the
65
+ name counts as used when an argument's full static text is that name: `xargs jq .`,
66
+ `find . -exec jq . {} \;`, `watch jq . f`, `command -v jq`. Part of a word doesn't count:
67
+ `echo jqfilter` doesn't use `jq`.
68
+ - **A path to it is used.** A command or argument word written as a path also uses its basename:
69
+ `/usr/bin/jq` and `./bin/jq` use `jq`. A declared path, such as `./bin/jq`, is used only by
70
+ that exact path.
71
+
72
+ Not counted:
73
+
74
+ - **Builtins and functions defined in the file are not used commands.** Declaring one is
75
+ reported unless the name also appears as an argument word. `# uses: cd` is stale even in a
76
+ script that runs `cd`. Which names are builtins depends on `languageOptions.dialect`; see
77
+ [`sh/no-undeclared-commands`](./no-undeclared-commands.md#rule-details) for the lists.
78
+ - **Commands run on another host are not counted.** In `ssh host 'jq . f'`, the remote host
79
+ runs `jq` and this script doesn't, so `jq` is not used. Written as separate words,
80
+ `ssh host jq . f` makes `jq` an argument word, and that does count.
81
+
82
+ Each declaration of an unused command is reported once. A name declared twice and never used is
83
+ reported at both declarations. Where the report goes depends on where the convention says the
84
+ declaration lives (see [where a declaration lives](../declaration-conventions.md#the-interface)):
85
+
86
+ - **In the script** (message `unusedDeclaration`): at the text that declares it. With
87
+ `commentPragma`, that is the name in the `# uses:` line.
88
+ - **In another file** (message `unusedFileDeclaration`) or **from a source described in words**, such as a
89
+ list in the config or a registry snapshot (message `unusedDescribedDeclaration`): the message names
90
+ the source, as `path:line` (or just `path`) or as the source's description, for example
91
+ `'git' is declared in scripts/deploy.commands:3 but this script never uses it. Remove it there
92
+ so the script's stated dependencies match what it runs.` ESLint can only report inside the file
93
+ it lints, so these reports sit at line 1, column 1, with no end: a position for the file as a
94
+ whole, not for any text in it. The message says where to look.
95
+
96
+ **No comment silences a report about a declaration outside the script when the script starts
97
+ with a shebang.** The shebang must be the whole first line, so `#!/bin/sh # eslint-disable-line …`
98
+ is not a directive, and a directive on line 2 (`# eslint-disable-next-line …` or
99
+ `# eslint-disable …`) covers only later lines. (Without a shebang, a `# eslint-disable …` comment
100
+ on line 1 works.) To silence one, do one of these:
101
+
102
+ - fix the declaration in the file or source the message names;
103
+ - turn the rule off for those scripts with a `files` override:
104
+ `{ files: ['scripts/legacy/*.sh'], rules: { 'sh/no-unused-declarations': 'off' } }`;
105
+ - record the current reports with ESLint's bulk suppressions,
106
+ `eslint --suppress-rule sh/no-unused-declarations`. Later runs hide them while a file has no
107
+ more reports of the rule than were recorded.
108
+
109
+ See [silencing a report about a declaration outside the
110
+ script](../declaration-conventions.md#silencing-a-report-about-a-declaration-outside-the-script).
111
+
112
+ ### Autofix
113
+
114
+ Only the convention knows its own syntax, so the convention makes the fix. When the bound
115
+ convention provides a `removeDeclaration` hook, each report about a declaration in the script
116
+ carries the edit that hook returns. When it has no hook, or the hook returns `null` for a
117
+ declaration, that report has no fix. A declaration in another file or a described source never has a
118
+ fix: an ESLint fix can only edit the script being linted.
119
+
120
+ With `commentPragma`, the fix removes the name and the space before or after it, so
121
+ `# uses: jq curl` becomes `# uses: jq` when only `jq` is used:
122
+
123
+ <!-- transform: {"kind": "fix", "convention": "commentPragma"} -->
124
+
125
+ ```sh
126
+ # uses: jq curl
127
+ jq .
128
+ # becomes
129
+ # uses: jq
130
+ jq .
131
+ ```
132
+
133
+ When the last name on a line goes,
134
+ the whole line goes with its `\n` or `\r\n`. When several names on one line are unused, their
135
+ edits touch, so ESLint applies one per pass. `eslint --fix` runs passes until nothing changes, so
136
+ a line whose every name is unused is removed entirely.
137
+
138
+ ## Options
139
+
140
+ - `transparentWrappers` (`string[]`, default `[]`): names of your project's own wrapper
141
+ commands, added to the built-in wrappers. The word after a listed wrapper is the command it
142
+ runs. With `['with-retry']`, `with-retry curl x` runs `curl`, and `with-retry "$tool"` runs a
143
+ command the rule can't name, so the file is incomplete usage and reports nothing. A listed
144
+ wrapper takes no flags, so `with-retry -n 3 curl` is incomplete usage too: the rule stops at
145
+ `-n` and can't tell what runs. Give this option the same value as
146
+ `sh/no-undeclared-commands`' option of the same name, or the two rules will disagree about what
147
+ the script uses. Your network wrapper doesn't need listing here when it is set in
148
+ `settings.sh.networkWrapper`: this rule looks through it, with the options its spec declares
149
+ (see [`sh/require-network-wrapper`](./require-network-wrapper.md#declaring-the-wrapper-once-settingsshnetworkwrapper)).
150
+
151
+ The convention is not a rule option. Bind it in `settings`, because ESLint copies rule options
152
+ with `structuredClone`, which cannot copy a convention's methods.
153
+
154
+ ```js
155
+ // eslint.config.js
156
+ import sh, { commentPragma } from '@eslint-sh/eslint-plugin';
157
+
158
+ const wrappers = { transparentWrappers: ['with-retry'] };
159
+
160
+ export default [
161
+ {
162
+ files: ['**/*.sh'],
163
+ plugins: { sh },
164
+ language: 'sh/sh',
165
+ settings: { sh: { declarationConvention: commentPragma } },
166
+ rules: {
167
+ 'sh/no-undeclared-commands': ['error', wrappers],
168
+ 'sh/no-unused-declarations': ['error', wrappers],
169
+ },
170
+ },
171
+ ];
172
+ ```
173
+
174
+ ## Examples
175
+
176
+ With `commentPragma` bound and `{ transparentWrappers: ['with-retry'] }`:
177
+
178
+ ### Incorrect
179
+
180
+ <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}, "findings": 3} -->
181
+ ```sh
182
+ #!/usr/bin/env bash
183
+ # uses: jq curl docker # docker is never run
184
+ # uses: cd # cd is a builtin, never a declared dependency
185
+ # uses: lint # lint is a function defined below
186
+ lint() { shellcheck "$@"; }
187
+ curl -s "$URL" | jq .
188
+ ```
189
+
190
+ ### Correct
191
+
192
+ <!-- example: {"convention": "commentPragma", "options": {"transparentWrappers": ["with-retry"]}} -->
193
+ ```sh
194
+ #!/usr/bin/env bash
195
+ # uses: jq curl git sudo date with-retry
196
+ with-retry curl -s "$URL" | jq . # curl runs through the configured wrapper
197
+ sudo git pull # git runs through sudo
198
+ echo "built $(date +%s)" # date is used inside $(…)
199
+ ```
200
+
201
+ ## When not to use it
202
+
203
+ Turn the rule off when your project doesn't track script dependencies, or when its declarations
204
+ deliberately list more than the script runs, such as tools a child script needs. The rule only
205
+ reads the file it lints, so a command that only `bash child.sh` or `./child.sh` runs reads as
206
+ unused here.
207
+
208
+ ## Relationship to shellcheck
209
+
210
+ eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
211
+ layer. Run shellcheck beside it.
212
+
213
+ shellcheck has no equivalent. It checks how a script uses commands, not which commands the
214
+ script says it depends on, and it has no notion of a project's declaration convention to compare
215
+ against. Keeping a dependency manifest honest is project policy, which is the kind of rule
216
+ shellcheck cannot be configured to express.