@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,73 @@
|
|
|
1
|
+
# `sh/compat/no-lastpipe-semantics`
|
|
2
|
+
|
|
3
|
+
Disallow `shopt -s lastpipe` in scripts whose target shell lacks the option: Bash older than 4.2,
|
|
4
|
+
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
|
+
`lastpipe` arrived in Bash 4.2 (Bash NEWS for 4.2: "a new `lastpipe` shell option"). It runs the last command of a pipeline in the current shell. Bash 3.2 has no such option: `shopt` fails with `invalid shell option name`, and the last command keeps running in a subshell, so a `cmd | while read …; do total=…; done` loop silently loses its assignments.
|
|
13
|
+
|
|
14
|
+
The rule reports a `shopt` command (also through `builtin` or `command`) whose options, up to the first operand, include `-s` and which names `lastpipe`; the report is on the `lastpipe` word. `shopt -u lastpipe`, a query (`shopt lastpipe`), `shopt -o` (which names `set -o` options) and a name that is not static are not reported.
|
|
15
|
+
|
|
16
|
+
The portable alternative is process substitution: `while read -r line; do …; done < <(cmd)` keeps the loop in the current shell on every Bash that has process substitution. Process substitution is not POSIX, so for `dialect: "posix"` the message suggests a temporary file (`cmd > "$tmp"`, then `done < "$tmp"`) or a here-document instead. It hides `cmd`'s exit status, though: the pipeline's status was the loop's (or, with `pipefail`, the first failure), and with `< <(cmd)` a failing `cmd` is invisible to the script, so check it another way (for example, write `cmd`'s output to a temporary file and test `cmd`'s status before reading it).
|
|
17
|
+
|
|
18
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
19
|
+
described in [version compatibility](../../compat.md):
|
|
20
|
+
|
|
21
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
22
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.2`.
|
|
23
|
+
- `dialect: "posix"`: reports at any `shellVersion`.
|
|
24
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
25
|
+
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// eslint.config.js
|
|
32
|
+
{
|
|
33
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
34
|
+
rules: {
|
|
35
|
+
'sh/compat/no-lastpipe-semantics': 'error',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### Incorrect
|
|
43
|
+
|
|
44
|
+
With `shellVersion: "3.2"`:
|
|
45
|
+
|
|
46
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 1} -->
|
|
47
|
+
```sh
|
|
48
|
+
shopt -s lastpipe
|
|
49
|
+
cmd | while read -r line; do total=$line; done
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Correct
|
|
53
|
+
|
|
54
|
+
With `shellVersion: "3.2"`:
|
|
55
|
+
|
|
56
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
57
|
+
```sh
|
|
58
|
+
while read -r line; do total=$line; done < <(cmd)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
With `shellVersion: "4.2"` or newer, or with no `shellVersion`, the original forms are fine.
|
|
62
|
+
|
|
63
|
+
## When not to use it
|
|
64
|
+
|
|
65
|
+
Turn the rule off when your scripts never run on Bash older than 4.2, or leave
|
|
66
|
+
`shellVersion` unset, which makes the rule report nothing.
|
|
67
|
+
|
|
68
|
+
## Relationship to shellcheck
|
|
69
|
+
|
|
70
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
71
|
+
layer. Run shellcheck beside it.
|
|
72
|
+
|
|
73
|
+
shellcheck warns that variables assigned in the last pipeline element are lost (SC2030/SC2031), and understands `shopt -s lastpipe` for that purpose, but it cannot target a Bash version, so it cannot report the option for a Bash 3.2 target. This rule exists for that target.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# `sh/compat/no-let`
|
|
2
|
+
|
|
3
|
+
Disallow the `let` builtin in scripts that target POSIX sh.
|
|
4
|
+
|
|
5
|
+
| Configs | Fixable | Languages |
|
|
6
|
+
| ------- | ------- | --------- |
|
|
7
|
+
| `posix-portable` | not fixable | `sh/sh` |
|
|
8
|
+
|
|
9
|
+
## Rule details
|
|
10
|
+
|
|
11
|
+
POSIX lists `let` among the command names whose results are unspecified ([Shell Command Language, section 2.9.1.4](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_09_01_04)). dash, the `/bin/sh` of Debian and Ubuntu, has no such command: `let n=1+2` fails with `let: not found` and status 127, and `n` stays unset. Under `set -e` that stops the script.
|
|
12
|
+
|
|
13
|
+
`bash --posix` and BusyBox ash both accept `let`, so a script can pass on them and still fail on dash.
|
|
14
|
+
|
|
15
|
+
Under `dialect: "posix"` the parser reads `let` as an ordinary command, and the rule reports a command whose effective name is `let`, after following wrappers such as `command`. It reports the `let` word. `command -v let` only asks about the word and is not reported. A use only a shell with a version variable can reach is not reported either, such as the body of `if [ -n "${BASH_VERSION-}" ]`, because dash never runs it; [version compatibility](../../compat.md#a-branch-for-one-shell) lists the shapes recognized.
|
|
16
|
+
|
|
17
|
+
A function named `let` anywhere in the file exempts a call that names it directly, since dash can then run that function. This is only a name check across the whole file: it ignores execution order and does not prove the function exists at the call. Each of these stays silent, and the `let` call still fails on dash with `let: not found` and status 127:
|
|
18
|
+
|
|
19
|
+
- A call that runs before the definition.
|
|
20
|
+
- A definition in a branch that is not taken.
|
|
21
|
+
- A definition in a subshell, or in a function that is never called.
|
|
22
|
+
- A definition removed by a later `unset -f let`.
|
|
23
|
+
|
|
24
|
+
A call through a wrapper gets no exemption and is reported. `command let n=1` skips the function and fails with status 127, as does `env let n=1`, and dash has no `builtin` at all.
|
|
25
|
+
|
|
26
|
+
The portable alternative is arithmetic expansion, `n=$((1 + 2))`. The rule offers no suggestion, because `let` sets its exit status from the value of its last expression and an assignment does not, so the rewrite can change what `set -e` or an `if` sees.
|
|
27
|
+
|
|
28
|
+
This is a POSIX-only gate. Every Bash has `let`, so whether the rule reports depends only on `dialect`, as described in [version compatibility](../../compat.md):
|
|
29
|
+
|
|
30
|
+
- `dialect: "posix"`: reports at any `shellVersion`.
|
|
31
|
+
- `dialect: "bash"` (the default), `"bats"`, `"mksh"` or `"zsh"`: never reports.
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// eslint.config.js
|
|
39
|
+
{
|
|
40
|
+
languageOptions: { dialect: 'posix' },
|
|
41
|
+
rules: {
|
|
42
|
+
'sh/compat/no-let': 'error',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### Incorrect
|
|
50
|
+
|
|
51
|
+
With `dialect: "posix"`:
|
|
52
|
+
|
|
53
|
+
<!-- example: {"dialect": "posix", "findings": 2} -->
|
|
54
|
+
```sh
|
|
55
|
+
let n=1+2
|
|
56
|
+
let "m = n * 2"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Correct
|
|
60
|
+
|
|
61
|
+
With `dialect: "posix"`:
|
|
62
|
+
|
|
63
|
+
<!-- example: {"dialect": "posix"} -->
|
|
64
|
+
```sh
|
|
65
|
+
n=$((1 + 2))
|
|
66
|
+
m=$((n * 2))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## When not to use it
|
|
70
|
+
|
|
71
|
+
Turn the rule off when your scripts only ever run where `let` exists, such as under Bash or BusyBox ash, and set `dialect` to match if you can.
|
|
72
|
+
|
|
73
|
+
## Relationship to shellcheck
|
|
74
|
+
|
|
75
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
76
|
+
layer. Run shellcheck beside it.
|
|
77
|
+
|
|
78
|
+
shellcheck reports this construct as SC3039 for a script whose shebang says `sh`. This rule reports it for the dialect you configure, whatever the shebang says, and at the severity you choose.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# `sh/compat/no-mapfile`
|
|
2
|
+
|
|
3
|
+
Disallow the `mapfile` builtin and its synonym `readarray` 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` | suggestions | `sh/sh` |
|
|
9
|
+
|
|
10
|
+
## Rule details
|
|
11
|
+
|
|
12
|
+
`mapfile` and `readarray` arrived in Bash 4.0. On Bash 3.2 neither is a builtin, so the shell
|
|
13
|
+
looks for an external command of that name, fails with `command not found`, and the array is
|
|
14
|
+
never filled.
|
|
15
|
+
|
|
16
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
17
|
+
described in [version compatibility](../../compat.md):
|
|
18
|
+
|
|
19
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
20
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.0`.
|
|
21
|
+
- `dialect: "posix"`: reports at any `shellVersion`.
|
|
22
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
23
|
+
|
|
24
|
+
The rule reports every command whose effective command is `mapfile` or `readarray`, after
|
|
25
|
+
following transparent wrappers such as `command` and `builtin`. The report is on the whole
|
|
26
|
+
command.
|
|
27
|
+
|
|
28
|
+
### Suggestion
|
|
29
|
+
|
|
30
|
+
For `mapfile -t NAME` or `readarray -t NAME`, written directly (no wrapper and no leading
|
|
31
|
+
`VAR=value`) in a Bash or Bats file, the rule suggests a `while IFS= read -r` loop that fills
|
|
32
|
+
the same array the same way:
|
|
33
|
+
|
|
34
|
+
<!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
mapfile -t lines < in.txt
|
|
38
|
+
# becomes
|
|
39
|
+
{ lines=(); while IFS= read -r || [ -n "$REPLY" ]; do lines+=("$REPLY"); done; } < in.txt
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `|| [ -n "$REPLY" ]` keeps a last line that has no trailing newline, as `mapfile -t` does.
|
|
43
|
+
- Reading with no variable name keeps each line unchanged, including leading and trailing
|
|
44
|
+
spaces and backslashes. The tradeoff is that `read` stores each line in `REPLY`, which
|
|
45
|
+
`mapfile` does not touch. After the loop, `REPLY` is the empty string, whatever it held
|
|
46
|
+
before. `REPLY` is `read`'s own default variable, so this overwrites less than a named loop
|
|
47
|
+
variable such as `line` would. If your script uses `REPLY` across this command, save it
|
|
48
|
+
first or edit the loop.
|
|
49
|
+
- The brace group keeps the replacement a single command, so a redirection (`< in.txt`,
|
|
50
|
+
`<<< "$text"`, `< <(cmd)`) or a pipeline (`cmd | mapfile -t lines`) still feeds the loop.
|
|
51
|
+
- With no `NAME`, `mapfile` fills `MAPFILE`, and so does the loop.
|
|
52
|
+
|
|
53
|
+
Other forms are reported without a suggestion, because no loop reproduces them exactly or the
|
|
54
|
+
target has no arrays at all: `mapfile` without `-t` (each element keeps its newline), any other
|
|
55
|
+
option (`-d`, `-n`, `-O`, `-s`, `-u`, `-C`), an array name that is not static, a leading
|
|
56
|
+
assignment, a wrapper, and every file under `dialect: "posix"`.
|
|
57
|
+
|
|
58
|
+
## Options
|
|
59
|
+
|
|
60
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
// eslint.config.js
|
|
64
|
+
{
|
|
65
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
66
|
+
rules: {
|
|
67
|
+
'sh/compat/no-mapfile': 'error',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
### Incorrect
|
|
75
|
+
|
|
76
|
+
With `shellVersion: "3.2"`:
|
|
77
|
+
|
|
78
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 3} -->
|
|
79
|
+
```sh
|
|
80
|
+
mapfile -t lines < in.txt # mapfile needs Bash 4.0
|
|
81
|
+
git ls-files | readarray -t files # readarray is the same builtin
|
|
82
|
+
command mapfile -t lines < in.txt # reached through the command wrapper
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Correct
|
|
86
|
+
|
|
87
|
+
With `shellVersion: "3.2"`:
|
|
88
|
+
|
|
89
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
90
|
+
```sh
|
|
91
|
+
{ lines=(); while IFS= read -r || [ -n "$REPLY" ]; do lines+=("$REPLY"); done; } < in.txt
|
|
92
|
+
|
|
93
|
+
while IFS= read -r line; do
|
|
94
|
+
printf '%s\n' "$line"
|
|
95
|
+
done < in.txt
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
With `shellVersion: "4.0"` or newer, or with no `shellVersion`:
|
|
99
|
+
|
|
100
|
+
<!-- example: {"dialect": "bash", "shellVersion": "4.0"} -->
|
|
101
|
+
```sh
|
|
102
|
+
mapfile -t lines < in.txt
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## When not to use it
|
|
106
|
+
|
|
107
|
+
Turn the rule off when your scripts never run on Bash older than 4.0, or leave `shellVersion`
|
|
108
|
+
unset, which makes the rule report nothing.
|
|
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 reports Bash-only features in scripts it treats as POSIX sh (the SC3xxx checks), but
|
|
116
|
+
it cannot target a Bash version, so it cannot report `mapfile` in a script that must run on
|
|
117
|
+
Bash 3.2. shellcheck 0.11.0 does recommend `mapfile` or `read -a` over `a=($(cmd))` splitting (SC2207) under `-s bash`, which is
|
|
118
|
+
the opposite advice for a Bash 3.2 target. This rule exists for that target.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `sh/compat/no-nameref`
|
|
2
|
+
|
|
3
|
+
Disallow namerefs (`declare -n`, `typeset -n`, `local -n`) in scripts whose target shell lacks
|
|
4
|
+
them: Bash older than 4.3, 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
|
+
Namerefs arrived in Bash 4.3 (Bash NEWS for 4.3). Bash 4.2 and older reject `-n` with `local: -n: invalid option`, so the reference is never made, and an assignment through it sets a variable of its own name instead of the caller's.
|
|
13
|
+
|
|
14
|
+
The rule reads the options of every `declare`, `typeset` and `local` command, up to the first operand, as the builtin does. An option word reports when it is a `-` cluster containing `n` (`-n`, `-rn`); the report is on that word. `export -n`, which removes the export attribute and is in every Bash, is not read, and neither is an `-n` after the first operand or after `--`.
|
|
15
|
+
|
|
16
|
+
The portable alternative is to pass the variable's name and use it directly. On Bash, assign through the name with `printf -v "$name" '%s' "$value"` and read it with `${!name}`; both are in Bash 3.2. A POSIX target has neither, so its message suggests `eval "$name=\$value"` to assign and `eval "value=\$$name"` to read, after checking that the name is a valid identifier, since `eval` runs whatever the name contains.
|
|
17
|
+
|
|
18
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
19
|
+
described in [version compatibility](../../compat.md):
|
|
20
|
+
|
|
21
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
22
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.3`.
|
|
23
|
+
- `dialect: "posix"`: reports at any `shellVersion`, with the `eval` advice.
|
|
24
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
25
|
+
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// eslint.config.js
|
|
32
|
+
{
|
|
33
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
34
|
+
rules: {
|
|
35
|
+
'sh/compat/no-nameref': 'error',
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### Incorrect
|
|
43
|
+
|
|
44
|
+
With `shellVersion: "3.2"`:
|
|
45
|
+
|
|
46
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
|
|
47
|
+
```sh
|
|
48
|
+
set_result() {
|
|
49
|
+
local -n out=$1
|
|
50
|
+
out=done
|
|
51
|
+
}
|
|
52
|
+
declare -n alias=target
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Correct
|
|
56
|
+
|
|
57
|
+
With `shellVersion: "3.2"`:
|
|
58
|
+
|
|
59
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
60
|
+
```sh
|
|
61
|
+
set_result() {
|
|
62
|
+
printf -v "$1" '%s' done
|
|
63
|
+
}
|
|
64
|
+
read_by_name() {
|
|
65
|
+
printf '%s\n' "${!1}"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
With `shellVersion: "4.3"` or newer, or with no `shellVersion`, namerefs are fine.
|
|
70
|
+
|
|
71
|
+
## When not to use it
|
|
72
|
+
|
|
73
|
+
Turn the rule off when your scripts never run on Bash older than 4.3, or leave `shellVersion`
|
|
74
|
+
unset, which makes the rule report nothing.
|
|
75
|
+
|
|
76
|
+
## Relationship to shellcheck
|
|
77
|
+
|
|
78
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
79
|
+
layer. Run shellcheck beside it.
|
|
80
|
+
|
|
81
|
+
Under `-s sh`, shellcheck 0.11.0 reports the command, not the option: `local -n` draws SC3043 (`local` is undefined in POSIX sh), and `declare -n` and `typeset -n` draw SC3044. Under `-s bash` it reports nothing for a nameref. It does not know which Bash release a script must run on, so it reports nothing for a nameref in a Bash 4.2 or 3.2 target. This rule exists for that target.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# `sh/compat/no-negative-array-index`
|
|
2
|
+
|
|
3
|
+
Disallow negative indexed-array subscripts written as a literal (`${arr[-1]}`, `arr[-1]=x`) in scripts
|
|
4
|
+
whose target shell lacks them: Bash older than 4.3, 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
|
+
Negative subscripts count back from the end of an indexed array. Bash NEWS for 4.3 introduces assigning, referencing and unsetting elements this way; Bash 4.2 already reads `${arr[-1]}`, but rejects the element length `${#arr[-1]}`, assigning and unsetting with `bad array subscript` (see the [evidence table](../../compat.md#evidence)). The rule uses 4.3, the first release where every use works, so a plain `${arr[-1]}` read at `shellVersion: "4.2"` is reported although 4.2 accepts it. Bash 3.2 fails with `bad array subscript`.
|
|
14
|
+
|
|
15
|
+
The rule reports a subscript that is a minus sign followed by a static decimal integer, on an expansion (`${arr[-1]}`, `${#arr[-1]}`) or on an assignment (`arr[-1]=x`). It also reports unsetting an element, `unset arr[-1]` (quoted or not, also after options such as `-v` and after `--`), which Bash NEWS for 4.3 covers; that form has no suggestion. It does not report `-0` (zero, which Bash 3.2 accepts), `${v: -1}` (a substring offset, not a subscript), a computed `-$n`, or a parenthesized `-(1)`. An associative array's `-1` key is a plain string; the rule cannot tell and reports it, but associative arrays need Bash 4.0 anyway.
|
|
16
|
+
|
|
17
|
+
### Suggestion
|
|
18
|
+
|
|
19
|
+
For a bare `${NAME[-N]}` read, the rule suggests counting from the length:
|
|
20
|
+
|
|
21
|
+
<!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
echo "${arr[-1]}"
|
|
25
|
+
# becomes
|
|
26
|
+
echo "${arr[$((${#arr[@]}-1))]}"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Negative subscripts count back from the highest assigned index plus one, while `${#arr[@]}` counts set elements, so the two agree only for an array without holes. Other forms are reported without a suggestion.
|
|
30
|
+
|
|
31
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
32
|
+
described in [version compatibility](../../compat.md):
|
|
33
|
+
|
|
34
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
35
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.3`.
|
|
36
|
+
- `dialect: "posix"`: reports nothing in practice, because POSIX sh has no arrays, because the POSIX parser rejects it with a parse error before any rule runs.
|
|
37
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
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-negative-array-index': '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": 2} -->
|
|
60
|
+
```sh
|
|
61
|
+
last=${items[-1]}
|
|
62
|
+
items[-1]=done
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Correct
|
|
66
|
+
|
|
67
|
+
With `shellVersion: "3.2"`:
|
|
68
|
+
|
|
69
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
70
|
+
```sh
|
|
71
|
+
last=${items[$((${#items[@]}-1))]}
|
|
72
|
+
items[$((${#items[@]}-1))]=done
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
With `shellVersion: "4.3"` or newer, or with no `shellVersion`, the original forms are fine.
|
|
76
|
+
|
|
77
|
+
## When not to use it
|
|
78
|
+
|
|
79
|
+
Turn the rule off when your scripts never run on Bash older than 4.3, or leave
|
|
80
|
+
`shellVersion` 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 reports negative array subscripts in scripts it treats as POSIX sh (SC3054), but it cannot target a Bash version, so it cannot report `${arr[-1]}` for a Bash 3.2 target. This rule exists for that target.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# `sh/compat/no-nounset-empty-array`
|
|
2
|
+
|
|
3
|
+
Disallow whole-array expansions (`"${a[@]}"`, `${a[*]}`) in a script that turns on `set -u`, when
|
|
4
|
+
its target shell aborts on an empty array: Bash older than 4.4, including the Bash 3.2 that macOS
|
|
5
|
+
ships as `/bin/bash`.
|
|
6
|
+
|
|
7
|
+
| Configs | Fixable | Languages |
|
|
8
|
+
| ------- | ------- | --------- |
|
|
9
|
+
| `bash-3.2`, `posix-portable` | suggestions | `sh/sh` |
|
|
10
|
+
|
|
11
|
+
## Rule details
|
|
12
|
+
|
|
13
|
+
The syntax is in every Bash, but through Bash 4.3, expanding an empty array while `nounset` is on aborts the script with `a[@]: unbound variable`. Bash 4.4 is the first release where it expands to nothing. This is the classic "works on Linux CI, breaks on a Mac" failure: a script that runs under `set -euo pipefail` and passes an optional, possibly empty, argument array dies on macOS `/bin/bash`.
|
|
14
|
+
|
|
15
|
+
The rule reports when both of these hold:
|
|
16
|
+
|
|
17
|
+
- the file turns `nounset` on somewhere: a `set` command that enables it (`set -u`, `set -eu`, `set -o nounset`), or a shebang that passes the flag (`#!/bin/bash -eu`, or `#!/usr/bin/env -S bash -eu`). Where in the file that happens is not considered, because a function defined before `set -u` can run after it;
|
|
18
|
+
- an expansion is `${name[@]}` or `${name[*]}` with no operator, in quotes or not.
|
|
19
|
+
|
|
20
|
+
A linter cannot know whether an array is empty when the script runs, so every such expansion reports. The check is file-wide: it ignores `set +u`, so an expansion after the file turned `nounset` back off still reports. It also reports arrays that are visibly non-empty (`a=(x); "${a[@]}"`) and arrays already guarded by a length test (`[[ ${#a[@]} -eq 0 ]] && return`). Its message therefore says "If this array is empty", not that it is. These forms work on an empty array under `set -u` in every Bash, and are not reported: `${#a[@]}`, `${!a[@]}`, `${a[@]-default}`, and the guarded forms below, including an expansion inside `${a[@]+…}` or `${a[@]:+…}` of the same array. The slice `${a[@]:1}` and the replacement `${a[@]/x/y}` were observed to work on Bash 3.2 to 4.1 and fail only on 4.2 and 4.3, which a minimum-version gate cannot express, so they are not reported either.
|
|
21
|
+
|
|
22
|
+
The rule offers a guarded spelling as a suggestion when the word is exactly the quoted expansion:
|
|
23
|
+
|
|
24
|
+
- `"${a[@]}"` becomes `${a[@]+"${a[@]}"}`, which gives no words for an empty array and the same words otherwise;
|
|
25
|
+
- `"${a[*]}"` becomes `"${a[*]-}"`, which gives the one empty word that `"${a[*]}"` gives for an empty array on Bash 4.4, and the same joined word otherwise.
|
|
26
|
+
|
|
27
|
+
An unquoted expansion, or one inside a longer word, reports with no suggestion. POSIX sh has no arrays; under `dialect: "posix"` the parser rejects array syntax, so this rule never sees one there.
|
|
28
|
+
|
|
29
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
30
|
+
described in [version compatibility](../../compat.md):
|
|
31
|
+
|
|
32
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
33
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports when `shellVersion` is older than `4.4`.
|
|
34
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
35
|
+
|
|
36
|
+
### Suggestion
|
|
37
|
+
|
|
38
|
+
A suggestion rewrites the quoted `[@]` form:
|
|
39
|
+
|
|
40
|
+
<!-- transform: {"kind": "suggestion", "dialect": "bash", "shellVersion": "3.2"} -->
|
|
41
|
+
```sh
|
|
42
|
+
set -u
|
|
43
|
+
run_tool "${extra[@]}"
|
|
44
|
+
# becomes
|
|
45
|
+
set -u
|
|
46
|
+
run_tool ${extra[@]+"${extra[@]}"}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// eslint.config.js
|
|
55
|
+
{
|
|
56
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
57
|
+
rules: {
|
|
58
|
+
'sh/compat/no-nounset-empty-array': 'error',
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
### Incorrect
|
|
66
|
+
|
|
67
|
+
With `shellVersion: "3.2"`:
|
|
68
|
+
|
|
69
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 2} -->
|
|
70
|
+
```sh
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
extra=()
|
|
73
|
+
run_tool "${extra[@]}"
|
|
74
|
+
echo "args: ${extra[*]}"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Correct
|
|
78
|
+
|
|
79
|
+
With `shellVersion: "3.2"`:
|
|
80
|
+
|
|
81
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
82
|
+
```sh
|
|
83
|
+
set -euo pipefail
|
|
84
|
+
extra=()
|
|
85
|
+
run_tool ${extra[@]+"${extra[@]}"}
|
|
86
|
+
echo "args: ${extra[*]-}"
|
|
87
|
+
echo "count: ${#extra[@]}"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
With `shellVersion: "4.4"` or newer, with no `shellVersion`, or in a file that never turns on
|
|
91
|
+
`set -u`, whole-array expansions are fine.
|
|
92
|
+
|
|
93
|
+
## When not to use it
|
|
94
|
+
|
|
95
|
+
Turn the rule off when your scripts never run on Bash older than 4.4, or leave `shellVersion`
|
|
96
|
+
unset, which makes the rule report nothing.
|
|
97
|
+
|
|
98
|
+
## Relationship to shellcheck
|
|
99
|
+
|
|
100
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
101
|
+
layer. Run shellcheck beside it.
|
|
102
|
+
|
|
103
|
+
shellcheck 0.11.0 reports nothing for an empty array expanded under `set -u` (`set -u; a=(); echo "${a[@]}"`) under `-s bash`. Under `-s sh` it reports only the array syntax (SC3030, SC3054), not the abort. An empty array under `set -u` is valid Bash from 4.4 on, and shellcheck does not know which Bash release a script must run on. This rule exists for an older target.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# `sh/compat/no-parameter-transformation`
|
|
2
|
+
|
|
3
|
+
Disallow the parameter transformations `${v@op}` in scripts whose target shell lacks the
|
|
4
|
+
operator: `@Q`, `@E`, `@P`, `@A` and `@a` need Bash 4.4, `@U`, `@u`, `@L` and `@K` need Bash 5.1,
|
|
5
|
+
and `@k` needs Bash 5.2. POSIX sh has none of them.
|
|
6
|
+
|
|
7
|
+
| Configs | Fixable | Languages |
|
|
8
|
+
| ---------------------------- | ----------- | --------- |
|
|
9
|
+
| `bash-3.2`, `posix-portable` | not fixable | `sh/sh` |
|
|
10
|
+
|
|
11
|
+
## Rule details
|
|
12
|
+
|
|
13
|
+
Bash 4.4 added the first transformations (Bash NEWS for 4.4), Bash 5.1 the case operators and `@K`, and Bash 5.2 `@k`. A release that lacks the operator fails with `${v@Q}: bad substitution` at the moment the word is expanded. The boundary of each operator was checked against real shells; see [Evidence](../../compat.md#evidence).
|
|
14
|
+
|
|
15
|
+
The rule reads every parameter expansion whose operator is `@` followed by one of the ten letters. It reports when the file's target lacks that letter's release; the report is on the whole expansion. A target of Bash 5.0, for example, gets reports for `@U` but not for `@Q`. An `@` inside another expansion's word (`${v:-@Q}`) and text in single quotes are not expansions and are not reported.
|
|
16
|
+
|
|
17
|
+
The message names a portable alternative where one exists for the target:
|
|
18
|
+
|
|
19
|
+
- `@Q`: `printf '%q' "$v"`. It quotes differently (`a\ b` instead of `'a b'`), but `eval` reads it back as the same value. Not offered for a POSIX target, whose `printf` has no `%q`.
|
|
20
|
+
- `@E`: `printf '%b' "$v"`.
|
|
21
|
+
- `@A`: `declare -p v`. It prints the declaration rather than expanding to it, so capture it (`s=$(declare -p v)`) and `eval "$s"` to re-create the variable. Not offered for a POSIX target.
|
|
22
|
+
- `@U`, `@u`, `@L`: the Bash 4.0 case expansions `"${v^^}"`, `"${v^}"`, `"${v,,}"` for a target from 4.0 to 5.0, or a `tr` conversion for an older Bash.
|
|
23
|
+
- `@P`, `@a`, `@K`, `@k`: no equivalent expansion; the message says so.
|
|
24
|
+
|
|
25
|
+
Under `dialect: "posix"` the parser itself rejects `${v@op}`, so a POSIX file that uses it fails to parse and this rule does not run on it.
|
|
26
|
+
|
|
27
|
+
This is a version-compat rule. Whether it reports depends only on `languageOptions`, as
|
|
28
|
+
described in [version compatibility](../../compat.md):
|
|
29
|
+
|
|
30
|
+
- `shellVersion` unset: never reports. An unset version means the newest Bash.
|
|
31
|
+
- `dialect: "bash"` (the default) or `"bats"`: reports an operator when `shellVersion` is older than its release.
|
|
32
|
+
- `dialect: "mksh"` or `"zsh"`: never reports.
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
This rule has no options. Set the target through `languageOptions`:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
// eslint.config.js
|
|
40
|
+
{
|
|
41
|
+
languageOptions: { dialect: 'bash', shellVersion: '3.2' },
|
|
42
|
+
rules: {
|
|
43
|
+
'sh/compat/no-parameter-transformation': 'error',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
### Incorrect
|
|
51
|
+
|
|
52
|
+
With `shellVersion: "3.2"`:
|
|
53
|
+
|
|
54
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2", "findings": 3} -->
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
log "running ${cmd@Q}"
|
|
58
|
+
upper=${name@U}
|
|
59
|
+
printf '%s\n' "${args[@]@K}"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
With `shellVersion: "5.0"`, only the case operator reports:
|
|
63
|
+
|
|
64
|
+
<!-- example: {"dialect": "bash", "shellVersion": "5.0", "findings": 1} -->
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
log "running ${cmd@Q}"
|
|
68
|
+
upper=${name@U}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Correct
|
|
72
|
+
|
|
73
|
+
With `shellVersion: "3.2"`:
|
|
74
|
+
|
|
75
|
+
<!-- example: {"dialect": "bash", "shellVersion": "3.2"} -->
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
log "running $(printf '%q' "$cmd")"
|
|
79
|
+
upper=$(printf '%s' "$name" | tr '[:lower:]' '[:upper:]')
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
With `shellVersion: "5.2"` or newer, or with no `shellVersion`, every transformation is fine.
|
|
83
|
+
|
|
84
|
+
## When not to use it
|
|
85
|
+
|
|
86
|
+
Turn the rule off when your scripts never run on a Bash older than the operators they use, or
|
|
87
|
+
leave `shellVersion` unset, which makes the rule report nothing.
|
|
88
|
+
|
|
89
|
+
## Relationship to shellcheck
|
|
90
|
+
|
|
91
|
+
eslint-sh is a convention and contract linter: a policy layer, where shellcheck is the gotcha
|
|
92
|
+
layer. Run shellcheck beside it.
|
|
93
|
+
|
|
94
|
+
shellcheck 0.11.0 reports nothing for `${v@U}`, `${v@Q}` or `${v@a}` under either `-s sh` or `-s bash` (a variable that is never assigned still draws its unrelated SC2154). So shellcheck gives no finding for these expansions in a POSIX sh script or in a Bash target older than the operator's release. This rule exists for those targets.
|