dotdo 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/README.md +238 -0
- package/cli/agent.ts +72 -0
- package/cli/bin.js +44 -0
- package/cli/bin.ts +38 -0
- package/cli/build.ts +157 -0
- package/cli/commands/auth/login.ts +14 -0
- package/cli/commands/auth/logout.ts +6 -0
- package/cli/commands/auth/whoami.ts +16 -0
- package/cli/commands/deploy-multi.ts +245 -0
- package/cli/commands/dev/deploy.ts +100 -0
- package/cli/commands/dev/dev.ts +95 -0
- package/cli/commands/dev/logs.ts +91 -0
- package/cli/commands/dev-local.ts +88 -0
- package/cli/commands/do-ops.ts +314 -0
- package/cli/commands/index.ts +100 -0
- package/cli/commands/init.ts +247 -0
- package/cli/commands/introspect/emitter.ts +315 -0
- package/cli/commands/introspect/index.ts +193 -0
- package/cli/commands/link.ts +598 -0
- package/cli/commands/snippets.ts +415 -0
- package/cli/commands/tunnel.ts +239 -0
- package/cli/device-auth.ts +289 -0
- package/cli/fallback.ts +12 -0
- package/cli/index.ts +121 -0
- package/cli/main.ts +246 -0
- package/cli/mcp-stdio.ts +790 -0
- package/cli/package.json +62 -0
- package/cli/runtime/do-registry.ts +193 -0
- package/cli/runtime/embedded-db.ts +344 -0
- package/cli/runtime/index.ts +9 -0
- package/cli/runtime/miniflare-adapter.ts +162 -0
- package/cli/sandbox.ts +82 -0
- package/cli/src/args.ts +174 -0
- package/cli/src/auth.ts +55 -0
- package/cli/src/commands/call.ts +84 -0
- package/cli/src/commands/charge.ts +96 -0
- package/cli/src/commands/config.ts +115 -0
- package/cli/src/commands/email.ts +112 -0
- package/cli/src/commands/llm.ts +115 -0
- package/cli/src/commands/queue.ts +134 -0
- package/cli/src/commands/text.ts +86 -0
- package/cli/src/config.ts +185 -0
- package/cli/src/output.ts +246 -0
- package/cli/src/rpc.ts +192 -0
- package/cli/utils/config.ts +282 -0
- package/cli/utils/detect.ts +73 -0
- package/cli/utils/index.ts +15 -0
- package/cli/utils/logger.ts +232 -0
- package/dist/ai/template-literals.js +2 -2
- package/dist/ai/template-literals.js.map +1 -1
- package/dist/api/middleware/auth.js +3 -2
- package/dist/api/middleware/auth.js.map +1 -1
- package/dist/db/iceberg/inverted-index.js +1 -1
- package/dist/db/iceberg/inverted-index.js.map +1 -1
- package/dist/db/iceberg/puffin.js.map +1 -1
- package/dist/db/json-indexes.js.map +1 -1
- package/dist/db/objects.js.map +1 -1
- package/dist/db/primitives/dag-scheduler/index.js +1 -1
- package/dist/db/primitives/dag-scheduler/index.js.map +1 -1
- package/dist/db/primitives/observability.js.map +1 -1
- package/dist/db/primitives/schema-evolution.js.map +1 -1
- package/dist/db/primitives/temporal-store.js.map +1 -1
- package/dist/db/primitives/typed-column-store.js.map +1 -1
- package/dist/db/primitives/utils/duration.js.map +1 -1
- package/dist/db/primitives/utils/murmur3.js +12 -14
- package/dist/db/primitives/utils/murmur3.js.map +1 -1
- package/dist/db/primitives/window-manager.js.map +1 -1
- package/dist/db/stores.js.map +1 -1
- package/dist/db/things.js.map +1 -1
- package/dist/lib/DODispatcher.js +2 -2
- package/dist/lib/DODispatcher.js.map +1 -1
- package/dist/lib/auto-wiring.js.map +1 -1
- package/dist/lib/channels/email.js +1 -1
- package/dist/lib/channels/email.js.map +1 -1
- package/dist/lib/channels/slack-blockkit.js.map +1 -1
- package/dist/lib/cloudflare/ai.js +1 -1
- package/dist/lib/cloudflare/ai.js.map +1 -1
- package/dist/lib/cloudflare/kv.js +1 -1
- package/dist/lib/cloudflare/kv.js.map +1 -1
- package/dist/lib/cloudflare/r2.js +3 -3
- package/dist/lib/cloudflare/r2.js.map +1 -1
- package/dist/lib/cloudflare/vectorize.js.map +1 -1
- package/dist/lib/cloudflare/workflows.js.map +1 -1
- package/dist/lib/executors/AgenticFunctionExecutor.js.map +1 -1
- package/dist/lib/executors/CodeFunctionExecutor.js.map +1 -1
- package/dist/lib/executors/GenerativeFunctionExecutor.js.map +1 -1
- package/dist/lib/executors/HumanFunctionExecutor.js +1 -1
- package/dist/lib/executors/HumanFunctionExecutor.js.map +1 -1
- package/dist/lib/executors/ParallelStepExecutor.js.map +1 -1
- package/dist/lib/experiments.js.map +1 -1
- package/dist/lib/flags/store.js.map +1 -1
- package/dist/lib/functions/FunctionComposition.js.map +1 -1
- package/dist/lib/functions/FunctionMiddleware.js.map +1 -1
- package/dist/lib/functions/FunctionRegistry.js.map +1 -1
- package/dist/lib/humans/templates.js.map +1 -1
- package/dist/lib/identity.js +2 -2
- package/dist/lib/identity.js.map +1 -1
- package/dist/lib/logging/index.js.map +1 -1
- package/dist/lib/mixins/bash.js +1 -73
- package/dist/lib/mixins/bash.js.map +1 -1
- package/dist/lib/mixins/git.js +0 -5
- package/dist/lib/mixins/git.js.map +1 -1
- package/dist/lib/mixins/npm.js.map +1 -1
- package/dist/lib/noun-id.js.map +1 -1
- package/dist/lib/rate-limit/sliding-window.js.map +1 -1
- package/dist/lib/rpc/bindings.js.map +1 -1
- package/dist/lib/safe-stringify.js.map +1 -1
- package/dist/lib/sandbox/miniflare-sandbox.js.map +1 -1
- package/dist/lib/sqids.js.map +1 -1
- package/dist/lib/sql/adapters/node-sql-parser.js.map +1 -1
- package/dist/lib/sql/adapters/pgsql-parser.js +19 -18
- package/dist/lib/sql/adapters/pgsql-parser.js.map +1 -1
- package/dist/metrics/hunch.js.map +1 -1
- package/dist/objects/API.js +1 -1
- package/dist/objects/API.js.map +1 -1
- package/dist/objects/Agent.js.map +1 -1
- package/dist/objects/Browser.js.map +1 -1
- package/dist/objects/CLI.js.map +1 -1
- package/dist/objects/DOBase.js.map +1 -1
- package/dist/objects/DOCache.js +153 -0
- package/dist/objects/DOCache.js.map +1 -0
- package/dist/objects/DOFull.js.map +1 -1
- package/dist/objects/Entity.js.map +1 -1
- package/dist/objects/Human.js.map +1 -1
- package/dist/objects/IcebergMetadataDO.js.map +1 -1
- package/dist/objects/IntegrationsDO.js.map +1 -1
- package/dist/objects/ObservabilityBroadcaster.js.map +1 -1
- package/dist/objects/Package.js.map +1 -1
- package/dist/objects/Product.js +1 -1
- package/dist/objects/Product.js.map +1 -1
- package/dist/objects/SaaS.js.map +1 -1
- package/dist/objects/SandboxDO.js.map +1 -1
- package/dist/objects/Service.js.map +1 -1
- package/dist/objects/VectorShardDO.js +9 -7
- package/dist/objects/VectorShardDO.js.map +1 -1
- package/dist/objects/Workflow.js.map +1 -1
- package/dist/objects/WorkflowFactory.js.map +1 -1
- package/dist/objects/WorkflowRuntime.js.map +1 -1
- package/dist/objects/lifecycle/Branch.js.map +1 -1
- package/dist/objects/lifecycle/Clone.js +1 -1
- package/dist/objects/lifecycle/Clone.js.map +1 -1
- package/dist/objects/lifecycle/Compact.js.map +1 -1
- package/dist/objects/lifecycle/Shard.js.map +1 -1
- package/dist/objects/persistence/checkpoint-manager.js.map +1 -1
- package/dist/objects/persistence/migration-runner.js.map +1 -1
- package/dist/objects/persistence/replication-manager.js +2 -2
- package/dist/objects/persistence/replication-manager.js.map +1 -1
- package/dist/objects/persistence/tiered-storage-manager.js.map +1 -1
- package/dist/objects/persistence/wal-manager.js.map +1 -1
- package/dist/objects/transport/auth-layer.js.map +1 -1
- package/dist/objects/transport/chain.js.map +1 -1
- package/dist/objects/transport/mcp-server.js +7 -6
- package/dist/objects/transport/mcp-server.js.map +1 -1
- package/dist/objects/transport/rest-autowire.js +3 -2
- package/dist/objects/transport/rest-autowire.js.map +1 -1
- package/dist/objects/transport/rest-router.js.map +1 -1
- package/dist/objects/transport/rpc-server.js +18 -15
- package/dist/objects/transport/rpc-server.js.map +1 -1
- package/dist/objects/transport/shared.js +2 -1
- package/dist/objects/transport/shared.js.map +1 -1
- package/dist/snippets/artifacts-ingest.js.map +1 -1
- package/dist/snippets/artifacts-serve.js.map +1 -1
- package/dist/snippets/search.js.map +1 -1
- package/dist/workflows/ScheduleManager.js.map +1 -1
- package/dist/workflows/StepResultStorage.js.map +1 -1
- package/dist/workflows/WaitForEventManager.js.map +1 -1
- package/dist/workflows/compat/backends/cloudflare-workflows.js.map +1 -1
- package/dist/workflows/compat/inngest/index.js.map +1 -1
- package/dist/workflows/compat/qstash/index.js.map +1 -1
- package/dist/workflows/compat/temporal/client.js.map +1 -1
- package/dist/workflows/compat/temporal/index.js.map +1 -1
- package/dist/workflows/compat/trigger/index.js.map +1 -1
- package/dist/workflows/compat/utils/index.js.map +1 -1
- package/dist/workflows/context/correlation.js +2 -2
- package/dist/workflows/context/correlation.js.map +1 -1
- package/dist/workflows/context/experiment.js +1 -1
- package/dist/workflows/context/experiment.js.map +1 -1
- package/dist/workflows/context/flag.js +1 -1
- package/dist/workflows/context/flag.js.map +1 -1
- package/dist/workflows/context/measure.js +1 -1
- package/dist/workflows/context/measure.js.map +1 -1
- package/dist/workflows/context/rate-limit.js.map +1 -1
- package/dist/workflows/data/entity-events/entity-events.js.map +1 -1
- package/dist/workflows/data/experiment/index.js.map +1 -1
- package/dist/workflows/data/goal/context.js +1 -1
- package/dist/workflows/data/goal/context.js.map +1 -1
- package/dist/workflows/data/measure/index.js +1 -1
- package/dist/workflows/data/measure/index.js.map +1 -1
- package/dist/workflows/data/stream/index.js +10 -76
- package/dist/workflows/data/stream/index.js.map +1 -1
- package/dist/workflows/data/track/context.js.map +1 -1
- package/dist/workflows/data/view/context.js.map +1 -1
- package/dist/workflows/domain.js.map +1 -1
- package/dist/workflows/flags.js +1 -1
- package/dist/workflows/flags.js.map +1 -1
- package/dist/workflows/hash.js.map +1 -1
- package/dist/workflows/on.js +1 -1
- package/dist/workflows/on.js.map +1 -1
- package/dist/workflows/schedule-builder.js.map +1 -1
- package/dist/workflows/visibility/index.js +0 -2
- package/dist/workflows/visibility/index.js.map +1 -1
- package/dist/workflows/visibility/query-parser.js.map +1 -1
- package/package.json +18 -3
- package/dist/api/analytics/router.js +0 -601
- package/dist/api/analytics/router.js.map +0 -1
- package/dist/api/index.js +0 -158
- package/dist/api/index.js.map +0 -1
- package/dist/api/middleware/error-handling.js +0 -176
- package/dist/api/middleware/error-handling.js.map +0 -1
- package/dist/api/middleware/request-id.js +0 -21
- package/dist/api/middleware/request-id.js.map +0 -1
- package/dist/api/pages.js +0 -1180
- package/dist/api/pages.js.map +0 -1
- package/dist/api/routes/api.js +0 -612
- package/dist/api/routes/api.js.map +0 -1
- package/dist/api/routes/browsers.js +0 -471
- package/dist/api/routes/browsers.js.map +0 -1
- package/dist/api/routes/do.js +0 -188
- package/dist/api/routes/do.js.map +0 -1
- package/dist/api/routes/mcp.js +0 -459
- package/dist/api/routes/mcp.js.map +0 -1
- package/dist/api/routes/obs.js +0 -445
- package/dist/api/routes/obs.js.map +0 -1
- package/dist/api/routes/openapi.js +0 -794
- package/dist/api/routes/openapi.js.map +0 -1
- package/dist/api/routes/rpc.js +0 -1103
- package/dist/api/routes/rpc.js.map +0 -1
- package/dist/api/routes/sandboxes.js +0 -389
- package/dist/api/routes/sandboxes.js.map +0 -1
- package/dist/api/test-do.js +0 -38
- package/dist/api/test-do.js.map +0 -1
- package/dist/api/types.js +0 -11
- package/dist/api/types.js.map +0 -1
- package/dist/cli/bin.js +0 -2
- package/dist/cli/main.js +0 -52342
- package/dist/do/bash.js +0 -35
- package/dist/do/bash.js.map +0 -1
- package/dist/do/fs.js +0 -25
- package/dist/do/fs.js.map +0 -1
- package/dist/do/full.js +0 -61
- package/dist/do/full.js.map +0 -1
- package/dist/do/git.js +0 -28
- package/dist/do/git.js.map +0 -1
- package/dist/do/index.js +0 -52
- package/dist/do/index.js.map +0 -1
- package/dist/lib/agent/tools/bash.js +0 -336
- package/dist/lib/agent/tools/bash.js.map +0 -1
- package/dist/lib/agent/tools/edit.js +0 -157
- package/dist/lib/agent/tools/edit.js.map +0 -1
- package/dist/lib/agent/tools/glob.js +0 -137
- package/dist/lib/agent/tools/glob.js.map +0 -1
- package/dist/lib/agent/tools/grep.js +0 -315
- package/dist/lib/agent/tools/grep.js.map +0 -1
- package/dist/lib/agent/tools/index.js +0 -71
- package/dist/lib/agent/tools/index.js.map +0 -1
- package/dist/lib/agent/tools/read.js +0 -212
- package/dist/lib/agent/tools/read.js.map +0 -1
- package/dist/lib/agent/tools/types.js +0 -197
- package/dist/lib/agent/tools/types.js.map +0 -1
- package/dist/lib/agent/tools/write.js +0 -159
- package/dist/lib/agent/tools/write.js.map +0 -1
- package/dist/lib/mixins/index.js +0 -29
- package/dist/lib/mixins/index.js.map +0 -1
- package/dist/primitives/bashx/src/ast/analyze.js +0 -1472
- package/dist/primitives/bashx/src/ast/analyze.js.map +0 -1
- package/dist/primitives/bashx/src/ast/parser.js +0 -1488
- package/dist/primitives/bashx/src/ast/parser.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/crypto.js +0 -1954
- package/dist/primitives/bashx/src/do/commands/crypto.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/data-processing.js +0 -1812
- package/dist/primitives/bashx/src/do/commands/data-processing.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/extended-utils.js +0 -804
- package/dist/primitives/bashx/src/do/commands/extended-utils.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/math-control.js +0 -1122
- package/dist/primitives/bashx/src/do/commands/math-control.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/posix-utils.js +0 -1015
- package/dist/primitives/bashx/src/do/commands/posix-utils.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/system-utils.js +0 -687
- package/dist/primitives/bashx/src/do/commands/system-utils.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/test-command.js +0 -523
- package/dist/primitives/bashx/src/do/commands/test-command.js.map +0 -1
- package/dist/primitives/bashx/src/do/commands/text-processing.js +0 -1550
- package/dist/primitives/bashx/src/do/commands/text-processing.js.map +0 -1
- package/dist/primitives/bashx/src/do/container-executor.js +0 -429
- package/dist/primitives/bashx/src/do/container-executor.js.map +0 -1
- package/dist/primitives/bashx/src/do/index.js +0 -668
- package/dist/primitives/bashx/src/do/index.js.map +0 -1
- package/dist/primitives/bashx/src/do/tiered-executor.js +0 -2647
- package/dist/primitives/bashx/src/do/tiered-executor.js.map +0 -1
- package/dist/primitives/bashx/src/do/worker.js +0 -352
- package/dist/primitives/bashx/src/do/worker.js.map +0 -1
- package/dist/primitives/bashx/src/types.js +0 -10
- package/dist/primitives/bashx/src/types.js.map +0 -1
- package/dist/primitives/fsx/core/backend.js +0 -480
- package/dist/primitives/fsx/core/backend.js.map +0 -1
- package/dist/primitives/fsx/core/constants.js +0 -140
- package/dist/primitives/fsx/core/constants.js.map +0 -1
- package/dist/primitives/fsx/core/fsx.js +0 -1184
- package/dist/primitives/fsx/core/fsx.js.map +0 -1
- package/dist/primitives/fsx/core/glob/glob.js +0 -438
- package/dist/primitives/fsx/core/glob/glob.js.map +0 -1
- package/dist/primitives/fsx/core/glob/index.js +0 -8
- package/dist/primitives/fsx/core/glob/index.js.map +0 -1
- package/dist/primitives/fsx/core/glob/match.js +0 -392
- package/dist/primitives/fsx/core/glob/match.js.map +0 -1
- package/dist/primitives/fsx/core/types.js +0 -307
- package/dist/primitives/fsx/core/types.js.map +0 -1
- package/dist/sdk/capnweb-compat.js +0 -42
- package/dist/sdk/capnweb-compat.js.map +0 -1
- package/dist/sdk/client.js +0 -20
- package/dist/sdk/client.js.map +0 -1
- package/dist/sdk/index.js +0 -17
- package/dist/sdk/index.js.map +0 -1
|
@@ -1,1488 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bash AST Parser
|
|
3
|
-
*
|
|
4
|
-
* A lightweight parser for bash commands that detects common syntax errors.
|
|
5
|
-
* Implements error detection for:
|
|
6
|
-
* - Unclosed quotes (single and double)
|
|
7
|
-
* - Missing terminators (fi, done, esac)
|
|
8
|
-
* - Unbalanced brackets/braces
|
|
9
|
-
* - Invalid pipe/redirect syntax
|
|
10
|
-
*/
|
|
11
|
-
// ============================================================================
|
|
12
|
-
// Lexer
|
|
13
|
-
// ============================================================================
|
|
14
|
-
class Lexer {
|
|
15
|
-
input;
|
|
16
|
-
pos = 0;
|
|
17
|
-
line = 1;
|
|
18
|
-
column = 1;
|
|
19
|
-
errors = [];
|
|
20
|
-
constructor(input) {
|
|
21
|
-
this.input = input;
|
|
22
|
-
}
|
|
23
|
-
peek(offset = 0) {
|
|
24
|
-
return this.input[this.pos + offset] ?? '';
|
|
25
|
-
}
|
|
26
|
-
advance() {
|
|
27
|
-
const ch = this.input[this.pos] ?? '';
|
|
28
|
-
this.pos++;
|
|
29
|
-
if (ch === '\n') {
|
|
30
|
-
this.line++;
|
|
31
|
-
this.column = 1;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
this.column++;
|
|
35
|
-
}
|
|
36
|
-
return ch;
|
|
37
|
-
}
|
|
38
|
-
skipWhitespace() {
|
|
39
|
-
while (this.pos < this.input.length) {
|
|
40
|
-
const ch = this.peek();
|
|
41
|
-
if (ch === ' ' || ch === '\t') {
|
|
42
|
-
this.advance();
|
|
43
|
-
}
|
|
44
|
-
else if (ch === '\\' && this.peek(1) === '\n') {
|
|
45
|
-
// Line continuation
|
|
46
|
-
this.advance();
|
|
47
|
-
this.advance();
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
isWordChar(ch) {
|
|
55
|
-
// Not a special character
|
|
56
|
-
return ch !== '' &&
|
|
57
|
-
ch !== ' ' && ch !== '\t' && ch !== '\n' &&
|
|
58
|
-
ch !== '|' && ch !== '&' && ch !== ';' &&
|
|
59
|
-
ch !== '(' && ch !== ')' && ch !== '{' && ch !== '}' &&
|
|
60
|
-
ch !== '<' && ch !== '>' && ch !== '#';
|
|
61
|
-
}
|
|
62
|
-
readQuotedString(quote) {
|
|
63
|
-
const startLine = this.line;
|
|
64
|
-
const startColumn = this.column;
|
|
65
|
-
let result = quote;
|
|
66
|
-
this.advance(); // consume opening quote
|
|
67
|
-
while (this.pos < this.input.length) {
|
|
68
|
-
const ch = this.peek();
|
|
69
|
-
if (ch === quote) {
|
|
70
|
-
result += this.advance();
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
if (ch === '\\' && quote === '"') {
|
|
74
|
-
result += this.advance();
|
|
75
|
-
if (this.pos < this.input.length) {
|
|
76
|
-
result += this.advance();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else if (ch === '$' && quote === '"') {
|
|
80
|
-
// Handle parameter expansion and command/arithmetic substitution inside double quotes
|
|
81
|
-
result += this.readParameterExpansion();
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
result += this.advance();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Unclosed quote
|
|
88
|
-
const quoteType = quote === '"' ? 'double' : 'single';
|
|
89
|
-
this.errors.push({
|
|
90
|
-
message: `Unclosed ${quoteType} quote`,
|
|
91
|
-
line: startLine,
|
|
92
|
-
column: startColumn,
|
|
93
|
-
suggestion: `Add closing ${quote} at the end`,
|
|
94
|
-
});
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Read a quoted string inside a parameter expansion - doesn't report errors itself
|
|
99
|
-
*/
|
|
100
|
-
readQuotedStringInExpansion(quote) {
|
|
101
|
-
let result = quote;
|
|
102
|
-
this.advance(); // consume opening quote
|
|
103
|
-
while (this.pos < this.input.length) {
|
|
104
|
-
const ch = this.peek();
|
|
105
|
-
if (ch === quote) {
|
|
106
|
-
result += this.advance();
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
if (ch === '\\' && quote === '"') {
|
|
110
|
-
result += this.advance();
|
|
111
|
-
if (this.pos < this.input.length) {
|
|
112
|
-
result += this.advance();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
result += this.advance();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
// Return without the closing quote - parent will detect
|
|
120
|
-
return result;
|
|
121
|
-
}
|
|
122
|
-
readParameterExpansion() {
|
|
123
|
-
const startLine = this.line;
|
|
124
|
-
const startColumn = this.column;
|
|
125
|
-
let result = '$';
|
|
126
|
-
this.advance(); // consume $
|
|
127
|
-
const next = this.peek();
|
|
128
|
-
if (next === '(') {
|
|
129
|
-
// Command substitution $(...) or arithmetic $((...))
|
|
130
|
-
result += this.advance(); // consume (
|
|
131
|
-
if (this.peek() === '(') {
|
|
132
|
-
// Arithmetic expansion $((
|
|
133
|
-
result += this.advance(); // consume second (
|
|
134
|
-
let depth = 2;
|
|
135
|
-
let foundClosing = false;
|
|
136
|
-
while (this.pos < this.input.length) {
|
|
137
|
-
const ch = this.peek();
|
|
138
|
-
if (ch === '(') {
|
|
139
|
-
depth++;
|
|
140
|
-
result += this.advance();
|
|
141
|
-
}
|
|
142
|
-
else if (ch === ')') {
|
|
143
|
-
depth--;
|
|
144
|
-
result += this.advance();
|
|
145
|
-
if (depth === 0) {
|
|
146
|
-
foundClosing = true;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
result += this.advance();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (!foundClosing) {
|
|
155
|
-
this.errors.push({
|
|
156
|
-
message: 'Unclosed arithmetic expansion $((',
|
|
157
|
-
line: startLine,
|
|
158
|
-
column: startColumn,
|
|
159
|
-
suggestion: 'Add closing )) at the end',
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
// Command substitution $(
|
|
165
|
-
let depth = 1;
|
|
166
|
-
let foundClosing = false;
|
|
167
|
-
while (this.pos < this.input.length) {
|
|
168
|
-
const ch = this.peek();
|
|
169
|
-
if (ch === '"' || ch === "'") {
|
|
170
|
-
result += this.readQuotedString(ch);
|
|
171
|
-
}
|
|
172
|
-
else if (ch === '(') {
|
|
173
|
-
depth++;
|
|
174
|
-
result += this.advance();
|
|
175
|
-
}
|
|
176
|
-
else if (ch === ')') {
|
|
177
|
-
depth--;
|
|
178
|
-
result += this.advance();
|
|
179
|
-
if (depth === 0) {
|
|
180
|
-
foundClosing = true;
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
result += this.advance();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (!foundClosing) {
|
|
189
|
-
this.errors.push({
|
|
190
|
-
message: 'Unclosed command substitution $(',
|
|
191
|
-
line: startLine,
|
|
192
|
-
column: startColumn,
|
|
193
|
-
suggestion: 'Add closing ) at the end',
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
else if (next === '{') {
|
|
199
|
-
// Parameter expansion ${...}
|
|
200
|
-
result += this.advance(); // consume {
|
|
201
|
-
let depth = 1;
|
|
202
|
-
let foundClosing = false;
|
|
203
|
-
let hasUnclosedQuote = false;
|
|
204
|
-
while (this.pos < this.input.length) {
|
|
205
|
-
const ch = this.peek();
|
|
206
|
-
if (ch === '"' || ch === "'") {
|
|
207
|
-
const quotedStr = this.readQuotedStringInExpansion(ch);
|
|
208
|
-
result += quotedStr;
|
|
209
|
-
// Check if quote was unclosed (doesn't end with the same quote char it started with)
|
|
210
|
-
if (!quotedStr.endsWith(ch)) {
|
|
211
|
-
hasUnclosedQuote = true;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
else if (ch === '{') {
|
|
215
|
-
depth++;
|
|
216
|
-
result += this.advance();
|
|
217
|
-
}
|
|
218
|
-
else if (ch === '}') {
|
|
219
|
-
depth--;
|
|
220
|
-
result += this.advance();
|
|
221
|
-
if (depth === 0) {
|
|
222
|
-
foundClosing = true;
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
result += this.advance();
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (!foundClosing) {
|
|
231
|
-
if (hasUnclosedQuote) {
|
|
232
|
-
this.errors.push({
|
|
233
|
-
message: 'Unclosed quote in parameter expansion',
|
|
234
|
-
line: startLine,
|
|
235
|
-
column: startColumn,
|
|
236
|
-
suggestion: 'Add closing quote and } at the end',
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
this.errors.push({
|
|
241
|
-
message: 'Unclosed brace in parameter expansion ${',
|
|
242
|
-
line: startLine,
|
|
243
|
-
column: startColumn,
|
|
244
|
-
suggestion: 'Add closing } at the end',
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
// Simple variable $VAR
|
|
251
|
-
while (this.pos < this.input.length) {
|
|
252
|
-
const ch = this.peek();
|
|
253
|
-
if (/[a-zA-Z0-9_]/.test(ch)) {
|
|
254
|
-
result += this.advance();
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return result;
|
|
262
|
-
}
|
|
263
|
-
readWord() {
|
|
264
|
-
let result = '';
|
|
265
|
-
while (this.pos < this.input.length) {
|
|
266
|
-
const ch = this.peek();
|
|
267
|
-
if (ch === '"' || ch === "'") {
|
|
268
|
-
result += this.readQuotedString(ch);
|
|
269
|
-
}
|
|
270
|
-
else if (ch === '$') {
|
|
271
|
-
result += this.readParameterExpansion();
|
|
272
|
-
}
|
|
273
|
-
else if (ch === '\\' && this.peek(1) !== '') {
|
|
274
|
-
// Escape sequence
|
|
275
|
-
result += this.advance();
|
|
276
|
-
result += this.advance();
|
|
277
|
-
}
|
|
278
|
-
else if (ch === '`') {
|
|
279
|
-
// Backtick command substitution
|
|
280
|
-
const startLine = this.line;
|
|
281
|
-
const startColumn = this.column;
|
|
282
|
-
result += this.advance();
|
|
283
|
-
let foundClosing = false;
|
|
284
|
-
while (this.pos < this.input.length) {
|
|
285
|
-
const c = this.peek();
|
|
286
|
-
if (c === '`') {
|
|
287
|
-
result += this.advance();
|
|
288
|
-
foundClosing = true;
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
else if (c === '\\') {
|
|
292
|
-
result += this.advance();
|
|
293
|
-
if (this.pos < this.input.length) {
|
|
294
|
-
result += this.advance();
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
result += this.advance();
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (!foundClosing) {
|
|
302
|
-
this.errors.push({
|
|
303
|
-
message: 'Unclosed backtick command substitution',
|
|
304
|
-
line: startLine,
|
|
305
|
-
column: startColumn,
|
|
306
|
-
suggestion: 'Add closing ` at the end',
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
else if (this.isWordChar(ch)) {
|
|
311
|
-
result += this.advance();
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return result;
|
|
318
|
-
}
|
|
319
|
-
nextToken() {
|
|
320
|
-
this.skipWhitespace();
|
|
321
|
-
if (this.pos >= this.input.length) {
|
|
322
|
-
return { type: 'EOF', value: '', line: this.line, column: this.column };
|
|
323
|
-
}
|
|
324
|
-
const startLine = this.line;
|
|
325
|
-
const startColumn = this.column;
|
|
326
|
-
const ch = this.peek();
|
|
327
|
-
// Comments
|
|
328
|
-
if (ch === '#') {
|
|
329
|
-
while (this.pos < this.input.length && this.peek() !== '\n') {
|
|
330
|
-
this.advance();
|
|
331
|
-
}
|
|
332
|
-
return this.nextToken();
|
|
333
|
-
}
|
|
334
|
-
// Newline
|
|
335
|
-
if (ch === '\n') {
|
|
336
|
-
this.advance();
|
|
337
|
-
return { type: 'NEWLINE', value: '\n', line: startLine, column: startColumn };
|
|
338
|
-
}
|
|
339
|
-
// Semicolons and case pattern end
|
|
340
|
-
if (ch === ';') {
|
|
341
|
-
this.advance();
|
|
342
|
-
if (this.peek() === ';') {
|
|
343
|
-
this.advance();
|
|
344
|
-
return { type: 'CASE_PATTERN_END', value: ';;', line: startLine, column: startColumn };
|
|
345
|
-
}
|
|
346
|
-
return { type: 'SEMICOLON', value: ';', line: startLine, column: startColumn };
|
|
347
|
-
}
|
|
348
|
-
// Pipes and logical operators
|
|
349
|
-
if (ch === '|') {
|
|
350
|
-
this.advance();
|
|
351
|
-
if (this.peek() === '|') {
|
|
352
|
-
this.advance();
|
|
353
|
-
return { type: 'OR', value: '||', line: startLine, column: startColumn };
|
|
354
|
-
}
|
|
355
|
-
return { type: 'PIPE', value: '|', line: startLine, column: startColumn };
|
|
356
|
-
}
|
|
357
|
-
// And/background/combined redirect
|
|
358
|
-
if (ch === '&') {
|
|
359
|
-
this.advance();
|
|
360
|
-
if (this.peek() === '&') {
|
|
361
|
-
this.advance();
|
|
362
|
-
return { type: 'AND', value: '&&', line: startLine, column: startColumn };
|
|
363
|
-
}
|
|
364
|
-
if (this.peek() === '>') {
|
|
365
|
-
this.advance();
|
|
366
|
-
// &> redirects both stdout and stderr
|
|
367
|
-
return { type: 'REDIRECT_BOTH', value: '&>', line: startLine, column: startColumn };
|
|
368
|
-
}
|
|
369
|
-
return { type: 'BACKGROUND', value: '&', line: startLine, column: startColumn };
|
|
370
|
-
}
|
|
371
|
-
// Parentheses
|
|
372
|
-
if (ch === '(') {
|
|
373
|
-
this.advance();
|
|
374
|
-
return { type: 'LPAREN', value: '(', line: startLine, column: startColumn };
|
|
375
|
-
}
|
|
376
|
-
if (ch === ')') {
|
|
377
|
-
this.advance();
|
|
378
|
-
return { type: 'RPAREN', value: ')', line: startLine, column: startColumn };
|
|
379
|
-
}
|
|
380
|
-
// Braces
|
|
381
|
-
if (ch === '{') {
|
|
382
|
-
this.advance();
|
|
383
|
-
return { type: 'LBRACE', value: '{', line: startLine, column: startColumn };
|
|
384
|
-
}
|
|
385
|
-
if (ch === '}') {
|
|
386
|
-
this.advance();
|
|
387
|
-
return { type: 'RBRACE', value: '}', line: startLine, column: startColumn };
|
|
388
|
-
}
|
|
389
|
-
// Brackets
|
|
390
|
-
if (ch === '[') {
|
|
391
|
-
this.advance();
|
|
392
|
-
if (this.peek() === '[') {
|
|
393
|
-
this.advance();
|
|
394
|
-
return { type: 'DOUBLE_LBRACKET', value: '[[', line: startLine, column: startColumn };
|
|
395
|
-
}
|
|
396
|
-
return { type: 'LBRACKET', value: '[', line: startLine, column: startColumn };
|
|
397
|
-
}
|
|
398
|
-
if (ch === ']') {
|
|
399
|
-
this.advance();
|
|
400
|
-
if (this.peek() === ']') {
|
|
401
|
-
this.advance();
|
|
402
|
-
return { type: 'DOUBLE_RBRACKET', value: ']]', line: startLine, column: startColumn };
|
|
403
|
-
}
|
|
404
|
-
return { type: 'RBRACKET', value: ']', line: startLine, column: startColumn };
|
|
405
|
-
}
|
|
406
|
-
// Redirects
|
|
407
|
-
if (ch === '>') {
|
|
408
|
-
this.advance();
|
|
409
|
-
if (this.peek() === '>') {
|
|
410
|
-
this.advance();
|
|
411
|
-
return { type: 'REDIRECT_APPEND', value: '>>', line: startLine, column: startColumn };
|
|
412
|
-
}
|
|
413
|
-
if (this.peek() === '&') {
|
|
414
|
-
this.advance();
|
|
415
|
-
return { type: 'REDIRECT_OUT', value: '>&', line: startLine, column: startColumn };
|
|
416
|
-
}
|
|
417
|
-
return { type: 'REDIRECT_OUT', value: '>', line: startLine, column: startColumn };
|
|
418
|
-
}
|
|
419
|
-
if (ch === '<') {
|
|
420
|
-
this.advance();
|
|
421
|
-
if (this.peek() === '<') {
|
|
422
|
-
this.advance();
|
|
423
|
-
if (this.peek() === '<') {
|
|
424
|
-
this.advance();
|
|
425
|
-
return { type: 'REDIRECT_HERESTRING', value: '<<<', line: startLine, column: startColumn };
|
|
426
|
-
}
|
|
427
|
-
return { type: 'REDIRECT_HEREDOC', value: '<<', line: startLine, column: startColumn };
|
|
428
|
-
}
|
|
429
|
-
if (this.peek() === '&') {
|
|
430
|
-
this.advance();
|
|
431
|
-
return { type: 'REDIRECT_IN', value: '<&', line: startLine, column: startColumn };
|
|
432
|
-
}
|
|
433
|
-
return { type: 'REDIRECT_IN', value: '<', line: startLine, column: startColumn };
|
|
434
|
-
}
|
|
435
|
-
// Check for digit followed by redirect (e.g., 2>&1, 2>file, 1>out)
|
|
436
|
-
if (/[0-9]/.test(ch)) {
|
|
437
|
-
const nextCh = this.peek(1);
|
|
438
|
-
if (nextCh === '>' || nextCh === '<') {
|
|
439
|
-
const fd = parseInt(this.advance(), 10);
|
|
440
|
-
const redirectCh = this.advance();
|
|
441
|
-
if (redirectCh === '>') {
|
|
442
|
-
if (this.peek() === '>') {
|
|
443
|
-
this.advance();
|
|
444
|
-
return { type: 'REDIRECT_APPEND', value: '>>', line: startLine, column: startColumn, fd };
|
|
445
|
-
}
|
|
446
|
-
if (this.peek() === '&') {
|
|
447
|
-
this.advance();
|
|
448
|
-
return { type: 'REDIRECT_OUT', value: '>&', line: startLine, column: startColumn, fd };
|
|
449
|
-
}
|
|
450
|
-
return { type: 'REDIRECT_OUT', value: '>', line: startLine, column: startColumn, fd };
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
// <
|
|
454
|
-
if (this.peek() === '<') {
|
|
455
|
-
this.advance();
|
|
456
|
-
if (this.peek() === '<') {
|
|
457
|
-
this.advance();
|
|
458
|
-
return { type: 'REDIRECT_HERESTRING', value: '<<<', line: startLine, column: startColumn, fd };
|
|
459
|
-
}
|
|
460
|
-
return { type: 'REDIRECT_HEREDOC', value: '<<', line: startLine, column: startColumn, fd };
|
|
461
|
-
}
|
|
462
|
-
if (this.peek() === '&') {
|
|
463
|
-
this.advance();
|
|
464
|
-
return { type: 'REDIRECT_IN', value: '<&', line: startLine, column: startColumn, fd };
|
|
465
|
-
}
|
|
466
|
-
return { type: 'REDIRECT_IN', value: '<', line: startLine, column: startColumn, fd };
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
// Words (including keywords)
|
|
471
|
-
const word = this.readWord();
|
|
472
|
-
if (word === '') {
|
|
473
|
-
// Unknown character, try to recover
|
|
474
|
-
const badChar = this.advance();
|
|
475
|
-
this.errors.push({
|
|
476
|
-
message: `Unexpected character '${badChar}'`,
|
|
477
|
-
line: startLine,
|
|
478
|
-
column: startColumn,
|
|
479
|
-
});
|
|
480
|
-
return { type: 'ERROR', value: badChar, line: startLine, column: startColumn };
|
|
481
|
-
}
|
|
482
|
-
// Check for keywords
|
|
483
|
-
const keywords = {
|
|
484
|
-
if: 'IF',
|
|
485
|
-
then: 'THEN',
|
|
486
|
-
else: 'ELSE',
|
|
487
|
-
elif: 'ELIF',
|
|
488
|
-
fi: 'FI',
|
|
489
|
-
for: 'FOR',
|
|
490
|
-
while: 'WHILE',
|
|
491
|
-
until: 'UNTIL',
|
|
492
|
-
do: 'DO',
|
|
493
|
-
done: 'DONE',
|
|
494
|
-
case: 'CASE',
|
|
495
|
-
esac: 'ESAC',
|
|
496
|
-
in: 'IN',
|
|
497
|
-
};
|
|
498
|
-
const keywordType = keywords[word];
|
|
499
|
-
if (keywordType) {
|
|
500
|
-
return { type: keywordType, value: word, line: startLine, column: startColumn };
|
|
501
|
-
}
|
|
502
|
-
// Check for assignment (VAR=value)
|
|
503
|
-
const assignMatch = word.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(=|\+=)(.*)$/);
|
|
504
|
-
if (assignMatch) {
|
|
505
|
-
return { type: 'ASSIGNMENT', value: word, line: startLine, column: startColumn };
|
|
506
|
-
}
|
|
507
|
-
return { type: 'WORD', value: word, line: startLine, column: startColumn };
|
|
508
|
-
}
|
|
509
|
-
tokenize() {
|
|
510
|
-
const tokens = [];
|
|
511
|
-
let token;
|
|
512
|
-
while ((token = this.nextToken()).type !== 'EOF') {
|
|
513
|
-
if (token.type !== 'ERROR') {
|
|
514
|
-
tokens.push(token);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
tokens.push(token); // Include EOF
|
|
518
|
-
return tokens;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
// ============================================================================
|
|
522
|
-
// Parser
|
|
523
|
-
// ============================================================================
|
|
524
|
-
class Parser {
|
|
525
|
-
tokens;
|
|
526
|
-
pos = 0;
|
|
527
|
-
errors = [];
|
|
528
|
-
constructor(tokens, lexerErrors) {
|
|
529
|
-
this.tokens = tokens;
|
|
530
|
-
this.errors = [...lexerErrors];
|
|
531
|
-
}
|
|
532
|
-
peek(offset = 0) {
|
|
533
|
-
const idx = this.pos + offset;
|
|
534
|
-
if (idx < 0 || idx >= this.tokens.length) {
|
|
535
|
-
return { type: 'EOF', value: '', line: 0, column: 0 };
|
|
536
|
-
}
|
|
537
|
-
return this.tokens[idx];
|
|
538
|
-
}
|
|
539
|
-
advance() {
|
|
540
|
-
const token = this.tokens[this.pos];
|
|
541
|
-
if (this.pos < this.tokens.length) {
|
|
542
|
-
this.pos++;
|
|
543
|
-
}
|
|
544
|
-
return token;
|
|
545
|
-
}
|
|
546
|
-
/** @internal Reserved for future use */
|
|
547
|
-
expect(type, message) {
|
|
548
|
-
const token = this.peek();
|
|
549
|
-
if (token.type !== type) {
|
|
550
|
-
if (message) {
|
|
551
|
-
this.errors.push({
|
|
552
|
-
message,
|
|
553
|
-
line: token.line,
|
|
554
|
-
column: token.column,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
return this.advance();
|
|
560
|
-
}
|
|
561
|
-
skipNewlines() {
|
|
562
|
-
while (this.peek().type === 'NEWLINE') {
|
|
563
|
-
this.advance();
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
isCommandTerminator(token) {
|
|
567
|
-
return token.type === 'NEWLINE' ||
|
|
568
|
-
token.type === 'SEMICOLON' ||
|
|
569
|
-
token.type === 'PIPE' ||
|
|
570
|
-
token.type === 'AND' ||
|
|
571
|
-
token.type === 'OR' ||
|
|
572
|
-
token.type === 'BACKGROUND' ||
|
|
573
|
-
token.type === 'RPAREN' ||
|
|
574
|
-
token.type === 'RBRACE' ||
|
|
575
|
-
token.type === 'EOF';
|
|
576
|
-
}
|
|
577
|
-
isRedirectToken(token) {
|
|
578
|
-
return token.type === 'REDIRECT_OUT' ||
|
|
579
|
-
token.type === 'REDIRECT_APPEND' ||
|
|
580
|
-
token.type === 'REDIRECT_IN' ||
|
|
581
|
-
token.type === 'REDIRECT_HEREDOC' ||
|
|
582
|
-
token.type === 'REDIRECT_HERESTRING' ||
|
|
583
|
-
token.type === 'REDIRECT_BOTH';
|
|
584
|
-
}
|
|
585
|
-
parseRedirect() {
|
|
586
|
-
const token = this.peek();
|
|
587
|
-
if (!this.isRedirectToken(token)) {
|
|
588
|
-
return null;
|
|
589
|
-
}
|
|
590
|
-
this.advance();
|
|
591
|
-
this.skipWhitespaceTokens();
|
|
592
|
-
const targetToken = this.peek();
|
|
593
|
-
// Check for consecutive redirects (like > >)
|
|
594
|
-
if (this.isRedirectToken(targetToken)) {
|
|
595
|
-
this.errors.push({
|
|
596
|
-
message: `Consecutive redirects: unexpected ${targetToken.value} after ${token.value}`,
|
|
597
|
-
line: targetToken.line,
|
|
598
|
-
column: targetToken.column,
|
|
599
|
-
suggestion: 'Remove one of the redirect operators',
|
|
600
|
-
});
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
// Accept WORD, ASSIGNMENT, or IN (which can be a filename like 'in')
|
|
604
|
-
if (targetToken.type !== 'WORD' && targetToken.type !== 'ASSIGNMENT' && targetToken.type !== 'IN') {
|
|
605
|
-
this.errors.push({
|
|
606
|
-
message: `Redirect incomplete: missing file after ${token.value}`,
|
|
607
|
-
line: token.line,
|
|
608
|
-
column: token.column,
|
|
609
|
-
suggestion: 'Specify a file name after the redirect operator',
|
|
610
|
-
});
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
const target = this.advance();
|
|
614
|
-
const opMap = {
|
|
615
|
-
'>': '>',
|
|
616
|
-
'>&': '>&',
|
|
617
|
-
'>>': '>>',
|
|
618
|
-
'<': '<',
|
|
619
|
-
'<&': '<&',
|
|
620
|
-
'<<': '<<',
|
|
621
|
-
'<<<': '<<<',
|
|
622
|
-
'&>': '>&', // &> is equivalent to >& for redirecting both stdout and stderr
|
|
623
|
-
};
|
|
624
|
-
const redirect = {
|
|
625
|
-
type: 'Redirect',
|
|
626
|
-
op: opMap[token.value] ?? '>',
|
|
627
|
-
target: { type: 'Word', value: target.value },
|
|
628
|
-
};
|
|
629
|
-
// Include file descriptor if present
|
|
630
|
-
if (token.fd !== undefined) {
|
|
631
|
-
redirect.fd = token.fd;
|
|
632
|
-
}
|
|
633
|
-
return redirect;
|
|
634
|
-
}
|
|
635
|
-
skipWhitespaceTokens() {
|
|
636
|
-
// No explicit whitespace tokens in our lexer, but keep for consistency
|
|
637
|
-
}
|
|
638
|
-
parseWord() {
|
|
639
|
-
const token = this.peek();
|
|
640
|
-
if (token.type === 'WORD' || token.type === 'ASSIGNMENT') {
|
|
641
|
-
this.advance();
|
|
642
|
-
let quoted = undefined;
|
|
643
|
-
// Check if quoted
|
|
644
|
-
if (token.value.startsWith('"') || token.value.includes('"')) {
|
|
645
|
-
quoted = 'double';
|
|
646
|
-
}
|
|
647
|
-
else if (token.value.startsWith("'") || token.value.includes("'")) {
|
|
648
|
-
quoted = 'single';
|
|
649
|
-
}
|
|
650
|
-
return {
|
|
651
|
-
type: 'Word',
|
|
652
|
-
value: token.value,
|
|
653
|
-
quoted,
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
parseSimpleCommand() {
|
|
659
|
-
const prefix = [];
|
|
660
|
-
const args = [];
|
|
661
|
-
const redirects = [];
|
|
662
|
-
let name = null;
|
|
663
|
-
// Parse assignments at the start
|
|
664
|
-
while (this.peek().type === 'ASSIGNMENT') {
|
|
665
|
-
const token = this.advance();
|
|
666
|
-
const match = token.value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(=|\+=)(.*)$/);
|
|
667
|
-
if (match) {
|
|
668
|
-
prefix.push({
|
|
669
|
-
type: 'Assignment',
|
|
670
|
-
name: match[1],
|
|
671
|
-
operator: match[2],
|
|
672
|
-
value: match[3] ? { type: 'Word', value: match[3] } : null,
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
// Handle single [ test command
|
|
677
|
-
if (this.peek().type === 'LBRACKET') {
|
|
678
|
-
const startToken = this.advance();
|
|
679
|
-
name = { type: 'Word', value: '[' };
|
|
680
|
-
// Parse test expression arguments until ]
|
|
681
|
-
while (this.peek().type !== 'RBRACKET' && this.peek().type !== 'EOF' &&
|
|
682
|
-
!this.isCommandTerminator(this.peek())) {
|
|
683
|
-
const token = this.peek();
|
|
684
|
-
if (token.type === 'WORD') {
|
|
685
|
-
const word = this.parseWord();
|
|
686
|
-
if (word) {
|
|
687
|
-
args.push(word);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
else {
|
|
691
|
-
break;
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
// Expect closing ]
|
|
695
|
-
if (this.peek().type !== 'RBRACKET') {
|
|
696
|
-
this.errors.push({
|
|
697
|
-
message: 'Unclosed [ test bracket: missing ]',
|
|
698
|
-
line: startToken.line,
|
|
699
|
-
column: startToken.column,
|
|
700
|
-
suggestion: 'Add ] to close the test bracket',
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
else {
|
|
704
|
-
this.advance(); // consume ]
|
|
705
|
-
args.push({ type: 'Word', value: ']' });
|
|
706
|
-
}
|
|
707
|
-
return { type: 'Command', name, prefix, args, redirects };
|
|
708
|
-
}
|
|
709
|
-
// Parse command name and arguments
|
|
710
|
-
while (true) {
|
|
711
|
-
const token = this.peek();
|
|
712
|
-
if (this.isRedirectToken(token)) {
|
|
713
|
-
// Check for consecutive redirects (like > >)
|
|
714
|
-
const prevToken = this.peek(-1);
|
|
715
|
-
if (prevToken && this.isRedirectToken(prevToken)) {
|
|
716
|
-
this.errors.push({
|
|
717
|
-
message: `Consecutive redirects: unexpected ${token.value} after ${prevToken.value}`,
|
|
718
|
-
line: token.line,
|
|
719
|
-
column: token.column,
|
|
720
|
-
suggestion: 'Remove one of the redirect operators',
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
const redirect = this.parseRedirect();
|
|
724
|
-
if (redirect) {
|
|
725
|
-
redirects.push(redirect);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
else if (token.type === 'WORD') {
|
|
729
|
-
const word = this.parseWord();
|
|
730
|
-
if (word) {
|
|
731
|
-
if (!name) {
|
|
732
|
-
name = word;
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
args.push(word);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
else if (token.type === 'IN') {
|
|
740
|
-
// 'in' can appear as a filename (e.g., cmd < in), treat as word in command context
|
|
741
|
-
this.advance();
|
|
742
|
-
const word = { type: 'Word', value: 'in' };
|
|
743
|
-
if (!name) {
|
|
744
|
-
name = word;
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
args.push(word);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
else if (this.isCommandTerminator(token) || token.type === 'THEN' || token.type === 'DO' ||
|
|
751
|
-
token.type === 'ELSE' || token.type === 'ELIF' || token.type === 'FI' ||
|
|
752
|
-
token.type === 'DONE' || token.type === 'ESAC' || token.type === 'CASE_PATTERN_END') {
|
|
753
|
-
break;
|
|
754
|
-
}
|
|
755
|
-
else {
|
|
756
|
-
break;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
if (!name && prefix.length === 0 && redirects.length === 0) {
|
|
760
|
-
return null;
|
|
761
|
-
}
|
|
762
|
-
return {
|
|
763
|
-
type: 'Command',
|
|
764
|
-
name,
|
|
765
|
-
prefix,
|
|
766
|
-
args,
|
|
767
|
-
redirects,
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
parseSubshell() {
|
|
771
|
-
if (this.peek().type !== 'LPAREN') {
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
|
-
const startToken = this.advance(); // consume (
|
|
775
|
-
this.skipNewlines();
|
|
776
|
-
const body = [];
|
|
777
|
-
while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
|
|
778
|
-
const node = this.parseCompoundList();
|
|
779
|
-
if (node) {
|
|
780
|
-
body.push(node);
|
|
781
|
-
}
|
|
782
|
-
this.skipNewlines();
|
|
783
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
784
|
-
this.advance();
|
|
785
|
-
this.skipNewlines();
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
if (this.peek().type !== 'RPAREN') {
|
|
789
|
-
this.errors.push({
|
|
790
|
-
message: 'Unclosed subshell: missing )',
|
|
791
|
-
line: startToken.line,
|
|
792
|
-
column: startToken.column,
|
|
793
|
-
suggestion: 'Add ) to close the subshell',
|
|
794
|
-
});
|
|
795
|
-
return { type: 'Subshell', body };
|
|
796
|
-
}
|
|
797
|
-
this.advance(); // consume )
|
|
798
|
-
return { type: 'Subshell', body };
|
|
799
|
-
}
|
|
800
|
-
parseBraceGroup() {
|
|
801
|
-
if (this.peek().type !== 'LBRACE') {
|
|
802
|
-
return null;
|
|
803
|
-
}
|
|
804
|
-
const startToken = this.advance(); // consume {
|
|
805
|
-
this.skipNewlines();
|
|
806
|
-
const body = [];
|
|
807
|
-
while (this.peek().type !== 'RBRACE' && this.peek().type !== 'EOF') {
|
|
808
|
-
const node = this.parseCompoundList();
|
|
809
|
-
if (node) {
|
|
810
|
-
body.push(node);
|
|
811
|
-
}
|
|
812
|
-
this.skipNewlines();
|
|
813
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
814
|
-
this.advance();
|
|
815
|
-
this.skipNewlines();
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
if (this.peek().type !== 'RBRACE') {
|
|
819
|
-
this.errors.push({
|
|
820
|
-
message: 'Unclosed brace group: missing }',
|
|
821
|
-
line: startToken.line,
|
|
822
|
-
column: startToken.column,
|
|
823
|
-
suggestion: 'Add } to close the brace group',
|
|
824
|
-
});
|
|
825
|
-
return { type: 'CompoundCommand', kind: 'brace', body };
|
|
826
|
-
}
|
|
827
|
-
this.advance(); // consume }
|
|
828
|
-
return { type: 'CompoundCommand', kind: 'brace', body };
|
|
829
|
-
}
|
|
830
|
-
parseIfStatement() {
|
|
831
|
-
if (this.peek().type !== 'IF') {
|
|
832
|
-
return null;
|
|
833
|
-
}
|
|
834
|
-
const startToken = this.advance(); // consume 'if'
|
|
835
|
-
this.skipNewlines();
|
|
836
|
-
const body = [];
|
|
837
|
-
// Parse condition
|
|
838
|
-
const condition = this.parseCondition();
|
|
839
|
-
if (condition) {
|
|
840
|
-
body.push(condition);
|
|
841
|
-
}
|
|
842
|
-
this.skipNewlines();
|
|
843
|
-
// Skip semicolon before 'then' (common in one-line if statements)
|
|
844
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
845
|
-
this.advance();
|
|
846
|
-
this.skipNewlines();
|
|
847
|
-
}
|
|
848
|
-
// Expect 'then'
|
|
849
|
-
if (this.peek().type !== 'THEN') {
|
|
850
|
-
this.errors.push({
|
|
851
|
-
message: "Missing 'then' in if statement",
|
|
852
|
-
line: this.peek().line,
|
|
853
|
-
column: this.peek().column,
|
|
854
|
-
suggestion: "Add 'then' after the condition",
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
else {
|
|
858
|
-
this.advance();
|
|
859
|
-
}
|
|
860
|
-
this.skipNewlines();
|
|
861
|
-
// Parse 'then' body
|
|
862
|
-
while (this.peek().type !== 'ELSE' && this.peek().type !== 'ELIF' &&
|
|
863
|
-
this.peek().type !== 'FI' && this.peek().type !== 'EOF') {
|
|
864
|
-
const node = this.parseCompoundList();
|
|
865
|
-
if (node) {
|
|
866
|
-
body.push(node);
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
break;
|
|
870
|
-
}
|
|
871
|
-
this.skipNewlines();
|
|
872
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
873
|
-
this.advance();
|
|
874
|
-
this.skipNewlines();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
// Parse 'elif' or 'else'
|
|
878
|
-
while (this.peek().type === 'ELIF' || this.peek().type === 'ELSE') {
|
|
879
|
-
this.advance();
|
|
880
|
-
this.skipNewlines();
|
|
881
|
-
if (this.peek(-1).type === 'ELIF') {
|
|
882
|
-
// Parse elif condition
|
|
883
|
-
const elifCondition = this.parseCondition();
|
|
884
|
-
if (elifCondition) {
|
|
885
|
-
body.push(elifCondition);
|
|
886
|
-
}
|
|
887
|
-
this.skipNewlines();
|
|
888
|
-
// Skip semicolon before 'then'
|
|
889
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
890
|
-
this.advance();
|
|
891
|
-
this.skipNewlines();
|
|
892
|
-
}
|
|
893
|
-
if (this.peek().type === 'THEN') {
|
|
894
|
-
this.advance();
|
|
895
|
-
}
|
|
896
|
-
this.skipNewlines();
|
|
897
|
-
}
|
|
898
|
-
// Parse else/elif body
|
|
899
|
-
while (this.peek().type !== 'ELSE' && this.peek().type !== 'ELIF' &&
|
|
900
|
-
this.peek().type !== 'FI' && this.peek().type !== 'EOF') {
|
|
901
|
-
const node = this.parseCompoundList();
|
|
902
|
-
if (node) {
|
|
903
|
-
body.push(node);
|
|
904
|
-
}
|
|
905
|
-
else {
|
|
906
|
-
break;
|
|
907
|
-
}
|
|
908
|
-
this.skipNewlines();
|
|
909
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
910
|
-
this.advance();
|
|
911
|
-
this.skipNewlines();
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
// Expect 'fi'
|
|
916
|
-
if (this.peek().type !== 'FI') {
|
|
917
|
-
this.errors.push({
|
|
918
|
-
message: "Missing 'fi' to close if statement",
|
|
919
|
-
line: startToken.line,
|
|
920
|
-
column: startToken.column,
|
|
921
|
-
suggestion: "Add 'fi' at the end",
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
else {
|
|
925
|
-
this.advance();
|
|
926
|
-
}
|
|
927
|
-
return { type: 'CompoundCommand', kind: 'if', body };
|
|
928
|
-
}
|
|
929
|
-
parseForLoop() {
|
|
930
|
-
if (this.peek().type !== 'FOR') {
|
|
931
|
-
return null;
|
|
932
|
-
}
|
|
933
|
-
const startToken = this.advance(); // consume 'for'
|
|
934
|
-
this.skipNewlines();
|
|
935
|
-
const body = [];
|
|
936
|
-
// Check for C-style for loop: for ((...))
|
|
937
|
-
if (this.peek().type === 'LPAREN' && this.peek(1).type === 'LPAREN') {
|
|
938
|
-
this.advance(); // consume first (
|
|
939
|
-
this.advance(); // consume second (
|
|
940
|
-
// Parse the arithmetic expression until ))
|
|
941
|
-
let depth = 2;
|
|
942
|
-
let exprContent = '';
|
|
943
|
-
while (depth > 0 && this.peek().type !== 'EOF') {
|
|
944
|
-
const currentToken = this.peek();
|
|
945
|
-
if (currentToken.type === 'LPAREN') {
|
|
946
|
-
depth++;
|
|
947
|
-
exprContent += '(';
|
|
948
|
-
this.advance();
|
|
949
|
-
}
|
|
950
|
-
else if (currentToken.type === 'RPAREN') {
|
|
951
|
-
depth--;
|
|
952
|
-
if (depth > 0) {
|
|
953
|
-
exprContent += ')';
|
|
954
|
-
}
|
|
955
|
-
this.advance();
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
exprContent += currentToken.value;
|
|
959
|
-
this.advance();
|
|
960
|
-
}
|
|
961
|
-
// Add space between tokens (simplified)
|
|
962
|
-
if (depth > 0) {
|
|
963
|
-
exprContent += ' ';
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
// Store the C-style expression
|
|
967
|
-
if (exprContent.trim()) {
|
|
968
|
-
body.push({
|
|
969
|
-
type: 'Command',
|
|
970
|
-
name: { type: 'Word', value: exprContent.trim() },
|
|
971
|
-
prefix: [],
|
|
972
|
-
args: [],
|
|
973
|
-
redirects: []
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
this.skipNewlines();
|
|
977
|
-
// Allow semicolon before do
|
|
978
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
979
|
-
this.advance();
|
|
980
|
-
this.skipNewlines();
|
|
981
|
-
}
|
|
982
|
-
// Expect 'do'
|
|
983
|
-
if (this.peek().type !== 'DO') {
|
|
984
|
-
this.errors.push({
|
|
985
|
-
message: "Missing 'do' in for loop",
|
|
986
|
-
line: this.peek().line,
|
|
987
|
-
column: this.peek().column,
|
|
988
|
-
suggestion: "Add 'do' before the loop body",
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
else {
|
|
992
|
-
this.advance();
|
|
993
|
-
}
|
|
994
|
-
this.skipNewlines();
|
|
995
|
-
// Parse loop body
|
|
996
|
-
while (this.peek().type !== 'DONE' && this.peek().type !== 'EOF') {
|
|
997
|
-
const node = this.parseCompoundList();
|
|
998
|
-
if (node) {
|
|
999
|
-
body.push(node);
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
break;
|
|
1003
|
-
}
|
|
1004
|
-
this.skipNewlines();
|
|
1005
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
1006
|
-
this.advance();
|
|
1007
|
-
this.skipNewlines();
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
// Expect 'done'
|
|
1011
|
-
if (this.peek().type !== 'DONE') {
|
|
1012
|
-
this.errors.push({
|
|
1013
|
-
message: "Missing 'done' to close for loop",
|
|
1014
|
-
line: startToken.line,
|
|
1015
|
-
column: startToken.column,
|
|
1016
|
-
suggestion: "Add 'done' at the end",
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
else {
|
|
1020
|
-
this.advance();
|
|
1021
|
-
}
|
|
1022
|
-
return { type: 'CompoundCommand', kind: 'for', body };
|
|
1023
|
-
}
|
|
1024
|
-
// Standard for loop: for var in list
|
|
1025
|
-
// Parse variable name
|
|
1026
|
-
if (this.peek().type === 'WORD') {
|
|
1027
|
-
body.push({ type: 'Command', name: this.parseWord(), prefix: [], args: [], redirects: [] });
|
|
1028
|
-
}
|
|
1029
|
-
this.skipNewlines();
|
|
1030
|
-
// Expect 'in' (optional)
|
|
1031
|
-
if (this.peek().type === 'IN') {
|
|
1032
|
-
this.advance();
|
|
1033
|
-
this.skipNewlines();
|
|
1034
|
-
// Parse word list
|
|
1035
|
-
while (this.peek().type === 'WORD') {
|
|
1036
|
-
body.push({ type: 'Command', name: this.parseWord(), prefix: [], args: [], redirects: [] });
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
this.skipNewlines();
|
|
1040
|
-
// Allow semicolon before do
|
|
1041
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
1042
|
-
this.advance();
|
|
1043
|
-
this.skipNewlines();
|
|
1044
|
-
}
|
|
1045
|
-
// Expect 'do'
|
|
1046
|
-
if (this.peek().type !== 'DO') {
|
|
1047
|
-
this.errors.push({
|
|
1048
|
-
message: "Missing 'do' in for loop",
|
|
1049
|
-
line: this.peek().line,
|
|
1050
|
-
column: this.peek().column,
|
|
1051
|
-
suggestion: "Add 'do' before the loop body",
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
else {
|
|
1055
|
-
this.advance();
|
|
1056
|
-
}
|
|
1057
|
-
this.skipNewlines();
|
|
1058
|
-
// Parse loop body
|
|
1059
|
-
while (this.peek().type !== 'DONE' && this.peek().type !== 'EOF') {
|
|
1060
|
-
const node = this.parseCompoundList();
|
|
1061
|
-
if (node) {
|
|
1062
|
-
body.push(node);
|
|
1063
|
-
}
|
|
1064
|
-
else {
|
|
1065
|
-
break;
|
|
1066
|
-
}
|
|
1067
|
-
this.skipNewlines();
|
|
1068
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
1069
|
-
this.advance();
|
|
1070
|
-
this.skipNewlines();
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
// Expect 'done'
|
|
1074
|
-
if (this.peek().type !== 'DONE') {
|
|
1075
|
-
this.errors.push({
|
|
1076
|
-
message: "Missing 'done' to close for loop",
|
|
1077
|
-
line: startToken.line,
|
|
1078
|
-
column: startToken.column,
|
|
1079
|
-
suggestion: "Add 'done' at the end",
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
else {
|
|
1083
|
-
this.advance();
|
|
1084
|
-
}
|
|
1085
|
-
return { type: 'CompoundCommand', kind: 'for', body };
|
|
1086
|
-
}
|
|
1087
|
-
parseWhileLoop() {
|
|
1088
|
-
const tokenType = this.peek().type;
|
|
1089
|
-
if (tokenType !== 'WHILE' && tokenType !== 'UNTIL') {
|
|
1090
|
-
return null;
|
|
1091
|
-
}
|
|
1092
|
-
const startToken = this.advance(); // consume 'while' or 'until'
|
|
1093
|
-
const kind = tokenType === 'WHILE' ? 'while' : 'until';
|
|
1094
|
-
this.skipNewlines();
|
|
1095
|
-
const body = [];
|
|
1096
|
-
// Parse condition
|
|
1097
|
-
const condition = this.parseCondition();
|
|
1098
|
-
if (condition) {
|
|
1099
|
-
body.push(condition);
|
|
1100
|
-
}
|
|
1101
|
-
this.skipNewlines();
|
|
1102
|
-
// Allow semicolon before do
|
|
1103
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
1104
|
-
this.advance();
|
|
1105
|
-
this.skipNewlines();
|
|
1106
|
-
}
|
|
1107
|
-
// Expect 'do'
|
|
1108
|
-
if (this.peek().type !== 'DO') {
|
|
1109
|
-
this.errors.push({
|
|
1110
|
-
message: `Missing 'do' in ${kind} loop`,
|
|
1111
|
-
line: this.peek().line,
|
|
1112
|
-
column: this.peek().column,
|
|
1113
|
-
suggestion: "Add 'do' before the loop body",
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
this.advance();
|
|
1118
|
-
}
|
|
1119
|
-
this.skipNewlines();
|
|
1120
|
-
// Parse loop body
|
|
1121
|
-
while (this.peek().type !== 'DONE' && this.peek().type !== 'EOF') {
|
|
1122
|
-
const node = this.parseCompoundList();
|
|
1123
|
-
if (node) {
|
|
1124
|
-
body.push(node);
|
|
1125
|
-
}
|
|
1126
|
-
else {
|
|
1127
|
-
break;
|
|
1128
|
-
}
|
|
1129
|
-
this.skipNewlines();
|
|
1130
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
1131
|
-
this.advance();
|
|
1132
|
-
this.skipNewlines();
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
// Expect 'done'
|
|
1136
|
-
if (this.peek().type !== 'DONE') {
|
|
1137
|
-
this.errors.push({
|
|
1138
|
-
message: `Missing 'done' to close ${kind} loop`,
|
|
1139
|
-
line: startToken.line,
|
|
1140
|
-
column: startToken.column,
|
|
1141
|
-
suggestion: "Add 'done' at the end",
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
else {
|
|
1145
|
-
this.advance();
|
|
1146
|
-
}
|
|
1147
|
-
return { type: 'CompoundCommand', kind, body };
|
|
1148
|
-
}
|
|
1149
|
-
parseCaseStatement() {
|
|
1150
|
-
if (this.peek().type !== 'CASE') {
|
|
1151
|
-
return null;
|
|
1152
|
-
}
|
|
1153
|
-
const startToken = this.advance(); // consume 'case'
|
|
1154
|
-
this.skipNewlines();
|
|
1155
|
-
const body = [];
|
|
1156
|
-
// Parse word
|
|
1157
|
-
if (this.peek().type === 'WORD') {
|
|
1158
|
-
body.push({ type: 'Command', name: this.parseWord(), prefix: [], args: [], redirects: [] });
|
|
1159
|
-
}
|
|
1160
|
-
this.skipNewlines();
|
|
1161
|
-
// Expect 'in'
|
|
1162
|
-
if (this.peek().type === 'IN') {
|
|
1163
|
-
this.advance();
|
|
1164
|
-
}
|
|
1165
|
-
this.skipNewlines();
|
|
1166
|
-
// Parse case items
|
|
1167
|
-
while (this.peek().type !== 'ESAC' && this.peek().type !== 'EOF') {
|
|
1168
|
-
// Parse pattern
|
|
1169
|
-
if (this.peek().type === 'WORD' || this.peek().type === 'LPAREN') {
|
|
1170
|
-
// Skip optional (
|
|
1171
|
-
if (this.peek().type === 'LPAREN') {
|
|
1172
|
-
this.advance();
|
|
1173
|
-
}
|
|
1174
|
-
// Parse pattern word(s)
|
|
1175
|
-
while (this.peek().type === 'WORD') {
|
|
1176
|
-
this.advance();
|
|
1177
|
-
if (this.peek().type === 'PIPE') {
|
|
1178
|
-
this.advance(); // pattern separator
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
break;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
// Expect )
|
|
1185
|
-
if (this.peek().type === 'RPAREN') {
|
|
1186
|
-
this.advance();
|
|
1187
|
-
}
|
|
1188
|
-
this.skipNewlines();
|
|
1189
|
-
// Parse case body until ;;
|
|
1190
|
-
while (this.peek().type !== 'CASE_PATTERN_END' &&
|
|
1191
|
-
this.peek().type !== 'ESAC' &&
|
|
1192
|
-
this.peek().type !== 'EOF') {
|
|
1193
|
-
const node = this.parseCompoundList();
|
|
1194
|
-
if (node) {
|
|
1195
|
-
body.push(node);
|
|
1196
|
-
}
|
|
1197
|
-
else {
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1200
|
-
this.skipNewlines();
|
|
1201
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
1202
|
-
this.advance();
|
|
1203
|
-
this.skipNewlines();
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
// Consume ;;
|
|
1207
|
-
if (this.peek().type === 'CASE_PATTERN_END') {
|
|
1208
|
-
this.advance();
|
|
1209
|
-
}
|
|
1210
|
-
this.skipNewlines();
|
|
1211
|
-
}
|
|
1212
|
-
else {
|
|
1213
|
-
break;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
// Expect 'esac'
|
|
1217
|
-
if (this.peek().type !== 'ESAC') {
|
|
1218
|
-
this.errors.push({
|
|
1219
|
-
message: "Missing 'esac' to close case statement",
|
|
1220
|
-
line: startToken.line,
|
|
1221
|
-
column: startToken.column,
|
|
1222
|
-
suggestion: "Add 'esac' at the end",
|
|
1223
|
-
});
|
|
1224
|
-
}
|
|
1225
|
-
else {
|
|
1226
|
-
this.advance();
|
|
1227
|
-
}
|
|
1228
|
-
return { type: 'CompoundCommand', kind: 'case', body };
|
|
1229
|
-
}
|
|
1230
|
-
parseTestCommand() {
|
|
1231
|
-
const token = this.peek();
|
|
1232
|
-
// [[ test ]]
|
|
1233
|
-
if (token.type === 'DOUBLE_LBRACKET') {
|
|
1234
|
-
const startToken = this.advance();
|
|
1235
|
-
const body = [];
|
|
1236
|
-
// Parse test expression - [[ ]] can contain && and || as operators within the test
|
|
1237
|
-
while (this.peek().type !== 'DOUBLE_RBRACKET' && this.peek().type !== 'EOF') {
|
|
1238
|
-
const currentToken = this.peek();
|
|
1239
|
-
// Handle && and || inside [[ ]]
|
|
1240
|
-
if (currentToken.type === 'AND' || currentToken.type === 'OR') {
|
|
1241
|
-
const opToken = this.advance();
|
|
1242
|
-
body.push({ type: 'Command', name: { type: 'Word', value: opToken.value }, prefix: [], args: [], redirects: [] });
|
|
1243
|
-
}
|
|
1244
|
-
else {
|
|
1245
|
-
const word = this.parseWord();
|
|
1246
|
-
if (word) {
|
|
1247
|
-
body.push({ type: 'Command', name: word, prefix: [], args: [], redirects: [] });
|
|
1248
|
-
}
|
|
1249
|
-
else {
|
|
1250
|
-
break;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
if (this.peek().type !== 'DOUBLE_RBRACKET') {
|
|
1255
|
-
this.errors.push({
|
|
1256
|
-
message: 'Unclosed [[ test: missing ]]',
|
|
1257
|
-
line: startToken.line,
|
|
1258
|
-
column: startToken.column,
|
|
1259
|
-
suggestion: 'Add ]] to close the test',
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
else {
|
|
1263
|
-
this.advance();
|
|
1264
|
-
}
|
|
1265
|
-
return { type: 'CompoundCommand', kind: 'arithmetic', body };
|
|
1266
|
-
}
|
|
1267
|
-
// [ test ] - this is handled as a regular command
|
|
1268
|
-
return null;
|
|
1269
|
-
}
|
|
1270
|
-
parseCompoundCommand() {
|
|
1271
|
-
const token = this.peek();
|
|
1272
|
-
switch (token.type) {
|
|
1273
|
-
case 'IF':
|
|
1274
|
-
return this.parseIfStatement();
|
|
1275
|
-
case 'FOR':
|
|
1276
|
-
return this.parseForLoop();
|
|
1277
|
-
case 'WHILE':
|
|
1278
|
-
case 'UNTIL':
|
|
1279
|
-
return this.parseWhileLoop();
|
|
1280
|
-
case 'CASE':
|
|
1281
|
-
return this.parseCaseStatement();
|
|
1282
|
-
case 'LPAREN':
|
|
1283
|
-
return this.parseSubshell();
|
|
1284
|
-
case 'LBRACE':
|
|
1285
|
-
return this.parseBraceGroup();
|
|
1286
|
-
case 'DOUBLE_LBRACKET':
|
|
1287
|
-
return this.parseTestCommand();
|
|
1288
|
-
default:
|
|
1289
|
-
return null;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
parsePipeline() {
|
|
1293
|
-
const commands = [];
|
|
1294
|
-
let negated = false;
|
|
1295
|
-
// Check for negation (! at start of pipeline)
|
|
1296
|
-
if (this.peek().type === 'WORD' && this.peek().value === '!') {
|
|
1297
|
-
negated = true;
|
|
1298
|
-
this.advance(); // consume !
|
|
1299
|
-
this.skipWhitespaceTokens();
|
|
1300
|
-
}
|
|
1301
|
-
// Check for leading pipe (error)
|
|
1302
|
-
if (this.peek().type === 'PIPE') {
|
|
1303
|
-
this.errors.push({
|
|
1304
|
-
message: 'Pipe at start of command: command expected before |',
|
|
1305
|
-
line: this.peek().line,
|
|
1306
|
-
column: this.peek().column,
|
|
1307
|
-
suggestion: 'Add a command before the pipe',
|
|
1308
|
-
});
|
|
1309
|
-
this.advance(); // consume the pipe and continue
|
|
1310
|
-
}
|
|
1311
|
-
// Try compound command first
|
|
1312
|
-
const compound = this.parseCompoundCommand();
|
|
1313
|
-
if (compound) {
|
|
1314
|
-
// Check for pipe after compound
|
|
1315
|
-
this.skipNewlines();
|
|
1316
|
-
if (this.peek().type === 'PIPE') {
|
|
1317
|
-
// Compound commands in pipelines - for now, return as-is
|
|
1318
|
-
return compound;
|
|
1319
|
-
}
|
|
1320
|
-
return compound;
|
|
1321
|
-
}
|
|
1322
|
-
// Parse first simple command
|
|
1323
|
-
const firstCmd = this.parseSimpleCommand();
|
|
1324
|
-
if (!firstCmd) {
|
|
1325
|
-
return null;
|
|
1326
|
-
}
|
|
1327
|
-
commands.push(firstCmd);
|
|
1328
|
-
// Parse pipeline continuation
|
|
1329
|
-
while (this.peek().type === 'PIPE') {
|
|
1330
|
-
const pipeToken = this.advance();
|
|
1331
|
-
// Check for consecutive pipes
|
|
1332
|
-
if (this.peek().type === 'PIPE') {
|
|
1333
|
-
this.errors.push({
|
|
1334
|
-
message: 'Consecutive pipes: remove extra |',
|
|
1335
|
-
line: this.peek().line,
|
|
1336
|
-
column: this.peek().column,
|
|
1337
|
-
suggestion: 'Remove one of the consecutive pipes',
|
|
1338
|
-
});
|
|
1339
|
-
this.advance(); // consume extra pipe
|
|
1340
|
-
}
|
|
1341
|
-
this.skipNewlines();
|
|
1342
|
-
// Check for missing command after pipe
|
|
1343
|
-
if (this.isCommandTerminator(this.peek()) || this.peek().type === 'EOF') {
|
|
1344
|
-
this.errors.push({
|
|
1345
|
-
message: 'Pipe incomplete: missing command after |',
|
|
1346
|
-
line: pipeToken.line,
|
|
1347
|
-
column: pipeToken.column,
|
|
1348
|
-
suggestion: 'Add a command after the pipe',
|
|
1349
|
-
});
|
|
1350
|
-
break;
|
|
1351
|
-
}
|
|
1352
|
-
const nextCmd = this.parseSimpleCommand();
|
|
1353
|
-
if (nextCmd) {
|
|
1354
|
-
commands.push(nextCmd);
|
|
1355
|
-
}
|
|
1356
|
-
else {
|
|
1357
|
-
break;
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
if (commands.length === 1 && !negated) {
|
|
1361
|
-
return commands[0];
|
|
1362
|
-
}
|
|
1363
|
-
return {
|
|
1364
|
-
type: 'Pipeline',
|
|
1365
|
-
negated,
|
|
1366
|
-
commands,
|
|
1367
|
-
};
|
|
1368
|
-
}
|
|
1369
|
-
parseAndOr() {
|
|
1370
|
-
let left = this.parsePipeline();
|
|
1371
|
-
if (!left) {
|
|
1372
|
-
return null;
|
|
1373
|
-
}
|
|
1374
|
-
while (this.peek().type === 'AND' || this.peek().type === 'OR') {
|
|
1375
|
-
const opToken = this.advance();
|
|
1376
|
-
const operator = opToken.type === 'AND' ? '&&' : '||';
|
|
1377
|
-
this.skipNewlines();
|
|
1378
|
-
// Check for missing command after && or ||
|
|
1379
|
-
if (this.isCommandTerminator(this.peek()) || this.peek().type === 'EOF') {
|
|
1380
|
-
this.errors.push({
|
|
1381
|
-
message: `Incomplete ${operator === '&&' ? 'AND' : 'OR'} list: missing command after ${operator}`,
|
|
1382
|
-
line: opToken.line,
|
|
1383
|
-
column: opToken.column,
|
|
1384
|
-
suggestion: `Add a command after ${operator}`,
|
|
1385
|
-
});
|
|
1386
|
-
return left;
|
|
1387
|
-
}
|
|
1388
|
-
const right = this.parsePipeline();
|
|
1389
|
-
if (!right) {
|
|
1390
|
-
return left;
|
|
1391
|
-
}
|
|
1392
|
-
left = {
|
|
1393
|
-
type: 'List',
|
|
1394
|
-
operator,
|
|
1395
|
-
left,
|
|
1396
|
-
right,
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
return left;
|
|
1400
|
-
}
|
|
1401
|
-
/**
|
|
1402
|
-
* Parse a condition for if/while/until statements.
|
|
1403
|
-
* Unlike parseCompoundList, this doesn't report "unexpected semicolon" errors
|
|
1404
|
-
* since conditions are typically followed by semicolons in one-line syntax.
|
|
1405
|
-
*/
|
|
1406
|
-
parseCondition() {
|
|
1407
|
-
this.skipNewlines();
|
|
1408
|
-
return this.parseAndOr();
|
|
1409
|
-
}
|
|
1410
|
-
parseCompoundList() {
|
|
1411
|
-
this.skipNewlines();
|
|
1412
|
-
// Check for unexpected token at start
|
|
1413
|
-
if (this.peek().type === 'SEMICOLON') {
|
|
1414
|
-
const token = this.peek();
|
|
1415
|
-
this.errors.push({
|
|
1416
|
-
message: 'Unexpected semicolon at start of command',
|
|
1417
|
-
line: token.line,
|
|
1418
|
-
column: token.column,
|
|
1419
|
-
suggestion: 'Remove the leading semicolon',
|
|
1420
|
-
});
|
|
1421
|
-
this.advance();
|
|
1422
|
-
this.skipNewlines();
|
|
1423
|
-
}
|
|
1424
|
-
return this.parseAndOr();
|
|
1425
|
-
}
|
|
1426
|
-
parse() {
|
|
1427
|
-
const body = [];
|
|
1428
|
-
while (this.peek().type !== 'EOF') {
|
|
1429
|
-
this.skipNewlines();
|
|
1430
|
-
if (this.peek().type === 'EOF') {
|
|
1431
|
-
break;
|
|
1432
|
-
}
|
|
1433
|
-
const node = this.parseCompoundList();
|
|
1434
|
-
if (node) {
|
|
1435
|
-
body.push(node);
|
|
1436
|
-
}
|
|
1437
|
-
// Consume terminators
|
|
1438
|
-
const nextToken = this.peek();
|
|
1439
|
-
if (nextToken.type === 'SEMICOLON' || nextToken.type === 'NEWLINE' || nextToken.type === 'BACKGROUND') {
|
|
1440
|
-
this.advance();
|
|
1441
|
-
}
|
|
1442
|
-
else if (nextToken.type !== 'EOF' && !node) {
|
|
1443
|
-
// Stuck, skip token to avoid infinite loop
|
|
1444
|
-
this.advance();
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
return {
|
|
1448
|
-
type: 'Program',
|
|
1449
|
-
body,
|
|
1450
|
-
errors: this.errors.length > 0 ? this.errors : undefined,
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
// ============================================================================
|
|
1455
|
-
// Public API
|
|
1456
|
-
// ============================================================================
|
|
1457
|
-
/**
|
|
1458
|
-
* Parse a bash command string into an AST
|
|
1459
|
-
*
|
|
1460
|
-
* @example
|
|
1461
|
-
* ```typescript
|
|
1462
|
-
* const ast = parse('ls -la | grep foo')
|
|
1463
|
-
* // Returns Program with Pipeline containing two Commands
|
|
1464
|
-
* ```
|
|
1465
|
-
*/
|
|
1466
|
-
export function parse(input) {
|
|
1467
|
-
// Handle empty or whitespace-only input
|
|
1468
|
-
const trimmed = input.trim();
|
|
1469
|
-
if (trimmed === '') {
|
|
1470
|
-
return {
|
|
1471
|
-
type: 'Program',
|
|
1472
|
-
body: [],
|
|
1473
|
-
errors: undefined,
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
const lexer = new Lexer(input);
|
|
1477
|
-
const tokens = lexer.tokenize();
|
|
1478
|
-
const parser = new Parser(tokens, lexer.errors);
|
|
1479
|
-
return parser.parse();
|
|
1480
|
-
}
|
|
1481
|
-
/**
|
|
1482
|
-
* Check if input is syntactically valid bash
|
|
1483
|
-
*/
|
|
1484
|
-
export function isValidSyntax(input) {
|
|
1485
|
-
const ast = parse(input);
|
|
1486
|
-
return !ast.errors || ast.errors.length === 0;
|
|
1487
|
-
}
|
|
1488
|
-
//# sourceMappingURL=parser.js.map
|