@eslint/shell 0.0.0-stage → 0.1.2
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/LICENSE +201 -0
- package/README.md +167 -2
- package/dist/index.d.ts +79 -0
- package/dist/index.js +74 -0
- package/dist/languages/shell-language.d.ts +36 -0
- package/dist/languages/shell-language.js +72 -0
- package/dist/languages/shell-source-code.d.ts +55 -0
- package/dist/languages/shell-source-code.js +184 -0
- package/dist/parser/parse.d.ts +32 -0
- package/dist/parser/parse.js +758 -0
- package/dist/rules/no-backticks.d.ts +8 -0
- package/dist/rules/no-backticks.js +46 -0
- package/dist/rules/no-expansions-in-single-quotes.d.ts +8 -0
- package/dist/rules/no-expansions-in-single-quotes.js +42 -0
- package/dist/rules/no-ls-iteration.d.ts +8 -0
- package/dist/rules/no-ls-iteration.js +42 -0
- package/dist/rules/no-unquoted-expansions.d.ts +9 -0
- package/dist/rules/no-unquoted-expansions.js +91 -0
- package/dist/rules/no-unused-vars.d.ts +11 -0
- package/dist/rules/no-unused-vars.js +382 -0
- package/dist/rules/no-useless-cat.d.ts +8 -0
- package/dist/rules/no-useless-cat.js +76 -0
- package/dist/rules/no-useless-echo.d.ts +8 -0
- package/dist/rules/no-useless-echo.js +42 -0
- package/dist/rules/no-variables-in-printf-format.d.ts +8 -0
- package/dist/rules/no-variables-in-printf-format.js +60 -0
- package/dist/rules/require-cd-guard.d.ts +8 -0
- package/dist/rules/require-cd-guard.js +74 -0
- package/dist/rules/require-read-r.d.ts +8 -0
- package/dist/rules/require-read-r.js +83 -0
- package/dist/rules/utils.d.ts +35 -0
- package/dist/rules/utils.js +100 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +7 -0
- package/dist/visitor-keys.d.ts +5 -0
- package/dist/visitor-keys.js +59 -0
- package/package.json +75 -4
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow piping from a single-file `cat`.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Imports
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
import { getCommandName, getStaticText } from "./utils.js";
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
function hasPotentialExpansion(word) {
|
|
12
|
+
const checkParts = (parts, quoted = false) => parts.some(part => {
|
|
13
|
+
switch (part.type) {
|
|
14
|
+
case "Literal":
|
|
15
|
+
return (!quoted &&
|
|
16
|
+
(part.value.includes("*") ||
|
|
17
|
+
part.value.includes("?") ||
|
|
18
|
+
part.value.includes("[") ||
|
|
19
|
+
part.value.includes("{") ||
|
|
20
|
+
part.value.includes("}")));
|
|
21
|
+
case "SingleQuotedString":
|
|
22
|
+
return part.dollar;
|
|
23
|
+
case "DoubleQuotedString":
|
|
24
|
+
return checkParts(part.parts, true);
|
|
25
|
+
default:
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return checkParts(word.parts);
|
|
30
|
+
}
|
|
31
|
+
//------------------------------------------------------------------------------
|
|
32
|
+
// Rule Definition
|
|
33
|
+
//------------------------------------------------------------------------------
|
|
34
|
+
const rule = {
|
|
35
|
+
meta: {
|
|
36
|
+
type: "suggestion",
|
|
37
|
+
docs: {
|
|
38
|
+
description: "Disallow useless `cat` at the start of a pipeline",
|
|
39
|
+
recommended: true,
|
|
40
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-useless-cat.md",
|
|
41
|
+
},
|
|
42
|
+
schema: [],
|
|
43
|
+
messages: {
|
|
44
|
+
uselessCat: "Useless cat. Consider 'cmd < file' or 'cmd file' instead.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
create(context) {
|
|
48
|
+
return {
|
|
49
|
+
Pipeline(node) {
|
|
50
|
+
const first = node.commands[0];
|
|
51
|
+
if (!first ||
|
|
52
|
+
first.type !== "Command" ||
|
|
53
|
+
getCommandName(first) !== "cat" ||
|
|
54
|
+
first.redirects.length > 0 ||
|
|
55
|
+
first.arguments.length !== 1) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const argument = first.arguments[0];
|
|
59
|
+
const text = argument ? getStaticText(argument) : null;
|
|
60
|
+
// Skip flags, stdin markers, and arguments that can expand to
|
|
61
|
+
// multiple words.
|
|
62
|
+
if (text === null ||
|
|
63
|
+
text.startsWith("-") ||
|
|
64
|
+
!argument ||
|
|
65
|
+
hasPotentialExpansion(argument)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
context.report({
|
|
69
|
+
node: first,
|
|
70
|
+
messageId: "uselessCat",
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
export default rule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow `$(echo ...)` command substitutions.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Imports
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
import { isCommandNamed } from "./utils.js";
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Rule Definition
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
const rule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "suggestion",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Disallow useless `echo` inside command substitutions",
|
|
16
|
+
recommended: true,
|
|
17
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-useless-echo.md",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
uselessEcho: "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
CommandSubstitution(node) {
|
|
27
|
+
const first = node.body[0];
|
|
28
|
+
if (node.body.length === 1 &&
|
|
29
|
+
first !== undefined &&
|
|
30
|
+
isCommandNamed(first, "echo") &&
|
|
31
|
+
!first.negated &&
|
|
32
|
+
first.redirects.length === 0) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: "uselessEcho",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export default rule;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow variables in the printf format string.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Imports
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
import { getCommandName, getExpansions, getStaticText } from "./utils.js";
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Rule Definition
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
const rule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Disallow variables in the printf format string; use %s placeholders instead",
|
|
16
|
+
recommended: true,
|
|
17
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-variables-in-printf-format.md",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
variableInFormat: "Don't use variables in the printf format string. Use printf '...%s...' \"$foo\".",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
Command(node) {
|
|
27
|
+
if (getCommandName(node) !== "printf") {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let formatIndex = 0;
|
|
31
|
+
const first = node.arguments[0]
|
|
32
|
+
? getStaticText(node.arguments[0])
|
|
33
|
+
: null;
|
|
34
|
+
if (first === "-v") {
|
|
35
|
+
// `printf -v var format ...`
|
|
36
|
+
formatIndex = 2;
|
|
37
|
+
}
|
|
38
|
+
else if (first !== null && first.startsWith("-v")) {
|
|
39
|
+
// `printf -vvar format ...`
|
|
40
|
+
formatIndex = 1;
|
|
41
|
+
}
|
|
42
|
+
const possibleFormat = node.arguments[formatIndex];
|
|
43
|
+
if (possibleFormat && getStaticText(possibleFormat) === "--") {
|
|
44
|
+
formatIndex++;
|
|
45
|
+
}
|
|
46
|
+
const format = node.arguments[formatIndex];
|
|
47
|
+
if (!format) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (getExpansions(format, { includeQuoted: true }).length > 0) {
|
|
51
|
+
context.report({
|
|
52
|
+
node: format,
|
|
53
|
+
messageId: "variableInFormat",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
export default rule;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to require handling `cd` failure.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Imports
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
import { getCommandName } from "./utils.js";
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Rule Definition
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
const rule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Require `cd` failures to be handled, e.g. `cd ... || exit`",
|
|
16
|
+
recommended: true,
|
|
17
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/require-cd-guard.md",
|
|
18
|
+
},
|
|
19
|
+
hasSuggestions: true,
|
|
20
|
+
schema: [],
|
|
21
|
+
messages: {
|
|
22
|
+
uncheckedCd: "Use 'cd ... || exit' or 'cd ... || return' in case cd fails.",
|
|
23
|
+
addGuard: "Add '|| exit' after the cd command.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
create(context) {
|
|
27
|
+
const { sourceCode } = context;
|
|
28
|
+
return {
|
|
29
|
+
Command(node) {
|
|
30
|
+
if (getCommandName(node) !== "cd") {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// `! cd ...` uses the exit status explicitly.
|
|
34
|
+
if (node.negated) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let expression = node;
|
|
38
|
+
let parent = sourceCode.getParent(expression);
|
|
39
|
+
while (parent?.type === "LogicalExpression") {
|
|
40
|
+
// A failed command can propagate through a left-associative
|
|
41
|
+
// chain to an expression whose status is checked.
|
|
42
|
+
if (parent.left === expression) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
expression = parent;
|
|
46
|
+
parent = sourceCode.getParent(expression);
|
|
47
|
+
}
|
|
48
|
+
if (parent) {
|
|
49
|
+
// `if cd ...`, `while cd ...`, `until cd ...`
|
|
50
|
+
if ((parent.type === "IfStatement" ||
|
|
51
|
+
parent.type === "WhileStatement" ||
|
|
52
|
+
parent.type === "UntilStatement") &&
|
|
53
|
+
parent.test.includes(node)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const canSuggest = !node.background && node.redirects.length === 0;
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: "uncheckedCd",
|
|
61
|
+
suggest: canSuggest
|
|
62
|
+
? [
|
|
63
|
+
{
|
|
64
|
+
messageId: "addGuard",
|
|
65
|
+
fix: fixer => fixer.insertTextAfter(node, " || exit"),
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
: [],
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
export default rule;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to require the `-r` flag when using `read`.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Imports
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
import { getCommandName, getStaticText } from "./utils.js";
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Helpers
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
const optionsWithArguments = new Set(["a", "d", "i", "n", "N", "p", "t", "u"]);
|
|
12
|
+
//------------------------------------------------------------------------------
|
|
13
|
+
// Rule Definition
|
|
14
|
+
//------------------------------------------------------------------------------
|
|
15
|
+
const rule = {
|
|
16
|
+
meta: {
|
|
17
|
+
type: "problem",
|
|
18
|
+
docs: {
|
|
19
|
+
description: "Require `read -r` so backslashes are not mangled",
|
|
20
|
+
recommended: true,
|
|
21
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/require-read-r.md",
|
|
22
|
+
},
|
|
23
|
+
fixable: "code",
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
missingR: "Using read without -r will mangle backslashes.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
Command(node) {
|
|
32
|
+
if (getCommandName(node) !== "read" || !node.name) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let hasRawOption = false;
|
|
36
|
+
for (let index = 0; index < node.arguments.length; index++) {
|
|
37
|
+
const argument = node.arguments[index];
|
|
38
|
+
if (!argument) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const text = getStaticText(argument);
|
|
42
|
+
if (text === "--") {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (text === null ||
|
|
46
|
+
!text.startsWith("-") ||
|
|
47
|
+
text === "-") {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (let optionIndex = 1; optionIndex < text.length; optionIndex++) {
|
|
51
|
+
const option = text[optionIndex];
|
|
52
|
+
if (!option) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (option === "r") {
|
|
56
|
+
hasRawOption = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (optionsWithArguments.has(option)) {
|
|
60
|
+
if (optionIndex === text.length - 1) {
|
|
61
|
+
index++;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (hasRawOption) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (hasRawOption) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const name = node.name;
|
|
74
|
+
context.report({
|
|
75
|
+
node: name,
|
|
76
|
+
messageId: "missingR",
|
|
77
|
+
fix: fixer => fixer.insertTextAfter(name, " -r"),
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
export default rule;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared helpers for shell rules.
|
|
3
|
+
*/
|
|
4
|
+
import type { CommandNode, CommandSubstitutionNode, ParameterExpansionNode, StatementNode, WordNode } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Returns the plain text of a word when it is fully static (made up only of
|
|
7
|
+
* literals and quoted literals), or `null` when the word contains
|
|
8
|
+
* expansions. Quotes are removed from the result.
|
|
9
|
+
*
|
|
10
|
+
* Strings whose runtime text can differ from their source text are not
|
|
11
|
+
* static: `$'...'` strings containing escapes, which are not decoded, and
|
|
12
|
+
* locale-translated `$"..."` strings.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getStaticText(word: WordNode): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the name of a command when it is a static word, or `null`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getCommandName(command: CommandNode): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Determines whether a command node is a simple command with the given name.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isCommandNamed(statement: StatementNode, name: string): statement is CommandNode;
|
|
23
|
+
/**
|
|
24
|
+
* Determines whether a command substitution starts with a command of the given
|
|
25
|
+
* name.
|
|
26
|
+
*/
|
|
27
|
+
export declare function startsWithCommand(body: StatementNode[], name: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the expansions that make up a word, including those nested inside
|
|
30
|
+
* double quotes when `includeQuoted` is `true`. Expansions nested inside
|
|
31
|
+
* another expansion, such as `$b` in `${a:-$b}`, are not returned.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getExpansions(word: WordNode, { includeQuoted }?: {
|
|
34
|
+
includeQuoted?: boolean | undefined;
|
|
35
|
+
}): (ParameterExpansionNode | CommandSubstitutionNode)[];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared helpers for shell rules.
|
|
3
|
+
*/
|
|
4
|
+
//------------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
/**
|
|
8
|
+
* Returns the plain text of a word when it is fully static (made up only of
|
|
9
|
+
* literals and quoted literals), or `null` when the word contains
|
|
10
|
+
* expansions. Quotes are removed from the result.
|
|
11
|
+
*
|
|
12
|
+
* Strings whose runtime text can differ from their source text are not
|
|
13
|
+
* static: `$'...'` strings containing escapes, which are not decoded, and
|
|
14
|
+
* locale-translated `$"..."` strings.
|
|
15
|
+
*/
|
|
16
|
+
export function getStaticText(word) {
|
|
17
|
+
let result = "";
|
|
18
|
+
for (const part of word.parts) {
|
|
19
|
+
const text = getStaticPartText(part);
|
|
20
|
+
if (text === null) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
result += text;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function getStaticPartText(part) {
|
|
28
|
+
switch (part.type) {
|
|
29
|
+
case "Literal":
|
|
30
|
+
return part.value;
|
|
31
|
+
case "SingleQuotedString":
|
|
32
|
+
return part.dollar && part.value.includes("\\") ? null : part.value;
|
|
33
|
+
case "DoubleQuotedString": {
|
|
34
|
+
if (part.dollar) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
let result = "";
|
|
38
|
+
for (const inner of part.parts) {
|
|
39
|
+
const text = getStaticPartText(inner);
|
|
40
|
+
if (text === null) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
result += text;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Returns the name of a command when it is a static word, or `null`.
|
|
53
|
+
*/
|
|
54
|
+
export function getCommandName(command) {
|
|
55
|
+
if (!command.name) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return getStaticText(command.name);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Determines whether a command node is a simple command with the given name.
|
|
62
|
+
*/
|
|
63
|
+
export function isCommandNamed(statement, name) {
|
|
64
|
+
return statement.type === "Command" && getCommandName(statement) === name;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Determines whether a command substitution starts with a command of the given
|
|
68
|
+
* name.
|
|
69
|
+
*/
|
|
70
|
+
export function startsWithCommand(body, name) {
|
|
71
|
+
const first = body[0];
|
|
72
|
+
if (!first) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const firstCommand = first.type === "Pipeline" ? first.commands[0] : first;
|
|
76
|
+
return firstCommand !== undefined && isCommandNamed(firstCommand, name);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns the expansions that make up a word, including those nested inside
|
|
80
|
+
* double quotes when `includeQuoted` is `true`. Expansions nested inside
|
|
81
|
+
* another expansion, such as `$b` in `${a:-$b}`, are not returned.
|
|
82
|
+
*/
|
|
83
|
+
export function getExpansions(word, { includeQuoted = false } = {}) {
|
|
84
|
+
const expansions = [];
|
|
85
|
+
const visitParts = (parts, quoted) => {
|
|
86
|
+
for (const part of parts) {
|
|
87
|
+
if (part.type === "ParameterExpansion" ||
|
|
88
|
+
part.type === "CommandSubstitution") {
|
|
89
|
+
if (!quoted || includeQuoted) {
|
|
90
|
+
expansions.push(part);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (part.type === "DoubleQuotedString") {
|
|
94
|
+
visitParts(part.parts, true);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
visitParts(word.parts, false);
|
|
99
|
+
return expansions;
|
|
100
|
+
}
|