@eslint-sh/eslint-plugin 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,221 @@
|
|
|
1
|
+
# `sh/prefer-local`
|
|
2
|
+
|
|
3
|
+
Require variables assigned inside a function to be declared `local`, so a function does not
|
|
4
|
+
overwrite globals its callers rely on.
|
|
5
|
+
|
|
6
|
+
| Configs | Fixable | Languages |
|
|
7
|
+
| ---------------- | ------- | --------- |
|
|
8
|
+
| `agent-authored` | autofix | `sh/sh` |
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
Shell variables are global by default. A helper that assigns `i`, `tmp` or `result` silently
|
|
13
|
+
overwrites the same-named variable of its caller, and of the caller's caller, because shell scoping
|
|
14
|
+
is dynamic. This rule reports an assignment inside a function body to a name that the function did
|
|
15
|
+
not declare `local`, `declare` or `typeset`.
|
|
16
|
+
|
|
17
|
+
What the rule treats as an assignment:
|
|
18
|
+
|
|
19
|
+
- A plain assignment statement: `x=1`, `x+=1`, `x=$(cmd)`. An assignment that prefixes a command
|
|
20
|
+
(`x=1 cmd`) only changes that command's environment and is not reported.
|
|
21
|
+
- The variables `read` assigns: `read x`, `read -r a b`, `read -a arr`. The rule skips `read`'s
|
|
22
|
+
options and their arguments (`-p "prompt: "`, `-d ''`, `-n 3`, …). A target that is not static
|
|
23
|
+
(`read "$name"`) is not guessed at.
|
|
24
|
+
- The loop variable of `for x in …` and `select x in …`.
|
|
25
|
+
|
|
26
|
+
What the rule does not report:
|
|
27
|
+
|
|
28
|
+
- A name declared `local`, `declare` or `typeset` **earlier** in the function. A declaration after
|
|
29
|
+
the assignment does not cover it.
|
|
30
|
+
- A name assigned at top level **before** the function is defined. The rule treats it as an
|
|
31
|
+
intentional global that the function is meant to share.
|
|
32
|
+
- A name declared global on purpose: `export x=…`, `readonly x=…`, `declare -g x=…`,
|
|
33
|
+
`typeset -g x=…`.
|
|
34
|
+
- A name listed in `allow`, or matched by `allowPattern`. By default `allowPattern` is
|
|
35
|
+
`^[A-Z][A-Z0-9_]*$`, so UPPER_CASE names, which scripts conventionally use for globals, are
|
|
36
|
+
allowed.
|
|
37
|
+
- Anything in a subshell `( … )`, a command substitution `$( … )` or a process substitution, and
|
|
38
|
+
functions whose body is a subshell (`f() ( … )`): those run in a child process, so nothing leaks.
|
|
39
|
+
- Anything in a pipeline element, such as `echo hi | read x` or `ls | while read -r line; do …; done`.
|
|
40
|
+
Pipeline elements run in subshells, so the assignment is lost rather than leaked. The exception is
|
|
41
|
+
a file that enables `shopt -s lastpipe` (anywhere in the file): the **last** element of each
|
|
42
|
+
pipeline then runs in the current shell, and the rule checks it like any other statement. Earlier
|
|
43
|
+
elements are still skipped.
|
|
44
|
+
|
|
45
|
+
Each function is its own scope. A function defined inside another function is checked on its own.
|
|
46
|
+
Only the **first** assignment to a name in a function is reported, since one declaration covers the
|
|
47
|
+
rest.
|
|
48
|
+
|
|
49
|
+
`local` is not part of POSIX, so under `languageOptions: { dialect: 'posix' }` the rule reports
|
|
50
|
+
nothing.
|
|
51
|
+
|
|
52
|
+
**Autofix.** The fix inserts one `local <name> <name> …` statement as the first statement of the
|
|
53
|
+
function body, naming every name the function leaks, and leaves the assignments untouched:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
# before
|
|
57
|
+
f() {
|
|
58
|
+
out=$(compute)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# after
|
|
62
|
+
f() {
|
|
63
|
+
local out
|
|
64
|
+
out=$(compute)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The fix does not rewrite `out=$(compute)` into `local out=$(compute)` on purpose. `local` is a
|
|
69
|
+
command, so its exit status is its own, not that of `compute`: `local out=$(false)` succeeds, and
|
|
70
|
+
`set -e` or `|| handle_error` never sees the failure (shellcheck SC2155). A separate declaration
|
|
71
|
+
keeps the assignment's exit status intact.
|
|
72
|
+
|
|
73
|
+
The fix declares a variable that was global. If the function _reads_ the global before assigning it
|
|
74
|
+
(`count=$((count + 1))`), declaring it `local` makes that read see an empty value instead, so
|
|
75
|
+
review the function before accepting the fix. Every report in a function carries the same fix, a
|
|
76
|
+
single `local` declaring all of the function's reported names, so `eslint --fix` settles the
|
|
77
|
+
function in one pass however many names it leaks (`a` then `b` becomes `local a b`). Fixes that
|
|
78
|
+
insert at the same point conflict, and ESLint applies one per pass and stops after 10, which is why
|
|
79
|
+
one fix per name would leave a function with more than 10 names unfixed. The declaration therefore
|
|
80
|
+
also covers a name whose report a user suppressed with a directive, so review the function if you
|
|
81
|
+
suppress a name in it. Each report, message and suggestion is still per name. A function whose body
|
|
82
|
+
is not a `{ … }` block (`f() if …; fi`) is reported without a fix.
|
|
83
|
+
|
|
84
|
+
## Options
|
|
85
|
+
|
|
86
|
+
This rule takes one options object:
|
|
87
|
+
|
|
88
|
+
- `allow` (`string[]`, default `[]`): variable names that may be assigned in a function without
|
|
89
|
+
`local`.
|
|
90
|
+
- `allowPattern` (`string`, default `"^[A-Z][A-Z0-9_]*$"`): a regular expression, compiled with the
|
|
91
|
+
`u` flag, that exempts every name it matches. A value that is not a valid regular expression is a
|
|
92
|
+
configuration error. Setting it replaces the default, so `allowPattern: '^g_'` stops exempting
|
|
93
|
+
UPPER_CASE names.
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
// eslint.config.js
|
|
97
|
+
{
|
|
98
|
+
rules: {
|
|
99
|
+
'sh/prefer-local': ['error', {
|
|
100
|
+
allow: ['result'],
|
|
101
|
+
allowPattern: '^[A-Z][A-Z0-9_]*$',
|
|
102
|
+
}],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Patterns run against script-controlled text
|
|
108
|
+
|
|
109
|
+
`allowPattern` is tested against every variable name the linted script assigns in a function, and
|
|
110
|
+
the script's author chooses that text. When you lint scripts you do not control, a pattern with
|
|
111
|
+
nested or overlapping quantifiers can make the regular expression engine backtrack for a time that
|
|
112
|
+
grows exponentially with the name's length. Write anchored patterns with one level of repetition
|
|
113
|
+
over a character class.
|
|
114
|
+
|
|
115
|
+
The rule rejects the most common accident: a pattern that repeats a group which itself repeats
|
|
116
|
+
(`*`, `+`, `{n,}` or `{n,m}` with `m > 1` applied to such a group). It is not a proof that an
|
|
117
|
+
accepted pattern is safe. Review every pattern you accept as if the guard were not there.
|
|
118
|
+
|
|
119
|
+
**What it does not catch.** These pass the check and can still be slow on a long name. Timings are
|
|
120
|
+
from Node 24 on a busy laptop and vary by machine; the shape of the growth is what matters.
|
|
121
|
+
|
|
122
|
+
- Alternation branches that overlap under a repetition grow exponentially. `^([A-Z]|[A-Z0-9])+_KEY$`
|
|
123
|
+
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
|
|
124
|
+
24 characters and 6.4 s at 26. `^(A|AA)*$` is the same shape.
|
|
125
|
+
- Adjacent quantifiers that can match the same text grow polynomially. `.*.*.*x` took 0.5 s on a
|
|
126
|
+
255-character name and 0.9 s at 300; each extra `.*` raises the exponent. Nothing bounds the length
|
|
127
|
+
of a name or flag in a script, so a long enough one is slow.
|
|
128
|
+
|
|
129
|
+
**What it rejects that is in fact linear.** The check does not look at what the repeated group can
|
|
130
|
+
match, so it also refuses some patterns that are safe, such as `(a*b)*`,
|
|
131
|
+
`^(?:[a-z]+-)*[a-z]+$` and `^(?:SECRET|TOKEN|PASSWORD)(?:_[A-Z]+)*$`. A rewrite without the nesting
|
|
132
|
+
exists for some of these and not for others, so if you cannot rewrite a pattern exactly, loosen it to
|
|
133
|
+
the character class you mean. The last one can be rewritten exactly with a lookahead
|
|
134
|
+
(it accepts the same names; compared on every suffix of up to seven characters over `A`, `_`, `a`
|
|
135
|
+
and `1` after `SECRET`, `TOKEN` and `PASSWORD`):
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
{ allowPattern: '^(?:SECRET|TOKEN|PASSWORD)(?!.*_(?:_|$))(?:_[A-Z][A-Z_]*)?$' }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
**When you see the error.** The error is thrown from the rule's `create()`, which ESLint calls for
|
|
143
|
+
each file it lints, so the check does not run when the config is loaded. ESLint stops the run at the
|
|
144
|
+
first file it lints with the rule enabled and prints "Error while loading rule" followed by
|
|
145
|
+
"Occurred while linting <that file>" and a stack trace, and exits with code 2. A run that lints no
|
|
146
|
+
file with the rule enabled never sees it.
|
|
147
|
+
|
|
148
|
+
The accident the check is for, and its fix:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
// Rejected: `([A-Z]+_?)*` repeats a group that already repeats, so a long
|
|
152
|
+
// name that does not match can take seconds.
|
|
153
|
+
{ allowPattern: '^([A-Z]+_?)*_KEY$' }
|
|
154
|
+
|
|
155
|
+
// Safe rewrite: one repetition over a character class.
|
|
156
|
+
{ allowPattern: '^[A-Z0-9_]*_KEY$' }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Examples
|
|
160
|
+
|
|
161
|
+
### Incorrect
|
|
162
|
+
|
|
163
|
+
<!-- example: {"findings": 3, "messageIds": ["preferLocal", "preferLocal", "preferLocal"]} -->
|
|
164
|
+
```sh
|
|
165
|
+
f() {
|
|
166
|
+
tmp=$(mktemp) # reported: tmp is global
|
|
167
|
+
read line # reported: read assigns line
|
|
168
|
+
for i in 1 2 3; do # reported: i is global
|
|
169
|
+
:
|
|
170
|
+
done
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
<!-- example: {"findings": 1} -->
|
|
175
|
+
```sh
|
|
176
|
+
f() {
|
|
177
|
+
x=1 # reported: the local comes after the assignment
|
|
178
|
+
local x
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Correct
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
f() {
|
|
186
|
+
local tmp line i
|
|
187
|
+
tmp=$(mktemp)
|
|
188
|
+
read line
|
|
189
|
+
for i in 1 2 3; do
|
|
190
|
+
:
|
|
191
|
+
done
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
verbose=0 # assigned at top level before the function: an intentional global
|
|
197
|
+
f() {
|
|
198
|
+
verbose=1
|
|
199
|
+
COUNT=2 # UPPER_CASE is allowed by default
|
|
200
|
+
declare -g seen=1
|
|
201
|
+
export LEVEL=3
|
|
202
|
+
x=1 some-command # only the command's environment
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## When not to use it
|
|
207
|
+
|
|
208
|
+
Turn this rule off for scripts that target POSIX `sh` (it is already inert there), or for scripts
|
|
209
|
+
that deliberately communicate through lower-case globals and don't want to list each one in
|
|
210
|
+
`allow`. Within a few functions, a directive comment can mark an intentional global.
|
|
211
|
+
|
|
212
|
+
## Relationship to shellcheck
|
|
213
|
+
|
|
214
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
215
|
+
layer. Run shellcheck beside it.
|
|
216
|
+
|
|
217
|
+
shellcheck has no check for variables that leak out of a function. Its nearest checks are about
|
|
218
|
+
`local` itself: SC2155 flags `local x=$(cmd)` because it hides the command's exit status, which is
|
|
219
|
+
why this rule's fix declares the variable on a separate line, and SC2168 flags `local` outside a
|
|
220
|
+
function. This rule enforces the policy that function variables are declared `local` in the first
|
|
221
|
+
place.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# `sh/quote-expansions`
|
|
2
|
+
|
|
3
|
+
Require double quotes around a parameter expansion or command substitution in any word the shell
|
|
4
|
+
splits into fields and expands as a glob.
|
|
5
|
+
|
|
6
|
+
| Configs | Fixable | Languages |
|
|
7
|
+
| ------- | ------------ | --------- |
|
|
8
|
+
| `recommended` (`warn`), `agent-authored` (`error`) | suggestions | `sh/sh` |
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
When a parameter expansion (`$var`, `${var}`, `$@`) or command substitution (`$(cmd)`,
|
|
13
|
+
`` `cmd` ``) is not inside double quotes, the shell splits its result on the characters in
|
|
14
|
+
`IFS` and then expands each piece as a glob pattern. With `file='my notes.txt'`, `rm $file`
|
|
15
|
+
removes `my` and `notes.txt`. With `pattern='*'`, `echo $pattern` lists the current directory.
|
|
16
|
+
Quoting the expansion (`rm "$file"`) keeps its result as one word, which is almost always what
|
|
17
|
+
the script means.
|
|
18
|
+
|
|
19
|
+
The rule reports each `ParamExp` and `CmdSubst` that is an unquoted part of a word in one of
|
|
20
|
+
these positions:
|
|
21
|
+
|
|
22
|
+
- a command word or argument of a simple command, including the operands of `[` / `test` and a
|
|
23
|
+
bare declaration operand such as `local $names` or `export $vars`;
|
|
24
|
+
- a word in a `for … in` or `select … in` list;
|
|
25
|
+
- an element of an array literal without a key, such as `arr=($a)`;
|
|
26
|
+
- a redirection target other than a here-document: `> $out`, `< $(pick)`, and the here-string
|
|
27
|
+
`<<< $x`, which bash split before version 4.4.
|
|
28
|
+
|
|
29
|
+
Each expansion is reported on its own, so `$a$b` reports twice. The rule does not report these
|
|
30
|
+
positions, which the shell does not split or glob:
|
|
31
|
+
|
|
32
|
+
- the operands of `[[ ]]`;
|
|
33
|
+
- the value of an assignment: `x=$y`, `local x=$y`, `declare x=$y`, `export x=$y`, a prefix
|
|
34
|
+
assignment such as `LC_ALL=$loc sort`, and a keyed array element such as `arr=([k]=$v)`;
|
|
35
|
+
- the subject word and the patterns of a `case`;
|
|
36
|
+
- arithmetic: `$(( ))`, `(( ))`, `let`, and array indexes;
|
|
37
|
+
- the special parameters that always expand to one word without glob characters (`$#`, `$?`,
|
|
38
|
+
`$$`, `$!`, `$-`), and length expansions such as `${#name}`;
|
|
39
|
+
- under `dialect: "bash"` and `dialect: "bats"`, the Bash variables that always hold an integer:
|
|
40
|
+
`$RANDOM`, `$SECONDS`, `$LINENO`, `$PPID`, `$OPTIND`, and `$BASH_SUBSHELL`;
|
|
41
|
+
- here-document delimiters and bodies;
|
|
42
|
+
- expansions already inside double quotes, and an expansion nested inside another parameter
|
|
43
|
+
expansion (`${a:-$b}` is judged as the outer expansion).
|
|
44
|
+
|
|
45
|
+
`"$@"` is clean. Unquoted `$@`, `$*`, and `${arr[@]}` report. An indirect expansion such as
|
|
46
|
+
`${!#}` (the last positional parameter) reports, because its value is arbitrary text.
|
|
47
|
+
|
|
48
|
+
The Bash integer variables are exempt only as a bare reference (`$RANDOM`, `${RANDOM}`); an
|
|
49
|
+
operator that substitutes other text (`${RANDOM/1/ }`) reports. Bash keeps each of them an integer
|
|
50
|
+
however a script assigns it or the environment supplies it, and each exists in every Bash from 3.2.
|
|
51
|
+
A digit in `IFS` splits these values, the same as `$?` and `$#`. They still report under
|
|
52
|
+
`dialect: "posix"`, where a shell such as dash treats them as ordinary variables. `$BASHPID`,
|
|
53
|
+
`$EPOCHSECONDS`, and `$SRANDOM` are not exempt: a Bash older than 4.0, 5.0, or 5.1 leaves them
|
|
54
|
+
unset, and an unset unquoted variable expands to no word at all. A script that unsets one of these
|
|
55
|
+
variables, or declares it `local`, removes its integer behavior; the rule does not detect that.
|
|
56
|
+
`$UID` and `$EUID` are not exempt either: Bash imports them verbatim from the environment, so
|
|
57
|
+
`env UID='a *' bash -c 'echo "[$UID]"'` prints `[a *]`, and an unquoted reference can split and
|
|
58
|
+
glob.
|
|
59
|
+
|
|
60
|
+
The rule decides from where the expansion is written, not from what it holds at run time. It does
|
|
61
|
+
no dataflow analysis, so `n=3; seq $n` reports even though `$n` can never split.
|
|
62
|
+
|
|
63
|
+
The positions are the same under every `languageOptions.dialect`, with one exception. Under
|
|
64
|
+
`dialect: "zsh"`, the rule does not report parameter expansions, because zsh neither splits nor
|
|
65
|
+
globs an unquoted parameter by default. It still reports command substitutions under zsh, because
|
|
66
|
+
zsh does split those. The rule does not detect a zsh script that turns on `SH_WORD_SPLIT` or
|
|
67
|
+
`GLOB_SUBST` (for example with `setopt sh_word_split`), so such a script gets no reports for its
|
|
68
|
+
unquoted parameters.
|
|
69
|
+
|
|
70
|
+
### Suggestion
|
|
71
|
+
|
|
72
|
+
The message says the expansion undergoes field splitting and pathname expansion. Field splitting
|
|
73
|
+
uses the characters in `IFS`, which are space, tab, and newline unless the script changes them.
|
|
74
|
+
|
|
75
|
+
Each report offers one suggestion: wrap the reported expansion, and only that part of the word, in
|
|
76
|
+
double quotes:
|
|
77
|
+
|
|
78
|
+
<!-- transform: {"kind": "suggestion"} -->
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
cp x$a"y" dest
|
|
82
|
+
# becomes
|
|
83
|
+
cp x"$a""y" dest
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The rule never autofixes, because a
|
|
87
|
+
script can rely on splitting, for example `$CFLAGS` holding several compiler flags, and only its
|
|
88
|
+
author knows. A backquoted substitution whose text contains `"` or `\` is reported without a
|
|
89
|
+
suggestion: inside double quotes, `\` inside backquotes means something different, and POSIX
|
|
90
|
+
leaves `"` inside them undefined. Rewrite it as `"$(…)"` by hand.
|
|
91
|
+
|
|
92
|
+
## Options
|
|
93
|
+
|
|
94
|
+
This rule has no options.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
// eslint.config.js
|
|
98
|
+
{
|
|
99
|
+
rules: {
|
|
100
|
+
'sh/quote-expansions': 'warn',
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
### Incorrect
|
|
108
|
+
|
|
109
|
+
<!-- example: {"findings": 9} -->
|
|
110
|
+
```sh
|
|
111
|
+
rm $file # an argument: splits on spaces and globs
|
|
112
|
+
echo $(date) # a command substitution argument
|
|
113
|
+
$cmd --version # the command word splits too
|
|
114
|
+
[ $a = b ] # `[` is a command; its operands are arguments
|
|
115
|
+
for f in $files; do :; done # a for list
|
|
116
|
+
arr=($a) # an array literal element
|
|
117
|
+
echo hi > $out # a redirection target
|
|
118
|
+
echo $@ $* # unquoted $@ and $* split each argument again
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Correct
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
rm "$file"
|
|
125
|
+
echo "$(date)"
|
|
126
|
+
for f in "${files[@]}"; do :; done
|
|
127
|
+
[[ $a == $b ]] # [[ ]] does not split its operands
|
|
128
|
+
x=$y # assignment values are not split
|
|
129
|
+
case $x in $pat) ;; esac # case words and patterns are not split
|
|
130
|
+
echo $(( n + 1 )) $# $? # arithmetic and single-word specials
|
|
131
|
+
sleep $RANDOM # a Bash integer variable (dialect: bash)
|
|
132
|
+
cat <<EOF
|
|
133
|
+
$x is not split inside a here-document body
|
|
134
|
+
EOF
|
|
135
|
+
exec "$@" # "$@" keeps every argument intact
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## When not to use it
|
|
139
|
+
|
|
140
|
+
Disable the rule if you run shellcheck, which already reports the same problem as SC2086
|
|
141
|
+
(see below). For one line that relies on splitting on purpose, such as passing `$CFLAGS` to a
|
|
142
|
+
compiler, disable it on that line (`# eslint-disable-next-line sh/quote-expansions`) or rewrite
|
|
143
|
+
the value as an array (`"${cflags[@]}"`).
|
|
144
|
+
|
|
145
|
+
## Relationship to shellcheck
|
|
146
|
+
|
|
147
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
148
|
+
layer. Run shellcheck beside it.
|
|
149
|
+
|
|
150
|
+
This rule is one of eslint-sh's two deliberate overlaps with shellcheck. It covers the same
|
|
151
|
+
problem as SC2086 (double quote to prevent globbing and word splitting), and SC2068 for unquoted
|
|
152
|
+
`$@`. It exists for teams that run eslint-sh without shellcheck, so that this common mistake is
|
|
153
|
+
still caught. **If you run shellcheck, disable this rule**, so the same line is not reported
|
|
154
|
+
twice. The rule does not try to match SC2086's heuristics: shellcheck skips some expansions it
|
|
155
|
+
can prove are safe, such as a variable it saw assigned a number, and this rule reports them
|
|
156
|
+
because it does no dataflow analysis. That is why `recommended` enables it at `warn` instead of
|
|
157
|
+
`error`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# `sh/require-network-wrapper`
|
|
2
|
+
|
|
3
|
+
Require network commands (`curl`, `wget`, `ssh`, `git fetch`, …) to be run through your project's
|
|
4
|
+
configured wrapper, such as a retry or audit helper.
|
|
5
|
+
|
|
6
|
+
| Configs | Fixable | Languages |
|
|
7
|
+
| ------- | ----------- | --------- |
|
|
8
|
+
| `agent-authored` (inert until a wrapper is configured) | not fixable | `sh/sh` |
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
Network calls fail in ways local commands don't: timeouts, rate limits, flaky DNS. Many projects
|
|
13
|
+
route every network call through one wrapper that retries, logs, or proxies it. This rule checks
|
|
14
|
+
that every network command a script runs goes through that wrapper.
|
|
15
|
+
|
|
16
|
+
**The wrapper has no default.** Which wrapper your project uses is your policy, and a generic
|
|
17
|
+
linter must not invent it. With no wrapper configured, the rule is inert: it reports nothing.
|
|
18
|
+
|
|
19
|
+
The rule checks every command in the file: top level, in functions, in pipelines, in
|
|
20
|
+
conditionals, and inside command substitutions (`$(…)`). A network command is reported (message
|
|
21
|
+
`unwrappedCommand`, on the whole command) unless the configured wrapper runs it. It is wrapped
|
|
22
|
+
when the wrapper encloses it, directly (`retry curl x`) or through transparent wrappers such as
|
|
23
|
+
`sudo`, `env`, or `nohup` (`retry sudo curl x`, `sudo retry curl x`). `sudo curl x` is not
|
|
24
|
+
wrapped, and neither is the `curl` in `retry echo "$(curl x)"`: the command substitution is its
|
|
25
|
+
own command. See [Wrapper rules](../wrapper-rules.md) for the full matching rules.
|
|
26
|
+
|
|
27
|
+
A command run by path is matched by its basename, so `/usr/bin/curl x` and `./bin/curl x` report
|
|
28
|
+
as `curl`. A wrapper run by path is the wrapper it names: `/usr/local/bin/retry curl x` is wrapped.
|
|
29
|
+
|
|
30
|
+
`git` counts only for its network subcommands, `fetch`, `clone`, `pull`, and `push`. git's global
|
|
31
|
+
options before the subcommand are skipped, so `git -C dir fetch` is reported as `git fetch`, and
|
|
32
|
+
`git status` is not reported.
|
|
33
|
+
|
|
34
|
+
Invocations that only act locally are not reported: `curl --version`, `curl --help`,
|
|
35
|
+
`curl --help all`, `wget --version` and `wget --help`, and their short forms `-V` and `-h`
|
|
36
|
+
(`curl -h all`). The argument list must be exactly that, so `curl --version https://example.com`
|
|
37
|
+
still reports. `git clone` is not reported when its source is a literal local path (`./x`, `../x`,
|
|
38
|
+
`/x`) or a `file://` URL, after any of git clone's own options (`git clone --depth 1 ./x`). Only
|
|
39
|
+
the source decides: a dynamic destination (`git clone ./x "$dest"`) does not matter, but a dynamic
|
|
40
|
+
source (`git clone "$src"`), a remote URL, and a clone option the rule does not know leave the
|
|
41
|
+
clone reported. So do `--recurse-submodules`, `--recursive` and `--remote-submodules` (a local
|
|
42
|
+
repository's `.gitmodules` can point at a remote URL, which those options fetch) and
|
|
43
|
+
`--bundle-uri` (it fetches from its URL, `--bundle-uri=URL` or `--bundle-uri URL`, whatever the
|
|
44
|
+
source is). `--no-recurse-submodules` and an attached or separate option value such as `-j4` or
|
|
45
|
+
`-j 4` do not change that the source is local. Options are read wherever they appear before a literal `--`, so
|
|
46
|
+
`git clone ./x --recurse-submodules` and `git clone ./x d --bundle-uri URL` report too, while
|
|
47
|
+
`git clone ./x -- --recurse-submodules` (everything after `--` is an operand) does not. A rule pack binds
|
|
48
|
+
the same kind of exemptions in [`createWrapperRule`](../wrapper-rules.md) with
|
|
49
|
+
`localInvocations`; the factory itself names no command.
|
|
50
|
+
|
|
51
|
+
A call to a function the file defines is not reported, even when the function is defined after the
|
|
52
|
+
call, because it runs the function and not the network client. A real invocation inside the
|
|
53
|
+
function (`command curl x`) is reported.
|
|
54
|
+
|
|
55
|
+
The rule never guesses. A dynamic command word (`"$client" x`), an option your wrapper does not
|
|
56
|
+
declare (`retry -n 3 curl x` when `retry` is configured as a plain name), and a dynamic git
|
|
57
|
+
subcommand (`git "$op"`) leave the command unknown, and nothing is reported. Command strings run
|
|
58
|
+
by another shell (`bash -c 'curl x'`) are not checked.
|
|
59
|
+
|
|
60
|
+
It does not depend on `languageOptions`.
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
- `wrapper` (`string` or `WrapperSpec` object, no default): the wrapper network commands must be
|
|
65
|
+
run through. When it is unset, the rule uses `settings.sh.networkWrapper` (below); with
|
|
66
|
+
neither, the rule reports nothing. A string names a wrapper that takes no options
|
|
67
|
+
(or one of the default transparent wrappers, such as `timeout`, whose options are known). An
|
|
68
|
+
object declares the wrapper's options so the command after them is found:
|
|
69
|
+
`{ names: ['retry'], argFlags: ['-n'] }` makes `retry -n 3 curl x` wrapped. Its fields are
|
|
70
|
+
`names` (required; any of them counts as the wrapper), `noArgFlags`, `argFlags`,
|
|
71
|
+
`stopsChainFlags`, `unresolvableFlags`, `positionalOperandsBeforeCommand`, and
|
|
72
|
+
`assignmentOperands`, as in `sh-ast/analyze`'s `WrapperSpec`.
|
|
73
|
+
- `commands` (`string[]`, default `["curl", "wget", "nc", "ssh", "scp", "rsync", "git"]`): the
|
|
74
|
+
network commands. Setting it replaces the default list. `git` keeps its subcommand restriction
|
|
75
|
+
wherever it is listed.
|
|
76
|
+
- `alsoTreatAsNetwork` (`string[]`, default `[]`): more commands to check, added to `commands` (or
|
|
77
|
+
to its default).
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// eslint.config.js
|
|
81
|
+
{
|
|
82
|
+
rules: {
|
|
83
|
+
'sh/require-network-wrapper': ['error', { wrapper: 'retry', alsoTreatAsNetwork: ['aws'] }],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Declaring the wrapper once: `settings.sh.networkWrapper`
|
|
89
|
+
|
|
90
|
+
`sh/no-undeclared-commands` and `sh/no-unused-declarations` also need to know about your wrapper:
|
|
91
|
+
in `with-retry curl x` the script runs `curl`, and only a rule that looks through `with-retry` sees
|
|
92
|
+
that. ESLint doesn't let one rule read another rule's options, so a wrapper set only as this rule's
|
|
93
|
+
`wrapper` option is invisible to them, and an undeclared `curl` behind it goes unreported.
|
|
94
|
+
|
|
95
|
+
Set the wrapper in shared settings instead, and all three rules read it:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// eslint.config.js
|
|
99
|
+
{
|
|
100
|
+
settings: { sh: { declarationConvention: commentPragma, networkWrapper: 'with-retry' } },
|
|
101
|
+
rules: { 'sh/require-network-wrapper': 'error' }, // or extend sh.configs['agent-authored']
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`networkWrapper` takes the same values as the `wrapper` option, a name or a `WrapperSpec` object,
|
|
106
|
+
and the declaration rules follow a spec's options too (`with-retry -n 3 curl`). This rule's
|
|
107
|
+
`wrapper` option, when set, takes precedence for this rule only. A malformed value stops linting
|
|
108
|
+
with an error starting `Invalid settings.sh.networkWrapper:`.
|
|
109
|
+
|
|
110
|
+
The setting names your **network** wrapper, a policy. Other wrappers that run a command, such as
|
|
111
|
+
a `with-lock` helper, aren't network policy: list those in the declaration rules'
|
|
112
|
+
`transparentWrappers` option.
|
|
113
|
+
|
|
114
|
+
## Examples
|
|
115
|
+
|
|
116
|
+
### Incorrect
|
|
117
|
+
|
|
118
|
+
With `{ wrapper: 'retry' }`:
|
|
119
|
+
|
|
120
|
+
<!-- example: {"options": {"wrapper": "retry"}, "findings": 6} -->
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
curl -fsSL https://example.com/install.sh # curl is not wrapped
|
|
124
|
+
retry echo start; curl x # retry wraps echo, not curl
|
|
125
|
+
sudo curl x # sudo is not the configured wrapper
|
|
126
|
+
git -C repo fetch # reported as `git fetch`
|
|
127
|
+
/usr/bin/curl x # a path to curl is still curl
|
|
128
|
+
body="$(wget -qO- https://example.com)" # commands inside $(…) are checked
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
An in-file function does not hide the real call inside it:
|
|
132
|
+
|
|
133
|
+
<!-- example: {"options": {"wrapper": "retry"}, "findings": 1} -->
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
curl() { command curl "$@"; } # `command curl` runs the external curl
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Correct
|
|
140
|
+
|
|
141
|
+
With `{ wrapper: 'retry' }`:
|
|
142
|
+
|
|
143
|
+
<!-- example: {"options": {"wrapper": "retry"}} -->
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
retry curl -fsSL https://example.com/install.sh
|
|
147
|
+
retry sudo rsync -a src/ host:dst/ # retry encloses sudo, which runs rsync
|
|
148
|
+
/usr/local/bin/retry curl x # a path to the wrapper is the wrapper
|
|
149
|
+
git status # not a network subcommand
|
|
150
|
+
body="$(retry wget -qO- https://example.com)"
|
|
151
|
+
echo curl # an argument, not a command
|
|
152
|
+
curl --version # prints a version, touches no network
|
|
153
|
+
git clone ./vendor/lib copy # a local source
|
|
154
|
+
wget() { :; }; wget x # a function the file defines, not the client
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## When not to use it
|
|
158
|
+
|
|
159
|
+
Leave the rule off if your project has no single wrapper for network calls, or if network
|
|
160
|
+
resilience is handled outside your scripts (for example by a proxy or by the CI runner). For a
|
|
161
|
+
policy about commands other than network clients, define your own rule with
|
|
162
|
+
[`createWrapperRule`](../wrapper-rules.md) rather than overloading `alsoTreatAsNetwork`.
|
|
163
|
+
|
|
164
|
+
## Relationship to shellcheck
|
|
165
|
+
|
|
166
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
167
|
+
layer. Run shellcheck beside it.
|
|
168
|
+
|
|
169
|
+
shellcheck has no equivalent. It checks how commands are written, not which wrapper a project
|
|
170
|
+
requires around them, and has no way to configure a project-specific wrapper. This rule exists
|
|
171
|
+
because that requirement is a project contract, not a shell gotcha.
|