@nimbus-sh/core 0.5.0 → 0.7.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.
Files changed (145) hide show
  1. package/README.md +47 -26
  2. package/dist/_shared/byte-stream.d.ts +5 -4
  3. package/dist/_shared/byte-stream.d.ts.map +1 -1
  4. package/dist/_shared/cache-stats.d.ts +23 -1
  5. package/dist/_shared/cache-stats.d.ts.map +1 -1
  6. package/dist/_shared/cache-stats.js +13 -1
  7. package/dist/_shared/exports-resolver.d.ts +1 -1
  8. package/dist/_shared/exports-resolver.js +1 -1
  9. package/dist/_shared/real-node-imports.d.ts +7 -0
  10. package/dist/_shared/real-node-imports.d.ts.map +1 -1
  11. package/dist/_shared/real-node-imports.js +8 -0
  12. package/dist/_shared/retry.js +1 -1
  13. package/dist/_shared/tarball.d.ts +23 -5
  14. package/dist/_shared/tarball.d.ts.map +1 -1
  15. package/dist/_shared/tarball.js +38 -5
  16. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
  17. package/dist/_shared/vfs-write-ledger.js +22 -16
  18. package/dist/constants.d.ts +5 -11
  19. package/dist/constants.d.ts.map +1 -1
  20. package/dist/constants.js +16 -56
  21. package/dist/runtime/assets-loader.js +1 -1
  22. package/dist/runtime/comment-strip.d.ts +0 -35
  23. package/dist/runtime/comment-strip.d.ts.map +1 -1
  24. package/dist/runtime/comment-strip.js +48 -20
  25. package/dist/runtime/esbuild-service.d.ts +38 -42
  26. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  27. package/dist/runtime/esbuild-service.js +494 -197
  28. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  29. package/dist/runtime/javascript-ast.js +45 -7
  30. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  31. package/dist/runtime/opentui-wasm-backend.js +18 -16
  32. package/dist/runtime/port-registry.d.ts +4 -2
  33. package/dist/runtime/port-registry.d.ts.map +1 -1
  34. package/dist/runtime/port-registry.js +60 -8
  35. package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
  36. package/dist/runtime/virtual-socket-kernel.generated.js +1 -1
  37. package/dist/shell/stdin-adapter.d.ts +11 -0
  38. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  39. package/dist/shell/stdin-adapter.js +59 -0
  40. package/dist/shell/unix-commands.d.ts +2 -1
  41. package/dist/shell/unix-commands.d.ts.map +1 -1
  42. package/dist/shell/unix-commands.js +1078 -50
  43. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  44. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  45. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  46. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  47. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  48. package/dist/substrate/lifo/commands/system/npm.js +26 -43
  49. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  50. package/dist/substrate/lifo/commands/text/head.js +2 -1
  51. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  52. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  53. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  54. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  55. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  56. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  57. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  58. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  59. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  60. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  61. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  62. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  63. package/dist/substrate/lifo/shell/pipe.js +62 -18
  64. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  65. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  66. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  67. package/dist/vfs/seed-project.d.ts.map +1 -1
  68. package/dist/vfs/seed-project.js +1 -1
  69. package/dist/vfs/sqlite-vfs.d.ts +21 -30
  70. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  71. package/dist/vfs/sqlite-vfs.js +113 -29
  72. package/dist/workspace/nimbus-workspace.d.ts +15 -0
  73. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  74. package/dist/workspace/nimbus-workspace.js +20 -3
  75. package/dist/workspace/supervisor-op.d.ts +23 -0
  76. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  77. package/dist/workspace/supervisor-op.js +92 -0
  78. package/package.json +3 -2
  79. package/src/_shared/byte-stream.ts +5 -4
  80. package/src/_shared/cache-stats.ts +27 -1
  81. package/src/_shared/exports-resolver.ts +1 -1
  82. package/src/_shared/real-node-imports.ts +8 -0
  83. package/src/_shared/retry.ts +1 -1
  84. package/src/_shared/tarball.ts +56 -5
  85. package/src/_shared/vfs-write-ledger.ts +22 -16
  86. package/src/constants.ts +30 -60
  87. package/src/runtime/assets-loader.ts +1 -1
  88. package/src/runtime/comment-strip.ts +42 -23
  89. package/src/runtime/esbuild-service.ts +566 -217
  90. package/src/runtime/javascript-ast.ts +40 -9
  91. package/src/runtime/opentui-wasm-backend.ts +19 -17
  92. package/src/runtime/port-registry.ts +66 -8
  93. package/src/runtime/virtual-socket-kernel.generated.ts +1 -1
  94. package/src/shell/stdin-adapter.ts +58 -0
  95. package/src/shell/unix-commands.ts +1054 -50
  96. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  97. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  98. package/src/substrate/lifo/commands/system/npm.ts +58 -38
  99. package/src/substrate/lifo/commands/text/head.ts +2 -1
  100. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  101. package/src/substrate/lifo/commands/types.ts +8 -5
  102. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  103. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  104. package/src/substrate/lifo/shell/pipe.ts +65 -24
  105. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  106. package/src/vfs/seed-project.ts +3 -3
  107. package/src/vfs/sqlite-vfs.ts +128 -64
  108. package/src/workspace/nimbus-workspace.ts +26 -2
  109. package/src/workspace/supervisor-op.ts +117 -0
  110. package/dist/_shared/install-phase.d.ts +0 -47
  111. package/dist/_shared/install-phase.d.ts.map +0 -1
  112. package/dist/_shared/install-phase.js +0 -1
  113. package/dist/_shared/rpc-dispose.d.ts +0 -4
  114. package/dist/_shared/rpc-dispose.d.ts.map +0 -1
  115. package/dist/_shared/rpc-dispose.js +0 -30
  116. package/dist/_shared/w7-frame.d.ts +0 -79
  117. package/dist/_shared/w7-frame.d.ts.map +0 -1
  118. package/dist/_shared/w7-frame.js +0 -840
  119. package/dist/_shared/weighted-credit-pool.d.ts +0 -61
  120. package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
  121. package/dist/_shared/weighted-credit-pool.js +0 -196
  122. package/dist/observability/diag-counters.d.ts +0 -236
  123. package/dist/observability/diag-counters.d.ts.map +0 -1
  124. package/dist/observability/diag-counters.js +0 -195
  125. package/dist/observability/heap-estimate.d.ts +0 -225
  126. package/dist/observability/heap-estimate.d.ts.map +0 -1
  127. package/dist/observability/heap-estimate.js +0 -185
  128. package/dist/observability/heavy-alloc-coord.d.ts +0 -93
  129. package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
  130. package/dist/observability/heavy-alloc-coord.js +0 -181
  131. package/dist/observability/oom-classify.d.ts +0 -138
  132. package/dist/observability/oom-classify.d.ts.map +0 -1
  133. package/dist/observability/oom-classify.js +0 -268
  134. package/dist/observability/oom-discriminator.d.ts +0 -176
  135. package/dist/observability/oom-discriminator.d.ts.map +0 -1
  136. package/dist/observability/oom-discriminator.js +0 -272
  137. package/src/_shared/install-phase.ts +0 -56
  138. package/src/_shared/rpc-dispose.ts +0 -31
  139. package/src/_shared/w7-frame.ts +0 -1013
  140. package/src/_shared/weighted-credit-pool.ts +0 -244
  141. package/src/observability/diag-counters.ts +0 -343
  142. package/src/observability/heap-estimate.ts +0 -331
  143. package/src/observability/heavy-alloc-coord.ts +0 -241
  144. package/src/observability/oom-classify.ts +0 -260
  145. package/src/observability/oom-discriminator.ts +0 -405
@@ -1,69 +1,275 @@
1
1
  import { resolve } from '../../utils/path.js';
2
2
  import { VFSError } from '../../kernel/vfs/index.js';
3
3
  import { getMimeType, isBinaryMime } from '../../utils/mime.js';
4
- function parseSedExpr(expr) {
5
- if (expr === 'd') {
6
- return { type: 'd' };
7
- }
8
- if (expr === 'p') {
9
- return { type: 'p' };
10
- }
11
- // s/pattern/replacement/flags
12
- if (expr.startsWith('s')) {
13
- const delim = expr[1];
14
- if (!delim)
15
- return null;
16
- const parts = [];
17
- let current = '';
4
+ class SedParseError extends Error {
5
+ expr;
6
+ constructor(expr) {
7
+ super(`invalid expression: ${expr}`);
8
+ this.expr = expr;
9
+ }
10
+ }
11
+ // GNU keeps already-written output when an empty pattern has nothing to
12
+ // repeat yet; whatever a pass emitted before the failure has already left
13
+ // through its sink.
14
+ class SedRuntimeError extends Error {
15
+ constructor() {
16
+ super('no previous regular expression');
17
+ }
18
+ }
19
+ // Yields logical lines on demand instead of materializing an array, so a
20
+ // multi-file pass holds only the current file's text plus one lookahead
21
+ // line. A file boundary starts a new logical line either way (GNU manual
22
+ // §6.1); `terminated` records whether the source ended the line itself.
23
+ function* iterateLogicalLines(text) {
24
+ if (text === '')
25
+ return;
26
+ const finalTerminated = text.endsWith('\n');
27
+ let start = 0;
28
+ for (let nl = text.indexOf('\n'); nl !== -1; nl = text.indexOf('\n', start)) {
29
+ yield { text: text.slice(start, nl), terminated: true };
30
+ start = nl + 1;
31
+ }
32
+ const tail = text.slice(start);
33
+ if (tail !== '' || !finalTerminated) {
34
+ yield { text: tail, terminated: finalTerminated };
35
+ }
36
+ }
37
+ function pullFrom(lines) {
38
+ return () => {
39
+ const step = lines.next();
40
+ return step.done ? null : step.value;
41
+ };
42
+ }
43
+ function parseSedScript(expr) {
44
+ const cur = { expr, i: 0 };
45
+ const commands = [];
46
+ for (;;) {
47
+ skipBlanks(cur);
48
+ const ch = peek(cur);
49
+ if (ch === undefined)
50
+ break;
51
+ if (ch === ';' || ch === '\n') {
52
+ cur.i++;
53
+ continue;
54
+ }
55
+ if (ch === '#') {
56
+ skipComment(cur);
57
+ continue;
58
+ }
59
+ commands.push(parseSedCommand(cur));
60
+ }
61
+ return commands;
62
+ }
63
+ function parseSedCommand(cur) {
64
+ let address;
65
+ const start = tryParseSedAddress(cur);
66
+ if (start) {
67
+ skipBlanks(cur);
68
+ if (peek(cur) === ',') {
69
+ cur.i++;
70
+ skipBlanks(cur);
71
+ const end = tryParseSedAddress(cur);
72
+ if (!end)
73
+ throw new SedParseError(cur.expr);
74
+ address = { kind: 'range', start, end };
75
+ }
76
+ else {
77
+ address = start;
78
+ }
79
+ }
80
+ skipBlanks(cur);
81
+ let negate = false;
82
+ if (peek(cur) === '!') {
83
+ negate = true;
84
+ cur.i++;
85
+ skipBlanks(cur);
86
+ }
87
+ if (!address && negate)
88
+ throw new SedParseError(cur.expr);
89
+ if (peek(cur) === '!')
90
+ throw new SedParseError(cur.expr);
91
+ const op = peek(cur);
92
+ if (op === 'd') {
93
+ cur.i++;
94
+ ensureCommandEnd(cur);
95
+ return { address, negate, type: 'd' };
96
+ }
97
+ if (op === 'p') {
98
+ cur.i++;
99
+ ensureCommandEnd(cur);
100
+ return { address, negate, type: 'p' };
101
+ }
102
+ if (op === 's') {
103
+ return { address, negate, ...parseSubstitution(cur) };
104
+ }
105
+ throw new SedParseError(cur.expr);
106
+ }
107
+ function tryParseSedAddress(cur) {
108
+ const ch = peek(cur);
109
+ if (ch === undefined)
110
+ return undefined;
111
+ if (isDigit(ch)) {
112
+ const start = cur.i;
113
+ while (isDigit(peek(cur)))
114
+ cur.i++;
115
+ const value = Number(cur.expr.slice(start, cur.i));
116
+ if (value === 0)
117
+ throw new SedParseError(cur.expr);
118
+ return { kind: 'line', value };
119
+ }
120
+ if (ch === '$') {
121
+ cur.i++;
122
+ return { kind: 'last' };
123
+ }
124
+ if (ch === '/') {
125
+ return parseDelimitedRegex(cur);
126
+ }
127
+ return undefined;
128
+ }
129
+ function parseDelimitedRegex(cur) {
130
+ cur.i++;
131
+ let raw = '';
132
+ let closed = false;
133
+ while (cur.i < cur.expr.length) {
134
+ const ch = peek(cur);
135
+ if (ch === '\\') {
136
+ // An escaped newline continues the pattern; everything else is literal.
137
+ const next = cur.expr[cur.i + 1];
138
+ if (next === undefined)
139
+ break;
140
+ raw += ch + next;
141
+ cur.i += 2;
142
+ continue;
143
+ }
144
+ if (ch === '\n')
145
+ throw new SedParseError(cur.expr);
146
+ if (ch === '/') {
147
+ cur.i++;
148
+ closed = true;
149
+ break;
150
+ }
151
+ raw += ch;
152
+ cur.i++;
153
+ }
154
+ if (!closed)
155
+ throw new SedParseError(cur.expr);
156
+ const source = toJavascriptPattern(raw);
157
+ if (source === '')
158
+ return { kind: 'empty' };
159
+ try {
160
+ return { kind: 'regex', regex: new RegExp(source) };
161
+ }
162
+ catch {
163
+ throw new SedParseError(cur.expr);
164
+ }
165
+ }
166
+ function parseSubstitution(cur) {
167
+ cur.i++;
168
+ const delim = peek(cur);
169
+ if (delim === undefined)
170
+ throw new SedParseError(cur.expr);
171
+ cur.i++;
172
+ const readPart = () => {
173
+ let part = '';
18
174
  let escaped = false;
19
- for (let i = 2; i < expr.length; i++) {
175
+ while (cur.i < cur.expr.length) {
176
+ const ch = peek(cur);
20
177
  if (escaped) {
21
- current += expr[i];
178
+ part += ch;
22
179
  escaped = false;
180
+ cur.i++;
181
+ continue;
23
182
  }
24
- else if (expr[i] === '\\') {
183
+ if (ch === '\\') {
25
184
  escaped = true;
26
- current += '\\';
27
- }
28
- else if (expr[i] === delim) {
29
- parts.push(current);
30
- current = '';
185
+ part += '\\';
186
+ cur.i++;
187
+ continue;
31
188
  }
32
- else {
33
- current += expr[i];
189
+ if (ch === delim) {
190
+ cur.i++;
191
+ return part;
34
192
  }
193
+ part += ch;
194
+ cur.i++;
35
195
  }
36
- parts.push(current); // remaining flags part
37
- if (parts.length < 2)
38
- return null;
39
- const patternStr = parts[0];
40
- const replacement = parts[1] ?? '';
41
- const flagStr = parts[2] || '';
42
- const globalFlag = flagStr.includes('g');
43
- const caseInsensitive = flagStr.includes('i');
44
- const print = flagStr.includes('p');
45
- let regex;
46
- try {
47
- let flags = '';
48
- if (globalFlag)
49
- flags += 'g';
50
- if (caseInsensitive)
51
- flags += 'i';
52
- regex = new RegExp(toJavascriptPattern(patternStr ?? ''), flags);
53
- }
54
- catch {
55
- return null;
56
- }
57
- return { type: 's', pattern: regex, replacement, global: globalFlag, print };
196
+ throw new SedParseError(cur.expr);
197
+ };
198
+ const patternStr = readPart();
199
+ const replacement = readPart();
200
+ // GNU's in_nonblank(): blanks may sit between flags; a flag run ends only
201
+ // at `;`, a newline, a comment, or the end of the script.
202
+ let flagStr = '';
203
+ for (;;) {
204
+ skipBlanks(cur);
205
+ const ch = peek(cur);
206
+ if (ch === undefined || ch === ';' || ch === '\n' || ch === '#')
207
+ break;
208
+ flagStr += ch;
209
+ cur.i++;
210
+ }
211
+ for (const flag of flagStr) {
212
+ if (flag !== 'g' && flag !== 'i' && flag !== 'p')
213
+ throw new SedParseError(cur.expr);
58
214
  }
59
- return null;
215
+ const global = flagStr.includes('g');
216
+ const insensitive = flagStr.includes('i');
217
+ if (patternStr === '') {
218
+ // Match modifiers belong to the repeated expression; only action flags
219
+ // may accompany an empty pattern.
220
+ if (insensitive)
221
+ throw new SedParseError(cur.expr);
222
+ return { type: 's', emptyPattern: true, replacement, global, insensitive, print: flagStr.includes('p') };
223
+ }
224
+ try {
225
+ let flags = '';
226
+ if (global)
227
+ flags += 'g';
228
+ if (insensitive)
229
+ flags += 'i';
230
+ const pattern = new RegExp(toJavascriptPattern(patternStr), flags);
231
+ return { type: 's', pattern, replacement, global, insensitive, print: flagStr.includes('p') };
232
+ }
233
+ catch {
234
+ throw new SedParseError(cur.expr);
235
+ }
236
+ }
237
+ // Blanks may surround an address or an operation, but a command ends only at
238
+ // `;`, a newline, a comment, or the end of the script — never at a following
239
+ // command letter, which GNU sed reports as extra characters.
240
+ function ensureCommandEnd(cur) {
241
+ skipBlanks(cur);
242
+ const ch = peek(cur);
243
+ if (ch === undefined || ch === ';' || ch === '\n' || ch === '#')
244
+ return;
245
+ throw new SedParseError(cur.expr);
246
+ }
247
+ function skipComment(cur) {
248
+ const nl = cur.expr.indexOf('\n', cur.i);
249
+ cur.i = nl === -1 ? cur.expr.length : nl + 1;
250
+ }
251
+ function skipBlanks(cur) {
252
+ while (isBlank(peek(cur)))
253
+ cur.i++;
254
+ }
255
+ function peek(cur) {
256
+ return cur.expr[cur.i];
257
+ }
258
+ function isBlank(value) {
259
+ return value === ' ' || value === '\t';
260
+ }
261
+ function isDigit(value) {
262
+ if (value === undefined)
263
+ return false;
264
+ const code = value.charCodeAt(0);
265
+ return code >= 48 && code <= 57;
60
266
  }
61
267
  function toJavascriptPattern(pattern) {
62
268
  let result = '';
63
269
  for (let i = 0; i < pattern.length; i++) {
64
270
  const char = pattern[i];
65
271
  const next = pattern[i + 1];
66
- if (char === '\\' && (next === '(' || next === ')')) {
272
+ if (char === '\\' && (next === '(' || next === ')' || next === '/')) {
67
273
  result += next;
68
274
  i++;
69
275
  continue;
@@ -72,9 +278,103 @@ function toJavascriptPattern(pattern) {
72
278
  }
73
279
  return result;
74
280
  }
75
- function isDigit(value) {
76
- const code = value.charCodeAt(0);
77
- return code >= 48 && code <= 57;
281
+ // One pass over a logical-line stream: per-command range state, cycle
282
+ // numbering, and GNU footnote-8 newline handling live here. A non-in-place
283
+ // run shares one pass across all files and hands every emitted chunk
284
+ // straight to its sink; an in-place run buffers a single file's chunks so
285
+ // its rewrite stays all-or-nothing. The previous-regex record is passed in
286
+ // so it spans passes.
287
+ class SedPass {
288
+ quiet;
289
+ out;
290
+ entries;
291
+ evalCtx;
292
+ pendingNewline = false;
293
+ constructor(commands, quiet, out, lastRegex) {
294
+ this.quiet = quiet;
295
+ this.out = out;
296
+ this.entries = commands.map((command) => ({ command, range: { open: false } }));
297
+ this.evalCtx = { line: '', lineNumber: 0, isLast: false, lastRegex };
298
+ }
299
+ // Runs one cycle per line with a single lookahead, so `$` still selects
300
+ // the true last line while later input is not read yet.
301
+ runAll(next) {
302
+ let current = next();
303
+ let lookahead = next();
304
+ let lineNumber = 0;
305
+ while (current !== null) {
306
+ this.runCycle(current.text, current.terminated, ++lineNumber, lookahead === null);
307
+ current = lookahead;
308
+ lookahead = next();
309
+ }
310
+ }
311
+ runCycle(text, terminated, lineNumber, isLast) {
312
+ const ctx = this.evalCtx;
313
+ ctx.lineNumber = lineNumber;
314
+ ctx.isLast = isLast;
315
+ let line = text;
316
+ let deleted = false;
317
+ for (const entry of this.entries) {
318
+ // Later addresses see the pattern space as earlier commands left it.
319
+ ctx.line = line;
320
+ if (!selects(entry.command, entry.range, ctx))
321
+ continue;
322
+ const command = entry.command;
323
+ if (command.type === 'p') {
324
+ this.emit(line, terminated);
325
+ }
326
+ else if (command.type === 'd') {
327
+ deleted = true;
328
+ break;
329
+ }
330
+ else if (command.type === 's' && command.replacement !== undefined) {
331
+ let regex = command.pattern;
332
+ if (command.emptyPattern) {
333
+ if (!ctx.lastRegex.source)
334
+ throw new SedRuntimeError();
335
+ // Match flags come from the repeated expression; only action
336
+ // flags such as g come from this substitution.
337
+ let flags = ctx.lastRegex.flags ?? '';
338
+ if (command.global && !flags.includes('g'))
339
+ flags += 'g';
340
+ regex = new RegExp(ctx.lastRegex.source, flags);
341
+ }
342
+ if (!regex)
343
+ continue;
344
+ let changed = false;
345
+ regex.lastIndex = 0;
346
+ line = line.replace(regex, (...match) => {
347
+ changed = true;
348
+ return expandReplacement(command.replacement ?? '', match.slice(1, -2), String(match[0]));
349
+ });
350
+ if (!command.emptyPattern) {
351
+ ctx.lastRegex.source = regex.source;
352
+ // Only match modifiers repeat with an empty pattern; g is an
353
+ // action flag of the substitution that used the expression.
354
+ ctx.lastRegex.flags = command.insensitive ? 'i' : '';
355
+ }
356
+ if (changed && command.print)
357
+ this.emit(line, terminated);
358
+ }
359
+ }
360
+ if (!deleted && !this.quiet)
361
+ this.emit(line, terminated);
362
+ }
363
+ emit(text, terminated) {
364
+ if (!terminated) {
365
+ // Footnote 8: the missing newline waits until more output follows.
366
+ if (this.pendingNewline)
367
+ this.out('\n');
368
+ this.out(text);
369
+ this.pendingNewline = true;
370
+ return;
371
+ }
372
+ if (this.pendingNewline) {
373
+ this.out('\n');
374
+ this.pendingNewline = false;
375
+ }
376
+ this.out(`${text}\n`);
377
+ }
78
378
  }
79
379
  export async function runSed(ctx) {
80
380
  const options = parseSedArgs(ctx.args);
@@ -82,86 +382,168 @@ export async function runSed(ctx) {
82
382
  ctx.stderr.write('sed: missing expression\n');
83
383
  return 1;
84
384
  }
85
- const parsedExprs = [];
385
+ // Execution state: the last regular expression any address or substitution
386
+ // actually evaluated, shared by `//` and empty s patterns across -e chunks
387
+ // and files.
388
+ const lastRegex = {};
389
+ const commands = [];
86
390
  for (const expr of options.expressions) {
87
- const parsed = parseSedExpr(expr);
88
- if (!parsed) {
89
- ctx.stderr.write(`sed: invalid expression: ${expr}\n`);
90
- return 1;
391
+ try {
392
+ commands.push(...parseSedScript(expr));
393
+ }
394
+ catch (e) {
395
+ if (e instanceof SedParseError) {
396
+ ctx.stderr.write(`sed: invalid expression: ${expr}\n`);
397
+ return 1;
398
+ }
399
+ throw e;
400
+ }
401
+ }
402
+ const streamOut = (chunk) => ctx.stdout.write(chunk);
403
+ try {
404
+ if (options.files.length === 0) {
405
+ if (ctx.stdin) {
406
+ const lines = iterateLogicalLines(await ctx.stdin.readAll());
407
+ new SedPass(commands, options.quiet, streamOut, lastRegex).runAll(pullFrom(lines));
408
+ }
409
+ else {
410
+ ctx.stderr.write('sed: missing file operand\n');
411
+ return 1;
412
+ }
413
+ return 0;
91
414
  }
92
- parsedExprs.push(parsed);
93
- }
94
- function processText(text) {
95
- const lines = text.replace(/\n$/, '').split('\n');
96
- const output = [];
97
- for (let line of lines) {
98
- let deleted = false;
99
- for (const expr of parsedExprs) {
100
- if (expr.type === 's' && expr.pattern && expr.replacement !== undefined) {
101
- let changed = false;
102
- expr.pattern.lastIndex = 0;
103
- line = line.replace(expr.pattern, (...match) => {
104
- changed = true;
105
- return expandReplacement(expr.replacement ?? '', match.slice(1, -2), String(match[0]));
106
- });
107
- if (changed && expr.print) {
108
- output.push(line);
415
+ if (options.inPlace) {
416
+ let exitCode = 0;
417
+ for (const file of options.files) {
418
+ const path = resolve(ctx.cwd, file);
419
+ try {
420
+ ctx.vfs.stat(path);
421
+ if (isBinaryMime(getMimeType(path))) {
422
+ ctx.stderr.write(`sed: ${file}: binary file, skipping\n`);
423
+ continue;
109
424
  }
425
+ const content = ctx.vfs.readFileString(path);
426
+ // One buffered pass per file: nothing touches the file until its
427
+ // whole result exists, keeping the rewrite all-or-nothing.
428
+ const chunks = [];
429
+ new SedPass(commands, options.quiet, (chunk) => chunks.push(chunk), lastRegex)
430
+ .runAll(pullFrom(iterateLogicalLines(content)));
431
+ ctx.vfs.writeFile(path, chunks.join(''));
110
432
  }
111
- else if (expr.type === 'd') {
112
- deleted = true;
113
- break;
114
- }
115
- else if (expr.type === 'p') {
116
- output.push(line);
433
+ catch (e) {
434
+ if (e instanceof VFSError) {
435
+ ctx.stderr.write(`sed: ${file}: ${e.message}\n`);
436
+ exitCode = 1;
437
+ }
438
+ else {
439
+ throw e;
440
+ }
117
441
  }
118
442
  }
119
- if (!deleted && !options.quiet) {
120
- output.push(line);
121
- }
443
+ return exitCode;
122
444
  }
123
- return output.join('\n') + '\n';
445
+ // All readable files feed one shared pass, so global line numbers, range
446
+ // state, `$` and the previous-regex record span files. Each file opens
447
+ // only after the previous one is fully consumed — at most one file's text
448
+ // is ever held — and output leaves as each line is decided.
449
+ let exitCode = 0;
450
+ let index = 0;
451
+ let lines = null;
452
+ const nextLine = () => {
453
+ for (;;) {
454
+ if (lines !== null) {
455
+ const step = lines.next();
456
+ if (!step.done)
457
+ return step.value;
458
+ lines = null; // release this file before any later file is read
459
+ }
460
+ if (index >= options.files.length)
461
+ return null;
462
+ const file = options.files[index++];
463
+ const path = resolve(ctx.cwd, file);
464
+ try {
465
+ ctx.vfs.stat(path);
466
+ if (isBinaryMime(getMimeType(path))) {
467
+ ctx.stderr.write(`sed: ${file}: binary file, skipping\n`);
468
+ continue;
469
+ }
470
+ lines = iterateLogicalLines(ctx.vfs.readFileString(path));
471
+ }
472
+ catch (e) {
473
+ if (e instanceof VFSError) {
474
+ ctx.stderr.write(`sed: ${file}: ${e.message}\n`);
475
+ exitCode = 1;
476
+ }
477
+ else {
478
+ throw e;
479
+ }
480
+ }
481
+ }
482
+ };
483
+ new SedPass(commands, options.quiet, streamOut, lastRegex).runAll(nextLine);
484
+ return exitCode;
124
485
  }
125
- if (options.files.length === 0) {
126
- if (ctx.stdin) {
127
- const text = await ctx.stdin.readAll();
128
- ctx.stdout.write(processText(text));
129
- }
130
- else {
131
- ctx.stderr.write('sed: missing file operand\n');
486
+ catch (e) {
487
+ if (e instanceof SedRuntimeError) {
488
+ // Output emitted before the failing cycle already reached stdout; an
489
+ // in-place run wrote no file for the aborted pass.
490
+ ctx.stderr.write(`sed: ${e.message}\n`);
132
491
  return 1;
133
492
  }
134
- return 0;
493
+ throw e;
135
494
  }
136
- let exitCode = 0;
137
- for (const file of options.files) {
138
- const path = resolve(ctx.cwd, file);
139
- try {
140
- ctx.vfs.stat(path);
141
- if (isBinaryMime(getMimeType(path))) {
142
- ctx.stderr.write(`sed: ${file}: binary file, skipping\n`);
143
- continue;
144
- }
145
- const content = ctx.vfs.readFileString(path);
146
- const result = processText(content);
147
- if (options.inPlace) {
148
- ctx.vfs.writeFile(path, result);
149
- }
150
- else {
151
- ctx.stdout.write(result);
152
- }
495
+ }
496
+ function selects(command, range, ctx) {
497
+ const address = command.address;
498
+ let selected;
499
+ if (!address) {
500
+ selected = true;
501
+ }
502
+ else if (address.kind === 'range') {
503
+ selected = rangeSelects(address, range, ctx);
504
+ }
505
+ else {
506
+ selected = addressSelects(address, ctx);
507
+ }
508
+ return command.negate ? !selected : selected;
509
+ }
510
+ function addressSelects(address, ctx) {
511
+ switch (address.kind) {
512
+ case 'line':
513
+ return ctx.lineNumber === address.value;
514
+ case 'last':
515
+ return ctx.isLast;
516
+ case 'empty': {
517
+ // `//` reads the record without writing it; nothing may establish one.
518
+ if (!ctx.lastRegex.source)
519
+ throw new SedRuntimeError();
520
+ return new RegExp(ctx.lastRegex.source, ctx.lastRegex.flags ?? '').test(ctx.line);
153
521
  }
154
- catch (e) {
155
- if (e instanceof VFSError) {
156
- ctx.stderr.write(`sed: ${file}: ${e.message}\n`);
157
- exitCode = 1;
158
- }
159
- else {
160
- throw e;
161
- }
522
+ case 'regex': {
523
+ const matched = address.regex.test(ctx.line);
524
+ ctx.lastRegex.source = address.regex.source;
525
+ ctx.lastRegex.flags = address.regex.flags;
526
+ return matched;
162
527
  }
163
528
  }
164
- return exitCode;
529
+ }
530
+ function rangeSelects(range, state, ctx) {
531
+ if (!state.open) {
532
+ if (!addressSelects(range.start, ctx))
533
+ return false;
534
+ // A numeric end at or before the start line leaves the range one line wide.
535
+ state.open = !(range.end.kind === 'line' && range.end.value <= ctx.lineNumber);
536
+ return true;
537
+ }
538
+ // A regular-expression end is first tested on the line after the range opens.
539
+ const closes = range.end.kind === 'line'
540
+ ? ctx.lineNumber >= range.end.value
541
+ : range.end.kind === 'last'
542
+ ? ctx.isLast
543
+ : addressSelects(range.end, ctx);
544
+ if (closes)
545
+ state.open = false;
546
+ return true;
165
547
  }
166
548
  function parseSedArgs(args) {
167
549
  const options = {