@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,157 @@
|
|
|
1
|
+
# `sh/require-shebang`
|
|
2
|
+
|
|
3
|
+
Require line 1 of every shell file to be a shebang that runs a recognized shell.
|
|
4
|
+
|
|
5
|
+
| Configs | Fixable | Languages |
|
|
6
|
+
| ------- | ------- | --------- |
|
|
7
|
+
| `recommended`, `agent-authored` | autofix (missing shebang only) | `sh/sh` |
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
A script without a shebang runs under whichever shell happens to invoke it. A bash script
|
|
12
|
+
started as `sh deploy.sh`, or by a tool that defaults to `/bin/sh`, then fails in confusing
|
|
13
|
+
ways far from the real cause. A shebang makes the file's interpreter explicit.
|
|
14
|
+
|
|
15
|
+
This rule reads line 1 of the file and reports two problems:
|
|
16
|
+
|
|
17
|
+
- **`missingShebang`**: line 1 does not start with `#!`. A comment, a blank line, or
|
|
18
|
+
whitespace before `#!` all count as missing. An empty file is missing a shebang too.
|
|
19
|
+
- **`unrecognizedInterpreter`**: line 1 is a shebang, but it does not run one of the
|
|
20
|
+
recognized shells: `bash`, `sh`, `dash`, `ksh`, `mksh`, `zsh` or `bats`. Each one is
|
|
21
|
+
accepted two ways:
|
|
22
|
+
- by absolute path: `#!/bin/bash`, `#!/usr/local/bin/bash`, with any arguments after it;
|
|
23
|
+
- through `env` named by absolute path: `#!/usr/bin/env bash`,
|
|
24
|
+
`#!/usr/bin/env -S bash -e`. The rule skips `env` options, their arguments and
|
|
25
|
+
`NAME=value` assignments to find the command `env` runs.
|
|
26
|
+
|
|
27
|
+
A relative interpreter such as `#!bash` or `#!env bash` is reported, because the kernel
|
|
28
|
+
resolves it against the caller's working directory, not `PATH`.
|
|
29
|
+
|
|
30
|
+
A UTF-8 byte order mark before `#!` does not stop the shebang from counting.
|
|
31
|
+
|
|
32
|
+
The rule does **not** check that the shebang agrees with `languageOptions.dialect`.
|
|
33
|
+
`#!/bin/sh` in a file linted with `dialect: 'bash'` is accepted here. That mismatch belongs
|
|
34
|
+
to [`sh/shebang-matches-dialect`](./shebang-matches-dialect.md), so one problem is never reported under two rule names.
|
|
35
|
+
|
|
36
|
+
### Autofix
|
|
37
|
+
|
|
38
|
+
For `missingShebang`, the fix inserts one line above line 1. The interpreter comes from
|
|
39
|
+
`languageOptions.dialect`:
|
|
40
|
+
|
|
41
|
+
| `dialect` | Inserted with `style: 'env'` | Inserted with `style: 'absolute'` |
|
|
42
|
+
| ------------------ | ---------------------------- | --------------------------------- |
|
|
43
|
+
| `bash` (default) | `#!/usr/bin/env bash` | `#!/bin/bash` |
|
|
44
|
+
| `posix` | `#!/usr/bin/env sh` | `#!/bin/sh` |
|
|
45
|
+
| `mksh` | `#!/usr/bin/env mksh` | `#!/bin/mksh` |
|
|
46
|
+
| `zsh` | `#!/usr/bin/env zsh` | `#!/bin/zsh` |
|
|
47
|
+
| `bats` | `#!/usr/bin/env bats` | `#!/bin/bats` |
|
|
48
|
+
|
|
49
|
+
The inserted line ends with the file's own line break (`\r\n` in a CRLF file). For an empty
|
|
50
|
+
file the fix produces exactly the shebang line and a newline.
|
|
51
|
+
|
|
52
|
+
`unrecognizedInterpreter` has no fix. The file names some other interpreter, and choosing a
|
|
53
|
+
shell for it is a decision for a person.
|
|
54
|
+
|
|
55
|
+
## Options
|
|
56
|
+
|
|
57
|
+
This rule takes one options object:
|
|
58
|
+
|
|
59
|
+
- `style` (`'env' | 'absolute'`, default `'env'`): how the autofix spells the shebang it
|
|
60
|
+
inserts. `'env'` writes `#!/usr/bin/env <shell>`, which finds the shell on `PATH`.
|
|
61
|
+
`'absolute'` writes `#!/bin/<shell>`. The option affects only the fix. Both spellings
|
|
62
|
+
are accepted when checking a file.
|
|
63
|
+
|
|
64
|
+
Any other key, or any other `style` value, is a configuration error.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
// eslint.config.js
|
|
68
|
+
{
|
|
69
|
+
rules: {
|
|
70
|
+
'sh/require-shebang': ['error', { style: 'absolute' }],
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Incorrect
|
|
78
|
+
|
|
79
|
+
<!-- example: {"findings": 1, "messageIds": ["missingShebang"]} -->
|
|
80
|
+
```sh
|
|
81
|
+
echo "hello" # line 1 is a command: missingShebang
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
<!-- example: {"findings": 1, "messageIds": ["missingShebang"]} -->
|
|
85
|
+
```sh
|
|
86
|
+
# Deploys the site. # line 1 is an ordinary comment: missingShebang
|
|
87
|
+
echo "hello"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
<!-- example: {"findings": 1, "messageIds": ["unrecognizedInterpreter"]} -->
|
|
91
|
+
```sh
|
|
92
|
+
#!/usr/bin/python3 # not a shell: unrecognizedInterpreter
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
<!-- example: {"findings": 1, "messageIds": ["unrecognizedInterpreter"]} -->
|
|
96
|
+
```sh
|
|
97
|
+
#!/usr/bin/env node # env runs a non-shell: unrecognizedInterpreter
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
<!-- example: {"findings": 1, "messageIds": ["unrecognizedInterpreter"]} -->
|
|
101
|
+
```sh
|
|
102
|
+
#!bash # relative interpreter path: unrecognizedInterpreter
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Correct
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
#!/usr/bin/env bash
|
|
109
|
+
echo "hello"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
#!/usr/local/bin/bash -euo pipefail
|
|
114
|
+
echo "hello"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
#!/usr/bin/env -S bash -e
|
|
119
|
+
echo "hello"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
#!/bin/sh
|
|
124
|
+
# Accepted even under dialect: 'bash'; see shebang-matches-dialect.
|
|
125
|
+
echo "hello"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## When not to use it
|
|
129
|
+
|
|
130
|
+
Files that are only ever sourced (`. ./lib.sh`, `source lib.sh`) are never executed, so their
|
|
131
|
+
shebang is never read. If your project keeps such libraries without a shebang, turn the rule
|
|
132
|
+
off for them with `files` or `ignores` in your config:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
// eslint.config.js
|
|
136
|
+
export default [
|
|
137
|
+
{
|
|
138
|
+
files: ['**/*.sh'],
|
|
139
|
+
ignores: ['lib/**/*.sh'],
|
|
140
|
+
rules: { 'sh/require-shebang': 'error' },
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The rule does not detect sourced libraries on its own.
|
|
146
|
+
|
|
147
|
+
## Relationship to shellcheck
|
|
148
|
+
|
|
149
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
150
|
+
layer. Run shellcheck beside it.
|
|
151
|
+
|
|
152
|
+
shellcheck's SC2148 ("Tips depend on target shell and yours is unknown") fires when a file has
|
|
153
|
+
no shebang and no `shell=` directive. shellcheck's aim is to know which shell to analyze for,
|
|
154
|
+
so a `# shellcheck shell=bash` directive or a `--shell` flag silences it. This rule enforces
|
|
155
|
+
the policy that the script itself names its interpreter, which a directive or a command-line
|
|
156
|
+
flag does not satisfy. It also autofixes the missing line from your configured dialect.
|
|
157
|
+
shellcheck's SC1008 ("This shebang was unrecognized") overlaps `unrecognizedInterpreter`.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# `sh/require-strict-mode`
|
|
2
|
+
|
|
3
|
+
Require a script to turn on its strict-mode shell options (`set -euo pipefail` by default, or
|
|
4
|
+
`set -eu` for POSIX `sh`) within its first few statements.
|
|
5
|
+
|
|
6
|
+
| Configs | Fixable | Languages |
|
|
7
|
+
| ------- | ------- | --------- |
|
|
8
|
+
| `recommended`, `agent-authored`, `posix-portable` (with set -eu) | autofix | `sh/sh` |
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
A script without strict mode keeps going after things fail. A command that fails, a variable that
|
|
13
|
+
was never set, or a failed stage in the middle of a pipeline does not stop the script, so one
|
|
14
|
+
failed step turns into wrong output or a damaged state further down. This rule enforces the policy
|
|
15
|
+
once, at the top of the file: by default `errexit` (`-e`), `nounset` (`-u`) and `pipefail` must be
|
|
16
|
+
on within the first 5 statements. Under the `posix` dialect, the default leaves out `pipefail` (see
|
|
17
|
+
[Dialects](#dialects)).
|
|
18
|
+
|
|
19
|
+
The rule checks the **set of options turned on**, not the literal text. All of these satisfy the
|
|
20
|
+
Bash default:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
set -o errexit -o nounset -o pipefail
|
|
25
|
+
set -e; set -u; set -o pipefail
|
|
26
|
+
set -ue -o pipefail
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
How the rule reads the file:
|
|
30
|
+
|
|
31
|
+
- **Statements, not lines.** The window counts top-level statements from the top of the file.
|
|
32
|
+
Comments and blank lines are not statements, and `set -e; set -u` on one line is two
|
|
33
|
+
statements.
|
|
34
|
+
- **Order matters.** Statements in the window apply in order, and the rule checks which options
|
|
35
|
+
are on after the last one. A `set +e` (or `set +o errexit`) in the window turns `errexit` off
|
|
36
|
+
again, so the file is reported unless a later statement in the window turns it back on. A
|
|
37
|
+
`set +e` after the window is not checked. Turning `errexit` off around one command later in the
|
|
38
|
+
script is a common, deliberate pattern.
|
|
39
|
+
- **Only plain top-level `set` commands count.** A `set` in a subshell, a pipeline, or a background
|
|
40
|
+
job (`set -e &`) runs in a child process and never changes the script's options. A `set` inside a
|
|
41
|
+
function, an `if`, a loop, a `{ …; }` group or an `&&`/`||` list does not count either, even
|
|
42
|
+
though the shell may apply it there. The rule reads only plain top-level statements, so put the
|
|
43
|
+
prologue on its own line.
|
|
44
|
+
- **Arguments must be static.** The rule reads `set`'s arguments as the shell does: flag clusters
|
|
45
|
+
(`-eu`, `+e`), `-o name` / `+o name`, and an `o` inside a cluster (`-euo pipefail`) that takes the
|
|
46
|
+
next word as an option name. `--` and the first argument that is not a flag end the options. When
|
|
47
|
+
an argument's value is only known at run time (`set -e "$flags"`), the rule stops reading that
|
|
48
|
+
`set` command there. Options turned on before that argument still count.
|
|
49
|
+
|
|
50
|
+
The report is placed at line 1, column 1 and lists the required options that are not on.
|
|
51
|
+
|
|
52
|
+
**Autofix.** The fix inserts the prologue on its own line right after the shebang, or at line 1 when
|
|
53
|
+
the file has no shebang:
|
|
54
|
+
|
|
55
|
+
<!-- transform: {"kind": "fix"} -->
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
#!/bin/bash
|
|
59
|
+
cd "$1"
|
|
60
|
+
# becomes
|
|
61
|
+
#!/bin/bash
|
|
62
|
+
set -euo pipefail
|
|
63
|
+
cd "$1"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It uses the file's own line ending, so a CRLF file stays CRLF. The fix is
|
|
67
|
+
offered only when inserting the prologue would satisfy the rule. For example, `set -e` followed by
|
|
68
|
+
`set +e` in the window is reported with no fix, because the `set +e` would turn `errexit` off
|
|
69
|
+
again. Decide what that `set +e` is for, then edit the file by hand.
|
|
70
|
+
|
|
71
|
+
### Dialects
|
|
72
|
+
|
|
73
|
+
When `mode` is not configured, the required prologue depends on `languageOptions.dialect`:
|
|
74
|
+
|
|
75
|
+
| `dialect` | Default prologue |
|
|
76
|
+
| -------------------------------------------- | ------------------- |
|
|
77
|
+
| `bash` (the default), `bats`, `mksh`, `zsh` | `set -euo pipefail` |
|
|
78
|
+
| `posix` | `set -eu` |
|
|
79
|
+
|
|
80
|
+
`pipefail` became part of POSIX only in POSIX.1-2024, and older `dash` and BusyBox `ash` builds
|
|
81
|
+
still reject `set -o pipefail`. An autofix that inserted it would break a working `#!/bin/sh` script
|
|
82
|
+
on those shells, so under `posix` the default requires and inserts only `set -eu`. A `posix` file
|
|
83
|
+
may still turn `pipefail` on; the rule just doesn't require it.
|
|
84
|
+
|
|
85
|
+
A configured `mode` always wins, whatever the dialect. To require `pipefail` in POSIX scripts too,
|
|
86
|
+
configure it (see below).
|
|
87
|
+
|
|
88
|
+
The dialect comes from `languageOptions`, not from the shebang. A `#!/bin/sh` file linted under the
|
|
89
|
+
default `bash` dialect gets the `set -euo pipefail` default. That mismatch is reported at the
|
|
90
|
+
shebang by [`sh/shebang-matches-dialect`](./shebang-matches-dialect.md); set
|
|
91
|
+
`languageOptions.dialect` to `posix` for such files so the `pipefail` note above does not apply.
|
|
92
|
+
|
|
93
|
+
## Options
|
|
94
|
+
|
|
95
|
+
This rule takes one options object:
|
|
96
|
+
|
|
97
|
+
- `mode` (`string | string[]`, default `"set -euo pipefail"`, or `"set -eu"` under the `posix`
|
|
98
|
+
dialect): the required prologue.
|
|
99
|
+
- A **string** must be a `set` command that only turns options on, such as `"set -eu"` or
|
|
100
|
+
`"set -o errexit -o nounset"`. The options it turns on are what the rule requires, and the
|
|
101
|
+
string is the exact text the autofix inserts. A string that is not such a command (for example
|
|
102
|
+
`"set +e"` or `"echo hi"`) is a configuration error.
|
|
103
|
+
- A **list** names the required options by their long names (`errexit`, `nounset`, `pipefail`,
|
|
104
|
+
`noclobber`, …, any name `set -o` accepts). The autofix inserts a `set` command built from the
|
|
105
|
+
list. Options with a one-letter flag go into one cluster in list order. The first option with no
|
|
106
|
+
letter is attached to that cluster with `o`, and any others follow as `-o <name>`. So
|
|
107
|
+
`["errexit", "nounset", "pipefail"]` inserts `set -euo pipefail` and `["errexit", "nounset"]`
|
|
108
|
+
inserts `set -eu`.
|
|
109
|
+
|
|
110
|
+
<!-- transform: {"kind": "fix", "options": {"mode": ["errexit", "nounset", "pipefail"]}} -->
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
echo hi
|
|
114
|
+
# becomes
|
|
115
|
+
set -euo pipefail
|
|
116
|
+
echo hi
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
<!-- transform: {"kind": "fix", "options": {"mode": ["errexit", "nounset"]}} -->
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
echo hi
|
|
123
|
+
# becomes
|
|
124
|
+
set -eu
|
|
125
|
+
echo hi
|
|
126
|
+
```
|
|
127
|
+
- `withinStatements` (integer ≥ 1, default `5`): how many top-level statements from the top of the
|
|
128
|
+
file the prologue may take to appear.
|
|
129
|
+
- `allowShebangOnly` (`boolean`, default `false`): when `true`, options passed to the shell on the
|
|
130
|
+
shebang line count as turned on, so a shebang alone can satisfy the rule. Shebang options combine
|
|
131
|
+
with `set` statements in the window. Only spellings the operating system passes to the shell as
|
|
132
|
+
written are read:
|
|
133
|
+
- `#!/usr/bin/env -S bash -euo pipefail`: `env -S` splits the rest of the line into words, so
|
|
134
|
+
every word after the shell name is read.
|
|
135
|
+
- `#!/bin/bash -eu`: the kernel passes everything after the interpreter as **one** argument, so
|
|
136
|
+
exactly one flag word is read. `#!/bin/bash -e -u` arrives as the single argument `-e -u`,
|
|
137
|
+
which the shell rejects, so it counts as nothing.
|
|
138
|
+
- `#!/usr/bin/env bash -e` counts as nothing. Without `-S`, Linux passes `bash -e` to `env` as
|
|
139
|
+
one command name.
|
|
140
|
+
|
|
141
|
+
It is off by default because shebang options are ignored when the script is run as
|
|
142
|
+
`bash script.sh`, so a `set` line is the only prologue that applies however the script is
|
|
143
|
+
started.
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
// eslint.config.js
|
|
147
|
+
{
|
|
148
|
+
rules: {
|
|
149
|
+
'sh/require-strict-mode': ['error', {
|
|
150
|
+
mode: 'set -euo pipefail',
|
|
151
|
+
withinStatements: 5,
|
|
152
|
+
allowShebangOnly: false,
|
|
153
|
+
}],
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For POSIX `sh` scripts that only run on shells with `pipefail` support, and should require it:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
// eslint.config.js
|
|
162
|
+
{
|
|
163
|
+
files: ['**/*.sh'],
|
|
164
|
+
languageOptions: { dialect: 'posix' },
|
|
165
|
+
rules: {
|
|
166
|
+
'sh/require-strict-mode': ['error', { mode: ['errexit', 'nounset', 'pipefail'] }],
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
### Incorrect
|
|
174
|
+
|
|
175
|
+
<!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
|
|
176
|
+
```sh
|
|
177
|
+
#!/bin/bash
|
|
178
|
+
# reported: no strict-mode options are turned on
|
|
179
|
+
cd "$1"
|
|
180
|
+
rm -rf build
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
<!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
|
|
184
|
+
```sh
|
|
185
|
+
#!/bin/bash
|
|
186
|
+
# reported: pipefail is not turned on
|
|
187
|
+
set -eu
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
<!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
|
|
191
|
+
```sh
|
|
192
|
+
#!/bin/bash
|
|
193
|
+
# reported: the set +e in the window turns errexit off again (no autofix)
|
|
194
|
+
set -euo pipefail
|
|
195
|
+
set +e
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
<!-- example: {"findings": 1, "messageIds": ["missingStrictMode"]} -->
|
|
199
|
+
```sh
|
|
200
|
+
#!/bin/bash
|
|
201
|
+
# reported: a set inside a function doesn't apply to the script's top level
|
|
202
|
+
main() { set -euo pipefail; }
|
|
203
|
+
main "$@"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Correct
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
#!/bin/bash
|
|
210
|
+
set -euo pipefail
|
|
211
|
+
cd "$1"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
#!/bin/bash
|
|
216
|
+
# the options may be spread over statements and spelled long-form
|
|
217
|
+
set -e
|
|
218
|
+
set -o nounset -o pipefail
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
#!/bin/bash
|
|
223
|
+
# errexit is off for one command and back on inside the window
|
|
224
|
+
set -euo pipefail
|
|
225
|
+
set +e
|
|
226
|
+
grep -q pattern file
|
|
227
|
+
set -e
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
<!-- example: {"dialect": "posix"} -->
|
|
231
|
+
```sh
|
|
232
|
+
#!/bin/sh
|
|
233
|
+
# with languageOptions: { dialect: 'posix' } and no configured mode
|
|
234
|
+
set -eu
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## When not to use it
|
|
238
|
+
|
|
239
|
+
Turn this rule off if your project does not hold the strict-mode policy. For example, you may have
|
|
240
|
+
scripts that handle every error explicitly, or files that are sourced into another shell rather
|
|
241
|
+
than run, where a `set -e` would change the caller's shell. For a few files that are meant to be
|
|
242
|
+
lenient, disable it with a directive comment or a `files`-scoped config entry instead.
|
|
243
|
+
|
|
244
|
+
## Relationship to shellcheck
|
|
245
|
+
|
|
246
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
247
|
+
layer. Run shellcheck beside it.
|
|
248
|
+
|
|
249
|
+
shellcheck has no check that requires strict mode. It warns about the _consequences_ of not
|
|
250
|
+
having it, case by case. For example, SC2164 flags a `cd` whose failure isn't handled, and SC2155
|
|
251
|
+
flags a `local x=$(cmd)` that hides the command's exit status. This rule enforces the _policy_
|
|
252
|
+
that prevents that whole class of problem, once, at the top of the file, which shellcheck's
|
|
253
|
+
per-construct checks cannot express.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# `sh/require-tty-guard`
|
|
2
|
+
|
|
3
|
+
Require interactive commands such as `read` and `select` to be guarded by a `[ -t 0 ]` test, or to
|
|
4
|
+
have their input supplied explicitly.
|
|
5
|
+
|
|
6
|
+
| Configs | Fixable | Languages |
|
|
7
|
+
| ---------------- | ---------------------------- | --------- |
|
|
8
|
+
| `agent-authored` | suggestions (wrap in a guard) | `sh/sh` |
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
A prompt that waits on the terminal hangs forever when a script runs without one: in CI, from
|
|
13
|
+
cron, or from an agent's tool call. In automation it also reads whatever happens to be on stdin.
|
|
14
|
+
This rule reports `read` and `select` (and any command you add with `commands`) that are not
|
|
15
|
+
guarded.
|
|
16
|
+
|
|
17
|
+
The check is **purely syntactic**. The rule never follows data or control flow, so what it treats
|
|
18
|
+
as guarded is exactly the following.
|
|
19
|
+
|
|
20
|
+
A command is guarded by a test when it is:
|
|
21
|
+
|
|
22
|
+
- lexically inside the `then` or `else` body of an `if` whose condition contains a TTY test, or
|
|
23
|
+
- on the right of an `&&` whose left side contains a TTY test (`[ -t 0 ] && [ -n "$X" ] && read x`).
|
|
24
|
+
|
|
25
|
+
A TTY test is `[ -t 0 ]`, `test -t 0` or `[[ -t 0 ]]`. Only file descriptor `0` counts, and the
|
|
26
|
+
test may sit anywhere in the condition: `[ ! -t 0 ]` is also recognized as a TTY test, because
|
|
27
|
+
the rule does not interpret negation.
|
|
28
|
+
|
|
29
|
+
A command has its input supplied explicitly, and is not reported, when:
|
|
30
|
+
|
|
31
|
+
- it, or any statement that encloses it, redirects fd 0 from a file, here-document or
|
|
32
|
+
here-string (`read x < file`, `read x <<< "$v"`, `while read l; do …; done < file`), or from
|
|
33
|
+
another descriptor (`read x <&3`);
|
|
34
|
+
- it is on the right of a pipe (`echo a | read x`, `cat f | while read l; do …; done`);
|
|
35
|
+
- it is inside a `coproc`, which reads its own input pipe;
|
|
36
|
+
- it is `read -u N` for a descriptor other than `0` (`read -u 3 x`, `read -ru3 x`). A descriptor
|
|
37
|
+
that is not a static value is also treated as not stdin.
|
|
38
|
+
|
|
39
|
+
A `read` with `-p` is still reported: the prompt does not make it safe without a terminal.
|
|
40
|
+
|
|
41
|
+
### Limits
|
|
42
|
+
|
|
43
|
+
The rule is honest about what it cannot see:
|
|
44
|
+
|
|
45
|
+
- **A guard in a calling function is not seen.** If `ask() { read x; }` is only ever called as
|
|
46
|
+
`[ -t 0 ] && ask`, the `read` is still reported.
|
|
47
|
+
- **Early exits are not seen.** `[ -t 0 ] || exit 1` followed by `read x` does not guard the
|
|
48
|
+
`read`; neither does a guard in an earlier statement, a `||` list, or a `while` condition.
|
|
49
|
+
- **Negation is not interpreted.** `if [ ! -t 0 ]; then read x; fi` counts as guarded.
|
|
50
|
+
- **Variables are not tracked.** `is_tty=$([ -t 0 ] && echo 1)` is not a guard.
|
|
51
|
+
- **Command names are read as written.** A command reached through a wrapper (`command read x`)
|
|
52
|
+
or with a dynamic name is not matched.
|
|
53
|
+
|
|
54
|
+
## Options
|
|
55
|
+
|
|
56
|
+
- `commands` (`string[]`, default `['read', 'select']`): the interactive commands to check. A
|
|
57
|
+
configured list **replaces** the default, as with `sh/require-network-wrapper`; include `read`
|
|
58
|
+
and `select` to keep them. `select` is parsed as a loop, not a command, and is checked whenever
|
|
59
|
+
the list contains `select`.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// eslint.config.js
|
|
63
|
+
{
|
|
64
|
+
rules: {
|
|
65
|
+
'sh/require-tty-guard': ['error', { commands: ['read', 'select', 'confirm'] }],
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Suggestion
|
|
71
|
+
|
|
72
|
+
The rule offers one suggestion per report: wrap the statement in `if [ -t 0 ]; then … fi`:
|
|
73
|
+
|
|
74
|
+
<!-- transform: {"kind": "suggestion"} -->
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
read name
|
|
78
|
+
# becomes
|
|
79
|
+
if [ -t 0 ]; then read name; fi
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A multi-line statement that starts its own line is wrapped on lines of its own. Anywhere else the
|
|
83
|
+
wrapper goes around it in place, because an earlier command on the line may have opened a
|
|
84
|
+
here-document whose body begins at the next line break; a line break goes before `fi` only when the
|
|
85
|
+
statement ends in a here-document of its own. A statement run in the background (`read x &`)
|
|
86
|
+
gets no suggestion, because wrapping it would change what is backgrounded. A statement whose exit
|
|
87
|
+
status steers control flow (an `if`/`while`/`until` condition, or an operand of `&&` or `||`) is
|
|
88
|
+
reported without a suggestion too: the wrapper would exit 0 when there is no terminal, which
|
|
89
|
+
changes which way the construct goes. The suggestion is
|
|
90
|
+
never applied by `--fix`; whether the script should skip the prompt or fail when there is no
|
|
91
|
+
terminal is your decision.
|
|
92
|
+
|
|
93
|
+
## Examples
|
|
94
|
+
|
|
95
|
+
### Incorrect
|
|
96
|
+
|
|
97
|
+
<!-- example: {"findings": 4, "messageIds": ["unguardedInteractive", "unguardedInteractive", "unguardedInteractive", "unguardedInteractive"]} -->
|
|
98
|
+
```sh
|
|
99
|
+
read name # waits on stdin with no terminal check
|
|
100
|
+
read -p "Name: " name # a prompt does not make it safe
|
|
101
|
+
select opt in a b; do :; done # select reads the terminal too
|
|
102
|
+
[ -t 0 ] || read x # `||` is not a guard
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Correct
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
if [ -t 0 ]; then read name; fi
|
|
109
|
+
[ -t 0 ] && read -p "Name: " name
|
|
110
|
+
read name < answers.txt
|
|
111
|
+
read name <<< "$default"
|
|
112
|
+
cat answers.txt | while read -r line; do :; done
|
|
113
|
+
read -u 3 name # from descriptor 3, not stdin
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## When not to use it
|
|
117
|
+
|
|
118
|
+
Turn the rule off for scripts that are interactive by definition and never run unattended, such
|
|
119
|
+
as a local setup wizard.
|
|
120
|
+
|
|
121
|
+
## Relationship to shellcheck
|
|
122
|
+
|
|
123
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
124
|
+
layer. Run shellcheck beside it.
|
|
125
|
+
|
|
126
|
+
shellcheck has no equivalent check: it does not report a `read` that can run without a terminal.
|
|
127
|
+
Its `read` diagnostics (for example SC2162, `read` without `-r`) concern how input is parsed, not
|
|
128
|
+
where it comes from.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# `sh/shebang-matches-dialect`
|
|
2
|
+
|
|
3
|
+
Require the shebang to name the same kind of shell, bash or POSIX, as `languageOptions.dialect`.
|
|
4
|
+
|
|
5
|
+
| Configs | Fixable | Languages |
|
|
6
|
+
| ------------------------------- | ----------- | --------- |
|
|
7
|
+
| `recommended`, `agent-authored`, `posix-portable` | not fixable | `sh/sh` |
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
Dialect-sensitive rules read `languageOptions.dialect` from your config, not the file. When the
|
|
12
|
+
two disagree they advise for the wrong shell. A `#!/bin/sh` script linted under the default
|
|
13
|
+
`bash` dialect is told, by [`sh/require-strict-mode`](./require-strict-mode.md), to turn on
|
|
14
|
+
`pipefail`, which dash and older BusyBox `ash` reject. This rule reports the disagreement once,
|
|
15
|
+
at the shebang (line 1, in full), so the fix is one config entry or one line.
|
|
16
|
+
|
|
17
|
+
It reports two cases:
|
|
18
|
+
|
|
19
|
+
- **`posixShebangBashDialect`**: the shebang runs a POSIX shell (`sh`, `dash` or `ash`) but the
|
|
20
|
+
dialect is `bash` (the default).
|
|
21
|
+
- **`bashShebangPosixDialect`**: the shebang runs `bash` but the dialect is `posix`.
|
|
22
|
+
|
|
23
|
+
The shell is found the way [`sh/require-shebang`](./require-shebang.md) finds it: an absolute
|
|
24
|
+
interpreter path (`#!/bin/sh`, `#!/usr/local/bin/bash`), or an absolute-path `env` and the command
|
|
25
|
+
it runs (`#!/usr/bin/env sh`, `#!/usr/bin/env -S bash -e`). A relative interpreter such as
|
|
26
|
+
`#!sh` is not read; `sh/require-shebang` reports it.
|
|
27
|
+
|
|
28
|
+
Nothing else is reported. A file with no shebang is clean here (that is `sh/require-shebang`'s
|
|
29
|
+
concern), as is a shebang that names another interpreter, and any file whose dialect is `mksh`,
|
|
30
|
+
`zsh` or `bats`. A shebang and dialect that already agree are clean: `#!/usr/bin/env bash` under
|
|
31
|
+
`bash`, `#!/bin/sh` under `posix`.
|
|
32
|
+
|
|
33
|
+
The message tells you to set the dialect for these files, or to change the shebang. To set it,
|
|
34
|
+
scope `languageOptions.dialect` to the files that use that shell:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// eslint.config.js
|
|
38
|
+
export default [
|
|
39
|
+
{
|
|
40
|
+
files: ['scripts/posix/**/*.sh'],
|
|
41
|
+
languageOptions: { dialect: 'posix' },
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
This rule has no options.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// eslint.config.js
|
|
52
|
+
{
|
|
53
|
+
rules: {
|
|
54
|
+
'sh/shebang-matches-dialect': 'error',
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
### Incorrect
|
|
62
|
+
|
|
63
|
+
<!-- example: {"dialect": "bash", "messageIds": ["posixShebangBashDialect"], "findings": 1} -->
|
|
64
|
+
```sh
|
|
65
|
+
#!/bin/sh
|
|
66
|
+
# dialect: 'bash' (the default): posixShebangBashDialect
|
|
67
|
+
echo "hello"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
<!-- example: {"dialect": "bash", "messageIds": ["posixShebangBashDialect"], "findings": 1} -->
|
|
71
|
+
```sh
|
|
72
|
+
#!/usr/bin/env dash
|
|
73
|
+
# dialect: 'bash': posixShebangBashDialect
|
|
74
|
+
echo "hello"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
<!-- example: {"dialect": "posix", "messageIds": ["bashShebangPosixDialect"], "findings": 1} -->
|
|
78
|
+
```sh
|
|
79
|
+
#!/usr/bin/env bash
|
|
80
|
+
# dialect: 'posix': bashShebangPosixDialect
|
|
81
|
+
echo "hello"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Correct
|
|
85
|
+
|
|
86
|
+
<!-- example: {"dialect": "bash"} -->
|
|
87
|
+
```sh
|
|
88
|
+
#!/usr/bin/env bash
|
|
89
|
+
# dialect: 'bash'
|
|
90
|
+
echo "hello"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
<!-- example: {"dialect": "posix"} -->
|
|
94
|
+
```sh
|
|
95
|
+
#!/bin/sh
|
|
96
|
+
# dialect: 'posix'
|
|
97
|
+
echo "hello"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
echo "no shebang: sh/require-shebang's concern, not this rule's"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## When not to use it
|
|
105
|
+
|
|
106
|
+
Turn it off if your project deliberately lints POSIX-shebang scripts as bash, for example
|
|
107
|
+
because every `sh` on your targets is bash. Also turn it off for files you only source, whose
|
|
108
|
+
shebang is never read.
|
|
109
|
+
|
|
110
|
+
## Relationship to shellcheck
|
|
111
|
+
|
|
112
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
113
|
+
layer. Run shellcheck beside it.
|
|
114
|
+
|
|
115
|
+
shellcheck chooses its dialect from the shebang (or a `shell=` directive) and then checks the file
|
|
116
|
+
against it, so it has no notion of a mismatch between the shebang and a separately configured
|
|
117
|
+
dialect. Its SC3xxx checks report bash-only features in a `#!/bin/sh` script, which is
|
|
118
|
+
a different question: that the syntax is wrong for the shebang, not that your lint configuration
|
|
119
|
+
disagrees with it. This rule keeps eslint-sh's own dialect-sensitive rules honest about the
|
|
120
|
+
shell they are advising for.
|
package/docs/selectors.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
classes** against shell ASTs, in addition to matching bare node type names
|
|
6
6
|
(`CallExpr`, `IfClause`, ...). Selector-first rule authoring — writing a rule as a selector string
|
|
7
7
|
rather than a `create()` full of manual type checks — is a headline feature of this plugin; this
|
|
8
|
-
page is a copy-paste cookbook of real selectors, each backed by a passing test
|
|
9
|
-
|
|
8
|
+
page is a copy-paste cookbook of real selectors, each backed by a passing test in this package's
|
|
9
|
+
test suite so the examples can't silently rot as the plugin evolves.
|
|
10
10
|
|
|
11
11
|
Looking for a complete, runnable `create()` that uses one of these selectors — the full
|
|
12
12
|
`{ meta, create(context) }` rule module, `context.report`, and the `eslint.config.mjs`
|