@eslint-sh/eslint-plugin 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -17
- package/dist/command-harvest/builtins/bash.d.ts +36 -0
- package/dist/command-harvest/builtins/bash.js +98 -0
- package/dist/command-harvest/builtins/bats.d.ts +45 -0
- package/dist/command-harvest/builtins/bats.js +33 -0
- package/dist/command-harvest/builtins/index.d.ts +18 -0
- package/dist/command-harvest/builtins/index.js +39 -0
- package/dist/command-harvest/builtins/posix.d.ts +51 -0
- package/dist/command-harvest/builtins/posix.js +93 -0
- package/dist/command-harvest/harvest-commands.d.ts +66 -0
- package/dist/command-harvest/harvest-commands.js +97 -0
- package/dist/command-harvest/wrapper-table.d.ts +24 -0
- package/dist/command-harvest/wrapper-table.js +49 -0
- package/dist/configs/agent-authored.d.ts +41 -0
- package/dist/configs/agent-authored.js +33 -0
- package/dist/configs/bash-3-2.d.ts +28 -0
- package/dist/configs/bash-3-2.js +51 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +53 -0
- package/dist/configs/recommended.d.ts +52 -0
- package/dist/configs/recommended.js +34 -0
- package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
- package/dist/declaration-conventions/comment-pragma.js +153 -0
- package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
- package/dist/declaration-conventions/declaration-convention.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
- package/dist/declaration-conventions/read-declared-commands.js +128 -0
- package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
- package/dist/eslint-plugin-syntax.d.ts +27 -0
- package/dist/eslint-plugin.d.ts +1211 -52
- package/dist/index.d.ts +56 -8
- package/dist/index.js +53 -6
- package/dist/language/command-resolution.d.ts +124 -0
- package/dist/language/command-resolution.js +120 -0
- package/dist/language/end-of-input-marker.d.ts +29 -0
- package/dist/language/end-of-input-marker.js +88 -0
- package/dist/language/line-breaks.d.ts +18 -0
- package/dist/language/line-breaks.js +22 -0
- package/dist/language/lint-view-types.d.ts +104 -0
- package/dist/language/lint-view-types.js +1 -0
- package/dist/language/non-shell-file-guard.d.ts +35 -0
- package/dist/language/non-shell-file-guard.js +59 -0
- package/dist/language/selector-classes.d.ts +0 -1
- package/dist/language/selector-classes.js +0 -1
- package/dist/language/shell-language-options.d.ts +7 -1
- package/dist/language/shell-language-options.js +4 -4
- package/dist/language/shell-language.d.ts +10 -2
- package/dist/language/shell-language.js +59 -8
- package/dist/language/shell-source-code.d.ts +342 -5
- package/dist/language/shell-source-code.js +559 -30
- package/dist/language/source-coordinates.d.ts +13 -2
- package/dist/language/source-coordinates.js +68 -14
- package/dist/language/syntax-view.d.ts +60 -39
- package/dist/language/syntax-view.js +200 -45
- package/dist/language/word-resolution.d.ts +4 -3
- package/dist/language/word-resolution.js +4 -3
- package/dist/rules/compat/create-compat-rule.d.ts +59 -0
- package/dist/rules/compat/create-compat-rule.js +65 -0
- package/dist/rules/compat/declaration-options.d.ts +19 -0
- package/dist/rules/compat/declaration-options.js +110 -0
- package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
- package/dist/rules/compat/no-append-all-redirect.js +73 -0
- package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
- package/dist/rules/compat/no-associative-arrays.js +39 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.d.ts +15 -0
- package/dist/rules/compat/no-brace-sequence-padding-step.js +287 -0
- package/dist/rules/compat/no-case-attributes.d.ts +12 -0
- package/dist/rules/compat/no-case-attributes.js +63 -0
- package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
- package/dist/rules/compat/no-case-fallthrough.js +67 -0
- package/dist/rules/compat/no-case-modification.d.ts +35 -0
- package/dist/rules/compat/no-case-modification.js +86 -0
- package/dist/rules/compat/no-coproc.d.ts +12 -0
- package/dist/rules/compat/no-coproc.js +47 -0
- package/dist/rules/compat/no-declare-g.d.ts +12 -0
- package/dist/rules/compat/no-declare-g.js +59 -0
- package/dist/rules/compat/no-double-bracket.d.ts +12 -0
- package/dist/rules/compat/no-double-bracket.js +52 -0
- package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
- package/dist/rules/compat/no-epoch-variables.js +57 -0
- package/dist/rules/compat/no-fd-variable.d.ts +12 -0
- package/dist/rules/compat/no-fd-variable.js +48 -0
- package/dist/rules/compat/no-globstar.d.ts +12 -0
- package/dist/rules/compat/no-globstar.js +39 -0
- package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
- package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
- package/dist/rules/compat/no-let.d.ts +12 -0
- package/dist/rules/compat/no-let.js +62 -0
- package/dist/rules/compat/no-mapfile.d.ts +13 -0
- package/dist/rules/compat/no-mapfile.js +122 -0
- package/dist/rules/compat/no-nameref.d.ts +12 -0
- package/dist/rules/compat/no-nameref.js +44 -0
- package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
- package/dist/rules/compat/no-negative-array-index.js +123 -0
- package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
- package/dist/rules/compat/no-nounset-empty-array.js +158 -0
- package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
- package/dist/rules/compat/no-parameter-transformation.js +123 -0
- package/dist/rules/compat/no-pipe-all.d.ts +13 -0
- package/dist/rules/compat/no-pipe-all.js +68 -0
- package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
- package/dist/rules/compat/no-printf-time-format.js +99 -0
- package/dist/rules/compat/no-source.d.ts +13 -0
- package/dist/rules/compat/no-source.js +103 -0
- package/dist/rules/compat/no-test-v.d.ts +12 -0
- package/dist/rules/compat/no-test-v.js +280 -0
- package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
- package/dist/rules/compat/no-unicode-escape.js +120 -0
- package/dist/rules/compat/no-wait-n.d.ts +12 -0
- package/dist/rules/compat/no-wait-n.js +73 -0
- package/dist/rules/compat/operator-range.d.ts +10 -0
- package/dist/rules/compat/operator-range.js +15 -0
- package/dist/rules/compat/posix-absent-command.d.ts +12 -0
- package/dist/rules/compat/posix-absent-command.js +134 -0
- package/dist/rules/compat/shopt-options.d.ts +9 -0
- package/dist/rules/compat/shopt-options.js +63 -0
- package/dist/rules/compat/version-gates.d.ts +76 -0
- package/dist/rules/compat/version-gates.js +205 -0
- package/dist/rules/create-rule.d.ts +91 -0
- package/dist/rules/create-rule.js +51 -0
- package/dist/rules/create-wrapper-rule.d.ts +166 -0
- package/dist/rules/create-wrapper-rule.js +407 -0
- package/dist/rules/define-shell-rule.d.ts +184 -0
- package/dist/rules/define-shell-rule.js +56 -0
- package/dist/rules/index.d.ts +26 -0
- package/dist/rules/index.js +90 -0
- package/dist/rules/max-script-complexity.d.ts +28 -0
- package/dist/rules/max-script-complexity.js +130 -0
- package/dist/rules/no-dynamic-invocation.d.ts +35 -0
- package/dist/rules/no-dynamic-invocation.js +117 -0
- package/dist/rules/no-eval.d.ts +13 -0
- package/dist/rules/no-eval.js +89 -0
- package/dist/rules/no-masked-exit-status.d.ts +21 -0
- package/dist/rules/no-masked-exit-status.js +667 -0
- package/dist/rules/no-secret-env-reads.d.ts +24 -0
- package/dist/rules/no-secret-env-reads.js +133 -0
- package/dist/rules/no-secret-flags.d.ts +32 -0
- package/dist/rules/no-secret-flags.js +162 -0
- package/dist/rules/no-unchecked-cd.d.ts +44 -0
- package/dist/rules/no-unchecked-cd.js +250 -0
- package/dist/rules/no-undeclared-commands.d.ts +20 -0
- package/dist/rules/no-undeclared-commands.js +115 -0
- package/dist/rules/no-unused-declarations.d.ts +23 -0
- package/dist/rules/no-unused-declarations.js +259 -0
- package/dist/rules/prefer-local.d.ts +16 -0
- package/dist/rules/prefer-local.js +249 -0
- package/dist/rules/quote-expansions.d.ts +13 -0
- package/dist/rules/quote-expansions.js +229 -0
- package/dist/rules/require-network-wrapper.d.ts +40 -0
- package/dist/rules/require-network-wrapper.js +244 -0
- package/dist/rules/require-shebang.d.ts +29 -0
- package/dist/rules/require-shebang.js +101 -0
- package/dist/rules/require-strict-mode.d.ts +52 -0
- package/dist/rules/require-strict-mode.js +177 -0
- package/dist/rules/require-tty-guard.d.ts +13 -0
- package/dist/rules/require-tty-guard.js +318 -0
- package/dist/rules/resolve-declaration-convention.d.ts +96 -0
- package/dist/rules/resolve-declaration-convention.js +109 -0
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +116 -0
- package/dist/rules/set-options.d.ts +109 -0
- package/dist/rules/set-options.js +201 -0
- package/dist/rules/shebang-interpreter.d.ts +18 -0
- package/dist/rules/shebang-interpreter.js +81 -0
- package/dist/rules/shebang-matches-dialect.d.ts +12 -0
- package/dist/rules/shebang-matches-dialect.js +69 -0
- package/dist/rules/transparent-wrappers.d.ts +52 -0
- package/dist/rules/transparent-wrappers.js +99 -0
- package/dist/rules/user-pattern.d.ts +63 -0
- package/dist/rules/user-pattern.js +184 -0
- package/dist/syntax/analyze.d.ts +19 -0
- package/dist/syntax/analyze.js +21 -0
- package/dist/syntax/index.d.ts +23 -0
- package/dist/syntax/index.js +25 -0
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/docs/compat.md +354 -0
- package/docs/custom-rule-tutorial.md +780 -34
- package/docs/declaration-conventions.md +478 -0
- package/docs/examples/audit-wrapper-pack.mjs +15 -0
- package/docs/examples/dep-line-convention.mjs +22 -0
- package/docs/examples/eslint.config.mjs +3 -2
- package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
- package/docs/examples/no-backticks-rule-tester.mjs +106 -0
- package/docs/examples/no-backticks-rule.mjs +60 -0
- package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
- package/docs/examples/no-function-keyword-rule.mjs +59 -0
- package/docs/examples/no-listed-commands-rule.ts +63 -0
- package/docs/examples/no-rm-rf-rule.mjs +26 -15
- package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
- package/docs/examples/sidecar-convention.mjs +38 -0
- package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
- package/docs/examples/sorted-declarations-rule.mjs +52 -0
- package/docs/reading-nodes.md +159 -0
- package/docs/rules/README.md +79 -0
- package/docs/rules/_template.md +83 -0
- package/docs/rules/compat/no-append-all-redirect.md +87 -0
- package/docs/rules/compat/no-associative-arrays.md +95 -0
- package/docs/rules/compat/no-brace-sequence-padding-step.md +90 -0
- package/docs/rules/compat/no-case-attributes.md +82 -0
- package/docs/rules/compat/no-case-fallthrough.md +90 -0
- package/docs/rules/compat/no-case-modification.md +74 -0
- package/docs/rules/compat/no-coproc.md +76 -0
- package/docs/rules/compat/no-declare-g.md +80 -0
- package/docs/rules/compat/no-double-bracket.md +72 -0
- package/docs/rules/compat/no-epoch-variables.md +79 -0
- package/docs/rules/compat/no-fd-variable.md +78 -0
- package/docs/rules/compat/no-globstar.md +74 -0
- package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
- package/docs/rules/compat/no-let.md +78 -0
- package/docs/rules/compat/no-mapfile.md +118 -0
- package/docs/rules/compat/no-nameref.md +81 -0
- package/docs/rules/compat/no-negative-array-index.md +87 -0
- package/docs/rules/compat/no-nounset-empty-array.md +103 -0
- package/docs/rules/compat/no-parameter-transformation.md +94 -0
- package/docs/rules/compat/no-pipe-all.md +85 -0
- package/docs/rules/compat/no-printf-time-format.md +75 -0
- package/docs/rules/compat/no-source.md +98 -0
- package/docs/rules/compat/no-test-v.md +82 -0
- package/docs/rules/compat/no-unicode-escape.md +91 -0
- package/docs/rules/compat/no-wait-n.md +85 -0
- package/docs/rules/max-script-complexity.md +109 -0
- package/docs/rules/no-dynamic-invocation.md +139 -0
- package/docs/rules/no-eval.md +106 -0
- package/docs/rules/no-masked-exit-status.md +239 -0
- package/docs/rules/no-secret-env-reads.md +173 -0
- package/docs/rules/no-secret-flags.md +169 -0
- package/docs/rules/no-unchecked-cd.md +146 -0
- package/docs/rules/no-undeclared-commands.md +141 -0
- package/docs/rules/no-unused-declarations.md +216 -0
- package/docs/rules/prefer-local.md +221 -0
- package/docs/rules/quote-expansions.md +157 -0
- package/docs/rules/require-network-wrapper.md +171 -0
- package/docs/rules/require-shebang.md +157 -0
- package/docs/rules/require-strict-mode.md +253 -0
- package/docs/rules/require-tty-guard.md +131 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +87 -0
- package/docs/untrusted-code.md +440 -0
- package/docs/wrapper-rules.md +206 -0
- package/package.json +26 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/language/selector-classes.d.ts.map +0 -1
- package/dist/language/selector-classes.js.map +0 -1
- package/dist/language/shell-language-options.d.ts.map +0 -1
- package/dist/language/shell-language-options.js.map +0 -1
- package/dist/language/shell-language.d.ts.map +0 -1
- package/dist/language/shell-language.js.map +0 -1
- package/dist/language/shell-source-code.d.ts.map +0 -1
- package/dist/language/shell-source-code.js.map +0 -1
- package/dist/language/source-coordinates.d.ts.map +0 -1
- package/dist/language/source-coordinates.js.map +0 -1
- package/dist/language/syntax-view.d.ts.map +0 -1
- package/dist/language/syntax-view.js.map +0 -1
- package/dist/language/word-resolution.d.ts.map +0 -1
- package/dist/language/word-resolution.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key a convention is bound under, both in a consuming rule's options
|
|
3
|
+
* object and in the plugin's `settings.sh` namespace.
|
|
4
|
+
*/
|
|
5
|
+
const DECLARATION_CONVENTION_KEY = 'declarationConvention';
|
|
6
|
+
/** `value` is an object (not `null`) whose properties can be read. */
|
|
7
|
+
function isObjectLike(value) {
|
|
8
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null;
|
|
9
|
+
}
|
|
10
|
+
/** A short description of a value that is not a convention, for an error message. */
|
|
11
|
+
function describeValue(value) {
|
|
12
|
+
if (value === null)
|
|
13
|
+
return 'null';
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return 'an array';
|
|
16
|
+
if (isObjectLike(value)) {
|
|
17
|
+
return typeof value.declaredCommands === 'undefined'
|
|
18
|
+
? 'an object with no `declaredCommands` method'
|
|
19
|
+
: `an object whose \`declaredCommands\` is ${typeof value.declaredCommands}, not a function`;
|
|
20
|
+
}
|
|
21
|
+
switch (typeof value) {
|
|
22
|
+
case 'string':
|
|
23
|
+
return `string ${JSON.stringify(value)}`;
|
|
24
|
+
case 'number':
|
|
25
|
+
case 'boolean':
|
|
26
|
+
case 'bigint':
|
|
27
|
+
case 'symbol':
|
|
28
|
+
return `${typeof value} ${String(value)}`;
|
|
29
|
+
default:
|
|
30
|
+
return typeof value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checks that `value`, configured at `setting`, implements
|
|
35
|
+
* {@link DeclarationConvention}: a `declaredCommands` function, and, when a
|
|
36
|
+
* `removeDeclaration` or `declarationHint` hook is present at all, a function
|
|
37
|
+
* there too. Throws an
|
|
38
|
+
* error naming `setting` otherwise.
|
|
39
|
+
*/
|
|
40
|
+
function assertDeclarationConvention(value, setting) {
|
|
41
|
+
const expected = 'expected a declaration convention: an object with a `declaredCommands(sourceCode)` method';
|
|
42
|
+
if (!isObjectLike(value) || typeof value.declaredCommands !== 'function') {
|
|
43
|
+
throw new Error(`Invalid ${setting}: ${expected}, but got ${describeValue(value)}.`);
|
|
44
|
+
}
|
|
45
|
+
for (const hook of ['removeDeclaration', 'declarationHint']) {
|
|
46
|
+
if (value[hook] !== undefined && typeof value[hook] !== 'function') {
|
|
47
|
+
throw new Error(`Invalid ${setting}: its optional \`${hook}\` hook must be a function when present, but got ${typeof value[hook]}.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolves the declaration convention bound for the rule running in
|
|
53
|
+
* `context`: the rule's `declarationConvention` option, else
|
|
54
|
+
* `settings.sh.declarationConvention`, else `null` (the rule is inert).
|
|
55
|
+
*
|
|
56
|
+
* Throws an `Error` naming the offending setting when the configured value is
|
|
57
|
+
* present but does not implement `declaredCommands` (or has a non-function
|
|
58
|
+
* `removeDeclaration`). The message names where the value came from and what
|
|
59
|
+
* it was, for example one beginning
|
|
60
|
+
* `Invalid settings.sh.declarationConvention: expected a declaration convention`
|
|
61
|
+
* and ending `but got string "jq".` Call it from `create()`, so the error
|
|
62
|
+
* surfaces as ESLint's
|
|
63
|
+
* configuration message for the file being linted.
|
|
64
|
+
*
|
|
65
|
+
* A rule that receives `null` should register no listeners: with no
|
|
66
|
+
* convention bound, a convention consumer has nothing to check.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import {
|
|
71
|
+
* defineShellRule,
|
|
72
|
+
* readDeclaredCommands,
|
|
73
|
+
* resolveDeclarationConvention,
|
|
74
|
+
* } from '@eslint-sh/eslint-plugin';
|
|
75
|
+
*
|
|
76
|
+
* export const maxDeclaredCommands = defineShellRule({
|
|
77
|
+
* meta: { schema: [], messages: { tooMany: 'Declare at most 5 commands.' } },
|
|
78
|
+
* create(context) {
|
|
79
|
+
* const convention = resolveDeclarationConvention(context);
|
|
80
|
+
* if (convention === null) return {};
|
|
81
|
+
* return {
|
|
82
|
+
* File(node) {
|
|
83
|
+
* // Validated declarations for the file `context` is linting.
|
|
84
|
+
* if (readDeclaredCommands(convention, context).length > 5) {
|
|
85
|
+
* context.report({ node, messageId: 'tooMany' });
|
|
86
|
+
* }
|
|
87
|
+
* },
|
|
88
|
+
* };
|
|
89
|
+
* },
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
95
|
+
export function resolveDeclarationConvention(context) {
|
|
96
|
+
const ruleOptions = context.options[0];
|
|
97
|
+
if (isObjectLike(ruleOptions) && ruleOptions[DECLARATION_CONVENTION_KEY] !== undefined) {
|
|
98
|
+
const fromOption = ruleOptions[DECLARATION_CONVENTION_KEY];
|
|
99
|
+
assertDeclarationConvention(fromOption, `option \`${DECLARATION_CONVENTION_KEY}\` of rule "${context.id}"`);
|
|
100
|
+
return fromOption;
|
|
101
|
+
}
|
|
102
|
+
const shSettings = context.settings.sh;
|
|
103
|
+
if (isObjectLike(shSettings) && shSettings[DECLARATION_CONVENTION_KEY] !== undefined) {
|
|
104
|
+
const fromSettings = shSettings[DECLARATION_CONVENTION_KEY];
|
|
105
|
+
assertDeclarationConvention(fromSettings, `settings.sh.${DECLARATION_CONVENTION_KEY}`);
|
|
106
|
+
return fromSettings;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WrapperSpec } from 'sh-ast/analyze';
|
|
2
|
+
/**
|
|
3
|
+
* Reads `settings.sh.networkWrapper`: the configured name or `WrapperSpec`,
|
|
4
|
+
* or `undefined` when it is not set. Throws an error that starts
|
|
5
|
+
* `Invalid settings.sh.networkWrapper:` when the value is malformed. Call it
|
|
6
|
+
* from `create()`, so the error surfaces as ESLint's configuration message.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export declare function readNetworkWrapperSetting(settings: Readonly<Record<string, unknown>>): string | WrapperSpec | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The `WrapperSpec` for the network wrapper set in `settings.sh.networkWrapper`,
|
|
13
|
+
* for a rule that looks through wrappers, or `null` when none is set. A name
|
|
14
|
+
* that is one of `sh-ast/analyze`'s default wrappers (`timeout`) keeps that
|
|
15
|
+
* entry's known options; any other name is a wrapper with no options.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function networkWrapperSpec(settings: Readonly<Record<string, unknown>>): WrapperSpec | null;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared `settings.sh.networkWrapper` binding: the one place a project
|
|
3
|
+
* names the wrapper its network commands must run through, so every rule
|
|
4
|
+
* whose findings depend on that wrapper reads the same declaration.
|
|
5
|
+
*
|
|
6
|
+
* Two different statements are involved, and this setting makes only one of
|
|
7
|
+
* them:
|
|
8
|
+
*
|
|
9
|
+
* - **Policy:** network commands must run through this wrapper. That is what
|
|
10
|
+
* `sh/require-network-wrapper` enforces, and this setting states it.
|
|
11
|
+
* - **Fact:** this wrapper runs the command named after it. The policy implies
|
|
12
|
+
* the fact (a wrapper that encloses `curl` runs `curl`), so
|
|
13
|
+
* `sh/no-undeclared-commands` and `sh/no-unused-declarations` look through
|
|
14
|
+
* the network wrapper when it is set here. The converse does not hold: a
|
|
15
|
+
* transparent wrapper such as `with-lock` is not the network wrapper, and
|
|
16
|
+
* stays in those rules' `transparentWrappers` option.
|
|
17
|
+
*
|
|
18
|
+
* ESLint gives a rule no way to read another rule's options, so a wrapper set
|
|
19
|
+
* only as `require-network-wrapper`'s `wrapper` option is invisible to the
|
|
20
|
+
* declaration rules. Shared settings are visible to every rule; that is why
|
|
21
|
+
* the one declaration lives here.
|
|
22
|
+
*/
|
|
23
|
+
import { DEFAULT_TRANSPARENT_WRAPPERS } from 'sh-ast/analyze';
|
|
24
|
+
/** The setting's dotted path, as error messages name it. */
|
|
25
|
+
const SETTING = 'settings.sh.networkWrapper';
|
|
26
|
+
/**
|
|
27
|
+
* The `WrapperSpec` fields the setting accepts: the JSON-expressible ones,
|
|
28
|
+
* the same set `sh/require-network-wrapper`'s `wrapper` option schema allows.
|
|
29
|
+
*/
|
|
30
|
+
const STRING_LIST_FIELDS = ['noArgFlags', 'argFlags', 'unresolvableFlags', 'stopsChainFlags'];
|
|
31
|
+
const ASSIGNMENT_OPERANDS = ['shell-identifier', 'env', 'sudo'];
|
|
32
|
+
const KNOWN_FIELDS = new Set([
|
|
33
|
+
'names',
|
|
34
|
+
...STRING_LIST_FIELDS,
|
|
35
|
+
'positionalOperandsBeforeCommand',
|
|
36
|
+
'assignmentOperands',
|
|
37
|
+
]);
|
|
38
|
+
/** `true` when `value` is an array of non-empty strings. */
|
|
39
|
+
function isStringList(value) {
|
|
40
|
+
return (Array.isArray(value) &&
|
|
41
|
+
value.every((item) => typeof item === 'string' && item.length > 0));
|
|
42
|
+
}
|
|
43
|
+
/** Throws the setting's configuration error with `detail`. */
|
|
44
|
+
function invalid(detail) {
|
|
45
|
+
throw new Error(`Invalid ${SETTING}: ${detail}`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Checks a configured value and returns it typed: a non-empty command name,
|
|
49
|
+
* or a `WrapperSpec` object made of the JSON-expressible fields.
|
|
50
|
+
*/
|
|
51
|
+
function validate(value) {
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
if (value.length === 0)
|
|
54
|
+
invalid('expected a command name or a wrapper spec, but got "".');
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
58
|
+
const got = value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value;
|
|
59
|
+
invalid(`expected a command name or a wrapper spec ({ names: [...] }), but got ${got}.`);
|
|
60
|
+
}
|
|
61
|
+
const spec = value;
|
|
62
|
+
for (const key of Object.keys(spec)) {
|
|
63
|
+
if (!KNOWN_FIELDS.has(key))
|
|
64
|
+
invalid(`unknown wrapper spec field \`${key}\`.`);
|
|
65
|
+
}
|
|
66
|
+
if (!isStringList(spec.names) || spec.names.length === 0) {
|
|
67
|
+
invalid('`names` must be a non-empty array of non-empty strings.');
|
|
68
|
+
}
|
|
69
|
+
for (const field of STRING_LIST_FIELDS) {
|
|
70
|
+
if (spec[field] !== undefined && !isStringList(spec[field])) {
|
|
71
|
+
invalid(`\`${field}\` must be an array of non-empty strings.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const positional = spec.positionalOperandsBeforeCommand;
|
|
75
|
+
if (positional !== undefined && !(Number.isInteger(positional) && positional >= 0)) {
|
|
76
|
+
invalid('`positionalOperandsBeforeCommand` must be a non-negative integer.');
|
|
77
|
+
}
|
|
78
|
+
const assignment = spec.assignmentOperands;
|
|
79
|
+
if (assignment !== undefined && !ASSIGNMENT_OPERANDS.includes(assignment)) {
|
|
80
|
+
invalid(`\`assignmentOperands\` must be one of ${ASSIGNMENT_OPERANDS.join(', ')}.`);
|
|
81
|
+
}
|
|
82
|
+
return spec;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reads `settings.sh.networkWrapper`: the configured name or `WrapperSpec`,
|
|
86
|
+
* or `undefined` when it is not set. Throws an error that starts
|
|
87
|
+
* `Invalid settings.sh.networkWrapper:` when the value is malformed. Call it
|
|
88
|
+
* from `create()`, so the error surfaces as ESLint's configuration message.
|
|
89
|
+
*
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function readNetworkWrapperSetting(settings) {
|
|
93
|
+
const sh = settings.sh;
|
|
94
|
+
if (typeof sh !== 'object' || sh === null)
|
|
95
|
+
return undefined;
|
|
96
|
+
const value = sh.networkWrapper;
|
|
97
|
+
return value === undefined ? undefined : validate(value);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The `WrapperSpec` for the network wrapper set in `settings.sh.networkWrapper`,
|
|
101
|
+
* for a rule that looks through wrappers, or `null` when none is set. A name
|
|
102
|
+
* that is one of `sh-ast/analyze`'s default wrappers (`timeout`) keeps that
|
|
103
|
+
* entry's known options; any other name is a wrapper with no options.
|
|
104
|
+
*
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
export function networkWrapperSpec(settings) {
|
|
108
|
+
const wrapper = readNetworkWrapperSetting(settings);
|
|
109
|
+
if (wrapper === undefined)
|
|
110
|
+
return null;
|
|
111
|
+
if (typeof wrapper !== 'string')
|
|
112
|
+
return wrapper;
|
|
113
|
+
return (DEFAULT_TRANSPARENT_WRAPPERS.find((spec) => spec.names.includes(wrapper)) ?? {
|
|
114
|
+
names: [wrapper],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A model of the shell's `set` option-flag syntax, shared by rules that ask
|
|
3
|
+
* "which shell options does this `set` command (or this shell invocation)
|
|
4
|
+
* turn on or off?".
|
|
5
|
+
*
|
|
6
|
+
* It knows two things and nothing else:
|
|
7
|
+
*
|
|
8
|
+
* - the option names `set -o` accepts in bash and POSIX sh, and the
|
|
9
|
+
* single-letter flag each one has (if any);
|
|
10
|
+
* - how `set` reads its arguments: `-abc` / `+abc` flag clusters, where each
|
|
11
|
+
* `o` in a cluster consumes the next argument as a long option name;
|
|
12
|
+
* `--` and a lone `-` end option processing; and the first argument that is
|
|
13
|
+
* not a flag cluster starts the positional parameters.
|
|
14
|
+
*
|
|
15
|
+
* It does not resolve words. Callers pass each argument's static text, or
|
|
16
|
+
* `null` for an argument whose text is not statically known; resolution is
|
|
17
|
+
* the `ShellSourceCode` helpers' job.
|
|
18
|
+
*
|
|
19
|
+
* @see https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
|
|
20
|
+
* @see https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#set
|
|
21
|
+
*/
|
|
22
|
+
/** The long option names in {@link SET_OPTION_FLAGS}, in table order. @internal */
|
|
23
|
+
export declare const SET_OPTION_NAMES: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* One option change a `set`-style argument list asks for: turn the named
|
|
26
|
+
* option on (`enable: true`, from `-x` / `-o name`) or off (`enable: false`,
|
|
27
|
+
* from `+x` / `+o name`).
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
export interface SetOptionChange {
|
|
32
|
+
/** The long option name, e.g. `errexit`. */
|
|
33
|
+
readonly option: string;
|
|
34
|
+
/** `true` for a `-` flag, `false` for a `+` flag. */
|
|
35
|
+
readonly enable: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What {@link parseSetArguments} read from an argument list.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export interface ParsedSetArguments {
|
|
43
|
+
/** The option changes, in the order the shell applies them. */
|
|
44
|
+
readonly changes: readonly SetOptionChange[];
|
|
45
|
+
/**
|
|
46
|
+
* Why option processing stopped before the end of the list, or `undefined`
|
|
47
|
+
* when every argument was an option flag (or an option name an `o` consumed):
|
|
48
|
+
*
|
|
49
|
+
* - `operand`: an argument that is not a flag cluster (or `--`, or a lone
|
|
50
|
+
* `-`) starts the positional parameters;
|
|
51
|
+
* - `unknown`: an argument's text is not statically known, so neither it nor
|
|
52
|
+
* anything after it can be read;
|
|
53
|
+
* - `malformed`: a cluster names a letter `set` does not have, or an `o` has
|
|
54
|
+
* no option name after it, or names an option `set` does not have.
|
|
55
|
+
*/
|
|
56
|
+
readonly stoppedBy: 'operand' | 'unknown' | 'malformed' | undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Reads a `set`-style argument list (the arguments after the command word, so
|
|
60
|
+
* `["-euo", "pipefail"]` for `set -euo pipefail`) into the option changes it
|
|
61
|
+
* makes, without applying them.
|
|
62
|
+
*
|
|
63
|
+
* Changes found before processing stops are kept: `set -e "$x"` still enables
|
|
64
|
+
* `errexit`, then stops (`unknown`) at `"$x"`, whose expansion could be
|
|
65
|
+
* anything. A lone `-` turns off `xtrace` and `verbose` (POSIX) and ends option
|
|
66
|
+
* processing, as `--` does. A lone `+` changes nothing and does not end
|
|
67
|
+
* option processing: bash and dash both read `set + -e` as `set -e`.
|
|
68
|
+
*
|
|
69
|
+
* @param args - Each argument's static text, or `null` when it is not static.
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseSetArguments(args: readonly (string | null)[]): ParsedSetArguments;
|
|
73
|
+
/**
|
|
74
|
+
* Applies `changes` to `enabled` in order, so a later `+e` undoes an earlier
|
|
75
|
+
* `-e`. Mutates and returns `enabled`.
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
export declare function applySetOptionChanges(enabled: Set<string>, changes: readonly SetOptionChange[]): Set<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Spells a `set` command that enables exactly `options`, preferring each
|
|
82
|
+
* option's single-letter flag: the letters form one cluster (in the given
|
|
83
|
+
* order), the first letterless option is attached to that cluster through a
|
|
84
|
+
* trailing `o`, and any further letterless options follow as `-o <name>`.
|
|
85
|
+
* So `["errexit", "nounset", "pipefail"]` is `set -euo pipefail`.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Long option names from {@link SET_OPTION_NAMES}; at least one.
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export declare function formatSetCommand(options: readonly string[]): string;
|
|
91
|
+
/**
|
|
92
|
+
* The option changes the shebang line passes to the shell (read by
|
|
93
|
+
* `require-strict-mode`'s `allowShebangOnly` and by
|
|
94
|
+
* `compat/no-nounset-empty-array`). Only spellings the kernel hands to the shell as written
|
|
95
|
+
* are read:
|
|
96
|
+
*
|
|
97
|
+
* - `#!/path/to/shell <word>`: the kernel passes everything after the
|
|
98
|
+
* interpreter as one argument, so exactly one flag word is read
|
|
99
|
+
* (`#!/bin/bash -eu`); more than one word (`#!/bin/bash -e -u`) arrives as a
|
|
100
|
+
* single argument the shell rejects, and counts as nothing.
|
|
101
|
+
* - `#!/usr/bin/env -S <shell> <words…>`: `env -S` splits the rest into words,
|
|
102
|
+
* so every word after the shell name is read.
|
|
103
|
+
*
|
|
104
|
+
* Plain `#!/usr/bin/env <shell> <flags>` counts as nothing: without `-S`,
|
|
105
|
+
* Linux passes `<shell> <flags>` to `env` as one command name.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export declare function shebangOptionChanges(text: string): readonly SetOptionChange[];
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A model of the shell's `set` option-flag syntax, shared by rules that ask
|
|
3
|
+
* "which shell options does this `set` command (or this shell invocation)
|
|
4
|
+
* turn on or off?".
|
|
5
|
+
*
|
|
6
|
+
* It knows two things and nothing else:
|
|
7
|
+
*
|
|
8
|
+
* - the option names `set -o` accepts in bash and POSIX sh, and the
|
|
9
|
+
* single-letter flag each one has (if any);
|
|
10
|
+
* - how `set` reads its arguments: `-abc` / `+abc` flag clusters, where each
|
|
11
|
+
* `o` in a cluster consumes the next argument as a long option name;
|
|
12
|
+
* `--` and a lone `-` end option processing; and the first argument that is
|
|
13
|
+
* not a flag cluster starts the positional parameters.
|
|
14
|
+
*
|
|
15
|
+
* It does not resolve words. Callers pass each argument's static text, or
|
|
16
|
+
* `null` for an argument whose text is not statically known; resolution is
|
|
17
|
+
* the `ShellSourceCode` helpers' job.
|
|
18
|
+
*
|
|
19
|
+
* @see https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html
|
|
20
|
+
* @see https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#set
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Every long option name `set -o` accepts in bash or POSIX sh, mapped to the
|
|
24
|
+
* single-letter flag that toggles the same option, or `undefined` when the
|
|
25
|
+
* option has no letter (and can only be spelled `-o <name>`).
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
const SET_OPTION_FLAGS = Object.freeze({
|
|
30
|
+
allexport: 'a',
|
|
31
|
+
braceexpand: 'B',
|
|
32
|
+
emacs: undefined,
|
|
33
|
+
errexit: 'e',
|
|
34
|
+
errtrace: 'E',
|
|
35
|
+
functrace: 'T',
|
|
36
|
+
hashall: 'h',
|
|
37
|
+
histexpand: 'H',
|
|
38
|
+
history: undefined,
|
|
39
|
+
ignoreeof: undefined,
|
|
40
|
+
'interactive-comments': undefined,
|
|
41
|
+
keyword: 'k',
|
|
42
|
+
monitor: 'm',
|
|
43
|
+
noclobber: 'C',
|
|
44
|
+
noexec: 'n',
|
|
45
|
+
noglob: 'f',
|
|
46
|
+
nolog: undefined,
|
|
47
|
+
notify: 'b',
|
|
48
|
+
nounset: 'u',
|
|
49
|
+
onecmd: 't',
|
|
50
|
+
physical: 'P',
|
|
51
|
+
pipefail: undefined,
|
|
52
|
+
posix: undefined,
|
|
53
|
+
privileged: 'p',
|
|
54
|
+
verbose: 'v',
|
|
55
|
+
vi: undefined,
|
|
56
|
+
xtrace: 'x',
|
|
57
|
+
});
|
|
58
|
+
/** The long option names in {@link SET_OPTION_FLAGS}, in table order. @internal */
|
|
59
|
+
export const SET_OPTION_NAMES = Object.freeze(Object.keys(SET_OPTION_FLAGS));
|
|
60
|
+
/** Single-letter flag → the long option name it toggles (the inverse of {@link SET_OPTION_FLAGS}). */
|
|
61
|
+
const OPTION_NAME_BY_FLAG = new Map(Object.entries(SET_OPTION_FLAGS).flatMap(([name, flag]) => flag === undefined ? [] : [[flag, name]]));
|
|
62
|
+
/**
|
|
63
|
+
* Reads a `set`-style argument list (the arguments after the command word, so
|
|
64
|
+
* `["-euo", "pipefail"]` for `set -euo pipefail`) into the option changes it
|
|
65
|
+
* makes, without applying them.
|
|
66
|
+
*
|
|
67
|
+
* Changes found before processing stops are kept: `set -e "$x"` still enables
|
|
68
|
+
* `errexit`, then stops (`unknown`) at `"$x"`, whose expansion could be
|
|
69
|
+
* anything. A lone `-` turns off `xtrace` and `verbose` (POSIX) and ends option
|
|
70
|
+
* processing, as `--` does. A lone `+` changes nothing and does not end
|
|
71
|
+
* option processing: bash and dash both read `set + -e` as `set -e`.
|
|
72
|
+
*
|
|
73
|
+
* @param args - Each argument's static text, or `null` when it is not static.
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export function parseSetArguments(args) {
|
|
77
|
+
const changes = [];
|
|
78
|
+
let index = 0;
|
|
79
|
+
while (index < args.length) {
|
|
80
|
+
const arg = args[index];
|
|
81
|
+
index += 1;
|
|
82
|
+
if (arg === null)
|
|
83
|
+
return { changes, stoppedBy: 'unknown' };
|
|
84
|
+
if (arg === '--')
|
|
85
|
+
return { changes, stoppedBy: undefined };
|
|
86
|
+
if (arg === '-') {
|
|
87
|
+
changes.push({ option: 'xtrace', enable: false }, { option: 'verbose', enable: false });
|
|
88
|
+
return { changes, stoppedBy: undefined };
|
|
89
|
+
}
|
|
90
|
+
if (arg === '+')
|
|
91
|
+
continue;
|
|
92
|
+
const sign = arg[0];
|
|
93
|
+
if ((sign !== '-' && sign !== '+') || arg.length < 2)
|
|
94
|
+
return { changes, stoppedBy: 'operand' };
|
|
95
|
+
const enable = sign === '-';
|
|
96
|
+
for (const flag of arg.slice(1)) {
|
|
97
|
+
if (flag === 'o') {
|
|
98
|
+
const name = index < args.length ? args[index] : undefined;
|
|
99
|
+
index += 1;
|
|
100
|
+
if (name === null)
|
|
101
|
+
return { changes, stoppedBy: 'unknown' };
|
|
102
|
+
if (name === undefined || !Object.hasOwn(SET_OPTION_FLAGS, name)) {
|
|
103
|
+
return { changes, stoppedBy: 'malformed' };
|
|
104
|
+
}
|
|
105
|
+
changes.push({ option: name, enable });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const option = OPTION_NAME_BY_FLAG.get(flag);
|
|
109
|
+
if (option === undefined)
|
|
110
|
+
return { changes, stoppedBy: 'malformed' };
|
|
111
|
+
changes.push({ option, enable });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { changes, stoppedBy: undefined };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Applies `changes` to `enabled` in order, so a later `+e` undoes an earlier
|
|
118
|
+
* `-e`. Mutates and returns `enabled`.
|
|
119
|
+
*
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
export function applySetOptionChanges(enabled, changes) {
|
|
123
|
+
for (const { option, enable } of changes) {
|
|
124
|
+
if (enable)
|
|
125
|
+
enabled.add(option);
|
|
126
|
+
else
|
|
127
|
+
enabled.delete(option);
|
|
128
|
+
}
|
|
129
|
+
return enabled;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Spells a `set` command that enables exactly `options`, preferring each
|
|
133
|
+
* option's single-letter flag: the letters form one cluster (in the given
|
|
134
|
+
* order), the first letterless option is attached to that cluster through a
|
|
135
|
+
* trailing `o`, and any further letterless options follow as `-o <name>`.
|
|
136
|
+
* So `["errexit", "nounset", "pipefail"]` is `set -euo pipefail`.
|
|
137
|
+
*
|
|
138
|
+
* @param options - Long option names from {@link SET_OPTION_NAMES}; at least one.
|
|
139
|
+
* @internal
|
|
140
|
+
*/
|
|
141
|
+
export function formatSetCommand(options) {
|
|
142
|
+
const letters = options.flatMap((option) => SET_OPTION_FLAGS[option] ?? []).join('');
|
|
143
|
+
const longOnly = options.filter((option) => SET_OPTION_FLAGS[option] === undefined);
|
|
144
|
+
const words = ['set'];
|
|
145
|
+
let remaining = longOnly;
|
|
146
|
+
if (letters.length > 0) {
|
|
147
|
+
if (longOnly.length === 0) {
|
|
148
|
+
words.push(`-${letters}`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const [first, ...rest] = longOnly;
|
|
152
|
+
words.push(`-${letters}o`, first);
|
|
153
|
+
remaining = rest;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const option of remaining)
|
|
157
|
+
words.push('-o', option);
|
|
158
|
+
return words.join(' ');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The option changes the shebang line passes to the shell (read by
|
|
162
|
+
* `require-strict-mode`'s `allowShebangOnly` and by
|
|
163
|
+
* `compat/no-nounset-empty-array`). Only spellings the kernel hands to the shell as written
|
|
164
|
+
* are read:
|
|
165
|
+
*
|
|
166
|
+
* - `#!/path/to/shell <word>`: the kernel passes everything after the
|
|
167
|
+
* interpreter as one argument, so exactly one flag word is read
|
|
168
|
+
* (`#!/bin/bash -eu`); more than one word (`#!/bin/bash -e -u`) arrives as a
|
|
169
|
+
* single argument the shell rejects, and counts as nothing.
|
|
170
|
+
* - `#!/usr/bin/env -S <shell> <words…>`: `env -S` splits the rest into words,
|
|
171
|
+
* so every word after the shell name is read.
|
|
172
|
+
*
|
|
173
|
+
* Plain `#!/usr/bin/env <shell> <flags>` counts as nothing: without `-S`,
|
|
174
|
+
* Linux passes `<shell> <flags>` to `env` as one command name.
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
export function shebangOptionChanges(text) {
|
|
179
|
+
if (!text.startsWith('#!'))
|
|
180
|
+
return [];
|
|
181
|
+
const lineEnd = text.search(/\r?\n|\r/u);
|
|
182
|
+
const words = text
|
|
183
|
+
.slice(2, lineEnd === -1 ? undefined : lineEnd)
|
|
184
|
+
.trim()
|
|
185
|
+
.split(/\s+/u);
|
|
186
|
+
const [interpreter = '', ...rest] = words;
|
|
187
|
+
const isEnv = interpreter === 'env' || interpreter.endsWith('/env');
|
|
188
|
+
let flagWords;
|
|
189
|
+
if (isEnv) {
|
|
190
|
+
const [split, , ...shellArgs] = rest;
|
|
191
|
+
if (split !== '-S' && split !== '--split-string')
|
|
192
|
+
return [];
|
|
193
|
+
flagWords = shellArgs;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (rest.length !== 1)
|
|
197
|
+
return [];
|
|
198
|
+
flagWords = rest;
|
|
199
|
+
}
|
|
200
|
+
return parseSetArguments(flagWords).changes;
|
|
201
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reading of a shebang line's interpreter, so every rule that asks
|
|
3
|
+
* "which shell does this file name?" gets one answer instead of re-deriving it.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The shell basename a shebang's interpreter part (everything after `#!`)
|
|
7
|
+
* runs, or `undefined` when it names no usable interpreter: either an absolute
|
|
8
|
+
* path (`/usr/local/bin/bash` → `bash`), or an absolute path to `env` whose
|
|
9
|
+
* command is the shell (`/usr/bin/env bash`, `/usr/bin/env -S bash -e` →
|
|
10
|
+
* `bash`). A relative interpreter (`#!bash`) yields `undefined`: the kernel
|
|
11
|
+
* would resolve it against the caller's working directory, not `PATH`.
|
|
12
|
+
*
|
|
13
|
+
* Which names count as a shell, and whether one agrees with the configured
|
|
14
|
+
* dialect, are the callers' questions; this only answers "what runs".
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function shebangShell(interpreterSpec: string): string | undefined;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reading of a shebang line's interpreter, so every rule that asks
|
|
3
|
+
* "which shell does this file name?" gets one answer instead of re-deriving it.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* `env` options that consume the following word as their argument
|
|
7
|
+
* (GNU coreutils and BSD spellings). Skipping that word keeps it from being
|
|
8
|
+
* mistaken for the command `env` runs.
|
|
9
|
+
*/
|
|
10
|
+
const ENV_OPTIONS_WITH_ARGUMENT = new Set([
|
|
11
|
+
'-u',
|
|
12
|
+
'--unset',
|
|
13
|
+
'-C',
|
|
14
|
+
'--chdir',
|
|
15
|
+
'-P',
|
|
16
|
+
]);
|
|
17
|
+
/** A `NAME=value` word, which `env` treats as an environment assignment. */
|
|
18
|
+
const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
|
|
19
|
+
/** The final path segment of `path` (`/usr/local/bin/bash` → `bash`). */
|
|
20
|
+
function basename(path) {
|
|
21
|
+
return path.slice(path.lastIndexOf('/') + 1);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The command an `env` shebang runs, given the words after `env`, or
|
|
25
|
+
* `undefined` when there is none. Options, their arguments, and `NAME=value`
|
|
26
|
+
* assignments are skipped; `--` ends the options, but assignments after it
|
|
27
|
+
* still apply (GNU `env -- FOO=1 sh` runs `sh`). `-S` (split string) is transparent here because
|
|
28
|
+
* the shebang's words are already split on whitespace, so `env -S bash -e`
|
|
29
|
+
* runs `bash`.
|
|
30
|
+
*/
|
|
31
|
+
function envCommand(words) {
|
|
32
|
+
for (let index = 0; index < words.length; index++) {
|
|
33
|
+
const word = words[index];
|
|
34
|
+
if (word === '--') {
|
|
35
|
+
return words.slice(index + 1).find((operand) => !ENV_ASSIGNMENT.test(operand));
|
|
36
|
+
}
|
|
37
|
+
if (word === '-S' || word === '--split-string') {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (word.startsWith('--split-string=')) {
|
|
41
|
+
return word.slice('--split-string='.length) || words[index + 1];
|
|
42
|
+
}
|
|
43
|
+
if (word.startsWith('-S')) {
|
|
44
|
+
return word.slice('-S'.length);
|
|
45
|
+
}
|
|
46
|
+
if (ENV_OPTIONS_WITH_ARGUMENT.has(word)) {
|
|
47
|
+
index++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (word.startsWith('-') || ENV_ASSIGNMENT.test(word)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
return word;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The shell basename a shebang's interpreter part (everything after `#!`)
|
|
59
|
+
* runs, or `undefined` when it names no usable interpreter: either an absolute
|
|
60
|
+
* path (`/usr/local/bin/bash` → `bash`), or an absolute path to `env` whose
|
|
61
|
+
* command is the shell (`/usr/bin/env bash`, `/usr/bin/env -S bash -e` →
|
|
62
|
+
* `bash`). A relative interpreter (`#!bash`) yields `undefined`: the kernel
|
|
63
|
+
* would resolve it against the caller's working directory, not `PATH`.
|
|
64
|
+
*
|
|
65
|
+
* Which names count as a shell, and whether one agrees with the configured
|
|
66
|
+
* dialect, are the callers' questions; this only answers "what runs".
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export function shebangShell(interpreterSpec) {
|
|
71
|
+
const [interpreter, ...args] = interpreterSpec.trim().split(/\s+/u);
|
|
72
|
+
if (!interpreter.startsWith('/')) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const name = basename(interpreter);
|
|
76
|
+
if (name !== 'env') {
|
|
77
|
+
return name;
|
|
78
|
+
}
|
|
79
|
+
const command = envCommand(args);
|
|
80
|
+
return command === undefined ? undefined : basename(command);
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule's report kinds, one per direction of the bash/POSIX mismatch.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type ShebangMatchesDialectMessageIds = 'posixShebangBashDialect' | 'bashShebangPosixDialect';
|
|
7
|
+
/**
|
|
8
|
+
* The rule module registered as `shebang-matches-dialect`.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const shebangMatchesDialect: import("./define-shell-rule.js").ShellRule<[], ShebangMatchesDialectMessageIds>;
|