lsh-framework 1.1.0 → 1.2.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 +70 -4
- package/dist/cli.js +104 -486
- package/dist/commands/doctor.js +427 -0
- package/dist/commands/init.js +371 -0
- package/dist/constants/api.js +94 -0
- package/dist/constants/commands.js +64 -0
- package/dist/constants/config.js +56 -0
- package/dist/constants/database.js +21 -0
- package/dist/constants/errors.js +79 -0
- package/dist/constants/index.js +28 -0
- package/dist/constants/paths.js +28 -0
- package/dist/constants/ui.js +73 -0
- package/dist/constants/validation.js +124 -0
- package/dist/daemon/lshd.js +11 -32
- package/dist/lib/daemon-client-helper.js +7 -4
- package/dist/lib/daemon-client.js +9 -2
- package/dist/lib/format-utils.js +163 -0
- package/dist/lib/job-manager.js +2 -1
- package/dist/lib/platform-utils.js +211 -0
- package/dist/lib/secrets-manager.js +11 -1
- package/dist/lib/string-utils.js +128 -0
- package/dist/services/daemon/daemon-registrar.js +3 -2
- package/dist/services/secrets/secrets.js +154 -30
- package/package.json +10 -74
- package/dist/app.js +0 -33
- package/dist/cicd/analytics.js +0 -261
- package/dist/cicd/auth.js +0 -269
- package/dist/cicd/cache-manager.js +0 -172
- package/dist/cicd/data-retention.js +0 -305
- package/dist/cicd/performance-monitor.js +0 -224
- package/dist/cicd/webhook-receiver.js +0 -640
- package/dist/commands/api.js +0 -346
- package/dist/commands/theme.js +0 -261
- package/dist/commands/zsh-import.js +0 -240
- package/dist/components/App.js +0 -1
- package/dist/components/Divider.js +0 -29
- package/dist/components/REPL.js +0 -43
- package/dist/components/Terminal.js +0 -232
- package/dist/components/UserInput.js +0 -30
- package/dist/daemon/api-server.js +0 -316
- package/dist/daemon/monitoring-api.js +0 -220
- package/dist/lib/api-error-handler.js +0 -185
- package/dist/lib/associative-arrays.js +0 -285
- package/dist/lib/base-api-server.js +0 -290
- package/dist/lib/brace-expansion.js +0 -160
- package/dist/lib/builtin-commands.js +0 -439
- package/dist/lib/executors/builtin-executor.js +0 -52
- package/dist/lib/extended-globbing.js +0 -411
- package/dist/lib/extended-parameter-expansion.js +0 -227
- package/dist/lib/interactive-shell.js +0 -460
- package/dist/lib/job-builtins.js +0 -582
- package/dist/lib/pathname-expansion.js +0 -216
- package/dist/lib/script-runner.js +0 -226
- package/dist/lib/shell-executor.js +0 -2504
- package/dist/lib/shell-parser.js +0 -958
- package/dist/lib/shell-types.js +0 -6
- package/dist/lib/shell.lib.js +0 -40
- package/dist/lib/theme-manager.js +0 -476
- package/dist/lib/variable-expansion.js +0 -385
- package/dist/lib/zsh-compatibility.js +0 -659
- package/dist/lib/zsh-import-manager.js +0 -707
- package/dist/lib/zsh-options.js +0 -328
- package/dist/pipeline/job-tracker.js +0 -491
- package/dist/pipeline/mcli-bridge.js +0 -309
- package/dist/pipeline/pipeline-service.js +0 -1119
- package/dist/pipeline/workflow-engine.js +0 -870
- package/dist/services/api/api.js +0 -58
- package/dist/services/api/auth.js +0 -35
- package/dist/services/api/config.js +0 -7
- package/dist/services/api/file.js +0 -22
- package/dist/services/shell/shell.js +0 -28
- package/dist/services/zapier.js +0 -16
- package/dist/simple-api-server.js +0 -148
|
@@ -1,2504 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POSIX Shell Command Executor
|
|
3
|
-
* Executes parsed AST nodes following POSIX semantics
|
|
4
|
-
*/
|
|
5
|
-
import { spawn, exec } from 'child_process';
|
|
6
|
-
import { promisify } from 'util';
|
|
7
|
-
import * as fs from 'fs';
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
import { parseShellCommand } from './shell-parser.js';
|
|
10
|
-
import { VariableExpander } from './variable-expansion.js';
|
|
11
|
-
import { PathnameExpander } from './pathname-expansion.js';
|
|
12
|
-
import { BraceExpander } from './brace-expansion.js';
|
|
13
|
-
import CompletionSystem from './completion-system.js';
|
|
14
|
-
import JobManager from './job-manager.js';
|
|
15
|
-
import JobBuiltins from './job-builtins.js';
|
|
16
|
-
import HistorySystem from './history-system.js';
|
|
17
|
-
import AssociativeArrayManager from './associative-arrays.js';
|
|
18
|
-
import ExtendedParameterExpander from './extended-parameter-expansion.js';
|
|
19
|
-
import ExtendedGlobber from './extended-globbing.js';
|
|
20
|
-
import ZshOptionsManager from './zsh-options.js';
|
|
21
|
-
import PromptSystem from './prompt-system.js';
|
|
22
|
-
import FloatingPointArithmetic from './floating-point-arithmetic.js';
|
|
23
|
-
const execAsync = promisify(exec);
|
|
24
|
-
export class ShellExecutor {
|
|
25
|
-
context;
|
|
26
|
-
expander;
|
|
27
|
-
pathExpander;
|
|
28
|
-
braceExpander;
|
|
29
|
-
jobManager;
|
|
30
|
-
jobBuiltins;
|
|
31
|
-
extendedExpander;
|
|
32
|
-
extendedGlobber;
|
|
33
|
-
constructor(initialContext) {
|
|
34
|
-
this.context = {
|
|
35
|
-
env: Object.fromEntries(Object.entries(process.env).filter(([_, v]) => v !== undefined)),
|
|
36
|
-
cwd: process.cwd(),
|
|
37
|
-
variables: {},
|
|
38
|
-
lastExitCode: 0,
|
|
39
|
-
lastReturnCode: 0,
|
|
40
|
-
positionalParams: [],
|
|
41
|
-
ifs: ' \t\n',
|
|
42
|
-
functions: {},
|
|
43
|
-
jobControl: {
|
|
44
|
-
jobs: new Map(),
|
|
45
|
-
nextJobId: 1,
|
|
46
|
-
lastBackgroundPid: 0,
|
|
47
|
-
},
|
|
48
|
-
options: {
|
|
49
|
-
errexit: false,
|
|
50
|
-
nounset: false,
|
|
51
|
-
xtrace: false,
|
|
52
|
-
verbose: false,
|
|
53
|
-
noglob: false,
|
|
54
|
-
monitor: false,
|
|
55
|
-
noclobber: false,
|
|
56
|
-
allexport: false,
|
|
57
|
-
},
|
|
58
|
-
history: new HistorySystem(),
|
|
59
|
-
completion: new CompletionSystem(),
|
|
60
|
-
arrays: new AssociativeArrayManager(),
|
|
61
|
-
zshOptions: new ZshOptionsManager(),
|
|
62
|
-
prompt: new PromptSystem(),
|
|
63
|
-
floatingPoint: new FloatingPointArithmetic(),
|
|
64
|
-
zshCompatibility: null, // Will be initialized after constructor
|
|
65
|
-
...initialContext,
|
|
66
|
-
};
|
|
67
|
-
this.expander = new VariableExpander(this.createVariableContext());
|
|
68
|
-
this.pathExpander = new PathnameExpander(this.context.cwd);
|
|
69
|
-
this.braceExpander = new BraceExpander();
|
|
70
|
-
this.jobManager = new JobManager();
|
|
71
|
-
this.jobBuiltins = new JobBuiltins(this.jobManager);
|
|
72
|
-
this.extendedExpander = new ExtendedParameterExpander({
|
|
73
|
-
variables: this.context.variables,
|
|
74
|
-
env: this.context.env,
|
|
75
|
-
arrays: this.context.arrays,
|
|
76
|
-
});
|
|
77
|
-
this.extendedGlobber = new ExtendedGlobber(this.context.cwd);
|
|
78
|
-
}
|
|
79
|
-
createVariableContext() {
|
|
80
|
-
return {
|
|
81
|
-
variables: this.context.variables,
|
|
82
|
-
env: this.context.env,
|
|
83
|
-
positionalParams: this.context.positionalParams,
|
|
84
|
-
specialParams: {
|
|
85
|
-
'$': process.pid.toString(),
|
|
86
|
-
'?': this.context.lastExitCode.toString(),
|
|
87
|
-
'#': this.context.positionalParams.length.toString(),
|
|
88
|
-
'*': this.context.positionalParams.join(' '),
|
|
89
|
-
'@': this.context.positionalParams,
|
|
90
|
-
'!': this.context.jobControl.lastBackgroundPid.toString(),
|
|
91
|
-
'0': 'lsh',
|
|
92
|
-
'-': this.formatShellOptions(),
|
|
93
|
-
},
|
|
94
|
-
options: this.context.options, // Pass shell options for set -u behavior
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
updateExpander() {
|
|
98
|
-
this.expander.updateContext(this.createVariableContext());
|
|
99
|
-
}
|
|
100
|
-
formatShellOptions() {
|
|
101
|
-
const options = this.context.options;
|
|
102
|
-
let optionString = '';
|
|
103
|
-
if (options.errexit)
|
|
104
|
-
optionString += 'e';
|
|
105
|
-
if (options.nounset)
|
|
106
|
-
optionString += 'u';
|
|
107
|
-
if (options.xtrace)
|
|
108
|
-
optionString += 'x';
|
|
109
|
-
if (options.verbose)
|
|
110
|
-
optionString += 'v';
|
|
111
|
-
if (options.noglob)
|
|
112
|
-
optionString += 'f';
|
|
113
|
-
if (options.monitor)
|
|
114
|
-
optionString += 'm';
|
|
115
|
-
if (options.noclobber)
|
|
116
|
-
optionString += 'C';
|
|
117
|
-
if (options.allexport)
|
|
118
|
-
optionString += 'a';
|
|
119
|
-
return optionString;
|
|
120
|
-
}
|
|
121
|
-
adaptJobBuiltinResult(result) {
|
|
122
|
-
return {
|
|
123
|
-
...result,
|
|
124
|
-
success: result.exitCode === 0
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
getContext() {
|
|
128
|
-
return { ...this.context };
|
|
129
|
-
}
|
|
130
|
-
async execute(node) {
|
|
131
|
-
try {
|
|
132
|
-
return await this.executeNode(node);
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
return {
|
|
136
|
-
stdout: '',
|
|
137
|
-
stderr: error.message,
|
|
138
|
-
exitCode: 1,
|
|
139
|
-
success: false,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
async executeNode(node) {
|
|
144
|
-
// Implement set -x (xtrace) - print commands before execution
|
|
145
|
-
if (this.context.options.xtrace && (node.type === 'SimpleCommand' || node.type === 'Pipeline')) {
|
|
146
|
-
console.error(`+ ${this.nodeToString(node)}`);
|
|
147
|
-
}
|
|
148
|
-
let result;
|
|
149
|
-
switch (node.type) {
|
|
150
|
-
case 'SimpleCommand':
|
|
151
|
-
result = await this.executeSimpleCommand(node);
|
|
152
|
-
break;
|
|
153
|
-
case 'Pipeline':
|
|
154
|
-
result = await this.executePipeline(node);
|
|
155
|
-
break;
|
|
156
|
-
case 'CommandList':
|
|
157
|
-
result = await this.executeCommandList(node);
|
|
158
|
-
break;
|
|
159
|
-
case 'Subshell':
|
|
160
|
-
result = await this.executeSubshell(node);
|
|
161
|
-
break;
|
|
162
|
-
case 'CommandGroup':
|
|
163
|
-
result = await this.executeCommandGroup(node);
|
|
164
|
-
break;
|
|
165
|
-
case 'IfStatement':
|
|
166
|
-
result = await this.executeIfStatement(node);
|
|
167
|
-
break;
|
|
168
|
-
case 'ForStatement':
|
|
169
|
-
result = await this.executeForStatement(node);
|
|
170
|
-
break;
|
|
171
|
-
case 'WhileStatement':
|
|
172
|
-
result = await this.executeWhileStatement(node);
|
|
173
|
-
break;
|
|
174
|
-
case 'CaseStatement':
|
|
175
|
-
result = await this.executeCaseStatement(node);
|
|
176
|
-
break;
|
|
177
|
-
case 'FunctionDefinition':
|
|
178
|
-
result = await this.executeFunctionDefinition(node);
|
|
179
|
-
break;
|
|
180
|
-
default:
|
|
181
|
-
throw new Error(`Unknown AST node type: ${node.type}`);
|
|
182
|
-
}
|
|
183
|
-
// Implement set -e (errexit) - exit immediately on command failure
|
|
184
|
-
if (this.context.options.errexit && !result.success && result.exitCode !== 0) {
|
|
185
|
-
// Only exit on actual command failures, not control structures or built-ins that expect failures
|
|
186
|
-
if (node.type === 'SimpleCommand' || node.type === 'Pipeline') {
|
|
187
|
-
throw new Error(`Command failed with exit code ${result.exitCode} (set -e)`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return result;
|
|
191
|
-
}
|
|
192
|
-
async executeSimpleCommand(cmd) {
|
|
193
|
-
if (!cmd.name) {
|
|
194
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
195
|
-
}
|
|
196
|
-
this.updateExpander();
|
|
197
|
-
// Expand command name and arguments
|
|
198
|
-
const expandedName = await this.expander.expandString(cmd.name);
|
|
199
|
-
const expandedArgs = [];
|
|
200
|
-
for (const arg of cmd.args) {
|
|
201
|
-
// Check for process substitution first
|
|
202
|
-
if (arg.startsWith('<(') || arg.startsWith('>(')) {
|
|
203
|
-
const fifoPath = await this.handleProcessSubstitution(arg);
|
|
204
|
-
expandedArgs.push(fifoPath);
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
// Step 1: Variable and command substitution expansion
|
|
208
|
-
const variableExpanded = await this.expander.expandString(arg);
|
|
209
|
-
// Step 2: Brace expansion
|
|
210
|
-
const braceExpanded = this.braceExpander.expandBraces(variableExpanded);
|
|
211
|
-
// Step 3: Process each brace-expanded result
|
|
212
|
-
for (const braceResult of braceExpanded) {
|
|
213
|
-
// Step 4: Field splitting (only if variable expansion occurred)
|
|
214
|
-
let fields;
|
|
215
|
-
if (variableExpanded !== arg && (arg.includes('$') || arg.includes('`'))) {
|
|
216
|
-
// Only split if variable expansion happened
|
|
217
|
-
fields = this.expander.splitFields(braceResult, this.context.ifs);
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
// No variable expansion, keep as single field
|
|
221
|
-
fields = [braceResult];
|
|
222
|
-
}
|
|
223
|
-
// Step 5: Pathname expansion for each field
|
|
224
|
-
for (const field of fields) {
|
|
225
|
-
const pathExpanded = await this.pathExpander.expandPathnames(field, {
|
|
226
|
-
cwd: this.context.cwd,
|
|
227
|
-
includeHidden: false,
|
|
228
|
-
});
|
|
229
|
-
// If pathname expansion found matches, use them; otherwise use original field
|
|
230
|
-
if (pathExpanded.length > 0 && pathExpanded[0] !== field) {
|
|
231
|
-
expandedArgs.push(...pathExpanded);
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
expandedArgs.push(field);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
// Create expanded command
|
|
240
|
-
const expandedCmd = {
|
|
241
|
-
type: 'SimpleCommand',
|
|
242
|
-
name: expandedName,
|
|
243
|
-
args: expandedArgs,
|
|
244
|
-
redirections: cmd.redirections,
|
|
245
|
-
};
|
|
246
|
-
// Handle redirections and execute command
|
|
247
|
-
return this.executeWithRedirections(expandedCmd);
|
|
248
|
-
}
|
|
249
|
-
async executeBuiltin(name, args) {
|
|
250
|
-
switch (name) {
|
|
251
|
-
case 'cd':
|
|
252
|
-
return this.builtin_cd(args);
|
|
253
|
-
case 'pwd':
|
|
254
|
-
return this.builtin_pwd(args);
|
|
255
|
-
case 'echo':
|
|
256
|
-
return this.builtin_echo(args);
|
|
257
|
-
case 'true':
|
|
258
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
259
|
-
case 'false':
|
|
260
|
-
return { stdout: '', stderr: '', exitCode: 1, success: false };
|
|
261
|
-
case 'exit':
|
|
262
|
-
return this.builtin_exit(args);
|
|
263
|
-
case 'export':
|
|
264
|
-
return this.builtin_export(args);
|
|
265
|
-
case 'unset':
|
|
266
|
-
return this.builtin_unset(args);
|
|
267
|
-
case 'set':
|
|
268
|
-
return this.builtin_set(args);
|
|
269
|
-
case 'test':
|
|
270
|
-
case '[':
|
|
271
|
-
return this.builtin_test(args);
|
|
272
|
-
case 'printf':
|
|
273
|
-
return this.builtin_printf(args);
|
|
274
|
-
case 'eval':
|
|
275
|
-
return this.builtin_eval(args);
|
|
276
|
-
case 'exec':
|
|
277
|
-
return this.builtin_exec(args);
|
|
278
|
-
case 'return':
|
|
279
|
-
return this.builtin_return(args);
|
|
280
|
-
case 'shift':
|
|
281
|
-
return this.builtin_shift(args);
|
|
282
|
-
case 'local':
|
|
283
|
-
return this.builtin_local(args);
|
|
284
|
-
case 'jobs':
|
|
285
|
-
return this.builtin_jobs(args);
|
|
286
|
-
case 'fg':
|
|
287
|
-
return this.builtin_fg(args);
|
|
288
|
-
case 'bg':
|
|
289
|
-
return this.builtin_bg(args);
|
|
290
|
-
case 'wait':
|
|
291
|
-
return this.builtin_wait(args);
|
|
292
|
-
case 'read':
|
|
293
|
-
return this.builtin_read(args);
|
|
294
|
-
case 'getopts':
|
|
295
|
-
return this.builtin_getopts(args);
|
|
296
|
-
case 'trap':
|
|
297
|
-
return this.builtin_trap(args);
|
|
298
|
-
case 'typeset':
|
|
299
|
-
return this.builtin_typeset(args);
|
|
300
|
-
case 'setopt':
|
|
301
|
-
return this.builtin_setopt(args);
|
|
302
|
-
case 'unsetopt':
|
|
303
|
-
return this.builtin_unsetopt(args);
|
|
304
|
-
case 'history':
|
|
305
|
-
return this.builtin_history(args);
|
|
306
|
-
case 'fc':
|
|
307
|
-
return this.builtin_fc(args);
|
|
308
|
-
case 'r':
|
|
309
|
-
return this.builtin_r(args);
|
|
310
|
-
case 'alias':
|
|
311
|
-
return this.builtin_alias(args);
|
|
312
|
-
case 'unalias':
|
|
313
|
-
return this.builtin_unalias(args);
|
|
314
|
-
case 'readonly':
|
|
315
|
-
return this.builtin_readonly(args);
|
|
316
|
-
case 'type':
|
|
317
|
-
return this.builtin_type(args);
|
|
318
|
-
case 'hash':
|
|
319
|
-
return this.builtin_hash(args);
|
|
320
|
-
case 'kill':
|
|
321
|
-
return this.builtin_kill(args);
|
|
322
|
-
case 'source':
|
|
323
|
-
return this.builtin_source(args);
|
|
324
|
-
case 'install':
|
|
325
|
-
return this.builtin_install(args);
|
|
326
|
-
case 'uninstall':
|
|
327
|
-
return this.builtin_uninstall(args);
|
|
328
|
-
case 'zsh-migrate':
|
|
329
|
-
return this.builtin_zsh_migrate(args);
|
|
330
|
-
case 'zsh-source':
|
|
331
|
-
return this.builtin_zsh_source(args);
|
|
332
|
-
// Job Management Built-ins
|
|
333
|
-
case 'job-create':
|
|
334
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobCreate(args));
|
|
335
|
-
case 'job-list':
|
|
336
|
-
case 'jlist':
|
|
337
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobList(args));
|
|
338
|
-
case 'job-show':
|
|
339
|
-
case 'jshow':
|
|
340
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobShow(args));
|
|
341
|
-
case 'job-start':
|
|
342
|
-
case 'jstart':
|
|
343
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobStart(args));
|
|
344
|
-
case 'job-stop':
|
|
345
|
-
case 'jstop':
|
|
346
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobStop(args));
|
|
347
|
-
case 'job-pause':
|
|
348
|
-
case 'jpause':
|
|
349
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobPause(args));
|
|
350
|
-
case 'job-resume':
|
|
351
|
-
case 'jresume':
|
|
352
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobResume(args));
|
|
353
|
-
case 'job-remove':
|
|
354
|
-
case 'jremove':
|
|
355
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobRemove(args));
|
|
356
|
-
case 'job-update':
|
|
357
|
-
case 'jupdate':
|
|
358
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobUpdate(args));
|
|
359
|
-
case 'job-run':
|
|
360
|
-
case 'jrun':
|
|
361
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobRun(args));
|
|
362
|
-
case 'job-monitor':
|
|
363
|
-
case 'jmonitor':
|
|
364
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobMonitor(args));
|
|
365
|
-
case 'job-stats':
|
|
366
|
-
case 'jstats':
|
|
367
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobStats(args));
|
|
368
|
-
case 'job-cleanup':
|
|
369
|
-
case 'jcleanup':
|
|
370
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.jobCleanup(args));
|
|
371
|
-
case 'ps-list':
|
|
372
|
-
case 'pslist':
|
|
373
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.psList(args));
|
|
374
|
-
case 'ps-kill':
|
|
375
|
-
case 'pskill':
|
|
376
|
-
return this.adaptJobBuiltinResult(await this.jobBuiltins.psKill(args));
|
|
377
|
-
default:
|
|
378
|
-
return null; // Not a built-in
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async builtin_cd(args) {
|
|
382
|
-
const target = args[0] || this.context.env.HOME || '/';
|
|
383
|
-
try {
|
|
384
|
-
const resolvedPath = path.resolve(this.context.cwd, target);
|
|
385
|
-
// Check if directory exists
|
|
386
|
-
if (!fs.existsSync(resolvedPath)) {
|
|
387
|
-
return {
|
|
388
|
-
stdout: '',
|
|
389
|
-
stderr: `cd: ${target}: No such file or directory`,
|
|
390
|
-
exitCode: 1,
|
|
391
|
-
success: false,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
const stats = fs.statSync(resolvedPath);
|
|
395
|
-
if (!stats.isDirectory()) {
|
|
396
|
-
return {
|
|
397
|
-
stdout: '',
|
|
398
|
-
stderr: `cd: ${target}: Not a directory`,
|
|
399
|
-
exitCode: 1,
|
|
400
|
-
success: false,
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
// Update context
|
|
404
|
-
this.context.cwd = resolvedPath;
|
|
405
|
-
process.chdir(resolvedPath);
|
|
406
|
-
// Update path expander with new working directory
|
|
407
|
-
this.pathExpander = new PathnameExpander(this.context.cwd);
|
|
408
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
return {
|
|
412
|
-
stdout: '',
|
|
413
|
-
stderr: `cd: ${target}: ${error.message}`,
|
|
414
|
-
exitCode: 1,
|
|
415
|
-
success: false,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
async builtin_pwd(_args) {
|
|
420
|
-
return {
|
|
421
|
-
stdout: this.context.cwd,
|
|
422
|
-
stderr: '',
|
|
423
|
-
exitCode: 0,
|
|
424
|
-
success: true,
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
async builtin_echo(args) {
|
|
428
|
-
let output = args.join(' ');
|
|
429
|
-
// Handle -n flag (no trailing newline)
|
|
430
|
-
if (args[0] === '-n') {
|
|
431
|
-
output = args.slice(1).join(' ');
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
output += '\n';
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
stdout: output,
|
|
438
|
-
stderr: '',
|
|
439
|
-
exitCode: 0,
|
|
440
|
-
success: true,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
async builtin_exit(args) {
|
|
444
|
-
const code = args[0] ? parseInt(args[0], 10) : this.context.lastExitCode;
|
|
445
|
-
// In a real shell, this would exit the process
|
|
446
|
-
// For now, we'll just return the exit code
|
|
447
|
-
return {
|
|
448
|
-
stdout: '',
|
|
449
|
-
stderr: '',
|
|
450
|
-
exitCode: code,
|
|
451
|
-
success: code === 0,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
async builtin_export(args) {
|
|
455
|
-
for (const arg of args) {
|
|
456
|
-
if (arg.includes('=')) {
|
|
457
|
-
const [name, value] = arg.split('=', 2);
|
|
458
|
-
this.context.env[name] = value;
|
|
459
|
-
this.context.variables[name] = value;
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
// Export existing variable
|
|
463
|
-
if (arg in this.context.variables) {
|
|
464
|
-
this.context.env[arg] = this.context.variables[arg];
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
this.updateExpander();
|
|
469
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
470
|
-
}
|
|
471
|
-
async builtin_unset(args) {
|
|
472
|
-
for (const name of args) {
|
|
473
|
-
delete this.context.variables[name];
|
|
474
|
-
delete this.context.env[name];
|
|
475
|
-
}
|
|
476
|
-
this.updateExpander();
|
|
477
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
478
|
-
}
|
|
479
|
-
async builtin_set(args) {
|
|
480
|
-
if (args.length === 0) {
|
|
481
|
-
// Show all variables and functions
|
|
482
|
-
const output = Object.entries(this.context.variables)
|
|
483
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
484
|
-
.join('\n');
|
|
485
|
-
return {
|
|
486
|
-
stdout: output,
|
|
487
|
-
stderr: '',
|
|
488
|
-
exitCode: 0,
|
|
489
|
-
success: true,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
// Handle set options
|
|
493
|
-
for (const arg of args) {
|
|
494
|
-
if (arg.startsWith('-')) {
|
|
495
|
-
// Enable options
|
|
496
|
-
for (let i = 1; i < arg.length; i++) {
|
|
497
|
-
const option = arg.charAt(i);
|
|
498
|
-
switch (option) {
|
|
499
|
-
case 'e':
|
|
500
|
-
this.context.options.errexit = true;
|
|
501
|
-
break;
|
|
502
|
-
case 'u':
|
|
503
|
-
this.context.options.nounset = true;
|
|
504
|
-
break;
|
|
505
|
-
case 'x':
|
|
506
|
-
this.context.options.xtrace = true;
|
|
507
|
-
break;
|
|
508
|
-
case 'v':
|
|
509
|
-
this.context.options.verbose = true;
|
|
510
|
-
break;
|
|
511
|
-
case 'f':
|
|
512
|
-
this.context.options.noglob = true;
|
|
513
|
-
break;
|
|
514
|
-
case 'm':
|
|
515
|
-
this.context.options.monitor = true;
|
|
516
|
-
break;
|
|
517
|
-
case 'C':
|
|
518
|
-
this.context.options.noclobber = true;
|
|
519
|
-
break;
|
|
520
|
-
case 'a':
|
|
521
|
-
this.context.options.allexport = true;
|
|
522
|
-
break;
|
|
523
|
-
default:
|
|
524
|
-
return {
|
|
525
|
-
stdout: '',
|
|
526
|
-
stderr: `set: illegal option -- ${option}`,
|
|
527
|
-
exitCode: 1,
|
|
528
|
-
success: false,
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
else if (arg.startsWith('+')) {
|
|
534
|
-
// Disable options
|
|
535
|
-
for (let i = 1; i < arg.length; i++) {
|
|
536
|
-
const option = arg.charAt(i);
|
|
537
|
-
switch (option) {
|
|
538
|
-
case 'e':
|
|
539
|
-
this.context.options.errexit = false;
|
|
540
|
-
break;
|
|
541
|
-
case 'u':
|
|
542
|
-
this.context.options.nounset = false;
|
|
543
|
-
break;
|
|
544
|
-
case 'x':
|
|
545
|
-
this.context.options.xtrace = false;
|
|
546
|
-
break;
|
|
547
|
-
case 'v':
|
|
548
|
-
this.context.options.verbose = false;
|
|
549
|
-
break;
|
|
550
|
-
case 'f':
|
|
551
|
-
this.context.options.noglob = false;
|
|
552
|
-
break;
|
|
553
|
-
case 'm':
|
|
554
|
-
this.context.options.monitor = false;
|
|
555
|
-
break;
|
|
556
|
-
case 'C':
|
|
557
|
-
this.context.options.noclobber = false;
|
|
558
|
-
break;
|
|
559
|
-
case 'a':
|
|
560
|
-
this.context.options.allexport = false;
|
|
561
|
-
break;
|
|
562
|
-
default:
|
|
563
|
-
return {
|
|
564
|
-
stdout: '',
|
|
565
|
-
stderr: `set: illegal option -- ${option}`,
|
|
566
|
-
exitCode: 1,
|
|
567
|
-
success: false,
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
// Set positional parameters
|
|
574
|
-
const index = args.indexOf(arg);
|
|
575
|
-
this.context.positionalParams = args.slice(index);
|
|
576
|
-
break;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
return {
|
|
580
|
-
stdout: '',
|
|
581
|
-
stderr: '',
|
|
582
|
-
exitCode: 0,
|
|
583
|
-
success: true,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
async builtin_test(args) {
|
|
587
|
-
// POSIX test command implementation
|
|
588
|
-
try {
|
|
589
|
-
const result = this.evaluateTestExpression(args);
|
|
590
|
-
return {
|
|
591
|
-
stdout: '',
|
|
592
|
-
stderr: '',
|
|
593
|
-
exitCode: result ? 0 : 1,
|
|
594
|
-
success: result,
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
catch (error) {
|
|
598
|
-
return {
|
|
599
|
-
stdout: '',
|
|
600
|
-
stderr: `test: ${error.message}`,
|
|
601
|
-
exitCode: 2,
|
|
602
|
-
success: false,
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
evaluateTestExpression(args) {
|
|
607
|
-
if (args.length === 0) {
|
|
608
|
-
return false; // No arguments means false
|
|
609
|
-
}
|
|
610
|
-
// Handle [ command - remove trailing ]
|
|
611
|
-
if (args[args.length - 1] === ']') {
|
|
612
|
-
args = args.slice(0, -1);
|
|
613
|
-
}
|
|
614
|
-
if (args.length === 1) {
|
|
615
|
-
// Single argument: test if string is non-empty
|
|
616
|
-
return args[0] !== '';
|
|
617
|
-
}
|
|
618
|
-
if (args.length === 2) {
|
|
619
|
-
const [operator, operand] = args;
|
|
620
|
-
return this.evaluateUnaryTest(operator, operand);
|
|
621
|
-
}
|
|
622
|
-
if (args.length === 3) {
|
|
623
|
-
const [left, operator, right] = args;
|
|
624
|
-
return this.evaluateBinaryTest(left, operator, right);
|
|
625
|
-
}
|
|
626
|
-
if (args.length >= 4) {
|
|
627
|
-
// Complex expressions with logical operators
|
|
628
|
-
return this.evaluateComplexTest(args);
|
|
629
|
-
}
|
|
630
|
-
return false;
|
|
631
|
-
}
|
|
632
|
-
evaluateUnaryTest(operator, operand) {
|
|
633
|
-
switch (operator) {
|
|
634
|
-
case '-z': return operand === ''; // String is empty
|
|
635
|
-
case '-n': return operand !== ''; // String is non-empty
|
|
636
|
-
case '-f': return this.isRegularFile(operand);
|
|
637
|
-
case '-d': return this.isDirectory(operand);
|
|
638
|
-
case '-e': return this.fileExists(operand);
|
|
639
|
-
case '-r': return this.isReadable(operand);
|
|
640
|
-
case '-w': return this.isWritable(operand);
|
|
641
|
-
case '-x': return this.isExecutable(operand);
|
|
642
|
-
case '-s': return this.hasSize(operand);
|
|
643
|
-
case '!': return !this.evaluateTestExpression([operand]);
|
|
644
|
-
default:
|
|
645
|
-
throw new Error(`unknown unary operator: ${operator}`);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
evaluateBinaryTest(left, operator, right) {
|
|
649
|
-
switch (operator) {
|
|
650
|
-
// String comparisons
|
|
651
|
-
case '=':
|
|
652
|
-
case '==':
|
|
653
|
-
return left === right;
|
|
654
|
-
case '!=':
|
|
655
|
-
return left !== right;
|
|
656
|
-
// Numeric comparisons
|
|
657
|
-
case '-eq': return parseInt(left, 10) === parseInt(right, 10);
|
|
658
|
-
case '-ne': return parseInt(left, 10) !== parseInt(right, 10);
|
|
659
|
-
case '-lt': return parseInt(left, 10) < parseInt(right, 10);
|
|
660
|
-
case '-le': return parseInt(left, 10) <= parseInt(right, 10);
|
|
661
|
-
case '-gt': return parseInt(left, 10) > parseInt(right, 10);
|
|
662
|
-
case '-ge': return parseInt(left, 10) >= parseInt(right, 10);
|
|
663
|
-
default:
|
|
664
|
-
throw new Error(`unknown binary operator: ${operator}`);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
evaluateComplexTest(args) {
|
|
668
|
-
// Handle logical operators -a (AND) and -o (OR)
|
|
669
|
-
for (let i = 1; i < args.length - 1; i++) {
|
|
670
|
-
if (args[i] === '-a') {
|
|
671
|
-
const leftArgs = args.slice(0, i);
|
|
672
|
-
const rightArgs = args.slice(i + 1);
|
|
673
|
-
return this.evaluateTestExpression(leftArgs) && this.evaluateTestExpression(rightArgs);
|
|
674
|
-
}
|
|
675
|
-
if (args[i] === '-o') {
|
|
676
|
-
const leftArgs = args.slice(0, i);
|
|
677
|
-
const rightArgs = args.slice(i + 1);
|
|
678
|
-
return this.evaluateTestExpression(leftArgs) || this.evaluateTestExpression(rightArgs);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
// If no logical operators, try to evaluate as a complex expression
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
|
-
// File test helper methods
|
|
685
|
-
fileExists(path) {
|
|
686
|
-
try {
|
|
687
|
-
fs.accessSync(path);
|
|
688
|
-
return true;
|
|
689
|
-
}
|
|
690
|
-
catch {
|
|
691
|
-
return false;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
isRegularFile(path) {
|
|
695
|
-
try {
|
|
696
|
-
return fs.statSync(path).isFile();
|
|
697
|
-
}
|
|
698
|
-
catch {
|
|
699
|
-
return false;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
isDirectory(path) {
|
|
703
|
-
try {
|
|
704
|
-
return fs.statSync(path).isDirectory();
|
|
705
|
-
}
|
|
706
|
-
catch {
|
|
707
|
-
return false;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
isReadable(path) {
|
|
711
|
-
try {
|
|
712
|
-
fs.accessSync(path, fs.constants.R_OK);
|
|
713
|
-
return true;
|
|
714
|
-
}
|
|
715
|
-
catch {
|
|
716
|
-
return false;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
isWritable(path) {
|
|
720
|
-
try {
|
|
721
|
-
fs.accessSync(path, fs.constants.W_OK);
|
|
722
|
-
return true;
|
|
723
|
-
}
|
|
724
|
-
catch {
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
isExecutable(path) {
|
|
729
|
-
try {
|
|
730
|
-
fs.accessSync(path, fs.constants.X_OK);
|
|
731
|
-
return true;
|
|
732
|
-
}
|
|
733
|
-
catch {
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
hasSize(path) {
|
|
738
|
-
try {
|
|
739
|
-
return fs.statSync(path).size > 0;
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
async builtin_printf(args) {
|
|
746
|
-
if (args.length === 0) {
|
|
747
|
-
return {
|
|
748
|
-
stdout: '',
|
|
749
|
-
stderr: 'printf: missing format string',
|
|
750
|
-
exitCode: 1,
|
|
751
|
-
success: false,
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
const format = args[0];
|
|
755
|
-
const values = args.slice(1);
|
|
756
|
-
try {
|
|
757
|
-
// Basic printf implementation - handle most common format specifiers
|
|
758
|
-
let result = format;
|
|
759
|
-
let valueIndex = 0;
|
|
760
|
-
// Simple substitution for %s, %d, %c, etc.
|
|
761
|
-
result = result.replace(/%[sdcxo%]/g, (match) => {
|
|
762
|
-
if (match === '%%')
|
|
763
|
-
return '%';
|
|
764
|
-
if (valueIndex >= values.length)
|
|
765
|
-
return match; // Keep placeholder if no value
|
|
766
|
-
const value = values[valueIndex++];
|
|
767
|
-
switch (match) {
|
|
768
|
-
case '%s': return value;
|
|
769
|
-
case '%d': return parseInt(value, 10).toString() || '0';
|
|
770
|
-
case '%c': return value.charAt(0);
|
|
771
|
-
case '%x': return parseInt(value, 10).toString(16) || '0';
|
|
772
|
-
case '%o': return parseInt(value, 10).toString(8) || '0';
|
|
773
|
-
default: return value;
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
// Handle escape sequences
|
|
777
|
-
result = result.replace(/\\n/g, '\n')
|
|
778
|
-
.replace(/\\t/g, '\t')
|
|
779
|
-
.replace(/\\r/g, '\r')
|
|
780
|
-
.replace(/\\b/g, '\b')
|
|
781
|
-
.replace(/\\f/g, '\f')
|
|
782
|
-
.replace(/\\v/g, '\v')
|
|
783
|
-
.replace(/\\\\/g, '\\');
|
|
784
|
-
return {
|
|
785
|
-
stdout: result,
|
|
786
|
-
stderr: '',
|
|
787
|
-
exitCode: 0,
|
|
788
|
-
success: true,
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
catch (error) {
|
|
792
|
-
return {
|
|
793
|
-
stdout: '',
|
|
794
|
-
stderr: `printf: ${error.message}`,
|
|
795
|
-
exitCode: 1,
|
|
796
|
-
success: false,
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
async builtin_eval(args) {
|
|
801
|
-
if (args.length === 0) {
|
|
802
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
803
|
-
}
|
|
804
|
-
// Join all arguments into a single command string
|
|
805
|
-
const command = args.join(' ');
|
|
806
|
-
try {
|
|
807
|
-
// Parse and execute the command
|
|
808
|
-
const { parseShellCommand } = await import('./shell-parser.js');
|
|
809
|
-
const ast = parseShellCommand(command);
|
|
810
|
-
return await this.execute(ast);
|
|
811
|
-
}
|
|
812
|
-
catch (error) {
|
|
813
|
-
return {
|
|
814
|
-
stdout: '',
|
|
815
|
-
stderr: `eval: ${error.message}`,
|
|
816
|
-
exitCode: 1,
|
|
817
|
-
success: false,
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
async builtin_exec(args) {
|
|
822
|
-
if (args.length === 0) {
|
|
823
|
-
return {
|
|
824
|
-
stdout: '',
|
|
825
|
-
stderr: 'exec: usage: exec [-cl] [-a name] [command [arguments]]',
|
|
826
|
-
exitCode: 2,
|
|
827
|
-
success: false,
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
// In a real shell, exec would replace the current process
|
|
831
|
-
// For our implementation, we'll just execute the command
|
|
832
|
-
try {
|
|
833
|
-
const cmd = {
|
|
834
|
-
type: 'SimpleCommand',
|
|
835
|
-
name: args[0],
|
|
836
|
-
args: args.slice(1),
|
|
837
|
-
redirections: [],
|
|
838
|
-
};
|
|
839
|
-
return await this.executeSimpleCommand(cmd);
|
|
840
|
-
}
|
|
841
|
-
catch (error) {
|
|
842
|
-
return {
|
|
843
|
-
stdout: '',
|
|
844
|
-
stderr: `exec: ${error.message}`,
|
|
845
|
-
exitCode: 126,
|
|
846
|
-
success: false,
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
async builtin_return(args) {
|
|
851
|
-
// Parse exit code argument (defaults to 0)
|
|
852
|
-
let exitCode = 0;
|
|
853
|
-
if (args.length > 0) {
|
|
854
|
-
const code = parseInt(args[0], 10);
|
|
855
|
-
if (isNaN(code)) {
|
|
856
|
-
return {
|
|
857
|
-
stdout: '',
|
|
858
|
-
stderr: `return: ${args[0]}: numeric argument required`,
|
|
859
|
-
exitCode: 2,
|
|
860
|
-
success: false,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
exitCode = code;
|
|
864
|
-
}
|
|
865
|
-
// In a real shell, return would exit from a function or sourced script
|
|
866
|
-
// For our implementation, we'll store the return value and indicate completion
|
|
867
|
-
this.context.lastReturnCode = exitCode;
|
|
868
|
-
return {
|
|
869
|
-
stdout: '',
|
|
870
|
-
stderr: '',
|
|
871
|
-
exitCode: exitCode,
|
|
872
|
-
success: exitCode === 0,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
async builtin_shift(args) {
|
|
876
|
-
// Parse shift count (defaults to 1)
|
|
877
|
-
let count = 1;
|
|
878
|
-
if (args.length > 0) {
|
|
879
|
-
count = parseInt(args[0], 10);
|
|
880
|
-
if (isNaN(count) || count < 0) {
|
|
881
|
-
return {
|
|
882
|
-
stdout: '',
|
|
883
|
-
stderr: `shift: ${args[0]}: numeric argument required`,
|
|
884
|
-
exitCode: 1,
|
|
885
|
-
success: false,
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
// Check if we have enough positional parameters to shift
|
|
890
|
-
if (!this.context.positionalParams) {
|
|
891
|
-
this.context.positionalParams = [];
|
|
892
|
-
}
|
|
893
|
-
const available = this.context.positionalParams.length;
|
|
894
|
-
if (count > available) {
|
|
895
|
-
return {
|
|
896
|
-
stdout: '',
|
|
897
|
-
stderr: `shift: shift count (${count}) exceeds number of positional parameters (${available})`,
|
|
898
|
-
exitCode: 1,
|
|
899
|
-
success: false,
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
// Perform the shift
|
|
903
|
-
this.context.positionalParams = this.context.positionalParams.slice(count);
|
|
904
|
-
return {
|
|
905
|
-
stdout: '',
|
|
906
|
-
stderr: '',
|
|
907
|
-
exitCode: 0,
|
|
908
|
-
success: true,
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
async builtin_local(args) {
|
|
912
|
-
// The local command is only meaningful within a function
|
|
913
|
-
// For now, we'll treat it the same as variable assignment
|
|
914
|
-
// In a full implementation, this would set function-local variables
|
|
915
|
-
if (args.length === 0) {
|
|
916
|
-
// List all local variables (in a full implementation)
|
|
917
|
-
return {
|
|
918
|
-
stdout: '',
|
|
919
|
-
stderr: '',
|
|
920
|
-
exitCode: 0,
|
|
921
|
-
success: true,
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
// Process variable assignments
|
|
925
|
-
for (const arg of args) {
|
|
926
|
-
const equalIndex = arg.indexOf('=');
|
|
927
|
-
if (equalIndex > 0) {
|
|
928
|
-
const name = arg.substring(0, equalIndex);
|
|
929
|
-
const value = arg.substring(equalIndex + 1);
|
|
930
|
-
this.context.variables[name] = value;
|
|
931
|
-
}
|
|
932
|
-
else {
|
|
933
|
-
// Just declare the variable as local (set to empty in our implementation)
|
|
934
|
-
this.context.variables[arg] = '';
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
// Update expander with new variables
|
|
938
|
-
this.updateExpander();
|
|
939
|
-
return {
|
|
940
|
-
stdout: '',
|
|
941
|
-
stderr: '',
|
|
942
|
-
exitCode: 0,
|
|
943
|
-
success: true,
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
// Job Control Built-ins
|
|
947
|
-
async builtin_jobs(_args) {
|
|
948
|
-
const jobs = Array.from(this.context.jobControl.jobs.values());
|
|
949
|
-
if (jobs.length === 0) {
|
|
950
|
-
return {
|
|
951
|
-
stdout: '',
|
|
952
|
-
stderr: '',
|
|
953
|
-
exitCode: 0,
|
|
954
|
-
success: true,
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
let output = '';
|
|
958
|
-
for (const job of jobs) {
|
|
959
|
-
const status = job.status === 'running' ? 'Running' : 'Done';
|
|
960
|
-
output += `[${job.id}]${job.status === 'running' ? '+' : '-'} ${status} ${job.command}\n`;
|
|
961
|
-
}
|
|
962
|
-
return {
|
|
963
|
-
stdout: output,
|
|
964
|
-
stderr: '',
|
|
965
|
-
exitCode: 0,
|
|
966
|
-
success: true,
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
async builtin_fg(_args) {
|
|
970
|
-
// In a real implementation, this would bring a background job to foreground
|
|
971
|
-
// For now, just return a message
|
|
972
|
-
return {
|
|
973
|
-
stdout: '',
|
|
974
|
-
stderr: 'fg: job control not fully implemented',
|
|
975
|
-
exitCode: 1,
|
|
976
|
-
success: false,
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
async builtin_bg(_args) {
|
|
980
|
-
// In a real implementation, this would resume a stopped job in background
|
|
981
|
-
// For now, just return a message
|
|
982
|
-
return {
|
|
983
|
-
stdout: '',
|
|
984
|
-
stderr: 'bg: job control not fully implemented',
|
|
985
|
-
exitCode: 1,
|
|
986
|
-
success: false,
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
async builtin_wait(args) {
|
|
990
|
-
// Wait for background jobs to complete
|
|
991
|
-
if (args.length === 0) {
|
|
992
|
-
// Wait for all background jobs
|
|
993
|
-
const runningJobs = Array.from(this.context.jobControl.jobs.values())
|
|
994
|
-
.filter(job => job.status === 'running');
|
|
995
|
-
if (runningJobs.length === 0) {
|
|
996
|
-
return {
|
|
997
|
-
stdout: '',
|
|
998
|
-
stderr: '',
|
|
999
|
-
exitCode: 0,
|
|
1000
|
-
success: true,
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
// In a real implementation, this would actually wait for process completion
|
|
1004
|
-
// For now, just simulate waiting briefly
|
|
1005
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1006
|
-
return {
|
|
1007
|
-
stdout: '',
|
|
1008
|
-
stderr: '',
|
|
1009
|
-
exitCode: 0,
|
|
1010
|
-
success: true,
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
// Wait for specific job (simplified implementation)
|
|
1014
|
-
const pid = parseInt(args[0], 10);
|
|
1015
|
-
const job = Array.from(this.context.jobControl.jobs.values())
|
|
1016
|
-
.find(j => j.pid === pid);
|
|
1017
|
-
if (!job) {
|
|
1018
|
-
return {
|
|
1019
|
-
stdout: '',
|
|
1020
|
-
stderr: `wait: ${pid}: no such job`,
|
|
1021
|
-
exitCode: 1,
|
|
1022
|
-
success: false,
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
return {
|
|
1026
|
-
stdout: '',
|
|
1027
|
-
stderr: '',
|
|
1028
|
-
exitCode: 0,
|
|
1029
|
-
success: true,
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
// Essential Built-ins for POSIX Compliance
|
|
1033
|
-
async builtin_read(args) {
|
|
1034
|
-
// Basic read implementation - reads from stdin
|
|
1035
|
-
// In a full implementation, this would handle options like -p, -t, -n, etc.
|
|
1036
|
-
if (args.length === 0) {
|
|
1037
|
-
return {
|
|
1038
|
-
stdout: '',
|
|
1039
|
-
stderr: 'read: missing variable name',
|
|
1040
|
-
exitCode: 1,
|
|
1041
|
-
success: false,
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
const varName = args[0];
|
|
1045
|
-
try {
|
|
1046
|
-
// For now, simulate reading input (in a real implementation, this would read from stdin)
|
|
1047
|
-
// Since we can't easily read from stdin in this context, we'll set a default value
|
|
1048
|
-
const inputValue = 'simulated_input';
|
|
1049
|
-
// Set the variable
|
|
1050
|
-
this.context.variables[varName] = inputValue;
|
|
1051
|
-
this.updateExpander();
|
|
1052
|
-
return {
|
|
1053
|
-
stdout: '',
|
|
1054
|
-
stderr: '',
|
|
1055
|
-
exitCode: 0,
|
|
1056
|
-
success: true,
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
catch (error) {
|
|
1060
|
-
return {
|
|
1061
|
-
stdout: '',
|
|
1062
|
-
stderr: `read: ${error.message}`,
|
|
1063
|
-
exitCode: 1,
|
|
1064
|
-
success: false,
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
async builtin_getopts(args) {
|
|
1069
|
-
// Basic getopts implementation for option parsing
|
|
1070
|
-
// Usage: getopts optstring name [args...]
|
|
1071
|
-
if (args.length < 2) {
|
|
1072
|
-
return {
|
|
1073
|
-
stdout: '',
|
|
1074
|
-
stderr: 'getopts: usage: getopts optstring name [args...]',
|
|
1075
|
-
exitCode: 1,
|
|
1076
|
-
success: false,
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
const _optstring = args[0]; // TODO: Use this to validate options
|
|
1080
|
-
const varName = args[1];
|
|
1081
|
-
const optargs = args.length > 2 ? args.slice(2) : this.context.positionalParams;
|
|
1082
|
-
// Initialize OPTIND if not set
|
|
1083
|
-
if (!this.context.variables.OPTIND) {
|
|
1084
|
-
this.context.variables.OPTIND = '1';
|
|
1085
|
-
}
|
|
1086
|
-
const optind = parseInt(this.context.variables.OPTIND, 10);
|
|
1087
|
-
// If we've processed all arguments
|
|
1088
|
-
if (optind > optargs.length) {
|
|
1089
|
-
this.context.variables[varName] = '?';
|
|
1090
|
-
return {
|
|
1091
|
-
stdout: '',
|
|
1092
|
-
stderr: '',
|
|
1093
|
-
exitCode: 1,
|
|
1094
|
-
success: false,
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
const currentArg = optargs[optind - 1];
|
|
1098
|
-
// Check if current argument is an option
|
|
1099
|
-
if (!currentArg || !currentArg.startsWith('-') || currentArg.length < 2) {
|
|
1100
|
-
this.context.variables[varName] = '?';
|
|
1101
|
-
return {
|
|
1102
|
-
stdout: '',
|
|
1103
|
-
stderr: '',
|
|
1104
|
-
exitCode: 1,
|
|
1105
|
-
success: false,
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
1108
|
-
// Simple implementation - just return the first character after -
|
|
1109
|
-
const option = currentArg.charAt(1);
|
|
1110
|
-
this.context.variables[varName] = option;
|
|
1111
|
-
this.context.variables.OPTIND = String(optind + 1);
|
|
1112
|
-
this.updateExpander();
|
|
1113
|
-
return {
|
|
1114
|
-
stdout: '',
|
|
1115
|
-
stderr: '',
|
|
1116
|
-
exitCode: 0,
|
|
1117
|
-
success: true,
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
async builtin_trap(args) {
|
|
1121
|
-
// Basic trap implementation for signal handling
|
|
1122
|
-
// Usage: trap [-lp] [command] [signal...]
|
|
1123
|
-
if (args.length === 0) {
|
|
1124
|
-
// List current traps (simplified)
|
|
1125
|
-
let output = '';
|
|
1126
|
-
if (this.context.variables.TRAP_INT) {
|
|
1127
|
-
output += `trap -- '${this.context.variables.TRAP_INT}' INT\n`;
|
|
1128
|
-
}
|
|
1129
|
-
if (this.context.variables.TRAP_TERM) {
|
|
1130
|
-
output += `trap -- '${this.context.variables.TRAP_TERM}' TERM\n`;
|
|
1131
|
-
}
|
|
1132
|
-
return {
|
|
1133
|
-
stdout: output,
|
|
1134
|
-
stderr: '',
|
|
1135
|
-
exitCode: 0,
|
|
1136
|
-
success: true,
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
if (args[0] === '-l') {
|
|
1140
|
-
// List signal names
|
|
1141
|
-
return {
|
|
1142
|
-
stdout: ' 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP\n' +
|
|
1143
|
-
' 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1\n' +
|
|
1144
|
-
'11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM\n',
|
|
1145
|
-
stderr: '',
|
|
1146
|
-
exitCode: 0,
|
|
1147
|
-
success: true,
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
if (args.length >= 2) {
|
|
1151
|
-
const command = args[0];
|
|
1152
|
-
const signals = args.slice(1);
|
|
1153
|
-
// Set trap for each signal (simplified implementation)
|
|
1154
|
-
for (const signal of signals) {
|
|
1155
|
-
const signalUpper = signal.toUpperCase();
|
|
1156
|
-
if (signalUpper === 'INT' || signalUpper === 'SIGINT') {
|
|
1157
|
-
this.context.variables.TRAP_INT = command;
|
|
1158
|
-
}
|
|
1159
|
-
else if (signalUpper === 'TERM' || signalUpper === 'SIGTERM') {
|
|
1160
|
-
this.context.variables.TRAP_TERM = command;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
this.updateExpander();
|
|
1164
|
-
return {
|
|
1165
|
-
stdout: '',
|
|
1166
|
-
stderr: '',
|
|
1167
|
-
exitCode: 0,
|
|
1168
|
-
success: true,
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
return {
|
|
1172
|
-
stdout: '',
|
|
1173
|
-
stderr: 'trap: usage: trap [-lp] [command] [signal...]',
|
|
1174
|
-
exitCode: 1,
|
|
1175
|
-
success: false,
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
async executeExternalCommand(cmd) {
|
|
1179
|
-
return new Promise((resolve) => {
|
|
1180
|
-
const child = spawn(cmd.name, cmd.args, {
|
|
1181
|
-
cwd: this.context.cwd,
|
|
1182
|
-
env: this.context.env,
|
|
1183
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1184
|
-
});
|
|
1185
|
-
let stdout = '';
|
|
1186
|
-
let stderr = '';
|
|
1187
|
-
child.stdout?.on('data', (data) => {
|
|
1188
|
-
stdout += data.toString();
|
|
1189
|
-
});
|
|
1190
|
-
child.stderr?.on('data', (data) => {
|
|
1191
|
-
stderr += data.toString();
|
|
1192
|
-
});
|
|
1193
|
-
child.on('close', (code) => {
|
|
1194
|
-
const exitCode = code || 0;
|
|
1195
|
-
this.context.lastExitCode = exitCode;
|
|
1196
|
-
resolve({
|
|
1197
|
-
stdout: stdout.trim(),
|
|
1198
|
-
stderr: stderr.trim(),
|
|
1199
|
-
exitCode,
|
|
1200
|
-
success: exitCode === 0,
|
|
1201
|
-
});
|
|
1202
|
-
});
|
|
1203
|
-
child.on('error', (_error) => {
|
|
1204
|
-
resolve({
|
|
1205
|
-
stdout: '',
|
|
1206
|
-
stderr: `${cmd.name}: command not found`,
|
|
1207
|
-
exitCode: 127,
|
|
1208
|
-
success: false,
|
|
1209
|
-
});
|
|
1210
|
-
});
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
async executePipeline(pipeline) {
|
|
1214
|
-
if (pipeline.commands.length === 1) {
|
|
1215
|
-
return this.executeNode(pipeline.commands[0]);
|
|
1216
|
-
}
|
|
1217
|
-
// For now, use simple shell delegation for pipelines
|
|
1218
|
-
// TODO: Implement proper pipeline with stdout/stdin chaining
|
|
1219
|
-
const commandStrings = pipeline.commands.map(cmd => this.nodeToString(cmd));
|
|
1220
|
-
const pipelineCommand = commandStrings.join(' | ');
|
|
1221
|
-
try {
|
|
1222
|
-
const { stdout, stderr } = await execAsync(pipelineCommand, {
|
|
1223
|
-
cwd: this.context.cwd,
|
|
1224
|
-
env: this.context.env,
|
|
1225
|
-
});
|
|
1226
|
-
return {
|
|
1227
|
-
stdout: stdout.trim(),
|
|
1228
|
-
stderr: stderr.trim(),
|
|
1229
|
-
exitCode: 0,
|
|
1230
|
-
success: true,
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
catch (error) {
|
|
1234
|
-
return {
|
|
1235
|
-
stdout: '',
|
|
1236
|
-
stderr: error.message,
|
|
1237
|
-
exitCode: error.code || 1,
|
|
1238
|
-
success: false,
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
// Helper method to convert AST node back to string (for fallback cases)
|
|
1243
|
-
nodeToString(node) {
|
|
1244
|
-
switch (node.type) {
|
|
1245
|
-
case 'SimpleCommand': {
|
|
1246
|
-
const cmd = node;
|
|
1247
|
-
return [cmd.name, ...cmd.args].join(' ');
|
|
1248
|
-
}
|
|
1249
|
-
case 'Pipeline': {
|
|
1250
|
-
const pipeline = node;
|
|
1251
|
-
return pipeline.commands.map(c => this.nodeToString(c)).join(' | ');
|
|
1252
|
-
}
|
|
1253
|
-
case 'CommandList': {
|
|
1254
|
-
const cmdList = node;
|
|
1255
|
-
const left = this.nodeToString(cmdList.left);
|
|
1256
|
-
const right = cmdList.right ? this.nodeToString(cmdList.right) : '';
|
|
1257
|
-
return `${left} ${cmdList.operator} ${right}`.trim();
|
|
1258
|
-
}
|
|
1259
|
-
case 'IfStatement':
|
|
1260
|
-
case 'ForStatement':
|
|
1261
|
-
case 'WhileStatement':
|
|
1262
|
-
case 'CaseStatement':
|
|
1263
|
-
return `[${node.type}]`;
|
|
1264
|
-
default:
|
|
1265
|
-
return '';
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
// Control Structure Execution Methods
|
|
1269
|
-
async executeIfStatement(ifStmt) {
|
|
1270
|
-
// Execute condition
|
|
1271
|
-
const conditionResult = await this.executeNode(ifStmt.condition);
|
|
1272
|
-
if (conditionResult.success) {
|
|
1273
|
-
// Condition succeeded, execute then clause
|
|
1274
|
-
return this.executeNode(ifStmt.thenClause);
|
|
1275
|
-
}
|
|
1276
|
-
else if (ifStmt.elseClause) {
|
|
1277
|
-
// Condition failed, execute else clause if present
|
|
1278
|
-
return this.executeNode(ifStmt.elseClause);
|
|
1279
|
-
}
|
|
1280
|
-
else {
|
|
1281
|
-
// No else clause, return success with empty output
|
|
1282
|
-
return {
|
|
1283
|
-
stdout: '',
|
|
1284
|
-
stderr: '',
|
|
1285
|
-
exitCode: 0,
|
|
1286
|
-
success: true,
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
async executeForStatement(forStmt) {
|
|
1291
|
-
let lastResult = {
|
|
1292
|
-
stdout: '',
|
|
1293
|
-
stderr: '',
|
|
1294
|
-
exitCode: 0,
|
|
1295
|
-
success: true,
|
|
1296
|
-
};
|
|
1297
|
-
this.updateExpander();
|
|
1298
|
-
// Determine the word list to iterate over
|
|
1299
|
-
let words = forStmt.words;
|
|
1300
|
-
if (words.length === 0) {
|
|
1301
|
-
// If no words specified, use positional parameters
|
|
1302
|
-
words = this.context.positionalParams;
|
|
1303
|
-
}
|
|
1304
|
-
// Expand each word before iteration
|
|
1305
|
-
const expandedWords = [];
|
|
1306
|
-
for (const word of words) {
|
|
1307
|
-
const expanded = await this.expander.expandString(word);
|
|
1308
|
-
// Apply field splitting to expanded words
|
|
1309
|
-
const fields = this.expander.splitFields(expanded, this.context.ifs);
|
|
1310
|
-
expandedWords.push(...fields);
|
|
1311
|
-
}
|
|
1312
|
-
// Iterate over each word
|
|
1313
|
-
for (const word of expandedWords) {
|
|
1314
|
-
// Set the loop variable
|
|
1315
|
-
this.context.variables[forStmt.variable] = word;
|
|
1316
|
-
this.updateExpander();
|
|
1317
|
-
// Execute the body
|
|
1318
|
-
lastResult = await this.executeNode(forStmt.body);
|
|
1319
|
-
// Update context with result
|
|
1320
|
-
this.context.lastExitCode = lastResult.exitCode;
|
|
1321
|
-
// TODO: Handle break/continue statements
|
|
1322
|
-
// For now, continue execution unless there's an error
|
|
1323
|
-
if (!lastResult.success) {
|
|
1324
|
-
break;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
return lastResult;
|
|
1328
|
-
}
|
|
1329
|
-
async executeWhileStatement(whileStmt) {
|
|
1330
|
-
let lastResult = {
|
|
1331
|
-
stdout: '',
|
|
1332
|
-
stderr: '',
|
|
1333
|
-
exitCode: 0,
|
|
1334
|
-
success: true,
|
|
1335
|
-
};
|
|
1336
|
-
// Continue looping while condition succeeds
|
|
1337
|
-
while (true) {
|
|
1338
|
-
// Execute condition
|
|
1339
|
-
const conditionResult = await this.executeNode(whileStmt.condition);
|
|
1340
|
-
if (!conditionResult.success) {
|
|
1341
|
-
// Condition failed, exit loop
|
|
1342
|
-
break;
|
|
1343
|
-
}
|
|
1344
|
-
// Execute body
|
|
1345
|
-
lastResult = await this.executeNode(whileStmt.body);
|
|
1346
|
-
// Update context with result
|
|
1347
|
-
this.context.lastExitCode = lastResult.exitCode;
|
|
1348
|
-
// TODO: Handle break/continue statements
|
|
1349
|
-
// For now, continue execution
|
|
1350
|
-
}
|
|
1351
|
-
return lastResult;
|
|
1352
|
-
}
|
|
1353
|
-
async executeCaseStatement(caseStmt) {
|
|
1354
|
-
this.updateExpander();
|
|
1355
|
-
// Expand the case word
|
|
1356
|
-
const expandedWord = await this.expander.expandString(caseStmt.word);
|
|
1357
|
-
// Try to match against each case item
|
|
1358
|
-
for (const item of caseStmt.items) {
|
|
1359
|
-
for (const pattern of item.patterns) {
|
|
1360
|
-
// Expand the pattern
|
|
1361
|
-
const expandedPattern = await this.expander.expandString(pattern);
|
|
1362
|
-
// Simple pattern matching (would need full glob pattern matching)
|
|
1363
|
-
if (this.matchesPattern(expandedWord, expandedPattern)) {
|
|
1364
|
-
if (item.command) {
|
|
1365
|
-
return this.executeNode(item.command);
|
|
1366
|
-
}
|
|
1367
|
-
else {
|
|
1368
|
-
return {
|
|
1369
|
-
stdout: '',
|
|
1370
|
-
stderr: '',
|
|
1371
|
-
exitCode: 0,
|
|
1372
|
-
success: true,
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
// No pattern matched, return success
|
|
1379
|
-
return {
|
|
1380
|
-
stdout: '',
|
|
1381
|
-
stderr: '',
|
|
1382
|
-
exitCode: 0,
|
|
1383
|
-
success: true,
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
matchesPattern(text, pattern) {
|
|
1387
|
-
// Simple pattern matching - exact match or '*' wildcard
|
|
1388
|
-
if (pattern === '*') {
|
|
1389
|
-
return true;
|
|
1390
|
-
}
|
|
1391
|
-
// Convert simple glob patterns to regex
|
|
1392
|
-
const regexPattern = pattern
|
|
1393
|
-
.replace(/\*/g, '.*') // * matches any string
|
|
1394
|
-
.replace(/\?/g, '.') // ? matches any single character
|
|
1395
|
-
.replace(/\[([^\]]+)\]/g, '[$1]'); // [abc] character class
|
|
1396
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
1397
|
-
return regex.test(text);
|
|
1398
|
-
}
|
|
1399
|
-
// Subshell and Command Grouping Implementation
|
|
1400
|
-
async executeSubshell(subshell) {
|
|
1401
|
-
// Create a new executor with isolated environment
|
|
1402
|
-
const subshellContext = {
|
|
1403
|
-
// Copy current context but isolate variables and working directory
|
|
1404
|
-
env: { ...this.context.env },
|
|
1405
|
-
cwd: this.context.cwd, // Subshells inherit CWD but changes don't affect parent
|
|
1406
|
-
variables: { ...this.context.variables }, // Copy variables but changes are isolated
|
|
1407
|
-
lastExitCode: this.context.lastExitCode,
|
|
1408
|
-
lastReturnCode: this.context.lastReturnCode,
|
|
1409
|
-
positionalParams: [...this.context.positionalParams],
|
|
1410
|
-
ifs: this.context.ifs,
|
|
1411
|
-
functions: this.context.functions, // Functions are shared (not isolated)
|
|
1412
|
-
};
|
|
1413
|
-
// Create isolated executor
|
|
1414
|
-
const subshellExecutor = new ShellExecutor(subshellContext);
|
|
1415
|
-
try {
|
|
1416
|
-
// Execute command in subshell
|
|
1417
|
-
const result = await subshellExecutor.execute(subshell.command);
|
|
1418
|
-
// Update parent's exit code but not other context
|
|
1419
|
-
this.context.lastExitCode = result.exitCode;
|
|
1420
|
-
return result;
|
|
1421
|
-
}
|
|
1422
|
-
catch (error) {
|
|
1423
|
-
return {
|
|
1424
|
-
stdout: '',
|
|
1425
|
-
stderr: `subshell: ${error.message}`,
|
|
1426
|
-
exitCode: 1,
|
|
1427
|
-
success: false,
|
|
1428
|
-
};
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
async executeCommandGroup(group) {
|
|
1432
|
-
// Command groups run in the current context (no isolation like subshells)
|
|
1433
|
-
try {
|
|
1434
|
-
return await this.executeNode(group.command);
|
|
1435
|
-
}
|
|
1436
|
-
catch (error) {
|
|
1437
|
-
return {
|
|
1438
|
-
stdout: '',
|
|
1439
|
-
stderr: `command group: ${error.message}`,
|
|
1440
|
-
exitCode: 1,
|
|
1441
|
-
success: false,
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
async executeBackgroundCommand(backgroundCommand, foregroundCommand) {
|
|
1446
|
-
// For now, use setImmediate for background execution
|
|
1447
|
-
// TODO: Implement proper process spawning for real background jobs
|
|
1448
|
-
const commandString = this.nodeToString(backgroundCommand);
|
|
1449
|
-
// Create job entry
|
|
1450
|
-
const jobId = this.context.jobControl.nextJobId++;
|
|
1451
|
-
// For now, use a fake PID (in real implementation, this would be the actual process PID)
|
|
1452
|
-
const fakePid = Date.now() % 100000;
|
|
1453
|
-
const job = {
|
|
1454
|
-
id: jobId,
|
|
1455
|
-
pid: fakePid,
|
|
1456
|
-
command: commandString,
|
|
1457
|
-
status: 'running',
|
|
1458
|
-
startTime: Date.now(),
|
|
1459
|
-
};
|
|
1460
|
-
// Add job to tracking
|
|
1461
|
-
this.context.jobControl.jobs.set(jobId, job);
|
|
1462
|
-
this.context.jobControl.lastBackgroundPid = fakePid;
|
|
1463
|
-
// Update expander context to reflect new background PID
|
|
1464
|
-
this.updateExpander();
|
|
1465
|
-
// Execute background command asynchronously
|
|
1466
|
-
setImmediate(async () => {
|
|
1467
|
-
try {
|
|
1468
|
-
await this.executeNode(backgroundCommand);
|
|
1469
|
-
// Mark job as done
|
|
1470
|
-
if (this.context.jobControl.jobs.has(jobId)) {
|
|
1471
|
-
this.context.jobControl.jobs.get(jobId).status = 'done';
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
catch (_error) {
|
|
1475
|
-
// Mark job as done with error
|
|
1476
|
-
if (this.context.jobControl.jobs.has(jobId)) {
|
|
1477
|
-
this.context.jobControl.jobs.get(jobId).status = 'done';
|
|
1478
|
-
}
|
|
1479
|
-
// Background errors are typically not displayed immediately
|
|
1480
|
-
}
|
|
1481
|
-
});
|
|
1482
|
-
// Execute foreground command if present
|
|
1483
|
-
if (foregroundCommand) {
|
|
1484
|
-
return this.executeNode(foregroundCommand);
|
|
1485
|
-
}
|
|
1486
|
-
// Return success for background job started
|
|
1487
|
-
return {
|
|
1488
|
-
stdout: `[${jobId}] ${fakePid}`,
|
|
1489
|
-
stderr: '',
|
|
1490
|
-
exitCode: 0,
|
|
1491
|
-
success: true,
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
async executeCommandList(cmdList) {
|
|
1495
|
-
// Execute left side first
|
|
1496
|
-
const leftResult = await this.executeNode(cmdList.left);
|
|
1497
|
-
// Update context with left result
|
|
1498
|
-
this.context.lastExitCode = leftResult.exitCode;
|
|
1499
|
-
this.updateExpander();
|
|
1500
|
-
// Handle different operators
|
|
1501
|
-
switch (cmdList.operator) {
|
|
1502
|
-
case '&&':
|
|
1503
|
-
// Execute right only if left succeeded
|
|
1504
|
-
if (leftResult.success) {
|
|
1505
|
-
if (cmdList.right) {
|
|
1506
|
-
return this.executeNode(cmdList.right);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
return leftResult;
|
|
1510
|
-
case '||':
|
|
1511
|
-
// Execute right only if left failed
|
|
1512
|
-
if (!leftResult.success) {
|
|
1513
|
-
if (cmdList.right) {
|
|
1514
|
-
return this.executeNode(cmdList.right);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
return leftResult;
|
|
1518
|
-
case ';':
|
|
1519
|
-
// Execute right regardless of left result
|
|
1520
|
-
if (cmdList.right) {
|
|
1521
|
-
const rightResult = await this.executeNode(cmdList.right);
|
|
1522
|
-
// Return the right result but preserve left output
|
|
1523
|
-
return {
|
|
1524
|
-
stdout: leftResult.stdout + rightResult.stdout,
|
|
1525
|
-
stderr: leftResult.stderr + rightResult.stderr,
|
|
1526
|
-
exitCode: rightResult.exitCode,
|
|
1527
|
-
success: rightResult.success,
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
return leftResult;
|
|
1531
|
-
case '&':
|
|
1532
|
-
// Execute left in background, then execute right immediately
|
|
1533
|
-
return this.executeBackgroundCommand(cmdList.left, cmdList.right);
|
|
1534
|
-
default:
|
|
1535
|
-
throw new Error(`Unknown command list operator: ${cmdList.operator}`);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
// Function Implementation
|
|
1539
|
-
async executeFunctionDefinition(funcDef) {
|
|
1540
|
-
// Store the function definition in the context
|
|
1541
|
-
this.context.functions[funcDef.name] = funcDef;
|
|
1542
|
-
return {
|
|
1543
|
-
stdout: '',
|
|
1544
|
-
stderr: '',
|
|
1545
|
-
exitCode: 0,
|
|
1546
|
-
success: true,
|
|
1547
|
-
};
|
|
1548
|
-
}
|
|
1549
|
-
async executeFunctionCall(name, args) {
|
|
1550
|
-
const funcDef = this.context.functions[name];
|
|
1551
|
-
if (!funcDef) {
|
|
1552
|
-
return {
|
|
1553
|
-
stdout: '',
|
|
1554
|
-
stderr: `${name}: function not found`,
|
|
1555
|
-
exitCode: 127,
|
|
1556
|
-
success: false,
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
// Save current context for restoration
|
|
1560
|
-
const savedPositionalParams = this.context.positionalParams;
|
|
1561
|
-
const savedVariables = { ...this.context.variables };
|
|
1562
|
-
try {
|
|
1563
|
-
// Set up function parameters
|
|
1564
|
-
this.context.positionalParams = args;
|
|
1565
|
-
// Update expander with new context
|
|
1566
|
-
this.updateExpander();
|
|
1567
|
-
// Execute function body
|
|
1568
|
-
const result = await this.executeNode(funcDef.body);
|
|
1569
|
-
return result;
|
|
1570
|
-
}
|
|
1571
|
-
finally {
|
|
1572
|
-
// Restore original context
|
|
1573
|
-
this.context.positionalParams = savedPositionalParams;
|
|
1574
|
-
// Restore non-local variables
|
|
1575
|
-
for (const key in savedVariables) {
|
|
1576
|
-
this.context.variables[key] = savedVariables[key];
|
|
1577
|
-
}
|
|
1578
|
-
// Remove any variables that were created during function execution
|
|
1579
|
-
// (this is a simple implementation - real shells have more complex scoping)
|
|
1580
|
-
this.updateExpander();
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
// I/O Redirection Implementation
|
|
1584
|
-
async executeWithRedirections(cmd) {
|
|
1585
|
-
if (!cmd.redirections || cmd.redirections.length === 0) {
|
|
1586
|
-
// No redirections, execute normally
|
|
1587
|
-
// Check for function call first
|
|
1588
|
-
if (this.context.functions[cmd.name]) {
|
|
1589
|
-
const result = await this.executeFunctionCall(cmd.name, cmd.args);
|
|
1590
|
-
this.context.lastExitCode = result.exitCode;
|
|
1591
|
-
this.updateExpander();
|
|
1592
|
-
return result;
|
|
1593
|
-
}
|
|
1594
|
-
const builtinResult = await this.executeBuiltin(cmd.name, cmd.args);
|
|
1595
|
-
if (builtinResult !== null) {
|
|
1596
|
-
this.context.lastExitCode = builtinResult.exitCode;
|
|
1597
|
-
this.updateExpander();
|
|
1598
|
-
return builtinResult;
|
|
1599
|
-
}
|
|
1600
|
-
return this.executeExternalCommand(cmd);
|
|
1601
|
-
}
|
|
1602
|
-
// Process redirections and execute command
|
|
1603
|
-
return this.executeWithRedirectionHandling(cmd);
|
|
1604
|
-
}
|
|
1605
|
-
async executeWithRedirectionHandling(cmd) {
|
|
1606
|
-
const fs = await import('fs');
|
|
1607
|
-
const originalFiles = {};
|
|
1608
|
-
const redirectionFiles = {};
|
|
1609
|
-
try {
|
|
1610
|
-
// Process each redirection
|
|
1611
|
-
for (const redir of cmd.redirections) {
|
|
1612
|
-
await this.processRedirection(redir, originalFiles, redirectionFiles, fs);
|
|
1613
|
-
}
|
|
1614
|
-
// Execute the command with redirections in place
|
|
1615
|
-
let result;
|
|
1616
|
-
// Check for function call first
|
|
1617
|
-
if (this.context.functions[cmd.name]) {
|
|
1618
|
-
// Functions don't directly support redirections in our implementation
|
|
1619
|
-
// We'll execute the function and handle output redirection manually
|
|
1620
|
-
const functionResult = await this.executeFunctionCall(cmd.name, cmd.args);
|
|
1621
|
-
result = await this.executeBuiltinWithRedirection(cmd.name, cmd.args, functionResult, redirectionFiles, fs);
|
|
1622
|
-
this.context.lastExitCode = result.exitCode;
|
|
1623
|
-
}
|
|
1624
|
-
else {
|
|
1625
|
-
const builtinResult = await this.executeBuiltin(cmd.name, cmd.args);
|
|
1626
|
-
if (builtinResult !== null) {
|
|
1627
|
-
// For built-ins, handle output redirection manually
|
|
1628
|
-
result = await this.executeBuiltinWithRedirection(cmd.name, cmd.args, builtinResult, redirectionFiles, fs);
|
|
1629
|
-
this.context.lastExitCode = result.exitCode;
|
|
1630
|
-
}
|
|
1631
|
-
else {
|
|
1632
|
-
// For external commands, redirections are handled by spawn
|
|
1633
|
-
result = await this.executeExternalCommandWithRedirection(cmd, redirectionFiles);
|
|
1634
|
-
this.context.lastExitCode = result.exitCode;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
return result;
|
|
1638
|
-
}
|
|
1639
|
-
catch (error) {
|
|
1640
|
-
return {
|
|
1641
|
-
stdout: '',
|
|
1642
|
-
stderr: `Redirection error: ${error.message}`,
|
|
1643
|
-
exitCode: 1,
|
|
1644
|
-
success: false,
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
finally {
|
|
1648
|
-
// Restore original file descriptors and clean up
|
|
1649
|
-
this.cleanupRedirections(originalFiles, redirectionFiles, fs);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
async processRedirection(redir, originalFiles, redirectionFiles, fs) {
|
|
1653
|
-
const target = await this.expander.expandString(redir.target);
|
|
1654
|
-
switch (redir.type) {
|
|
1655
|
-
case 'output': // >
|
|
1656
|
-
try {
|
|
1657
|
-
const fd = fs.openSync(target, 'w');
|
|
1658
|
-
redirectionFiles[1] = fd; // stdout
|
|
1659
|
-
break;
|
|
1660
|
-
}
|
|
1661
|
-
catch (error) {
|
|
1662
|
-
throw new Error(`cannot create ${target}: ${error.message}`);
|
|
1663
|
-
}
|
|
1664
|
-
case 'append': // >>
|
|
1665
|
-
try {
|
|
1666
|
-
const fd = fs.openSync(target, 'a');
|
|
1667
|
-
redirectionFiles[1] = fd; // stdout
|
|
1668
|
-
break;
|
|
1669
|
-
}
|
|
1670
|
-
catch (error) {
|
|
1671
|
-
throw new Error(`cannot create ${target}: ${error.message}`);
|
|
1672
|
-
}
|
|
1673
|
-
case 'input': // <
|
|
1674
|
-
try {
|
|
1675
|
-
if (!fs.existsSync(target)) {
|
|
1676
|
-
throw new Error(`${target}: No such file or directory`);
|
|
1677
|
-
}
|
|
1678
|
-
const fd = fs.openSync(target, 'r');
|
|
1679
|
-
redirectionFiles[0] = fd; // stdin
|
|
1680
|
-
break;
|
|
1681
|
-
}
|
|
1682
|
-
catch (error) {
|
|
1683
|
-
throw new Error(`cannot open ${target}: ${error.message}`);
|
|
1684
|
-
}
|
|
1685
|
-
case 'heredoc': { // <<
|
|
1686
|
-
// Create a temporary file with the here document content
|
|
1687
|
-
const tmpFile = `/tmp/lsh-heredoc-${Date.now()}`;
|
|
1688
|
-
fs.writeFileSync(tmpFile, target); // target contains the here-doc content
|
|
1689
|
-
const fd = fs.openSync(tmpFile, 'r');
|
|
1690
|
-
redirectionFiles[0] = fd; // stdin
|
|
1691
|
-
// Schedule cleanup of temp file
|
|
1692
|
-
setTimeout(() => {
|
|
1693
|
-
try {
|
|
1694
|
-
fs.unlinkSync(tmpFile);
|
|
1695
|
-
}
|
|
1696
|
-
catch (_) {
|
|
1697
|
-
// Ignore cleanup errors
|
|
1698
|
-
}
|
|
1699
|
-
}, 1000);
|
|
1700
|
-
break;
|
|
1701
|
-
}
|
|
1702
|
-
default:
|
|
1703
|
-
throw new Error(`unsupported redirection type: ${redir.type}`);
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
async executeBuiltinWithRedirection(name, args, result, redirectionFiles, fs) {
|
|
1707
|
-
// For built-ins, we need to manually handle output redirection
|
|
1708
|
-
if (redirectionFiles[1] !== undefined) {
|
|
1709
|
-
// Redirect stdout to file
|
|
1710
|
-
if (result.stdout) {
|
|
1711
|
-
fs.writeSync(redirectionFiles[1], result.stdout);
|
|
1712
|
-
}
|
|
1713
|
-
// Return result with empty stdout since it was redirected
|
|
1714
|
-
return {
|
|
1715
|
-
stdout: '',
|
|
1716
|
-
stderr: result.stderr,
|
|
1717
|
-
exitCode: result.exitCode,
|
|
1718
|
-
success: result.success,
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
return result;
|
|
1722
|
-
}
|
|
1723
|
-
async executeExternalCommandWithRedirection(cmd, redirectionFiles) {
|
|
1724
|
-
return new Promise((resolve) => {
|
|
1725
|
-
const stdio = ['inherit', 'pipe', 'pipe'];
|
|
1726
|
-
// Configure stdio based on redirections
|
|
1727
|
-
if (redirectionFiles[0] !== undefined) {
|
|
1728
|
-
stdio[0] = redirectionFiles[0]; // stdin
|
|
1729
|
-
}
|
|
1730
|
-
if (redirectionFiles[1] !== undefined) {
|
|
1731
|
-
stdio[1] = redirectionFiles[1]; // stdout
|
|
1732
|
-
}
|
|
1733
|
-
if (redirectionFiles[2] !== undefined) {
|
|
1734
|
-
stdio[2] = redirectionFiles[2]; // stderr
|
|
1735
|
-
}
|
|
1736
|
-
const child = spawn(cmd.name, cmd.args, {
|
|
1737
|
-
stdio,
|
|
1738
|
-
cwd: this.context.cwd,
|
|
1739
|
-
env: { ...this.context.env, ...this.context.variables },
|
|
1740
|
-
});
|
|
1741
|
-
let stdout = '';
|
|
1742
|
-
let stderr = '';
|
|
1743
|
-
if (child.stdout && stdio[1] === 'pipe') {
|
|
1744
|
-
child.stdout.on('data', (data) => {
|
|
1745
|
-
stdout += data.toString();
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
if (child.stderr && stdio[2] === 'pipe') {
|
|
1749
|
-
child.stderr.on('data', (data) => {
|
|
1750
|
-
stderr += data.toString();
|
|
1751
|
-
});
|
|
1752
|
-
}
|
|
1753
|
-
child.on('close', (code) => {
|
|
1754
|
-
resolve({
|
|
1755
|
-
stdout: stdio[1] === 'pipe' ? stdout : '', // Empty if redirected
|
|
1756
|
-
stderr: stdio[2] === 'pipe' ? stderr : '',
|
|
1757
|
-
exitCode: code || 0,
|
|
1758
|
-
success: (code || 0) === 0,
|
|
1759
|
-
});
|
|
1760
|
-
});
|
|
1761
|
-
child.on('error', (error) => {
|
|
1762
|
-
resolve({
|
|
1763
|
-
stdout: '',
|
|
1764
|
-
stderr: `Command failed: ${error.message}`,
|
|
1765
|
-
exitCode: 127,
|
|
1766
|
-
success: false,
|
|
1767
|
-
});
|
|
1768
|
-
});
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
cleanupRedirections(originalFiles, redirectionFiles, fs) {
|
|
1772
|
-
// Close redirection files
|
|
1773
|
-
for (const fd of Object.values(redirectionFiles)) {
|
|
1774
|
-
try {
|
|
1775
|
-
if (typeof fd === 'number') {
|
|
1776
|
-
fs.closeSync(fd);
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
catch (_error) {
|
|
1780
|
-
// Ignore cleanup errors
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
async handleProcessSubstitution(procSubArg) {
|
|
1785
|
-
const os = await import('os');
|
|
1786
|
-
const path = await import('path');
|
|
1787
|
-
const fs = await import('fs');
|
|
1788
|
-
const { parseShellCommand } = await import('./shell-parser.js');
|
|
1789
|
-
// Extract direction and command from the process substitution
|
|
1790
|
-
const isInput = procSubArg.startsWith('<(');
|
|
1791
|
-
const command = procSubArg.slice(2, -1); // Remove <( or >( and )
|
|
1792
|
-
// Create a temporary file to simulate named pipe behavior
|
|
1793
|
-
const tmpDir = await fs.promises.mkdtemp(path.default.join(os.default.tmpdir(), 'lsh-procsub-'));
|
|
1794
|
-
const fifoPath = path.default.join(tmpDir, isInput ? 'input' : 'output');
|
|
1795
|
-
try {
|
|
1796
|
-
if (isInput) {
|
|
1797
|
-
// For <(command), execute command and write output to temp file
|
|
1798
|
-
const ast = parseShellCommand(command);
|
|
1799
|
-
const executor = new ShellExecutor(this.context);
|
|
1800
|
-
const result = await executor.execute(ast);
|
|
1801
|
-
// Write command output to temporary file
|
|
1802
|
-
await fs.promises.writeFile(fifoPath, result.stdout || '');
|
|
1803
|
-
return fifoPath;
|
|
1804
|
-
}
|
|
1805
|
-
else {
|
|
1806
|
-
// For >(command), create a temporary file that can be written to
|
|
1807
|
-
// The command will process the file content after the main command finishes
|
|
1808
|
-
// This is a simplified implementation - full POSIX would use actual named pipes
|
|
1809
|
-
await fs.promises.writeFile(fifoPath, '');
|
|
1810
|
-
// Store the command to execute later when the file has content
|
|
1811
|
-
// For now, return the file path - the command would process it post-execution
|
|
1812
|
-
return fifoPath;
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
catch (error) {
|
|
1816
|
-
// Clean up on error
|
|
1817
|
-
try {
|
|
1818
|
-
await fs.promises.unlink(fifoPath);
|
|
1819
|
-
}
|
|
1820
|
-
catch (_) {
|
|
1821
|
-
// Ignore cleanup errors
|
|
1822
|
-
}
|
|
1823
|
-
throw new Error(`Process substitution failed: ${error.message}`);
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
// ZSH-Style Built-in Commands
|
|
1827
|
-
async builtin_typeset(args) {
|
|
1828
|
-
const result = this.context.arrays.parseTypesetCommand(args);
|
|
1829
|
-
return {
|
|
1830
|
-
stdout: '',
|
|
1831
|
-
stderr: result.message,
|
|
1832
|
-
exitCode: result.success ? 0 : 1,
|
|
1833
|
-
success: result.success,
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
|
-
async builtin_setopt(args) {
|
|
1837
|
-
const result = this.context.zshOptions.parseSetoptCommand(args);
|
|
1838
|
-
return {
|
|
1839
|
-
stdout: '',
|
|
1840
|
-
stderr: result.message,
|
|
1841
|
-
exitCode: result.success ? 0 : 1,
|
|
1842
|
-
success: result.success,
|
|
1843
|
-
};
|
|
1844
|
-
}
|
|
1845
|
-
async builtin_unsetopt(args) {
|
|
1846
|
-
const result = this.context.zshOptions.parseUnsetoptCommand(args);
|
|
1847
|
-
return {
|
|
1848
|
-
stdout: '',
|
|
1849
|
-
stderr: result.message,
|
|
1850
|
-
exitCode: result.success ? 0 : 1,
|
|
1851
|
-
success: result.success,
|
|
1852
|
-
};
|
|
1853
|
-
}
|
|
1854
|
-
async builtin_history(args) {
|
|
1855
|
-
if (args.length === 0) {
|
|
1856
|
-
// Show history
|
|
1857
|
-
const entries = this.context.history.getAllEntries();
|
|
1858
|
-
const output = entries
|
|
1859
|
-
.map(entry => `${entry.lineNumber.toString().padStart(4)} ${entry.command}`)
|
|
1860
|
-
.join('\n');
|
|
1861
|
-
return {
|
|
1862
|
-
stdout: output,
|
|
1863
|
-
stderr: '',
|
|
1864
|
-
exitCode: 0,
|
|
1865
|
-
success: true,
|
|
1866
|
-
};
|
|
1867
|
-
}
|
|
1868
|
-
// Handle history options
|
|
1869
|
-
if (args[0] === '-c') {
|
|
1870
|
-
this.context.history.clearHistory();
|
|
1871
|
-
return {
|
|
1872
|
-
stdout: '',
|
|
1873
|
-
stderr: '',
|
|
1874
|
-
exitCode: 0,
|
|
1875
|
-
success: true,
|
|
1876
|
-
};
|
|
1877
|
-
}
|
|
1878
|
-
if (args[0] === '-d') {
|
|
1879
|
-
// Delete specific history entry
|
|
1880
|
-
const lineNumber = parseInt(args[1], 10);
|
|
1881
|
-
if (isNaN(lineNumber)) {
|
|
1882
|
-
return {
|
|
1883
|
-
stdout: '',
|
|
1884
|
-
stderr: 'history: invalid line number',
|
|
1885
|
-
exitCode: 1,
|
|
1886
|
-
success: false,
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
|
-
// Implementation would delete specific entry
|
|
1890
|
-
return {
|
|
1891
|
-
stdout: '',
|
|
1892
|
-
stderr: '',
|
|
1893
|
-
exitCode: 0,
|
|
1894
|
-
success: true,
|
|
1895
|
-
};
|
|
1896
|
-
}
|
|
1897
|
-
return {
|
|
1898
|
-
stdout: '',
|
|
1899
|
-
stderr: 'history: unknown option',
|
|
1900
|
-
exitCode: 1,
|
|
1901
|
-
success: false,
|
|
1902
|
-
};
|
|
1903
|
-
}
|
|
1904
|
-
async builtin_fc(args) {
|
|
1905
|
-
// Fix command - edit and re-execute last command
|
|
1906
|
-
if (args.length === 0) {
|
|
1907
|
-
const entries = this.context.history.getAllEntries();
|
|
1908
|
-
if (entries.length === 0) {
|
|
1909
|
-
return {
|
|
1910
|
-
stdout: '',
|
|
1911
|
-
stderr: 'fc: no history',
|
|
1912
|
-
exitCode: 1,
|
|
1913
|
-
success: false,
|
|
1914
|
-
};
|
|
1915
|
-
}
|
|
1916
|
-
const lastCommand = entries[entries.length - 1].command;
|
|
1917
|
-
// In a real implementation, this would open an editor
|
|
1918
|
-
return {
|
|
1919
|
-
stdout: `Would edit: ${lastCommand}`,
|
|
1920
|
-
stderr: '',
|
|
1921
|
-
exitCode: 0,
|
|
1922
|
-
success: true,
|
|
1923
|
-
};
|
|
1924
|
-
}
|
|
1925
|
-
return {
|
|
1926
|
-
stdout: '',
|
|
1927
|
-
stderr: 'fc: not fully implemented',
|
|
1928
|
-
exitCode: 1,
|
|
1929
|
-
success: false,
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
async builtin_r(_args) {
|
|
1933
|
-
// Repeat last command
|
|
1934
|
-
const entries = this.context.history.getAllEntries();
|
|
1935
|
-
if (entries.length === 0) {
|
|
1936
|
-
return {
|
|
1937
|
-
stdout: '',
|
|
1938
|
-
stderr: 'r: no history',
|
|
1939
|
-
exitCode: 1,
|
|
1940
|
-
success: false,
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
const lastCommand = entries[entries.length - 1].command;
|
|
1944
|
-
try {
|
|
1945
|
-
const { parseShellCommand } = await import('./shell-parser.js');
|
|
1946
|
-
const ast = parseShellCommand(lastCommand);
|
|
1947
|
-
return await this.execute(ast);
|
|
1948
|
-
}
|
|
1949
|
-
catch (error) {
|
|
1950
|
-
return {
|
|
1951
|
-
stdout: '',
|
|
1952
|
-
stderr: `r: ${error.message}`,
|
|
1953
|
-
exitCode: 1,
|
|
1954
|
-
success: false,
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
async builtin_alias(args) {
|
|
1959
|
-
if (args.length === 0) {
|
|
1960
|
-
// List all aliases
|
|
1961
|
-
const aliases = Object.entries(this.context.variables)
|
|
1962
|
-
.filter(([key, _value]) => key.startsWith('alias_'))
|
|
1963
|
-
.map(([key, value]) => `${key.substring(6)}='${value}'`)
|
|
1964
|
-
.join('\n');
|
|
1965
|
-
return {
|
|
1966
|
-
stdout: aliases,
|
|
1967
|
-
stderr: '',
|
|
1968
|
-
exitCode: 0,
|
|
1969
|
-
success: true,
|
|
1970
|
-
};
|
|
1971
|
-
}
|
|
1972
|
-
// Set alias
|
|
1973
|
-
const aliasStr = args.join(' ');
|
|
1974
|
-
const equalIndex = aliasStr.indexOf('=');
|
|
1975
|
-
if (equalIndex === -1) {
|
|
1976
|
-
return {
|
|
1977
|
-
stdout: '',
|
|
1978
|
-
stderr: 'alias: invalid syntax',
|
|
1979
|
-
exitCode: 1,
|
|
1980
|
-
success: false,
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
|
-
const name = aliasStr.substring(0, equalIndex);
|
|
1984
|
-
const value = aliasStr.substring(equalIndex + 1);
|
|
1985
|
-
this.context.variables[`alias_${name}`] = value;
|
|
1986
|
-
this.updateExpander();
|
|
1987
|
-
return {
|
|
1988
|
-
stdout: '',
|
|
1989
|
-
stderr: '',
|
|
1990
|
-
exitCode: 0,
|
|
1991
|
-
success: true,
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
async builtin_unalias(args) {
|
|
1995
|
-
if (args.length === 0) {
|
|
1996
|
-
return {
|
|
1997
|
-
stdout: '',
|
|
1998
|
-
stderr: 'unalias: missing argument',
|
|
1999
|
-
exitCode: 1,
|
|
2000
|
-
success: false,
|
|
2001
|
-
};
|
|
2002
|
-
}
|
|
2003
|
-
for (const aliasName of args) {
|
|
2004
|
-
delete this.context.variables[`alias_${aliasName}`];
|
|
2005
|
-
}
|
|
2006
|
-
this.updateExpander();
|
|
2007
|
-
return {
|
|
2008
|
-
stdout: '',
|
|
2009
|
-
stderr: '',
|
|
2010
|
-
exitCode: 0,
|
|
2011
|
-
success: true,
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
// Enhanced parameter expansion with ZSH features
|
|
2015
|
-
async expandParameterWithZshFeatures(paramExpr) {
|
|
2016
|
-
// Try extended parameter expansion first
|
|
2017
|
-
try {
|
|
2018
|
-
return this.extendedExpander.expandParameter(paramExpr);
|
|
2019
|
-
}
|
|
2020
|
-
catch (_error) {
|
|
2021
|
-
// Fall back to regular parameter expansion
|
|
2022
|
-
return this.expander.expandParameterExpression(paramExpr);
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
// Enhanced globbing with ZSH features
|
|
2026
|
-
async expandPathnamesWithZshFeatures(pattern) {
|
|
2027
|
-
// Check if extended globbing is enabled
|
|
2028
|
-
if (this.context.zshOptions.isOptionSet('EXTENDED_GLOB')) {
|
|
2029
|
-
try {
|
|
2030
|
-
return await this.extendedGlobber.expandPattern(pattern, {
|
|
2031
|
-
cwd: this.context.cwd,
|
|
2032
|
-
includeHidden: this.context.zshOptions.isOptionSet('GLOB_DOTS'),
|
|
2033
|
-
extendedGlob: true,
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
catch (_error) {
|
|
2037
|
-
// Fall back to regular globbing
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
// Use regular pathname expansion
|
|
2041
|
-
return await this.pathExpander.expandPathnames(pattern, {
|
|
2042
|
-
cwd: this.context.cwd,
|
|
2043
|
-
includeHidden: this.context.zshOptions.isOptionSet('GLOB_DOTS'),
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
// Enhanced arithmetic expansion with floating point
|
|
2047
|
-
evaluateArithmeticWithFloatingPoint(expression) {
|
|
2048
|
-
try {
|
|
2049
|
-
const result = this.context.floatingPoint.evaluate(expression);
|
|
2050
|
-
return result.toString();
|
|
2051
|
-
}
|
|
2052
|
-
catch (_error) {
|
|
2053
|
-
// Fall back to integer arithmetic
|
|
2054
|
-
return this.expander.evaluateArithmeticExpression(expression).toString();
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
// Get completions for current context
|
|
2058
|
-
async getCompletions(command, args, currentWord, wordIndex) {
|
|
2059
|
-
const context = {
|
|
2060
|
-
command,
|
|
2061
|
-
args,
|
|
2062
|
-
currentWord,
|
|
2063
|
-
wordIndex,
|
|
2064
|
-
cwd: this.context.cwd,
|
|
2065
|
-
env: this.context.env,
|
|
2066
|
-
};
|
|
2067
|
-
const candidates = await this.context.completion.getCompletions(context);
|
|
2068
|
-
return candidates.map(c => c.word);
|
|
2069
|
-
}
|
|
2070
|
-
// Register completion function
|
|
2071
|
-
registerCompletion(command, func) {
|
|
2072
|
-
this.context.completion.registerCompletion(command, func);
|
|
2073
|
-
}
|
|
2074
|
-
// Add command to history
|
|
2075
|
-
addToHistory(command, exitCode) {
|
|
2076
|
-
this.context.history.addCommand(command, exitCode);
|
|
2077
|
-
}
|
|
2078
|
-
// Get all history entries
|
|
2079
|
-
getHistoryEntries() {
|
|
2080
|
-
return this.context.history.getAllEntries();
|
|
2081
|
-
}
|
|
2082
|
-
// Set positional parameters
|
|
2083
|
-
setPositionalParams(params) {
|
|
2084
|
-
this.context.positionalParams = params;
|
|
2085
|
-
}
|
|
2086
|
-
// Get ZSH compatibility instance
|
|
2087
|
-
getZshCompatibility() {
|
|
2088
|
-
return this.context.zshCompatibility;
|
|
2089
|
-
}
|
|
2090
|
-
// Get current prompt
|
|
2091
|
-
getPrompt() {
|
|
2092
|
-
return this.context.prompt.getCurrentPrompt({
|
|
2093
|
-
user: process.env.USER || 'user',
|
|
2094
|
-
host: process.env.HOSTNAME || 'localhost',
|
|
2095
|
-
cwd: this.context.cwd,
|
|
2096
|
-
home: process.env.HOME || '/',
|
|
2097
|
-
exitCode: this.context.lastExitCode,
|
|
2098
|
-
jobCount: this.context.jobControl.jobs.size,
|
|
2099
|
-
time: new Date(),
|
|
2100
|
-
});
|
|
2101
|
-
}
|
|
2102
|
-
// Get current right prompt
|
|
2103
|
-
getRPrompt() {
|
|
2104
|
-
return this.context.prompt.getCurrentRPrompt({
|
|
2105
|
-
user: process.env.USER || 'user',
|
|
2106
|
-
host: process.env.HOSTNAME || 'localhost',
|
|
2107
|
-
cwd: this.context.cwd,
|
|
2108
|
-
home: process.env.HOME || '/',
|
|
2109
|
-
exitCode: this.context.lastExitCode,
|
|
2110
|
-
jobCount: this.context.jobControl.jobs.size,
|
|
2111
|
-
time: new Date(),
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
// ZSH Compatibility Built-in Commands
|
|
2115
|
-
async builtin_source(args) {
|
|
2116
|
-
if (args.length === 0) {
|
|
2117
|
-
return {
|
|
2118
|
-
stdout: '',
|
|
2119
|
-
stderr: 'source: missing filename',
|
|
2120
|
-
exitCode: 1,
|
|
2121
|
-
success: false,
|
|
2122
|
-
};
|
|
2123
|
-
}
|
|
2124
|
-
const filename = args[0];
|
|
2125
|
-
try {
|
|
2126
|
-
const fs = await import('fs');
|
|
2127
|
-
const content = fs.readFileSync(filename, 'utf8');
|
|
2128
|
-
const lines = content.split('\n');
|
|
2129
|
-
for (const line of lines) {
|
|
2130
|
-
const trimmed = line.trim();
|
|
2131
|
-
if (trimmed.startsWith('#') || trimmed === '') {
|
|
2132
|
-
continue;
|
|
2133
|
-
}
|
|
2134
|
-
try {
|
|
2135
|
-
const ast = parseShellCommand(trimmed);
|
|
2136
|
-
await this.execute(ast);
|
|
2137
|
-
}
|
|
2138
|
-
catch (error) {
|
|
2139
|
-
console.error(`Error in ${filename}: ${error.message}`);
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
return {
|
|
2143
|
-
stdout: '',
|
|
2144
|
-
stderr: '',
|
|
2145
|
-
exitCode: 0,
|
|
2146
|
-
success: true,
|
|
2147
|
-
};
|
|
2148
|
-
}
|
|
2149
|
-
catch (error) {
|
|
2150
|
-
return {
|
|
2151
|
-
stdout: '',
|
|
2152
|
-
stderr: `source: ${filename}: ${error.message}`,
|
|
2153
|
-
exitCode: 1,
|
|
2154
|
-
success: false,
|
|
2155
|
-
};
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
async builtin_install(args) {
|
|
2159
|
-
if (args.length === 0) {
|
|
2160
|
-
return {
|
|
2161
|
-
stdout: '',
|
|
2162
|
-
stderr: 'install: missing package name',
|
|
2163
|
-
exitCode: 1,
|
|
2164
|
-
success: false,
|
|
2165
|
-
};
|
|
2166
|
-
}
|
|
2167
|
-
const packageName = args[0];
|
|
2168
|
-
const result = await this.context.zshCompatibility.installPackage(packageName);
|
|
2169
|
-
return {
|
|
2170
|
-
stdout: result.message,
|
|
2171
|
-
stderr: '',
|
|
2172
|
-
exitCode: result.success ? 0 : 1,
|
|
2173
|
-
success: result.success,
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
async builtin_uninstall(args) {
|
|
2177
|
-
if (args.length === 0) {
|
|
2178
|
-
return {
|
|
2179
|
-
stdout: '',
|
|
2180
|
-
stderr: 'uninstall: missing package name',
|
|
2181
|
-
exitCode: 1,
|
|
2182
|
-
success: false,
|
|
2183
|
-
};
|
|
2184
|
-
}
|
|
2185
|
-
const packageName = args[0];
|
|
2186
|
-
const result = await this.context.zshCompatibility.uninstallPackage(packageName);
|
|
2187
|
-
return {
|
|
2188
|
-
stdout: result.message,
|
|
2189
|
-
stderr: '',
|
|
2190
|
-
exitCode: result.success ? 0 : 1,
|
|
2191
|
-
success: result.success,
|
|
2192
|
-
};
|
|
2193
|
-
}
|
|
2194
|
-
async builtin_zsh_migrate(_args) {
|
|
2195
|
-
const result = await this.context.zshCompatibility.migrateZshConfig();
|
|
2196
|
-
return {
|
|
2197
|
-
stdout: result.message,
|
|
2198
|
-
stderr: '',
|
|
2199
|
-
exitCode: result.success ? 0 : 1,
|
|
2200
|
-
success: result.success,
|
|
2201
|
-
};
|
|
2202
|
-
}
|
|
2203
|
-
async builtin_zsh_source(_args) {
|
|
2204
|
-
const result = await this.context.zshCompatibility.sourceZshConfig();
|
|
2205
|
-
return {
|
|
2206
|
-
stdout: result.message,
|
|
2207
|
-
stderr: '',
|
|
2208
|
-
exitCode: result.success ? 0 : 1,
|
|
2209
|
-
success: result.success,
|
|
2210
|
-
};
|
|
2211
|
-
}
|
|
2212
|
-
/**
|
|
2213
|
-
* readonly - Make variables read-only
|
|
2214
|
-
* Usage: readonly [name[=value]]...
|
|
2215
|
-
*/
|
|
2216
|
-
async builtin_readonly(args) {
|
|
2217
|
-
// Initialize readonly set if not exists
|
|
2218
|
-
if (!this.context.readonlyVariables) {
|
|
2219
|
-
this.context.readonlyVariables = new Set();
|
|
2220
|
-
}
|
|
2221
|
-
const readonlyVars = this.context.readonlyVariables;
|
|
2222
|
-
if (args.length === 0) {
|
|
2223
|
-
// List all readonly variables
|
|
2224
|
-
let output = '';
|
|
2225
|
-
for (const name of readonlyVars) {
|
|
2226
|
-
const value = this.context.variables[name] ?? this.context.env[name] ?? '';
|
|
2227
|
-
output += `readonly ${name}=${value}\n`;
|
|
2228
|
-
}
|
|
2229
|
-
return { stdout: output, stderr: '', exitCode: 0, success: true };
|
|
2230
|
-
}
|
|
2231
|
-
// Make variables readonly
|
|
2232
|
-
for (const arg of args) {
|
|
2233
|
-
if (arg.includes('=')) {
|
|
2234
|
-
const [name, value] = arg.split('=', 2);
|
|
2235
|
-
if (readonlyVars.has(name)) {
|
|
2236
|
-
return {
|
|
2237
|
-
stdout: '',
|
|
2238
|
-
stderr: `readonly: ${name}: readonly variable\n`,
|
|
2239
|
-
exitCode: 1,
|
|
2240
|
-
success: false,
|
|
2241
|
-
};
|
|
2242
|
-
}
|
|
2243
|
-
this.context.variables[name] = value;
|
|
2244
|
-
readonlyVars.add(name);
|
|
2245
|
-
}
|
|
2246
|
-
else {
|
|
2247
|
-
if (readonlyVars.has(arg)) {
|
|
2248
|
-
return {
|
|
2249
|
-
stdout: '',
|
|
2250
|
-
stderr: `readonly: ${arg}: readonly variable\n`,
|
|
2251
|
-
exitCode: 1,
|
|
2252
|
-
success: false,
|
|
2253
|
-
};
|
|
2254
|
-
}
|
|
2255
|
-
readonlyVars.add(arg);
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
2259
|
-
}
|
|
2260
|
-
/**
|
|
2261
|
-
* type - Display command type
|
|
2262
|
-
* Usage: type name...
|
|
2263
|
-
*/
|
|
2264
|
-
async builtin_type(args) {
|
|
2265
|
-
if (args.length === 0) {
|
|
2266
|
-
return {
|
|
2267
|
-
stdout: '',
|
|
2268
|
-
stderr: 'type: usage: type name [name ...]\n',
|
|
2269
|
-
exitCode: 1,
|
|
2270
|
-
success: false,
|
|
2271
|
-
};
|
|
2272
|
-
}
|
|
2273
|
-
let output = '';
|
|
2274
|
-
let hasError = false;
|
|
2275
|
-
for (const name of args) {
|
|
2276
|
-
// Check if it's a built-in
|
|
2277
|
-
const builtinResult = await this.executeBuiltin(name, []);
|
|
2278
|
-
if (builtinResult !== null) {
|
|
2279
|
-
output += `${name} is a shell builtin\n`;
|
|
2280
|
-
continue;
|
|
2281
|
-
}
|
|
2282
|
-
// Check if it's a function
|
|
2283
|
-
if (this.context.functions[name]) {
|
|
2284
|
-
output += `${name} is a function\n`;
|
|
2285
|
-
continue;
|
|
2286
|
-
}
|
|
2287
|
-
// Check if it's an alias
|
|
2288
|
-
const aliasKey = `alias_${name}`;
|
|
2289
|
-
if (this.context.variables[aliasKey]) {
|
|
2290
|
-
const aliasValue = this.context.variables[aliasKey];
|
|
2291
|
-
output += `${name} is aliased to \`${aliasValue}'\n`;
|
|
2292
|
-
continue;
|
|
2293
|
-
}
|
|
2294
|
-
// Check if it's an external command
|
|
2295
|
-
try {
|
|
2296
|
-
const which = await import('child_process');
|
|
2297
|
-
const result = await new Promise((resolve) => {
|
|
2298
|
-
which.exec(`which ${name}`, (error, stdout) => {
|
|
2299
|
-
resolve(error ? '' : stdout.trim());
|
|
2300
|
-
});
|
|
2301
|
-
});
|
|
2302
|
-
if (result) {
|
|
2303
|
-
output += `${name} is ${result}\n`;
|
|
2304
|
-
}
|
|
2305
|
-
else {
|
|
2306
|
-
output += `${name}: not found\n`;
|
|
2307
|
-
hasError = true;
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
catch {
|
|
2311
|
-
output += `${name}: not found\n`;
|
|
2312
|
-
hasError = true;
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
return {
|
|
2316
|
-
stdout: output,
|
|
2317
|
-
stderr: '',
|
|
2318
|
-
exitCode: hasError ? 1 : 0,
|
|
2319
|
-
success: !hasError,
|
|
2320
|
-
};
|
|
2321
|
-
}
|
|
2322
|
-
/**
|
|
2323
|
-
* hash - Remember command locations
|
|
2324
|
-
* Usage: hash [-lr] [name...]
|
|
2325
|
-
*/
|
|
2326
|
-
async builtin_hash(args) {
|
|
2327
|
-
// Initialize command hash table if not exists
|
|
2328
|
-
if (!this.context.commandHash) {
|
|
2329
|
-
this.context.commandHash = new Map();
|
|
2330
|
-
}
|
|
2331
|
-
const commandHash = this.context.commandHash;
|
|
2332
|
-
// Parse options
|
|
2333
|
-
let listAll = false;
|
|
2334
|
-
let remove = false;
|
|
2335
|
-
const names = [];
|
|
2336
|
-
for (const arg of args) {
|
|
2337
|
-
if (arg === '-l') {
|
|
2338
|
-
listAll = true;
|
|
2339
|
-
}
|
|
2340
|
-
else if (arg === '-r') {
|
|
2341
|
-
remove = true;
|
|
2342
|
-
}
|
|
2343
|
-
else if (!arg.startsWith('-')) {
|
|
2344
|
-
names.push(arg);
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
// Remove all entries
|
|
2348
|
-
if (remove && names.length === 0) {
|
|
2349
|
-
commandHash.clear();
|
|
2350
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
2351
|
-
}
|
|
2352
|
-
// Remove specific entries
|
|
2353
|
-
if (remove && names.length > 0) {
|
|
2354
|
-
for (const name of names) {
|
|
2355
|
-
commandHash.delete(name);
|
|
2356
|
-
}
|
|
2357
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
2358
|
-
}
|
|
2359
|
-
// List all commands
|
|
2360
|
-
if (listAll || names.length === 0) {
|
|
2361
|
-
let output = '';
|
|
2362
|
-
for (const [name, path] of commandHash.entries()) {
|
|
2363
|
-
output += `${name}\t${path}\n`;
|
|
2364
|
-
}
|
|
2365
|
-
return { stdout: output, stderr: '', exitCode: 0, success: true };
|
|
2366
|
-
}
|
|
2367
|
-
// Hash specific commands
|
|
2368
|
-
for (const name of names) {
|
|
2369
|
-
try {
|
|
2370
|
-
const which = await import('child_process');
|
|
2371
|
-
const result = await new Promise((resolve) => {
|
|
2372
|
-
which.exec(`which ${name}`, (error, stdout) => {
|
|
2373
|
-
resolve(error ? '' : stdout.trim());
|
|
2374
|
-
});
|
|
2375
|
-
});
|
|
2376
|
-
if (result) {
|
|
2377
|
-
commandHash.set(name, result);
|
|
2378
|
-
}
|
|
2379
|
-
else {
|
|
2380
|
-
return {
|
|
2381
|
-
stdout: '',
|
|
2382
|
-
stderr: `hash: ${name}: not found\n`,
|
|
2383
|
-
exitCode: 1,
|
|
2384
|
-
success: false,
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
catch (_error) {
|
|
2389
|
-
return {
|
|
2390
|
-
stdout: '',
|
|
2391
|
-
stderr: `hash: ${name}: not found\n`,
|
|
2392
|
-
exitCode: 1,
|
|
2393
|
-
success: false,
|
|
2394
|
-
};
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
2398
|
-
}
|
|
2399
|
-
/**
|
|
2400
|
-
* kill - Send signal to process
|
|
2401
|
-
* Usage: kill [-s sigspec | -sigspec] pid...
|
|
2402
|
-
*/
|
|
2403
|
-
async builtin_kill(args) {
|
|
2404
|
-
if (args.length === 0) {
|
|
2405
|
-
return {
|
|
2406
|
-
stdout: '',
|
|
2407
|
-
stderr: 'kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...\n',
|
|
2408
|
-
exitCode: 1,
|
|
2409
|
-
success: false,
|
|
2410
|
-
};
|
|
2411
|
-
}
|
|
2412
|
-
let signal = 'SIGTERM';
|
|
2413
|
-
const pids = [];
|
|
2414
|
-
for (let i = 0; i < args.length; i++) {
|
|
2415
|
-
const arg = args[i];
|
|
2416
|
-
if (arg === '-s') {
|
|
2417
|
-
// -s SIGNAL
|
|
2418
|
-
if (i + 1 < args.length) {
|
|
2419
|
-
signal = args[++i];
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
else if (arg === '-l') {
|
|
2423
|
-
// List signals
|
|
2424
|
-
return {
|
|
2425
|
-
stdout: 'HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM\n',
|
|
2426
|
-
stderr: '',
|
|
2427
|
-
exitCode: 0,
|
|
2428
|
-
success: true,
|
|
2429
|
-
};
|
|
2430
|
-
}
|
|
2431
|
-
else if (arg.startsWith('-') && arg.length > 1) {
|
|
2432
|
-
// -SIGNAL format
|
|
2433
|
-
signal = arg.substring(1);
|
|
2434
|
-
if (signal.match(/^\d+$/)) {
|
|
2435
|
-
// Numeric signal, convert to name (simplified)
|
|
2436
|
-
const sigNum = parseInt(signal);
|
|
2437
|
-
if (sigNum === 9)
|
|
2438
|
-
signal = 'SIGKILL';
|
|
2439
|
-
else if (sigNum === 15)
|
|
2440
|
-
signal = 'SIGTERM';
|
|
2441
|
-
else
|
|
2442
|
-
signal = `SIG${sigNum}`;
|
|
2443
|
-
}
|
|
2444
|
-
else if (!signal.startsWith('SIG')) {
|
|
2445
|
-
signal = `SIG${signal}`;
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
else {
|
|
2449
|
-
// PID or job spec
|
|
2450
|
-
if (arg.startsWith('%')) {
|
|
2451
|
-
// Job spec - look up in job control
|
|
2452
|
-
const jobId = arg.substring(1);
|
|
2453
|
-
const job = this.context.jobControl.jobs.get(parseInt(jobId));
|
|
2454
|
-
if (job && job.pid) {
|
|
2455
|
-
pids.push(job.pid);
|
|
2456
|
-
}
|
|
2457
|
-
else {
|
|
2458
|
-
return {
|
|
2459
|
-
stdout: '',
|
|
2460
|
-
stderr: `kill: ${arg}: no such job\n`,
|
|
2461
|
-
exitCode: 1,
|
|
2462
|
-
success: false,
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
else {
|
|
2467
|
-
const pid = parseInt(arg);
|
|
2468
|
-
if (isNaN(pid)) {
|
|
2469
|
-
return {
|
|
2470
|
-
stdout: '',
|
|
2471
|
-
stderr: `kill: ${arg}: arguments must be process or job IDs\n`,
|
|
2472
|
-
exitCode: 1,
|
|
2473
|
-
success: false,
|
|
2474
|
-
};
|
|
2475
|
-
}
|
|
2476
|
-
pids.push(pid);
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
if (pids.length === 0) {
|
|
2481
|
-
return {
|
|
2482
|
-
stdout: '',
|
|
2483
|
-
stderr: 'kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...\n',
|
|
2484
|
-
exitCode: 1,
|
|
2485
|
-
success: false,
|
|
2486
|
-
};
|
|
2487
|
-
}
|
|
2488
|
-
// Send signal to all PIDs
|
|
2489
|
-
for (const pid of pids) {
|
|
2490
|
-
try {
|
|
2491
|
-
process.kill(pid, signal);
|
|
2492
|
-
}
|
|
2493
|
-
catch (error) {
|
|
2494
|
-
return {
|
|
2495
|
-
stdout: '',
|
|
2496
|
-
stderr: `kill: (${pid}) - ${error.message}\n`,
|
|
2497
|
-
exitCode: 1,
|
|
2498
|
-
success: false,
|
|
2499
|
-
};
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
return { stdout: '', stderr: '', exitCode: 0, success: true };
|
|
2503
|
-
}
|
|
2504
|
-
}
|