@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.
- package/README.md +223 -17
- package/dist/command-harvest/builtins/bash.d.ts +36 -0
- package/dist/command-harvest/builtins/bash.js +98 -0
- package/dist/command-harvest/builtins/bats.d.ts +45 -0
- package/dist/command-harvest/builtins/bats.js +33 -0
- package/dist/command-harvest/builtins/index.d.ts +18 -0
- package/dist/command-harvest/builtins/index.js +39 -0
- package/dist/command-harvest/builtins/posix.d.ts +51 -0
- package/dist/command-harvest/builtins/posix.js +93 -0
- package/dist/command-harvest/harvest-commands.d.ts +66 -0
- package/dist/command-harvest/harvest-commands.js +97 -0
- package/dist/command-harvest/wrapper-table.d.ts +24 -0
- package/dist/command-harvest/wrapper-table.js +49 -0
- package/dist/configs/agent-authored.d.ts +41 -0
- package/dist/configs/agent-authored.js +33 -0
- package/dist/configs/bash-3-2.d.ts +28 -0
- package/dist/configs/bash-3-2.js +50 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +52 -0
- package/dist/configs/recommended.d.ts +52 -0
- package/dist/configs/recommended.js +34 -0
- package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
- package/dist/declaration-conventions/comment-pragma.js +153 -0
- package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
- package/dist/declaration-conventions/declaration-convention.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
- package/dist/declaration-conventions/read-declared-commands.js +128 -0
- package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
- package/dist/eslint-plugin-syntax.d.ts +27 -0
- package/dist/eslint-plugin.d.ts +1211 -52
- package/dist/index.d.ts +56 -8
- package/dist/index.js +53 -6
- package/dist/language/command-resolution.d.ts +124 -0
- package/dist/language/command-resolution.js +120 -0
- package/dist/language/end-of-input-marker.d.ts +29 -0
- package/dist/language/end-of-input-marker.js +88 -0
- package/dist/language/line-breaks.d.ts +18 -0
- package/dist/language/line-breaks.js +22 -0
- package/dist/language/lint-view-types.d.ts +104 -0
- package/dist/language/lint-view-types.js +1 -0
- package/dist/language/non-shell-file-guard.d.ts +35 -0
- package/dist/language/non-shell-file-guard.js +59 -0
- package/dist/language/selector-classes.d.ts +0 -1
- package/dist/language/selector-classes.js +0 -1
- package/dist/language/shell-language-options.d.ts +7 -1
- package/dist/language/shell-language-options.js +4 -4
- package/dist/language/shell-language.d.ts +10 -2
- package/dist/language/shell-language.js +59 -8
- package/dist/language/shell-source-code.d.ts +342 -5
- package/dist/language/shell-source-code.js +559 -30
- package/dist/language/source-coordinates.d.ts +13 -2
- package/dist/language/source-coordinates.js +68 -14
- package/dist/language/syntax-view.d.ts +60 -39
- package/dist/language/syntax-view.js +200 -45
- package/dist/language/word-resolution.d.ts +4 -3
- package/dist/language/word-resolution.js +4 -3
- package/dist/rules/compat/create-compat-rule.d.ts +59 -0
- package/dist/rules/compat/create-compat-rule.js +65 -0
- package/dist/rules/compat/declaration-options.d.ts +19 -0
- package/dist/rules/compat/declaration-options.js +110 -0
- package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
- package/dist/rules/compat/no-append-all-redirect.js +73 -0
- package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
- package/dist/rules/compat/no-associative-arrays.js +39 -0
- package/dist/rules/compat/no-case-attributes.d.ts +12 -0
- package/dist/rules/compat/no-case-attributes.js +63 -0
- package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
- package/dist/rules/compat/no-case-fallthrough.js +67 -0
- package/dist/rules/compat/no-case-modification.d.ts +35 -0
- package/dist/rules/compat/no-case-modification.js +86 -0
- package/dist/rules/compat/no-coproc.d.ts +12 -0
- package/dist/rules/compat/no-coproc.js +47 -0
- package/dist/rules/compat/no-declare-g.d.ts +12 -0
- package/dist/rules/compat/no-declare-g.js +59 -0
- package/dist/rules/compat/no-double-bracket.d.ts +12 -0
- package/dist/rules/compat/no-double-bracket.js +52 -0
- package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
- package/dist/rules/compat/no-epoch-variables.js +57 -0
- package/dist/rules/compat/no-fd-variable.d.ts +12 -0
- package/dist/rules/compat/no-fd-variable.js +48 -0
- package/dist/rules/compat/no-globstar.d.ts +12 -0
- package/dist/rules/compat/no-globstar.js +39 -0
- package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
- package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
- package/dist/rules/compat/no-let.d.ts +12 -0
- package/dist/rules/compat/no-let.js +62 -0
- package/dist/rules/compat/no-mapfile.d.ts +13 -0
- package/dist/rules/compat/no-mapfile.js +122 -0
- package/dist/rules/compat/no-nameref.d.ts +12 -0
- package/dist/rules/compat/no-nameref.js +44 -0
- package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
- package/dist/rules/compat/no-negative-array-index.js +123 -0
- package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
- package/dist/rules/compat/no-nounset-empty-array.js +158 -0
- package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
- package/dist/rules/compat/no-parameter-transformation.js +123 -0
- package/dist/rules/compat/no-pipe-all.d.ts +13 -0
- package/dist/rules/compat/no-pipe-all.js +68 -0
- package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
- package/dist/rules/compat/no-printf-time-format.js +99 -0
- package/dist/rules/compat/no-source.d.ts +13 -0
- package/dist/rules/compat/no-source.js +103 -0
- package/dist/rules/compat/no-test-v.d.ts +12 -0
- package/dist/rules/compat/no-test-v.js +280 -0
- package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
- package/dist/rules/compat/no-unicode-escape.js +120 -0
- package/dist/rules/compat/no-wait-n.d.ts +12 -0
- package/dist/rules/compat/no-wait-n.js +73 -0
- package/dist/rules/compat/operator-range.d.ts +10 -0
- package/dist/rules/compat/operator-range.js +15 -0
- package/dist/rules/compat/posix-absent-command.d.ts +12 -0
- package/dist/rules/compat/posix-absent-command.js +134 -0
- package/dist/rules/compat/shopt-options.d.ts +9 -0
- package/dist/rules/compat/shopt-options.js +63 -0
- package/dist/rules/compat/version-gates.d.ts +76 -0
- package/dist/rules/compat/version-gates.js +197 -0
- package/dist/rules/create-rule.d.ts +91 -0
- package/dist/rules/create-rule.js +51 -0
- package/dist/rules/create-wrapper-rule.d.ts +166 -0
- package/dist/rules/create-wrapper-rule.js +407 -0
- package/dist/rules/define-shell-rule.d.ts +184 -0
- package/dist/rules/define-shell-rule.js +56 -0
- package/dist/rules/index.d.ts +26 -0
- package/dist/rules/index.js +88 -0
- package/dist/rules/max-script-complexity.d.ts +28 -0
- package/dist/rules/max-script-complexity.js +130 -0
- package/dist/rules/no-dynamic-invocation.d.ts +35 -0
- package/dist/rules/no-dynamic-invocation.js +117 -0
- package/dist/rules/no-eval.d.ts +13 -0
- package/dist/rules/no-eval.js +89 -0
- package/dist/rules/no-masked-exit-status.d.ts +21 -0
- package/dist/rules/no-masked-exit-status.js +667 -0
- package/dist/rules/no-secret-env-reads.d.ts +24 -0
- package/dist/rules/no-secret-env-reads.js +133 -0
- package/dist/rules/no-secret-flags.d.ts +32 -0
- package/dist/rules/no-secret-flags.js +162 -0
- package/dist/rules/no-unchecked-cd.d.ts +44 -0
- package/dist/rules/no-unchecked-cd.js +250 -0
- package/dist/rules/no-undeclared-commands.d.ts +20 -0
- package/dist/rules/no-undeclared-commands.js +115 -0
- package/dist/rules/no-unused-declarations.d.ts +23 -0
- package/dist/rules/no-unused-declarations.js +259 -0
- package/dist/rules/prefer-local.d.ts +16 -0
- package/dist/rules/prefer-local.js +249 -0
- package/dist/rules/quote-expansions.d.ts +13 -0
- package/dist/rules/quote-expansions.js +229 -0
- package/dist/rules/require-network-wrapper.d.ts +40 -0
- package/dist/rules/require-network-wrapper.js +244 -0
- package/dist/rules/require-shebang.d.ts +29 -0
- package/dist/rules/require-shebang.js +101 -0
- package/dist/rules/require-strict-mode.d.ts +52 -0
- package/dist/rules/require-strict-mode.js +177 -0
- package/dist/rules/require-tty-guard.d.ts +13 -0
- package/dist/rules/require-tty-guard.js +289 -0
- package/dist/rules/resolve-declaration-convention.d.ts +96 -0
- package/dist/rules/resolve-declaration-convention.js +109 -0
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +116 -0
- package/dist/rules/set-options.d.ts +109 -0
- package/dist/rules/set-options.js +201 -0
- package/dist/rules/shebang-interpreter.d.ts +18 -0
- package/dist/rules/shebang-interpreter.js +81 -0
- package/dist/rules/shebang-matches-dialect.d.ts +12 -0
- package/dist/rules/shebang-matches-dialect.js +69 -0
- package/dist/rules/transparent-wrappers.d.ts +52 -0
- package/dist/rules/transparent-wrappers.js +99 -0
- package/dist/rules/user-pattern.d.ts +63 -0
- package/dist/rules/user-pattern.js +184 -0
- package/dist/syntax/analyze.d.ts +19 -0
- package/dist/syntax/analyze.js +21 -0
- package/dist/syntax/index.d.ts +23 -0
- package/dist/syntax/index.js +25 -0
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/docs/compat.md +347 -0
- package/docs/custom-rule-tutorial.md +780 -34
- package/docs/declaration-conventions.md +478 -0
- package/docs/examples/audit-wrapper-pack.mjs +15 -0
- package/docs/examples/dep-line-convention.mjs +22 -0
- package/docs/examples/eslint.config.mjs +3 -2
- package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
- package/docs/examples/no-backticks-rule-tester.mjs +106 -0
- package/docs/examples/no-backticks-rule.mjs +60 -0
- package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
- package/docs/examples/no-function-keyword-rule.mjs +59 -0
- package/docs/examples/no-listed-commands-rule.ts +63 -0
- package/docs/examples/no-rm-rf-rule.mjs +26 -15
- package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
- package/docs/examples/sidecar-convention.mjs +38 -0
- package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
- package/docs/examples/sorted-declarations-rule.mjs +52 -0
- package/docs/reading-nodes.md +159 -0
- package/docs/rules/README.md +78 -0
- package/docs/rules/_template.md +83 -0
- package/docs/rules/compat/no-append-all-redirect.md +87 -0
- package/docs/rules/compat/no-associative-arrays.md +95 -0
- package/docs/rules/compat/no-case-attributes.md +82 -0
- package/docs/rules/compat/no-case-fallthrough.md +90 -0
- package/docs/rules/compat/no-case-modification.md +74 -0
- package/docs/rules/compat/no-coproc.md +76 -0
- package/docs/rules/compat/no-declare-g.md +80 -0
- package/docs/rules/compat/no-double-bracket.md +72 -0
- package/docs/rules/compat/no-epoch-variables.md +79 -0
- package/docs/rules/compat/no-fd-variable.md +78 -0
- package/docs/rules/compat/no-globstar.md +74 -0
- package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
- package/docs/rules/compat/no-let.md +78 -0
- package/docs/rules/compat/no-mapfile.md +118 -0
- package/docs/rules/compat/no-nameref.md +81 -0
- package/docs/rules/compat/no-negative-array-index.md +87 -0
- package/docs/rules/compat/no-nounset-empty-array.md +103 -0
- package/docs/rules/compat/no-parameter-transformation.md +94 -0
- package/docs/rules/compat/no-pipe-all.md +85 -0
- package/docs/rules/compat/no-printf-time-format.md +75 -0
- package/docs/rules/compat/no-source.md +98 -0
- package/docs/rules/compat/no-test-v.md +82 -0
- package/docs/rules/compat/no-unicode-escape.md +91 -0
- package/docs/rules/compat/no-wait-n.md +85 -0
- package/docs/rules/max-script-complexity.md +109 -0
- package/docs/rules/no-dynamic-invocation.md +139 -0
- package/docs/rules/no-eval.md +106 -0
- package/docs/rules/no-masked-exit-status.md +239 -0
- package/docs/rules/no-secret-env-reads.md +173 -0
- package/docs/rules/no-secret-flags.md +169 -0
- package/docs/rules/no-unchecked-cd.md +146 -0
- package/docs/rules/no-undeclared-commands.md +141 -0
- package/docs/rules/no-unused-declarations.md +216 -0
- package/docs/rules/prefer-local.md +221 -0
- package/docs/rules/quote-expansions.md +157 -0
- package/docs/rules/require-network-wrapper.md +171 -0
- package/docs/rules/require-shebang.md +157 -0
- package/docs/rules/require-strict-mode.md +253 -0
- package/docs/rules/require-tty-guard.md +128 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +86 -0
- package/docs/wrapper-rules.md +206 -0
- package/package.json +26 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/language/selector-classes.d.ts.map +0 -1
- package/dist/language/selector-classes.js.map +0 -1
- package/dist/language/shell-language-options.d.ts.map +0 -1
- package/dist/language/shell-language-options.js.map +0 -1
- package/dist/language/shell-language.d.ts.map +0 -1
- package/dist/language/shell-language.js.map +0 -1
- package/dist/language/shell-source-code.d.ts.map +0 -1
- package/dist/language/shell-source-code.js.map +0 -1
- package/dist/language/source-coordinates.d.ts.map +0 -1
- package/dist/language/source-coordinates.js.map +0 -1
- package/dist/language/syntax-view.d.ts.map +0 -1
- package/dist/language/syntax-view.js.map +0 -1
- package/dist/language/word-resolution.d.ts.map +0 -1
- package/dist/language/word-resolution.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- 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.
|