@aliou/pi-guardrails 0.5.4 → 0.6.1
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 +63 -94
- package/commands/settings-command.ts +278 -0
- package/{pattern-editor.ts → components/pattern-editor.ts} +61 -10
- package/config.ts +185 -142
- package/hooks/index.ts +1 -7
- package/hooks/permission-gate.ts +247 -143
- package/hooks/protect-env-files.ts +122 -45
- package/index.ts +6 -3
- package/package.json +9 -3
- package/{events.ts → utils/events.ts} +1 -6
- package/utils/glob-expander.ts +128 -0
- package/utils/matching.ts +119 -0
- package/utils/migration.ts +135 -0
- package/utils/shell-utils.ts +139 -0
- package/array-editor.ts +0 -213
- package/config-schema.ts +0 -64
- package/hooks/enforce-package-manager.ts +0 -96
- package/hooks/prevent-brew.ts +0 -41
- package/hooks/prevent-python.ts +0 -45
- package/sectioned-settings.ts +0 -345
- package/settings-command.ts +0 -458
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-guardrails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": [
|
|
@@ -26,10 +26,16 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"*.ts",
|
|
28
28
|
"hooks",
|
|
29
|
+
"commands",
|
|
30
|
+
"components",
|
|
31
|
+
"utils",
|
|
29
32
|
"README.md"
|
|
30
33
|
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@aliou/pi-utils-settings": "^0.1.0",
|
|
36
|
+
"@aliou/sh": "^0.1.0"
|
|
37
|
+
},
|
|
31
38
|
"peerDependencies": {
|
|
32
|
-
"@mariozechner/pi-coding-agent": "0.51.0"
|
|
33
|
-
"@mariozechner/pi-tui": "0.51.0"
|
|
39
|
+
"@mariozechner/pi-coding-agent": ">=0.51.0"
|
|
34
40
|
}
|
|
35
41
|
}
|
|
@@ -4,12 +4,7 @@ export const GUARDRAILS_BLOCKED_EVENT = "guardrails:blocked";
|
|
|
4
4
|
export const GUARDRAILS_DANGEROUS_EVENT = "guardrails:dangerous";
|
|
5
5
|
|
|
6
6
|
export interface GuardrailsBlockedEvent {
|
|
7
|
-
feature:
|
|
8
|
-
| "preventBrew"
|
|
9
|
-
| "preventPython"
|
|
10
|
-
| "protectEnvFiles"
|
|
11
|
-
| "permissionGate"
|
|
12
|
-
| "enforcePackageManager";
|
|
7
|
+
feature: "protectEnvFiles" | "permissionGate";
|
|
13
8
|
toolName: string;
|
|
14
9
|
input: Record<string, unknown>;
|
|
15
10
|
reason: string;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob expansion using `fd` for env file protection.
|
|
3
|
+
*
|
|
4
|
+
* When a bash command contains shell globs referencing env files
|
|
5
|
+
* (e.g. `.env*`), we expand them against the filesystem to check
|
|
6
|
+
* if any expanded path matches a protected pattern.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
interface ExpandGlobOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
maxDepth?: number;
|
|
15
|
+
maxResults?: number;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Expand a glob pattern using `fd`.
|
|
21
|
+
* Returns matching file paths, or empty array on failure.
|
|
22
|
+
*
|
|
23
|
+
* fd is available at `~/.pi/agent/bin/fd` (in pi's PATH).
|
|
24
|
+
*/
|
|
25
|
+
export async function expandGlob(
|
|
26
|
+
pattern: string,
|
|
27
|
+
options: ExpandGlobOptions = {},
|
|
28
|
+
): Promise<string[]> {
|
|
29
|
+
const {
|
|
30
|
+
cwd = process.cwd(),
|
|
31
|
+
maxDepth = 3,
|
|
32
|
+
maxResults = 50,
|
|
33
|
+
timeout = 2000,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
// Convert glob to fd-compatible regex.
|
|
37
|
+
// fd uses regex by default, so we convert glob chars.
|
|
38
|
+
const fdPattern = globToFdRegex(pattern);
|
|
39
|
+
|
|
40
|
+
return new Promise((res) => {
|
|
41
|
+
const args = [
|
|
42
|
+
"--type",
|
|
43
|
+
"f",
|
|
44
|
+
"--max-depth",
|
|
45
|
+
String(maxDepth),
|
|
46
|
+
"--max-results",
|
|
47
|
+
String(maxResults),
|
|
48
|
+
"--no-ignore",
|
|
49
|
+
"--hidden",
|
|
50
|
+
fdPattern,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const child = execFile("fd", args, { cwd, timeout }, (err, stdout) => {
|
|
54
|
+
if (err) {
|
|
55
|
+
res([]);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const files = stdout
|
|
60
|
+
.trim()
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((f) => resolve(cwd, f));
|
|
64
|
+
|
|
65
|
+
res(files);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Safety net: kill if timeout isn't handled by execFile
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
child.kill();
|
|
71
|
+
res([]);
|
|
72
|
+
}, timeout + 500);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert a shell glob to an fd-compatible regex pattern.
|
|
78
|
+
* Handles `*`, `?`, and character classes `[...]`.
|
|
79
|
+
*/
|
|
80
|
+
function globToFdRegex(glob: string): string {
|
|
81
|
+
let regex = "";
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i < glob.length) {
|
|
84
|
+
const ch = glob[i] as string;
|
|
85
|
+
switch (ch) {
|
|
86
|
+
case "*":
|
|
87
|
+
regex += "[^/]*";
|
|
88
|
+
break;
|
|
89
|
+
case "?":
|
|
90
|
+
regex += "[^/]";
|
|
91
|
+
break;
|
|
92
|
+
case "[": {
|
|
93
|
+
// Pass character classes through
|
|
94
|
+
const end = glob.indexOf("]", i + 1);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
regex += glob.slice(i, end + 1);
|
|
97
|
+
i = end;
|
|
98
|
+
} else {
|
|
99
|
+
regex += "\\[";
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case ".":
|
|
104
|
+
case "(":
|
|
105
|
+
case ")":
|
|
106
|
+
case "+":
|
|
107
|
+
case "^":
|
|
108
|
+
case "$":
|
|
109
|
+
case "{":
|
|
110
|
+
case "}":
|
|
111
|
+
case "|":
|
|
112
|
+
case "\\":
|
|
113
|
+
regex += `\\${ch}`;
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
regex += ch;
|
|
117
|
+
}
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
return `^${regex}$`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a string contains shell glob characters.
|
|
125
|
+
*/
|
|
126
|
+
export function hasGlobChars(s: string): boolean {
|
|
127
|
+
return /[*?[\]]/.test(s);
|
|
128
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern compilation for guardrails matching.
|
|
3
|
+
*
|
|
4
|
+
* Two contexts with different default semantics:
|
|
5
|
+
* - File context: default is glob matching against filename.
|
|
6
|
+
* - Command context: default is substring matching against raw command string.
|
|
7
|
+
*
|
|
8
|
+
* Both support `regex: true` for full regex matching.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PatternConfig } from "../config";
|
|
12
|
+
|
|
13
|
+
export interface CompiledPattern {
|
|
14
|
+
test: (input: string) => boolean;
|
|
15
|
+
source: PatternConfig;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert a glob pattern to a regex.
|
|
20
|
+
* `*` matches any non-`/` chars, `?` matches a single char.
|
|
21
|
+
* The rest is escaped.
|
|
22
|
+
*/
|
|
23
|
+
export function globToRegex(glob: string): RegExp {
|
|
24
|
+
let regex = "";
|
|
25
|
+
for (const ch of glob) {
|
|
26
|
+
switch (ch) {
|
|
27
|
+
case "*":
|
|
28
|
+
regex += "[^/]*";
|
|
29
|
+
break;
|
|
30
|
+
case "?":
|
|
31
|
+
regex += "[^/]";
|
|
32
|
+
break;
|
|
33
|
+
case ".":
|
|
34
|
+
case "(":
|
|
35
|
+
case ")":
|
|
36
|
+
case "+":
|
|
37
|
+
case "^":
|
|
38
|
+
case "$":
|
|
39
|
+
case "{":
|
|
40
|
+
case "}":
|
|
41
|
+
case "|":
|
|
42
|
+
case "\\":
|
|
43
|
+
case "[":
|
|
44
|
+
case "]":
|
|
45
|
+
regex += `\\${ch}`;
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
regex += ch;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return new RegExp(`^${regex}$`, "i");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compile a single pattern for file-context matching.
|
|
56
|
+
* Default: glob against the basename of the path.
|
|
57
|
+
* regex: true -> full regex (case-insensitive) against the full path.
|
|
58
|
+
*/
|
|
59
|
+
export function compileFilePattern(config: PatternConfig): CompiledPattern {
|
|
60
|
+
if (config.regex) {
|
|
61
|
+
try {
|
|
62
|
+
const re = new RegExp(config.pattern, "i");
|
|
63
|
+
return { test: (input) => re.test(input), source: config };
|
|
64
|
+
} catch {
|
|
65
|
+
console.error(`Invalid regex in guardrails config: ${config.pattern}`);
|
|
66
|
+
return { test: () => false, source: config };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const re = globToRegex(config.pattern);
|
|
71
|
+
return {
|
|
72
|
+
test: (input) => {
|
|
73
|
+
// Match against basename
|
|
74
|
+
const basename = input.split("/").pop() ?? input;
|
|
75
|
+
return re.test(basename);
|
|
76
|
+
},
|
|
77
|
+
source: config,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compile a single pattern for command-context matching.
|
|
83
|
+
* Default: substring match against raw command string.
|
|
84
|
+
* regex: true -> full regex against raw command string.
|
|
85
|
+
*/
|
|
86
|
+
export function compileCommandPattern(config: PatternConfig): CompiledPattern {
|
|
87
|
+
if (config.regex) {
|
|
88
|
+
try {
|
|
89
|
+
const re = new RegExp(config.pattern);
|
|
90
|
+
return { test: (input) => re.test(input), source: config };
|
|
91
|
+
} catch {
|
|
92
|
+
console.error(`Invalid regex in guardrails config: ${config.pattern}`);
|
|
93
|
+
return { test: () => false, source: config };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
test: (input) => input.includes(config.pattern),
|
|
99
|
+
source: config,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Compile an array of patterns for file-context matching.
|
|
105
|
+
*/
|
|
106
|
+
export function compileFilePatterns(
|
|
107
|
+
configs: PatternConfig[],
|
|
108
|
+
): CompiledPattern[] {
|
|
109
|
+
return configs.map(compileFilePattern);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compile an array of patterns for command-context matching.
|
|
114
|
+
*/
|
|
115
|
+
export function compileCommandPatterns(
|
|
116
|
+
configs: PatternConfig[],
|
|
117
|
+
): CompiledPattern[] {
|
|
118
|
+
return configs.map(compileCommandPattern);
|
|
119
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config migration from v0 (no version field) to current format.
|
|
3
|
+
*
|
|
4
|
+
* v0 configs store patterns as plain strings (regex). The migration
|
|
5
|
+
* converts them to PatternConfig objects with `regex: true` to preserve
|
|
6
|
+
* existing behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { copyFile, stat } from "node:fs/promises";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
|
+
import type {
|
|
12
|
+
DangerousPattern,
|
|
13
|
+
GuardrailsConfig,
|
|
14
|
+
PatternConfig,
|
|
15
|
+
} from "../config";
|
|
16
|
+
|
|
17
|
+
export const CURRENT_VERSION = "0.6.0-20260204";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a config needs migration (no version field = v0).
|
|
21
|
+
*/
|
|
22
|
+
export function needsMigration(config: GuardrailsConfig): boolean {
|
|
23
|
+
return config.version === undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Migrate a v0 config to the current format.
|
|
28
|
+
* All string patterns become `{ pattern, regex: true }` to preserve behavior.
|
|
29
|
+
*/
|
|
30
|
+
export function migrateV0(config: GuardrailsConfig): GuardrailsConfig {
|
|
31
|
+
const migrated = structuredClone(config);
|
|
32
|
+
|
|
33
|
+
// Migrate envFiles patterns
|
|
34
|
+
if (migrated.envFiles) {
|
|
35
|
+
if (migrated.envFiles.protectedPatterns) {
|
|
36
|
+
migrated.envFiles.protectedPatterns = migrateStringArray(
|
|
37
|
+
migrated.envFiles.protectedPatterns,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (migrated.envFiles.allowedPatterns) {
|
|
41
|
+
migrated.envFiles.allowedPatterns = migrateStringArray(
|
|
42
|
+
migrated.envFiles.allowedPatterns,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (migrated.envFiles.protectedDirectories) {
|
|
46
|
+
migrated.envFiles.protectedDirectories = migrateStringArray(
|
|
47
|
+
migrated.envFiles.protectedDirectories,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Migrate permissionGate patterns
|
|
53
|
+
if (migrated.permissionGate) {
|
|
54
|
+
if (migrated.permissionGate.patterns) {
|
|
55
|
+
migrated.permissionGate.patterns = migrateDangerousPatterns(
|
|
56
|
+
migrated.permissionGate.patterns,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (migrated.permissionGate.customPatterns) {
|
|
60
|
+
migrated.permissionGate.customPatterns = migrateDangerousPatterns(
|
|
61
|
+
migrated.permissionGate.customPatterns,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (migrated.permissionGate.allowedPatterns) {
|
|
65
|
+
migrated.permissionGate.allowedPatterns = migrateStringArray(
|
|
66
|
+
migrated.permissionGate.allowedPatterns,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (migrated.permissionGate.autoDenyPatterns) {
|
|
70
|
+
migrated.permissionGate.autoDenyPatterns = migrateStringArray(
|
|
71
|
+
migrated.permissionGate.autoDenyPatterns,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
migrated.version = CURRENT_VERSION;
|
|
77
|
+
return migrated;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Migrate a string[] or PatternConfig[] to PatternConfig[] with regex: true.
|
|
82
|
+
* Handles mixed arrays (some already migrated, some still strings).
|
|
83
|
+
*/
|
|
84
|
+
function migrateStringArray(
|
|
85
|
+
items: (string | PatternConfig)[],
|
|
86
|
+
): PatternConfig[] {
|
|
87
|
+
return items.map((item) => {
|
|
88
|
+
if (typeof item === "string") {
|
|
89
|
+
return { pattern: item, regex: true };
|
|
90
|
+
}
|
|
91
|
+
// Already a PatternConfig, ensure regex is set
|
|
92
|
+
if (item.regex === undefined) {
|
|
93
|
+
return { ...item, regex: true };
|
|
94
|
+
}
|
|
95
|
+
return item;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Migrate dangerous pattern arrays. Handles both legacy
|
|
101
|
+
* `{ pattern: string, description: string }` and already-migrated formats.
|
|
102
|
+
*/
|
|
103
|
+
function migrateDangerousPatterns(
|
|
104
|
+
items: (DangerousPattern | { pattern: string; description: string })[],
|
|
105
|
+
): DangerousPattern[] {
|
|
106
|
+
return items.map((item) => {
|
|
107
|
+
if ("regex" in item && item.regex !== undefined) {
|
|
108
|
+
return item as DangerousPattern;
|
|
109
|
+
}
|
|
110
|
+
return { ...item, regex: true };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Back up a config file before migration.
|
|
116
|
+
* Creates `<name>.v0.json` in the same directory.
|
|
117
|
+
* Skips if backup already exists.
|
|
118
|
+
*/
|
|
119
|
+
export async function backupConfig(configPath: string): Promise<void> {
|
|
120
|
+
const dir = dirname(configPath);
|
|
121
|
+
const basename = configPath.split("/").pop() ?? "guardrails.json";
|
|
122
|
+
const backupName = basename.replace(".json", ".v0.json");
|
|
123
|
+
const backupPath = resolve(dir, backupName);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await stat(backupPath);
|
|
127
|
+
// Backup already exists, skip
|
|
128
|
+
} catch {
|
|
129
|
+
try {
|
|
130
|
+
await copyFile(configPath, backupPath);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.warn(`guardrails: could not back up config: ${err}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shell AST helpers used by guardrails hooks.
|
|
3
|
+
*
|
|
4
|
+
* Each hook imports `parse` from `@aliou/sh` directly and uses these
|
|
5
|
+
* for common AST operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Command,
|
|
10
|
+
Program,
|
|
11
|
+
SimpleCommand,
|
|
12
|
+
Statement,
|
|
13
|
+
Word,
|
|
14
|
+
WordPart,
|
|
15
|
+
} from "@aliou/sh";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a Word node to its literal string value.
|
|
19
|
+
* Concatenates Literal, SglQuoted, and simple DblQuoted parts.
|
|
20
|
+
* For parts containing parameter expansions, command substitutions, etc.,
|
|
21
|
+
* includes the raw text representation (e.g. `$VAR`).
|
|
22
|
+
*/
|
|
23
|
+
export function wordToString(word: Word): string {
|
|
24
|
+
return word.parts.map(partToString).join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function partToString(part: WordPart): string {
|
|
28
|
+
switch (part.type) {
|
|
29
|
+
case "Literal":
|
|
30
|
+
return part.value;
|
|
31
|
+
case "SglQuoted":
|
|
32
|
+
return part.value;
|
|
33
|
+
case "DblQuoted":
|
|
34
|
+
return part.parts.map(partToString).join("");
|
|
35
|
+
case "ParamExp":
|
|
36
|
+
return part.short
|
|
37
|
+
? `$${part.param.value}`
|
|
38
|
+
: `\${${part.param.value}${part.op ?? ""}${part.value ? wordToString(part.value) : ""}}`;
|
|
39
|
+
case "CmdSubst":
|
|
40
|
+
return "$(...)";
|
|
41
|
+
case "ArithExp":
|
|
42
|
+
return `$((${part.expr}))`;
|
|
43
|
+
case "ProcSubst":
|
|
44
|
+
return `${part.op}(...)`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Walk the AST and call `callback` for every SimpleCommand found at any
|
|
50
|
+
* nesting depth. Returns early if callback returns `true`.
|
|
51
|
+
*/
|
|
52
|
+
export function walkCommands(
|
|
53
|
+
node: Program,
|
|
54
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
55
|
+
): void {
|
|
56
|
+
for (const stmt of node.body) {
|
|
57
|
+
if (walkStatement(stmt, callback)) return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walkStatement(
|
|
62
|
+
stmt: Statement,
|
|
63
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
64
|
+
): boolean {
|
|
65
|
+
return walkCommand(stmt.command, callback);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function walkStatements(
|
|
69
|
+
stmts: Statement[],
|
|
70
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
71
|
+
): boolean {
|
|
72
|
+
for (const stmt of stmts) {
|
|
73
|
+
if (walkStatement(stmt, callback)) return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function walkCommand(
|
|
79
|
+
cmd: Command,
|
|
80
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
81
|
+
): boolean {
|
|
82
|
+
switch (cmd.type) {
|
|
83
|
+
case "SimpleCommand":
|
|
84
|
+
return callback(cmd) === true;
|
|
85
|
+
|
|
86
|
+
case "Pipeline":
|
|
87
|
+
return walkStatements(cmd.commands, callback);
|
|
88
|
+
|
|
89
|
+
case "Logical":
|
|
90
|
+
return (
|
|
91
|
+
walkStatement(cmd.left, callback) || walkStatement(cmd.right, callback)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
case "Subshell":
|
|
95
|
+
case "Block":
|
|
96
|
+
return walkStatements(cmd.body, callback);
|
|
97
|
+
|
|
98
|
+
case "IfClause":
|
|
99
|
+
return (
|
|
100
|
+
walkStatements(cmd.cond, callback) ||
|
|
101
|
+
walkStatements(cmd.then, callback) ||
|
|
102
|
+
(cmd.else ? walkStatements(cmd.else, callback) : false)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
case "ForClause":
|
|
106
|
+
case "SelectClause":
|
|
107
|
+
case "WhileClause":
|
|
108
|
+
return (
|
|
109
|
+
("cond" in cmd && cmd.cond
|
|
110
|
+
? walkStatements(cmd.cond, callback)
|
|
111
|
+
: false) || walkStatements(cmd.body, callback)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
case "CaseClause":
|
|
115
|
+
for (const item of cmd.items) {
|
|
116
|
+
if (walkStatements(item.body, callback)) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
|
|
120
|
+
case "FunctionDecl":
|
|
121
|
+
return walkStatements(cmd.body, callback);
|
|
122
|
+
|
|
123
|
+
case "TimeClause":
|
|
124
|
+
return walkStatement(cmd.command, callback);
|
|
125
|
+
|
|
126
|
+
case "CoprocClause":
|
|
127
|
+
return walkStatement(cmd.body, callback);
|
|
128
|
+
|
|
129
|
+
case "CStyleLoop":
|
|
130
|
+
return walkStatements(cmd.body, callback);
|
|
131
|
+
|
|
132
|
+
// These don't contain nested commands we need to walk
|
|
133
|
+
case "TestClause":
|
|
134
|
+
case "ArithCmd":
|
|
135
|
+
case "DeclClause":
|
|
136
|
+
case "LetClause":
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|