@eslint-sh/eslint-plugin 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -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 +51 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +53 -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-brace-sequence-padding-step.d.ts +15 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -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 +205 -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 +90 -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 +318 -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 +354 -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 +79 -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-brace-sequence-padding-step.md +90 -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 +131 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +87 -0
- package/docs/untrusted-code.md +440 -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,159 @@
|
|
|
1
|
+
# Reading nodes
|
|
2
|
+
|
|
3
|
+
A rule's listeners receive **lint-view nodes**: the plugin's own projection of the parsed script,
|
|
4
|
+
typed so you can read a node's fields without a cast or a hand-written guard. This page is the
|
|
5
|
+
field reference and one worked rule.
|
|
6
|
+
|
|
7
|
+
## The shape of a node
|
|
8
|
+
|
|
9
|
+
Every node has:
|
|
10
|
+
|
|
11
|
+
- `type` — the syntax kind (`'Stmt'`, `'CallExpr'`, `'Word'`, `'Lit'`, …). It is the discriminator.
|
|
12
|
+
- `range` — a half-open `[start, end]` span of UTF-16 offsets into the source text.
|
|
13
|
+
- `loc` — the same span as ESLint `{ start: { line, column }, end: { line, column } }`, both
|
|
14
|
+
one-based.
|
|
15
|
+
|
|
16
|
+
A listener keyed by a node type, with or without `:exit`, receives exactly that node type, so its
|
|
17
|
+
fields are typed with nothing to check first:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
CallExpr(node) {
|
|
21
|
+
node.args; // readonly Word nodes
|
|
22
|
+
node.assigns; // readonly Assign nodes
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Any other selector (`*`, `CallExpr[args.0.parts.0.value='jq']`, `:command`, `Stmt > CallExpr`)
|
|
27
|
+
can match nodes of more than one type, so its listener receives `ShellNode`, the union of every
|
|
28
|
+
node type. **Narrow it on `type`** and the node's own fields appear, typed:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
':command'(node) {
|
|
32
|
+
if (node.type !== 'CallExpr') return;
|
|
33
|
+
node.args;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`ShellNodeOf<'Stmt'>` names one node type (for a helper's parameter), and `ShellNodeType` is the
|
|
38
|
+
union of every `type` value. Field access that does not exist is a compile error, so a typo or a
|
|
39
|
+
field from another node type is caught by the compiler.
|
|
40
|
+
|
|
41
|
+
## Where the fields come from
|
|
42
|
+
|
|
43
|
+
The nodes mirror [mvdan/sh](https://github.com/mvdan/sh)'s syntax tree, as `sh-ast` exposes it.
|
|
44
|
+
An upstream field `Cmd` on `Stmt` is `stmt.cmd` on the node: **the same name, lowercased**. The
|
|
45
|
+
rules that turn an upstream record into a lint-view node are fixed:
|
|
46
|
+
|
|
47
|
+
| Upstream field | On the node |
|
|
48
|
+
| ----------------------------- | ---------------------------------------------------------------- |
|
|
49
|
+
| `Type` | `type`, the discriminator |
|
|
50
|
+
| a child (`Stmt.Cmd`) | the child node, or **absent** (`undefined`) when there is none |
|
|
51
|
+
| a slice (`CallExpr.Args`) | a read-only array of nodes; **always present**, empty when none |
|
|
52
|
+
| a string (`Lit.Value`) | a JavaScript `string` (decoded from Go bytes) |
|
|
53
|
+
| a boolean (`Stmt.Background`) | a `boolean` |
|
|
54
|
+
| an operator (`BinaryCmd.Op`) | a number; compare with the constants from `…/syntax` (`AndStmt`) |
|
|
55
|
+
| a position (`Stmt.Semicolon`) | none: use `range` and `loc` |
|
|
56
|
+
|
|
57
|
+
A child that may be absent is an **optional** property, so the compiler makes you check it before
|
|
58
|
+
use (`stmt.cmd?.type`). A slice is never absent and never holds a missing entry, so
|
|
59
|
+
`call.args.length` and `for (const word of call.args)` need no check.
|
|
60
|
+
|
|
61
|
+
To find a field, hover the narrowed node in your editor, or read the `sh-ast` record of the same
|
|
62
|
+
name: its field list is the node's field list, minus positions, lowercased.
|
|
63
|
+
|
|
64
|
+
## What the view adapts
|
|
65
|
+
|
|
66
|
+
The types describe the view as it is, not the raw parse:
|
|
67
|
+
|
|
68
|
+
- `range` and `loc` are in UTF-16 units of the file's text, not the parser's byte offsets.
|
|
69
|
+
- A CRLF file reads like its LF twin. The parser treats only `\n` as a line break, so it keeps the
|
|
70
|
+
`\r` before it as part of whatever ends the line. The view ends such a node **before** the `\r`,
|
|
71
|
+
and a text field of that node (`Lit.value`) loses that one trailing `\r`. A quoted string or
|
|
72
|
+
here-document that continues past the line break is left alone.
|
|
73
|
+
- Some nodes have no source extent of their own (`Expansion`, `Replace`, `Slice` are parts of a
|
|
74
|
+
parameter expansion). Their range covers their children.
|
|
75
|
+
|
|
76
|
+
For anything the view leaves out (token positions, the exact bytes of a non-UTF-8 string), reach
|
|
77
|
+
the parser's own record with `context.sourceCode.getSyntax(node)`; see the [custom-rule
|
|
78
|
+
tutorial](./custom-rule-tutorial.md).
|
|
79
|
+
|
|
80
|
+
## `range` goes straight into `fixer`
|
|
81
|
+
|
|
82
|
+
`range` has the type ESLint's fixer methods take, so `node.range` is accepted by
|
|
83
|
+
`insertTextBeforeRange`, `insertTextAfterRange`, `removeRange` and `replaceTextRange` as is.
|
|
84
|
+
Treat it as read-only.
|
|
85
|
+
|
|
86
|
+
## Example: quote the variables `rm` receives
|
|
87
|
+
|
|
88
|
+
`docs/examples/no-unquoted-rm-variable-rule.ts`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
/**
|
|
92
|
+
* The TypeScript example rule from "Reading nodes" (`../reading-nodes.md`).
|
|
93
|
+
* This package's test suite asserts the page's code fence matches this
|
|
94
|
+
* file byte-for-byte, type-checks it against the package's published
|
|
95
|
+
* declarations, and runs it, with its fix, through a real `eslint` `Linter`.
|
|
96
|
+
*
|
|
97
|
+
* It reports an `rm` argument that is a lone, unquoted variable (`rm -rf $dir`)
|
|
98
|
+
* and quotes it. Every field it reads is typed by `ShellNode`: narrowing on
|
|
99
|
+
* `type` is the only thing between a listener's node and its fields.
|
|
100
|
+
*
|
|
101
|
+
* @see ../reading-nodes.md — the page this rule backs
|
|
102
|
+
*/
|
|
103
|
+
import { defineShellRule } from '@eslint-sh/eslint-plugin';
|
|
104
|
+
|
|
105
|
+
const noUnquotedRmVariable = defineShellRule({
|
|
106
|
+
meta: {
|
|
107
|
+
type: 'problem',
|
|
108
|
+
fixable: 'code',
|
|
109
|
+
docs: { description: 'require the variable operands of `rm` to be quoted' },
|
|
110
|
+
schema: [],
|
|
111
|
+
messages: {
|
|
112
|
+
unquoted: 'Quote `${{name}}`: unquoted, it is split and globbed before `rm` runs.',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
create(context) {
|
|
116
|
+
return {
|
|
117
|
+
CallExpr(node) {
|
|
118
|
+
// A listener keyed by a node type receives that node type: `args`, `assigns`, … are typed.
|
|
119
|
+
if (context.sourceCode.getCommandName(node) !== 'rm') return;
|
|
120
|
+
|
|
121
|
+
// `args` is always an array of `Word`s (the command word first), never null.
|
|
122
|
+
for (const word of node.args.slice(1)) {
|
|
123
|
+
// A `Word` is a list of parts; a lone `$name` is a single `ParamExp` part.
|
|
124
|
+
const [part] = word.parts;
|
|
125
|
+
if (word.parts.length !== 1 || part.type !== 'ParamExp') continue;
|
|
126
|
+
|
|
127
|
+
context.report({
|
|
128
|
+
node: word,
|
|
129
|
+
messageId: 'unquoted',
|
|
130
|
+
// `param` is optional: a `ParamExp` with no name (rare) has no `param` child.
|
|
131
|
+
data: { name: part.param?.value ?? '' },
|
|
132
|
+
// `word.range` is the tuple ESLint's fixer methods take, so it needs no cast.
|
|
133
|
+
fix: (fixer) => [
|
|
134
|
+
fixer.insertTextBeforeRange(word.range, '"'),
|
|
135
|
+
fixer.insertTextAfterRange(word.range, '"'),
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export default noUnquotedRmVariable;
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Enable it like any rule in step 3 of the [tutorial](./custom-rule-tutorial.md#3-wire-it-into-a-flat-config).
|
|
148
|
+
For `rm -rf $dir "$kept" ${base}/x` it reports only `$dir`: `"$kept"` is a `DblQuoted` part, and
|
|
149
|
+
`${base}/x` has two parts. Its fix produces `rm -rf "$dir" "$kept" ${base}/x`.
|
|
150
|
+
|
|
151
|
+
What the rule reads, in order: the `CallExpr` key gives the listener a `CallExpr` node, so
|
|
152
|
+
`node.args` is its `Word`s; `word.parts` is a `Word`'s pieces, which `part.type` narrows again
|
|
153
|
+
(`'ParamExp'`); `part.param` is optional, so it is read with `?.`.
|
|
154
|
+
|
|
155
|
+
## Staying current with mvdan/sh
|
|
156
|
+
|
|
157
|
+
The node types are computed from `sh-ast`'s generated typings; nothing in this package restates a
|
|
158
|
+
field list. When the pinned `sh-ast` moves to a new mvdan/sh, the types follow it, and a rule that
|
|
159
|
+
reads a field the new version removed stops compiling instead of reading `undefined` at run time.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Built-in rules
|
|
2
|
+
|
|
3
|
+
eslint-sh enforces shell script conventions and contracts. Run shellcheck alongside it for
|
|
4
|
+
general shell correctness checks. Start with `sh.configs.recommended`; use
|
|
5
|
+
`sh.configs['agent-authored']` for machine-written scripts that people review, and
|
|
6
|
+
`sh.configs['bash-3.2']` or `sh.configs['posix-portable']` for a Bash 3.2 or POSIX `sh`
|
|
7
|
+
deployment target. Every preset includes the shell language and plugin registration; extend them
|
|
8
|
+
under your own `files` globs. See [running eslint-sh beside shellcheck](../shellcheck.md).
|
|
9
|
+
See the [canonical quickstart](https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-sh/README.md).
|
|
10
|
+
|
|
11
|
+
| Rule | Configs | Autofix | Suggestions | Purpose |
|
|
12
|
+
| ---- | ------- | ------- | ----------- | ------- |
|
|
13
|
+
| [`sh/compat/no-append-all-redirect`](./compat/no-append-all-redirect.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow the `&>>` redirection when the target shell lacks it, suggesting `>> file 2>&1`. |
|
|
14
|
+
| [`sh/compat/no-associative-arrays`](./compat/no-associative-arrays.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `declare -A` / `typeset -A` when the target shell lacks associative arrays. |
|
|
15
|
+
| [`sh/compat/no-brace-sequence-padding-step`](./compat/no-brace-sequence-padding-step.md) | `bash-3.2`, `posix-portable` | No | No | Disallow zero-padded (`{01..10}`) and stepped (`{1..10..2}`) brace sequences when the target shell lacks them, pointing at a counting loop. |
|
|
16
|
+
| [`sh/compat/no-case-attributes`](./compat/no-case-attributes.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `declare -l` / `declare -u` (and `typeset`, `local`) when the target shell lacks them, pointing at `tr`. |
|
|
17
|
+
| [`sh/compat/no-case-fallthrough`](./compat/no-case-fallthrough.md) | `bash-3.2`, `posix-portable` | No | No | Disallow the `case` terminators `;&` and `;;&` when the target shell lacks them. |
|
|
18
|
+
| [`sh/compat/no-case-modification`](./compat/no-case-modification.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `${v^^}`, `${v,,}` and variants when the target shell lacks them, pointing at `tr`. |
|
|
19
|
+
| [`sh/compat/no-coproc`](./compat/no-coproc.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `coproc` when the target shell lacks it. |
|
|
20
|
+
| [`sh/compat/no-declare-g`](./compat/no-declare-g.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `declare -g` / `typeset -g` when the target shell lacks the option. |
|
|
21
|
+
| [`sh/compat/no-double-bracket`](./compat/no-double-bracket.md) | `posix-portable` | No | No | Disallow `[[ … ]]` when the target shell is POSIX sh, pointing at `[` and `case`. |
|
|
22
|
+
| [`sh/compat/no-epoch-variables`](./compat/no-epoch-variables.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `$EPOCHSECONDS` / `$EPOCHREALTIME` when the target shell lacks them. |
|
|
23
|
+
| [`sh/compat/no-fd-variable`](./compat/no-fd-variable.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `{varname}` descriptor redirections (`exec {fd}>file`) when the target shell lacks them. |
|
|
24
|
+
| [`sh/compat/no-globstar`](./compat/no-globstar.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `shopt -s globstar` when the target shell lacks the option, pointing at `find`. |
|
|
25
|
+
| [`sh/compat/no-lastpipe-semantics`](./compat/no-lastpipe-semantics.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `shopt -s lastpipe` when the target shell lacks the option. |
|
|
26
|
+
| [`sh/compat/no-let`](./compat/no-let.md) | `posix-portable` | No | No | Disallow `let` when the target shell is POSIX sh, pointing at arithmetic expansion. |
|
|
27
|
+
| [`sh/compat/no-mapfile`](./compat/no-mapfile.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow `mapfile` / `readarray` when the target shell lacks them, suggesting a read loop. |
|
|
28
|
+
| [`sh/compat/no-nameref`](./compat/no-nameref.md) | `bash-3.2`, `posix-portable` | No | No | Disallow namerefs (`declare -n`, `local -n`) when the target shell lacks them. |
|
|
29
|
+
| [`sh/compat/no-negative-array-index`](./compat/no-negative-array-index.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow negative array subscripts when the target shell lacks them, suggesting a length-based index. |
|
|
30
|
+
| [`sh/compat/no-nounset-empty-array`](./compat/no-nounset-empty-array.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow whole-array expansions under `set -u` when the target shell aborts on an empty array, suggesting a guarded form. |
|
|
31
|
+
| [`sh/compat/no-parameter-transformation`](./compat/no-parameter-transformation.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `${v@op}` transformations when the target shell lacks the operator. |
|
|
32
|
+
| [`sh/compat/no-pipe-all`](./compat/no-pipe-all.md) | `bash-3.2`, `posix-portable` | No | Yes | Disallow the stderr-merging pipe (pipe-ampersand) when the target shell lacks it, suggesting an explicit `2>&1` before the pipe. |
|
|
33
|
+
| [`sh/compat/no-printf-time-format`](./compat/no-printf-time-format.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `printf`'s `%(fmt)T` date format when the target shell lacks it. |
|
|
34
|
+
| [`sh/compat/no-source`](./compat/no-source.md) | `posix-portable` | No | Yes | Disallow `source` when the target shell is POSIX sh, suggesting `.`. |
|
|
35
|
+
| [`sh/compat/no-test-v`](./compat/no-test-v.md) | `bash-3.2`, `posix-portable` | No | No | Disallow the `-v name` test (`[[ -v x ]]`, `test -v x`) when the target shell lacks it. |
|
|
36
|
+
| [`sh/compat/no-unicode-escape`](./compat/no-unicode-escape.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `\u` / `\U` escapes in `$'…'` when the target shell does not decode them to UTF-8. |
|
|
37
|
+
| [`sh/compat/no-wait-n`](./compat/no-wait-n.md) | `bash-3.2`, `posix-portable` | No | No | Disallow `wait -n` when the target shell lacks it. |
|
|
38
|
+
| [`sh/max-script-complexity`](./max-script-complexity.md) | `agent-authored` | No | No | Cap a script's function count, nesting depth and statement count. |
|
|
39
|
+
| [`sh/no-dynamic-invocation`](./no-dynamic-invocation.md) | `agent-authored` | No | No | Require each executed command to be statically identifiable, including through wrappers. |
|
|
40
|
+
| [`sh/no-eval`](./no-eval.md) | `recommended`, `agent-authored` | No | No | Disallow `eval` and sourcing a path that is not a static literal. |
|
|
41
|
+
| [`sh/no-masked-exit-status`](./no-masked-exit-status.md) | `agent-authored` | No | Yes | Disallow `local`, `declare`, `typeset`, `export` and `readonly` with a value taken from a command substitution, which hides the command's exit status. |
|
|
42
|
+
| [`sh/no-secret-env-reads`](./no-secret-env-reads.md) | `agent-authored` | No | No | Flag reads of secret-shaped environment variables outside allowed sinks. |
|
|
43
|
+
| [`sh/no-secret-flags`](./no-secret-flags.md) | `recommended`, `agent-authored` | No | No | Flag literal secrets passed through command-line flags. |
|
|
44
|
+
| [`sh/no-unchecked-cd`](./no-unchecked-cd.md) | `recommended`, `agent-authored` | No | Yes | Require the failure of `cd`, `pushd` and `popd` to be checked. |
|
|
45
|
+
| [`sh/no-undeclared-commands`](./no-undeclared-commands.md) | `agent-authored` | No | No | Require external command usage to be declared through your convention. |
|
|
46
|
+
| [`sh/no-unused-declarations`](./no-unused-declarations.md) | `agent-authored` | Yes (when the convention provides an edit hook) | No | Remove command declarations that a fully understood script does not use. |
|
|
47
|
+
| [`sh/prefer-local`](./prefer-local.md) | `agent-authored` | Yes (declaration inserted at the top of the function body) | No | Require variables assigned in a function to be declared `local`. |
|
|
48
|
+
| [`sh/quote-expansions`](./quote-expansions.md) | `recommended`, `agent-authored` | No | Yes | Require double quotes around expansions in words the shell splits and globs. |
|
|
49
|
+
| [`sh/require-network-wrapper`](./require-network-wrapper.md) | `agent-authored` | No | No | Require network commands to run through your configured wrapper. |
|
|
50
|
+
| [`sh/require-shebang`](./require-shebang.md) | `recommended`, `agent-authored` | Yes (missing shebang only) | No | Require a recognized shell interpreter on line 1. |
|
|
51
|
+
| [`sh/require-strict-mode`](./require-strict-mode.md) | `recommended`, `agent-authored`, `posix-portable` (with set -eu) | Yes (when insertion satisfies the rule) | No | Require strict-mode options near the start of the script. |
|
|
52
|
+
| [`sh/require-tty-guard`](./require-tty-guard.md) | `agent-authored` | No | Yes | Require `read`, `select` and configured interactive commands to be guarded by a TTY test or supplied input. |
|
|
53
|
+
| [`sh/shebang-matches-dialect`](./shebang-matches-dialect.md) | `recommended`, `agent-authored`, `posix-portable` | No | No | Require the shebang to name the same kind of shell (bash or POSIX) as `languageOptions.dialect`. |
|
|
54
|
+
|
|
55
|
+
`recommended` enables `no-secret-flags` and `quote-expansions` at `warn` and its other rules at
|
|
56
|
+
`error`. `agent-authored` enables every rule above except the gated `compat/*` rules, all at `error`. `compat/no-append-all-redirect`, `compat/no-mapfile`,
|
|
57
|
+
`compat/no-negative-array-index`, `compat/no-nounset-empty-array`, `compat/no-pipe-all`, `compat/no-source`, `no-masked-exit-status`, `no-unchecked-cd`, `quote-expansions` and `require-tty-guard`
|
|
58
|
+
offer editor suggestions, which you apply one at a time from your editor; the Autofix column
|
|
59
|
+
describes edits available through `eslint --fix`. `quote-expansions` suggests quoting instead of
|
|
60
|
+
fixing because only the script's author knows whether the splitting was intended.
|
|
61
|
+
|
|
62
|
+
The version-gated `compat/*` rules (`compat/no-append-all-redirect`, `compat/no-associative-arrays`, `compat/no-brace-sequence-padding-step`, `compat/no-case-attributes`, `compat/no-case-fallthrough`, `compat/no-case-modification`, `compat/no-coproc`, `compat/no-declare-g`, `compat/no-epoch-variables`, `compat/no-fd-variable`, `compat/no-globstar`, `compat/no-lastpipe-semantics`, `compat/no-mapfile`, `compat/no-nameref`, `compat/no-negative-array-index`, `compat/no-nounset-empty-array`, `compat/no-parameter-transformation`, `compat/no-pipe-all`, `compat/no-printf-time-format`, `compat/no-test-v`, `compat/no-unicode-escape`, `compat/no-wait-n`) report
|
|
63
|
+
shell features that your target shell lacks, as set by `languageOptions.shellVersion` and
|
|
64
|
+
`dialect`. They report nothing when `shellVersion` is unset, so the `bash-3.2` preset sets it to `3.2`
|
|
65
|
+
and the `posix-portable` preset sets `dialect: "posix"`; each enables all of them.
|
|
66
|
+
See [version compatibility](../compat.md). `compat/*` means target gates only; `shebang-matches-dialect`
|
|
67
|
+
compares the file with your config whatever the target, so it lives outside that namespace. `posix-portable` also enables
|
|
68
|
+
`shebang-matches-dialect`, `require-strict-mode` (as `set -eu`) and the POSIX-only gates
|
|
69
|
+
`compat/no-double-bracket`, `compat/no-let` and `compat/no-source`, which report those constructs
|
|
70
|
+
under `dialect: "posix"` because they fail on dash. It does not report `local`, `echo -e` or the
|
|
71
|
+
other constructs listed under
|
|
72
|
+
[what `posix-portable` does not report](../compat.md#what-posix-portable-does-not-report).
|
|
73
|
+
To run these rules beside shellcheck, see [shellcheck](../shellcheck.md).
|
|
74
|
+
|
|
75
|
+
`no-undeclared-commands` and `no-unused-declarations` report nothing until you bind
|
|
76
|
+
`settings.sh.declarationConvention` (see [declaration conventions](../declaration-conventions.md)).
|
|
77
|
+
`require-network-wrapper` reports nothing until you set its `wrapper` option (see
|
|
78
|
+
[wrapper rules](../wrapper-rules.md)). Each linked rule page documents its options, limitations,
|
|
79
|
+
and relationship to shellcheck.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# `sh/<rule-name>`
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
Template for a built-in rule's docs page. Copy it to `docs/rules/<rule-name>.md`
|
|
5
|
+
(the same name the rule is registered under in `src/rules/index.ts`), replace
|
|
6
|
+
every `<...>` placeholder, and delete these comments. The rule's `meta.docs.url`
|
|
7
|
+
already points at that path. A test fails if a registered rule has no page here,
|
|
8
|
+
or a page here has no registered rule; this template is the only exception.
|
|
9
|
+
|
|
10
|
+
Examples are executed. Every `sh` fence under `### Incorrect` must make this rule report
|
|
11
|
+
at least one finding, and every one under `### Correct` must report none, when linted with
|
|
12
|
+
only this rule enabled through the built plugin. A fence that needs options, a dialect or
|
|
13
|
+
a declaration convention says so in an HTML comment on the line above it. The comment is
|
|
14
|
+
the opening "angle bracket, bang, two hyphens", the word `example:`, a JSON object such as
|
|
15
|
+
`{"options": {"maxNesting": 2}, "dialect": "bash"}`, then the usual closing. See any
|
|
16
|
+
existing page, e.g. `max-script-complexity.md`, for the exact spelling.
|
|
17
|
+
|
|
18
|
+
Keys: `options` (the rule's options value), `dialect`, `shellVersion`, `convention`
|
|
19
|
+
(`"commentPragma"`) and `filename`. Under `### Incorrect` you may also state what the prose
|
|
20
|
+
claims exactly: `findings` (a count), `messageIds` (in report order), `output` (the source
|
|
21
|
+
after autofix) and `suggestionOutputs`. A typo in the annotation fails the test.
|
|
22
|
+
|
|
23
|
+
A before/after pair shown elsewhere on the page (a suggestion or autofix result) goes in a
|
|
24
|
+
fence with a `# becomes` line between the two, under a comment spelled like the one above
|
|
25
|
+
but with `transform:` and a `"kind"` of `"suggestion"` or `"fix"` (see `quote-expansions.md`).
|
|
26
|
+
|
|
27
|
+
The first table's `Configs` and `Fixable` cells are checked against the presets and the
|
|
28
|
+
rule's `meta`: start `Fixable` with `not fixable`, `suggestions` or `autofix`.
|
|
29
|
+
-->
|
|
30
|
+
|
|
31
|
+
<One sentence: what the rule reports, phrased as the policy it enforces.>
|
|
32
|
+
|
|
33
|
+
| Configs | Fixable | Languages |
|
|
34
|
+
| ---------------- | -------------------------------------- | --------- |
|
|
35
|
+
| <`recommended`…> | <autofix / suggestions / not fixable> | `sh/sh` |
|
|
36
|
+
|
|
37
|
+
## Rule details
|
|
38
|
+
|
|
39
|
+
<Why this policy matters for shell scripts, and exactly what the rule reports. Say which
|
|
40
|
+
node types or constructs it inspects and which it deliberately ignores. If the rule depends on
|
|
41
|
+
`languageOptions` (`dialect`, `shellVersion`), say how.>
|
|
42
|
+
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
<Each option: name, type, default, and effect. If the rule has no options, say so:>
|
|
46
|
+
|
|
47
|
+
This rule has no options.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// eslint.config.js
|
|
51
|
+
{
|
|
52
|
+
rules: {
|
|
53
|
+
'sh/<rule-name>': ['error', { /* <options> */ }],
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
### Incorrect
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# <code this rule reports, with a comment on each reported line saying why>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Correct
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
# <code this rule accepts, including the edge cases it intentionally allows>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## When not to use it
|
|
73
|
+
|
|
74
|
+
<The situations where turning this rule off is the right call, for example a policy your
|
|
75
|
+
project does not hold, or another tool already enforcing it.>
|
|
76
|
+
|
|
77
|
+
## Relationship to shellcheck
|
|
78
|
+
|
|
79
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
80
|
+
layer. Run shellcheck beside it.
|
|
81
|
+
|
|
82
|
+
<Name any shellcheck check (`SCxxxx`) this rule overlaps, and why this rule exists anyway, or
|
|
83
|
+
state that shellcheck has no equivalent and why it cannot express this policy.>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# `sh/compat/no-append-all-redirect`
|
|
2
|
+
|
|
3
|
+
Disallow the `&>>` redirection, which appends both stdout and stderr to a file, in scripts whose
|
|
4
|
+
target shell lacks it: Bash older than 4.0, including the Bash 3.2 that macOS ships as
|
|
5
|
+
`/bin/bash`, and POSIX sh.
|
|
6
|
+
|
|
7
|
+
| Configs | Fixable | Languages |
|
|
8
|
+
| ------- | ------- | --------- |
|
|
9
|
+
| `bash-3.2`, `posix-portable` | suggestions | `sh/sh` |
|
|
10
|
+
|
|
11
|
+
## Rule details
|
|
12
|
+
|
|
13
|
+
`&>>` arrived in Bash 4.0 (Bash NEWS for 4.0). Bash 3.2 rejects it as a syntax error (``syntax error near unexpected token `>'``), so the whole script fails to run, however rarely the line runs.
|
|
14
|
+
|
|
15
|
+
The rule reports a redirection whose operator is `&>>`; the report is on the whole redirection. The two-way `&>` (which Bash 3.2 has), `>>`, `2>>`, `>&` and `2>&1` are other operators and are not reported, and neither is any of them in quotes.
|
|
16
|
+
|
|
17
|
+
The portable spelling is `>> file 2>&1`, which the Bash manual defines `&>>` to mean. The rule offers it as a suggestion: the operator becomes `>>` and ` 2>&1` follows the target word, so the redirection keeps its place among the command's other redirections (`&>> log 2>/dev/null` becomes `>> log 2>&1 2>/dev/null`, which still sends stderr to `/dev/null`).
|
|
18
|
+
|
|
19
|
+
Under `dialect: "posix"` the parser itself rejects `&>>`, so a POSIX file that uses it fails to parse and this rule does not run on it.
|
|
20
|
+
|
|
21
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
22
|
+
described in [version compatibility](../../compat.md):
|
|
23
|
+
|
|
24
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
25
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
|
|
26
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
27
|
+
|
|
28
|
+
### Suggestion
|
|
29
|
+
|
|
30
|
+
A suggestion rewrites the redirection:
|
|
31
|
+
|
|
32
|
+
<!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
|
|
33
|
+
```sh
|
|
34
|
+
build &>> build.log
|
|
35
|
+
# becomes
|
|
36
|
+
build >> build.log 2>&1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// eslint.config.js
|
|
45
|
+
{
|
|
46
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
47
|
+
rules: {
|
|
48
|
+
'sh/compat/no-append-all-redirect': 'error',
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Examples
|
|
54
|
+
|
|
55
|
+
### Incorrect
|
|
56
|
+
|
|
57
|
+
With `shellVersion: "3.2"`:
|
|
58
|
+
|
|
59
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
|
|
60
|
+
```sh
|
|
61
|
+
build &>> build.log
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Correct
|
|
65
|
+
|
|
66
|
+
With `shellVersion: "3.2"`:
|
|
67
|
+
|
|
68
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
69
|
+
```sh
|
|
70
|
+
build >> build.log 2>&1
|
|
71
|
+
build &> build.log
|
|
72
|
+
echo "use &>> on Bash 4"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
With `shellVersion: "4.0"` or newer, or with no `shellVersion`, `&>>` is fine.
|
|
76
|
+
|
|
77
|
+
## When not to use it
|
|
78
|
+
|
|
79
|
+
Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
|
|
80
|
+
unset, which makes the rule report nothing.
|
|
81
|
+
|
|
82
|
+
## Relationship to shellcheck
|
|
83
|
+
|
|
84
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
85
|
+
layer. Run shellcheck beside it.
|
|
86
|
+
|
|
87
|
+
shellcheck 0.11.0 reports nothing for `cmd &>> log` under either `-s sh` or `-s bash`. (`cmd &> log` is different: it reports SC3020 under `-s sh`.) So shellcheck gives no finding for `&>>` in a POSIX sh script or in a Bash 3.2 target. This rule exists for both.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# `sh/compat/no-associative-arrays`
|
|
2
|
+
|
|
3
|
+
Disallow associative arrays (`declare -A`, `typeset -A`) in scripts whose target shell lacks
|
|
4
|
+
them: Bash older than 4.0, including the Bash 3.2 that macOS ships as `/bin/bash`, and POSIX sh.
|
|
5
|
+
|
|
6
|
+
| Configs | Fixable | Languages |
|
|
7
|
+
| ------- | ----------- | --------- |
|
|
8
|
+
| `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
Associative arrays arrived in Bash 4.0. Bash 3.2 rejects `declare -A` as an invalid option, so
|
|
13
|
+
the script fails where the array is declared. Worse, if the script keeps going, every later
|
|
14
|
+
`${map[key]}` silently indexes an ordinary array by the arithmetic value of `key`, so lookups
|
|
15
|
+
return the wrong values without an error.
|
|
16
|
+
|
|
17
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
18
|
+
described in [version compatibility](../../compat.md):
|
|
19
|
+
|
|
20
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
21
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
|
|
22
|
+
- `dialect: "posix"`: reports at any `shellVersion`, because POSIX sh has no arrays.
|
|
23
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
24
|
+
|
|
25
|
+
The rule reads the options of every `declare` and `typeset` command, up to the first operand,
|
|
26
|
+
as the builtin itself does. Option processing stops at a variable name, at `--`, and at a word
|
|
27
|
+
that is not static. An option word reports when it is a `-` cluster containing `A`, such as
|
|
28
|
+
`-A`, `-gA`, or `-Ar`. The report is on that option word. Commands written through `builtin`
|
|
29
|
+
or `command` are recognized too, and so is `declare` under `dialect: "posix"`, where it parses
|
|
30
|
+
as an ordinary command.
|
|
31
|
+
|
|
32
|
+
The rule deliberately ignores:
|
|
33
|
+
|
|
34
|
+
- `local -A`, `readonly -A`, and `export -A`;
|
|
35
|
+
- `declare -a` (indexed arrays exist in every Bash) and `declare +A`;
|
|
36
|
+
- an `-A` after the first operand (`declare m -A`) or after `--`.
|
|
37
|
+
|
|
38
|
+
## Options
|
|
39
|
+
|
|
40
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// eslint.config.js
|
|
44
|
+
{
|
|
45
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
46
|
+
rules: {
|
|
47
|
+
'sh/compat/no-associative-arrays': 'error',
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
### Incorrect
|
|
55
|
+
|
|
56
|
+
With `shellVersion: "3.2"`:
|
|
57
|
+
|
|
58
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 4} -->
|
|
59
|
+
```sh
|
|
60
|
+
declare -A ports # associative arrays need Bash 4.0
|
|
61
|
+
typeset -A ports # the same declaration through typeset
|
|
62
|
+
declare -gA ports=([web]=80) # `A` inside an option cluster
|
|
63
|
+
builtin declare -A ports # reached through the builtin wrapper
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Correct
|
|
67
|
+
|
|
68
|
+
With `shellVersion: "3.2"`:
|
|
69
|
+
|
|
70
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
71
|
+
```sh
|
|
72
|
+
# Keep the mapping as JSON and look keys up with jq.
|
|
73
|
+
port=$(jq -r --arg k "$service" '.[$k]' ports.json)
|
|
74
|
+
|
|
75
|
+
declare -a hosts # indexed arrays exist in Bash 3.2
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
With `shellVersion: "4.0"` or newer, or with no `shellVersion`:
|
|
79
|
+
|
|
80
|
+
<!-- example: {"dialect": "bash", "shellVersion": "4.0"} -->
|
|
81
|
+
```sh
|
|
82
|
+
declare -A ports
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## When not to use it
|
|
86
|
+
|
|
87
|
+
Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
|
|
88
|
+
unset, which makes the rule report nothing.
|
|
89
|
+
|
|
90
|
+
## Relationship to shellcheck
|
|
91
|
+
|
|
92
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
93
|
+
layer. Run shellcheck beside it.
|
|
94
|
+
|
|
95
|
+
shellcheck 0.11.0 reports the command, not the option: under `-s sh`, `declare -A` draws SC3044 (`declare` is undefined in POSIX sh), and `local -A` draws SC3043. Under `-s bash` it reports nothing. It has no way to target a Bash version, so it reports nothing for `declare -A` in a script that must run on Bash 3.2. This rule exists for that target.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# `sh/compat/no-brace-sequence-padding-step`
|
|
2
|
+
|
|
3
|
+
Disallow zero-padded (`{01..10}`) and stepped (`{1..10..2}`, `{a..e..2}`) brace sequences in scripts
|
|
4
|
+
whose target shell lacks them: Bash older than 4.0, including the Bash 3.2 that macOS ships as
|
|
5
|
+
`/bin/bash`, and POSIX sh.
|
|
6
|
+
|
|
7
|
+
| Configs | Fixable | Languages |
|
|
8
|
+
| ---------------------------- | ----------- | --------- |
|
|
9
|
+
| `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
|
|
10
|
+
|
|
11
|
+
## Rule details
|
|
12
|
+
|
|
13
|
+
Both forms arrived in Bash 4.0, and neither fails on Bash 3.2, which is what makes them easy to miss. On Bash 3.2.57, `echo {01..03} {1..9..4}` prints `1 2 3 {1..9..4}` and exits 0: the padding is dropped and the stepped sequence stays as a literal word. The script carries on with different words. POSIX does not specify brace expansion, so what a POSIX target does with these words depends on the shell: dash prints `{01..03} {1..9..4}`, macOS's `/bin/sh` prints `1 2 3` for `{01..03}`, and Bash 5.3 under `--posix` prints `01 02 03`. A plain `{1..3}` is as shell-dependent, but this rule reports only the two forms Bash 4.0 added.
|
|
14
|
+
|
|
15
|
+
The parser keeps a brace sequence as literal text inside a word, so the rule reads the literal text of the words the shell brace-expands. It reports a `{…}` that holds nothing but a sequence:
|
|
16
|
+
|
|
17
|
+
- two integer endpoints (an optional sign and digits) or two single ASCII letters, with an optional third `..` field that is an integer step. A step makes the sequence stepped, for numbers and letters alike, and the step may be `0` or negative.
|
|
18
|
+
- integer endpoints are padded when either has more than one digit and starts with `0` after an optional `-`: `{01..10}`, `{1..03}`, `{-01..1}`, `{00..10}`. A lone `0` (`{0..3}`) and `{-0..2}` are not padded, and neither is a `+01` endpoint, which Bash 5.3 expands without padding, as Bash 3.2 does.
|
|
19
|
+
|
|
20
|
+
Everything else is left alone, because it is the same on every Bash or is not a sequence at all: `{1..3}`, `{a..c}`, a descending `{3..1}`, a comma list (`{01,02}`, `{01..03,x}`), mixed endpoints (`{a..5..2}`), multi-character endpoints, a variable endpoint (`{01..$n}`, which the shell never expands), and a malformed one (`{01..}`, `{1..9..x}`, spaces inside). Quoting and escaping are honoured: `"{01..10}"`, `'{01..10}'`, `$'{01..10}'`, `\{01..10}` and `{01..10\}` are plain text.
|
|
21
|
+
|
|
22
|
+
Only code the parser sees is read: a sequence inside a quoted string that is run later (`eval '...'`, `trap '...'`) is not seen. A sequence is read where the shell brace-expands: command words and arguments, `for` and `select` lists, the elements of an array literal without an index (`a=({01..03})`), and the operands of `declare`, `local`, `export` and `readonly` (also the value in `declare d={01..03}`). It is not read where the shell does no brace expansion: a plain assignment (`a={01..03}`), a `case` subject or pattern, the operands of `[[ ]]`, here-strings and here-documents, parameter-expansion words (`${v:-{01..03}}`), and a keyed array element (`a=([5]={01..03})`). A redirection target (`: > out{01..01}`) is read only when the sequence provably yields one word. With several words the target is an "ambiguous redirect" error on Bash 5.3 (and for a padded sequence on 3.2 too), so the script is broken on every Bash that has the feature and there is nothing to port. With one word the shells disagree silently: `out{01..01}` writes `out1` on Bash 3.2.57 and `out01` on 5.3.9, and `out{1..3..5}` writes a file literally named `out{1..3..5}` on 3.2.57 and `out1` on 5.3.9. A sequence is always static text, so its length is always known; a target that also holds another brace group (`out{01..01}{a,b}`) is not read, because the product cannot be counted here, so a redirect target with more than one brace group is skipped. The same selection applies under `dialect: "posix"`, although the system `dash` creates a literal file such as `out{01..02}` for any of them without an error: a several-word target is still a sequence that no Bash accepts. A here-string or here-document word is never read. A descriptor duplication whose whole target is a one-word sequence is read only where the shells differ (checked on 3.2.57 and 5.3.9). A padded non-negative number names the same descriptor on both (`>&{01..01}`, `2>&{02..02}`, `<&{00..00}`, and `>&{-00..-00}`, which is 0), so it is not reported. A negative number in a descriptor position (`2>&{-01..-01}`, `<&{-01..-01}`, `3>&{-01..-01}`) is an "ambiguous redirect" error on both, so it is not reported either. A stepped sequence still differs (`>&{1..3..5}`, `3>&{1..3..5}` and `<&{0..0..2}` are an error or a literal word on 3.2.57 and descriptor 1 or 0 on 5.3.9), and a bare `>&` with a negative number writes a file named `-1` on 3.2.57 and `-01` on 5.3.9, as does `&>{-01..-01}`, so those are reported. The report is on the whole word, once per word, for the first sequence in it.
|
|
23
|
+
|
|
24
|
+
Numbers are bounded the way Bash 5.3 bounds them, because a word outside the bounds stays literal on every Bash and nothing differs: each endpoint and the distance between them must fit a signed 64-bit integer, and so must the step's magnitude (a step of `-9223372036854775808` is not a step). Leading zeros do not count towards the value, so `{1..00000000000000000003}` is a sequence. `{1..3..999999999999999999999}`, `{1..99999999999999999999}` and `{-1..9223372036854775807..9223372036854775807}` are not reported. These bounds were measured on Bash 5.3.9 and 3.2.57 only; the 4.x releases' bounds were not measured.
|
|
25
|
+
|
|
26
|
+
The portable alternative is to count in a loop and format with `printf`, for example `i=1; while [ "$i" -le 3 ]; do printf '%02d\n' "$i"; i=$((i + 1)); done` for a padded sequence, or `i=1; while [ "$i" -le 9 ]; do echo "$i"; i=$((i + 4)); done` for a stepped one. Both run on Bash 3.2 and on dash.
|
|
27
|
+
|
|
28
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
29
|
+
described in [version compatibility](../../compat.md):
|
|
30
|
+
|
|
31
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
32
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
|
|
33
|
+
- `dialect: "posix"`: reports at any `shellVersion`, because POSIX does not specify brace expansion.
|
|
34
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// eslint.config.js
|
|
42
|
+
{
|
|
43
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
44
|
+
rules: {
|
|
45
|
+
'sh/compat/no-brace-sequence-padding-step': 'error',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
### Incorrect
|
|
53
|
+
|
|
54
|
+
With `shellVersion: "3.2"`:
|
|
55
|
+
|
|
56
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 3} -->
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
for n in {01..12}; do echo "report-$n"; done
|
|
60
|
+
echo {1..9..4}
|
|
61
|
+
cp backup{a..e..2}.tar /tmp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Correct
|
|
65
|
+
|
|
66
|
+
With `shellVersion: "3.2"`:
|
|
67
|
+
|
|
68
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
n=1
|
|
72
|
+
while [ "$n" -le 12 ]; do printf 'report-%02d\n' "$n"; n=$((n + 1)); done
|
|
73
|
+
echo {1..3} {a..c}
|
|
74
|
+
echo '{01..12}'
|
|
75
|
+
a={01..03}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
With `shellVersion: "4.0"` or newer, or with no `shellVersion`, both forms are fine.
|
|
79
|
+
|
|
80
|
+
## When not to use it
|
|
81
|
+
|
|
82
|
+
Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
|
|
83
|
+
unset, which makes the rule report nothing.
|
|
84
|
+
|
|
85
|
+
## Relationship to shellcheck
|
|
86
|
+
|
|
87
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
88
|
+
layer. Run shellcheck beside it.
|
|
89
|
+
|
|
90
|
+
shellcheck 0.11.0 reports SC3009 (brace expansion is undefined in POSIX sh) under `-s sh` for every brace expansion, a plain `{1..3}` and `{a,b}` included. Under `-s bash` it reports nothing for these sequences. It does not know which Bash release a script must run on, so it cannot report the padded and stepped forms for a Bash 3.2 target. This rule exists for that target.
|