@eslint/shell 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +157 -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 +9 -0
- package/dist/rules/no-backticks.js +47 -0
- package/dist/rules/no-expansions-in-single-quotes.d.ts +9 -0
- package/dist/rules/no-expansions-in-single-quotes.js +43 -0
- package/dist/rules/no-ls-iteration.d.ts +9 -0
- package/dist/rules/no-ls-iteration.js +43 -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 +12 -0
- package/dist/rules/no-unused-vars.js +383 -0
- package/dist/rules/no-useless-cat.d.ts +9 -0
- package/dist/rules/no-useless-cat.js +77 -0
- package/dist/rules/no-useless-echo.d.ts +9 -0
- package/dist/rules/no-useless-echo.js +43 -0
- package/dist/rules/no-variables-in-printf-format.d.ts +9 -0
- package/dist/rules/no-variables-in-printf-format.js +61 -0
- package/dist/rules/require-cd-guard.d.ts +9 -0
- package/dist/rules/require-cd-guard.js +75 -0
- package/dist/rules/require-read-r.d.ts +9 -0
- package/dist/rules/require-read-r.js +84 -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 +74 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow piping from a single-file `cat`.
|
|
3
|
+
* Mirrors ShellCheck SC2002.
|
|
4
|
+
*/
|
|
5
|
+
//------------------------------------------------------------------------------
|
|
6
|
+
// Imports
|
|
7
|
+
//------------------------------------------------------------------------------
|
|
8
|
+
import { getCommandName, getStaticText } from "./utils.js";
|
|
9
|
+
//------------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
//------------------------------------------------------------------------------
|
|
12
|
+
function hasPotentialExpansion(word) {
|
|
13
|
+
const checkParts = (parts, quoted = false) => parts.some(part => {
|
|
14
|
+
switch (part.type) {
|
|
15
|
+
case "Literal":
|
|
16
|
+
return (!quoted &&
|
|
17
|
+
(part.value.includes("*") ||
|
|
18
|
+
part.value.includes("?") ||
|
|
19
|
+
part.value.includes("[") ||
|
|
20
|
+
part.value.includes("{") ||
|
|
21
|
+
part.value.includes("}")));
|
|
22
|
+
case "SingleQuotedString":
|
|
23
|
+
return part.dollar;
|
|
24
|
+
case "DoubleQuotedString":
|
|
25
|
+
return checkParts(part.parts, true);
|
|
26
|
+
default:
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return checkParts(word.parts);
|
|
31
|
+
}
|
|
32
|
+
//------------------------------------------------------------------------------
|
|
33
|
+
// Rule Definition
|
|
34
|
+
//------------------------------------------------------------------------------
|
|
35
|
+
const rule = {
|
|
36
|
+
meta: {
|
|
37
|
+
type: "suggestion",
|
|
38
|
+
docs: {
|
|
39
|
+
description: "Disallow useless `cat` at the start of a pipeline",
|
|
40
|
+
recommended: true,
|
|
41
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-useless-cat.md",
|
|
42
|
+
},
|
|
43
|
+
schema: [],
|
|
44
|
+
messages: {
|
|
45
|
+
uselessCat: "Useless cat. Consider 'cmd < file' or 'cmd file' instead. (ShellCheck SC2002)",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
create(context) {
|
|
49
|
+
return {
|
|
50
|
+
Pipeline(node) {
|
|
51
|
+
const first = node.commands[0];
|
|
52
|
+
if (!first ||
|
|
53
|
+
first.type !== "Command" ||
|
|
54
|
+
getCommandName(first) !== "cat" ||
|
|
55
|
+
first.redirects.length > 0 ||
|
|
56
|
+
first.arguments.length !== 1) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const argument = first.arguments[0];
|
|
60
|
+
const text = argument ? getStaticText(argument) : null;
|
|
61
|
+
// Skip flags, stdin markers, and arguments that can expand to
|
|
62
|
+
// multiple words.
|
|
63
|
+
if (text === null ||
|
|
64
|
+
text.startsWith("-") ||
|
|
65
|
+
!argument ||
|
|
66
|
+
hasPotentialExpansion(argument)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
context.report({
|
|
70
|
+
node: first,
|
|
71
|
+
messageId: "uselessCat",
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
export default rule;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow `$(echo ...)` command substitutions.
|
|
3
|
+
* Mirrors ShellCheck SC2116.
|
|
4
|
+
*/
|
|
5
|
+
import type { ShellRuleDefinition } from "../types.js";
|
|
6
|
+
declare const rule: ShellRuleDefinition<{
|
|
7
|
+
MessageIds: "uselessEcho";
|
|
8
|
+
}>;
|
|
9
|
+
export default rule;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow `$(echo ...)` command substitutions.
|
|
3
|
+
* Mirrors ShellCheck SC2116.
|
|
4
|
+
*/
|
|
5
|
+
//------------------------------------------------------------------------------
|
|
6
|
+
// Imports
|
|
7
|
+
//------------------------------------------------------------------------------
|
|
8
|
+
import { isCommandNamed } from "./utils.js";
|
|
9
|
+
//------------------------------------------------------------------------------
|
|
10
|
+
// Rule Definition
|
|
11
|
+
//------------------------------------------------------------------------------
|
|
12
|
+
const rule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "suggestion",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Disallow useless `echo` inside command substitutions",
|
|
17
|
+
recommended: true,
|
|
18
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-useless-echo.md",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
messages: {
|
|
22
|
+
uselessEcho: "Useless echo? Instead of 'cmd $(echo foo)', just use 'cmd foo'. (ShellCheck SC2116)",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
CommandSubstitution(node) {
|
|
28
|
+
const first = node.body[0];
|
|
29
|
+
if (node.body.length === 1 &&
|
|
30
|
+
first !== undefined &&
|
|
31
|
+
isCommandNamed(first, "echo") &&
|
|
32
|
+
!first.negated &&
|
|
33
|
+
first.redirects.length === 0) {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: "uselessEcho",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export default rule;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow variables in the printf format string.
|
|
3
|
+
* Mirrors ShellCheck SC2059.
|
|
4
|
+
*/
|
|
5
|
+
import type { ShellRuleDefinition } from "../types.js";
|
|
6
|
+
declare const rule: ShellRuleDefinition<{
|
|
7
|
+
MessageIds: "variableInFormat";
|
|
8
|
+
}>;
|
|
9
|
+
export default rule;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow variables in the printf format string.
|
|
3
|
+
* Mirrors ShellCheck SC2059.
|
|
4
|
+
*/
|
|
5
|
+
//------------------------------------------------------------------------------
|
|
6
|
+
// Imports
|
|
7
|
+
//------------------------------------------------------------------------------
|
|
8
|
+
import { getCommandName, getExpansions, getStaticText } from "./utils.js";
|
|
9
|
+
//------------------------------------------------------------------------------
|
|
10
|
+
// Rule Definition
|
|
11
|
+
//------------------------------------------------------------------------------
|
|
12
|
+
const rule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Disallow variables in the printf format string; use %s placeholders instead",
|
|
17
|
+
recommended: true,
|
|
18
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/no-variables-in-printf-format.md",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
messages: {
|
|
22
|
+
variableInFormat: "Don't use variables in the printf format string. Use printf '...%s...' \"$foo\". (ShellCheck SC2059)",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
Command(node) {
|
|
28
|
+
if (getCommandName(node) !== "printf") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let formatIndex = 0;
|
|
32
|
+
const first = node.arguments[0]
|
|
33
|
+
? getStaticText(node.arguments[0])
|
|
34
|
+
: null;
|
|
35
|
+
if (first === "-v") {
|
|
36
|
+
// `printf -v var format ...`
|
|
37
|
+
formatIndex = 2;
|
|
38
|
+
}
|
|
39
|
+
else if (first !== null && first.startsWith("-v")) {
|
|
40
|
+
// `printf -vvar format ...`
|
|
41
|
+
formatIndex = 1;
|
|
42
|
+
}
|
|
43
|
+
const possibleFormat = node.arguments[formatIndex];
|
|
44
|
+
if (possibleFormat && getStaticText(possibleFormat) === "--") {
|
|
45
|
+
formatIndex++;
|
|
46
|
+
}
|
|
47
|
+
const format = node.arguments[formatIndex];
|
|
48
|
+
if (!format) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (getExpansions(format, { includeQuoted: true }).length > 0) {
|
|
52
|
+
context.report({
|
|
53
|
+
node: format,
|
|
54
|
+
messageId: "variableInFormat",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
export default rule;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to require handling `cd` failure.
|
|
3
|
+
* Mirrors ShellCheck SC2164.
|
|
4
|
+
*/
|
|
5
|
+
import type { ShellRuleDefinition } from "../types.js";
|
|
6
|
+
declare const rule: ShellRuleDefinition<{
|
|
7
|
+
MessageIds: "uncheckedCd" | "addGuard";
|
|
8
|
+
}>;
|
|
9
|
+
export default rule;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to require handling `cd` failure.
|
|
3
|
+
* Mirrors ShellCheck SC2164.
|
|
4
|
+
*/
|
|
5
|
+
//------------------------------------------------------------------------------
|
|
6
|
+
// Imports
|
|
7
|
+
//------------------------------------------------------------------------------
|
|
8
|
+
import { getCommandName } from "./utils.js";
|
|
9
|
+
//------------------------------------------------------------------------------
|
|
10
|
+
// Rule Definition
|
|
11
|
+
//------------------------------------------------------------------------------
|
|
12
|
+
const rule = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Require `cd` failures to be handled, e.g. `cd ... || exit`",
|
|
17
|
+
recommended: true,
|
|
18
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/require-cd-guard.md",
|
|
19
|
+
},
|
|
20
|
+
hasSuggestions: true,
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
uncheckedCd: "Use 'cd ... || exit' or 'cd ... || return' in case cd fails. (ShellCheck SC2164)",
|
|
24
|
+
addGuard: "Add '|| exit' after the cd command.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const { sourceCode } = context;
|
|
29
|
+
return {
|
|
30
|
+
Command(node) {
|
|
31
|
+
if (getCommandName(node) !== "cd") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// `! cd ...` uses the exit status explicitly.
|
|
35
|
+
if (node.negated) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let expression = node;
|
|
39
|
+
let parent = sourceCode.getParent(expression);
|
|
40
|
+
while (parent?.type === "LogicalExpression") {
|
|
41
|
+
// A failed command can propagate through a left-associative
|
|
42
|
+
// chain to an expression whose status is checked.
|
|
43
|
+
if (parent.left === expression) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
expression = parent;
|
|
47
|
+
parent = sourceCode.getParent(expression);
|
|
48
|
+
}
|
|
49
|
+
if (parent) {
|
|
50
|
+
// `if cd ...`, `while cd ...`, `until cd ...`
|
|
51
|
+
if ((parent.type === "IfStatement" ||
|
|
52
|
+
parent.type === "WhileStatement" ||
|
|
53
|
+
parent.type === "UntilStatement") &&
|
|
54
|
+
parent.test.includes(node)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const canSuggest = !node.background && node.redirects.length === 0;
|
|
59
|
+
context.report({
|
|
60
|
+
node,
|
|
61
|
+
messageId: "uncheckedCd",
|
|
62
|
+
suggest: canSuggest
|
|
63
|
+
? [
|
|
64
|
+
{
|
|
65
|
+
messageId: "addGuard",
|
|
66
|
+
fix: fixer => fixer.insertTextAfter(node, " || exit"),
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
: [],
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
export default rule;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to require the `-r` flag when using `read`.
|
|
3
|
+
* Mirrors ShellCheck SC2162.
|
|
4
|
+
*/
|
|
5
|
+
//------------------------------------------------------------------------------
|
|
6
|
+
// Imports
|
|
7
|
+
//------------------------------------------------------------------------------
|
|
8
|
+
import { getCommandName, getStaticText } from "./utils.js";
|
|
9
|
+
//------------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
//------------------------------------------------------------------------------
|
|
12
|
+
const optionsWithArguments = new Set(["a", "d", "i", "n", "N", "p", "t", "u"]);
|
|
13
|
+
//------------------------------------------------------------------------------
|
|
14
|
+
// Rule Definition
|
|
15
|
+
//------------------------------------------------------------------------------
|
|
16
|
+
const rule = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Require `read -r` so backslashes are not mangled",
|
|
21
|
+
recommended: true,
|
|
22
|
+
url: "https://github.com/eslint/shell/blob/main/docs/rules/require-read-r.md",
|
|
23
|
+
},
|
|
24
|
+
fixable: "code",
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
missingR: "read without -r will mangle backslashes. (ShellCheck SC2162)",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
create(context) {
|
|
31
|
+
return {
|
|
32
|
+
Command(node) {
|
|
33
|
+
if (getCommandName(node) !== "read" || !node.name) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let hasRawOption = false;
|
|
37
|
+
for (let index = 0; index < node.arguments.length; index++) {
|
|
38
|
+
const argument = node.arguments[index];
|
|
39
|
+
if (!argument) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const text = getStaticText(argument);
|
|
43
|
+
if (text === "--") {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (text === null ||
|
|
47
|
+
!text.startsWith("-") ||
|
|
48
|
+
text === "-") {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (let optionIndex = 1; optionIndex < text.length; optionIndex++) {
|
|
52
|
+
const option = text[optionIndex];
|
|
53
|
+
if (!option) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (option === "r") {
|
|
57
|
+
hasRawOption = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (optionsWithArguments.has(option)) {
|
|
61
|
+
if (optionIndex === text.length - 1) {
|
|
62
|
+
index++;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (hasRawOption) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (hasRawOption) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const name = node.name;
|
|
75
|
+
context.report({
|
|
76
|
+
node: name,
|
|
77
|
+
messageId: "missingR",
|
|
78
|
+
fix: fixer => fixer.insertTextAfter(name, " -r"),
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
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
|
+
}
|