@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.
- package/README.md +47 -26
- package/dist/_shared/byte-stream.d.ts +5 -4
- package/dist/_shared/byte-stream.d.ts.map +1 -1
- package/dist/_shared/cache-stats.d.ts +23 -1
- package/dist/_shared/cache-stats.d.ts.map +1 -1
- package/dist/_shared/cache-stats.js +13 -1
- package/dist/_shared/exports-resolver.d.ts +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/real-node-imports.d.ts +7 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -1
- package/dist/_shared/real-node-imports.js +8 -0
- package/dist/_shared/retry.js +1 -1
- package/dist/_shared/tarball.d.ts +23 -5
- package/dist/_shared/tarball.d.ts.map +1 -1
- package/dist/_shared/tarball.js +38 -5
- package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
- package/dist/_shared/vfs-write-ledger.js +22 -16
- package/dist/constants.d.ts +5 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -56
- package/dist/runtime/assets-loader.js +1 -1
- package/dist/runtime/comment-strip.d.ts +0 -35
- package/dist/runtime/comment-strip.d.ts.map +1 -1
- package/dist/runtime/comment-strip.js +48 -20
- package/dist/runtime/esbuild-service.d.ts +38 -42
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +494 -197
- package/dist/runtime/javascript-ast.d.ts.map +1 -1
- package/dist/runtime/javascript-ast.js +45 -7
- package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-backend.js +18 -16
- package/dist/runtime/port-registry.d.ts +4 -2
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +60 -8
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +1 -1
- package/dist/shell/stdin-adapter.d.ts +11 -0
- package/dist/shell/stdin-adapter.d.ts.map +1 -0
- package/dist/shell/stdin-adapter.js +59 -0
- package/dist/shell/unix-commands.d.ts +2 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +1078 -50
- package/dist/substrate/lifo/commands/io/dd.js +1 -1
- package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/net/curl.js +270 -54
- package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
- package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/npm.js +26 -43
- package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/head.js +2 -1
- package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/sed.js +499 -117
- package/dist/substrate/lifo/commands/types.d.ts +8 -4
- package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
- package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.js +122 -23
- package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
- package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/interpreter.js +79 -29
- package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
- package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/pipe.js +62 -18
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +1 -1
- package/dist/vfs/sqlite-vfs.d.ts +21 -30
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +113 -29
- package/dist/workspace/nimbus-workspace.d.ts +15 -0
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +20 -3
- package/dist/workspace/supervisor-op.d.ts +23 -0
- package/dist/workspace/supervisor-op.d.ts.map +1 -0
- package/dist/workspace/supervisor-op.js +92 -0
- package/package.json +3 -2
- package/src/_shared/byte-stream.ts +5 -4
- package/src/_shared/cache-stats.ts +27 -1
- package/src/_shared/exports-resolver.ts +1 -1
- package/src/_shared/real-node-imports.ts +8 -0
- package/src/_shared/retry.ts +1 -1
- package/src/_shared/tarball.ts +56 -5
- package/src/_shared/vfs-write-ledger.ts +22 -16
- package/src/constants.ts +30 -60
- package/src/runtime/assets-loader.ts +1 -1
- package/src/runtime/comment-strip.ts +42 -23
- package/src/runtime/esbuild-service.ts +566 -217
- package/src/runtime/javascript-ast.ts +40 -9
- package/src/runtime/opentui-wasm-backend.ts +19 -17
- package/src/runtime/port-registry.ts +66 -8
- package/src/runtime/virtual-socket-kernel.generated.ts +1 -1
- package/src/shell/stdin-adapter.ts +58 -0
- package/src/shell/unix-commands.ts +1054 -50
- package/src/substrate/lifo/commands/io/dd.ts +2 -2
- package/src/substrate/lifo/commands/net/curl.ts +310 -58
- package/src/substrate/lifo/commands/system/npm.ts +58 -38
- package/src/substrate/lifo/commands/text/head.ts +2 -1
- package/src/substrate/lifo/commands/text/sed.ts +519 -109
- package/src/substrate/lifo/commands/types.ts +8 -5
- package/src/substrate/lifo/node-compat/zlib.ts +155 -26
- package/src/substrate/lifo/shell/interpreter.ts +74 -27
- package/src/substrate/lifo/shell/pipe.ts +65 -24
- package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
- package/src/vfs/seed-project.ts +3 -3
- package/src/vfs/sqlite-vfs.ts +128 -64
- package/src/workspace/nimbus-workspace.ts +26 -2
- package/src/workspace/supervisor-op.ts +117 -0
- package/dist/_shared/install-phase.d.ts +0 -47
- package/dist/_shared/install-phase.d.ts.map +0 -1
- package/dist/_shared/install-phase.js +0 -1
- package/dist/_shared/rpc-dispose.d.ts +0 -4
- package/dist/_shared/rpc-dispose.d.ts.map +0 -1
- package/dist/_shared/rpc-dispose.js +0 -30
- package/dist/_shared/w7-frame.d.ts +0 -79
- package/dist/_shared/w7-frame.d.ts.map +0 -1
- package/dist/_shared/w7-frame.js +0 -840
- package/dist/_shared/weighted-credit-pool.d.ts +0 -61
- package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
- package/dist/_shared/weighted-credit-pool.js +0 -196
- package/dist/observability/diag-counters.d.ts +0 -236
- package/dist/observability/diag-counters.d.ts.map +0 -1
- package/dist/observability/diag-counters.js +0 -195
- package/dist/observability/heap-estimate.d.ts +0 -225
- package/dist/observability/heap-estimate.d.ts.map +0 -1
- package/dist/observability/heap-estimate.js +0 -185
- package/dist/observability/heavy-alloc-coord.d.ts +0 -93
- package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
- package/dist/observability/heavy-alloc-coord.js +0 -181
- package/dist/observability/oom-classify.d.ts +0 -138
- package/dist/observability/oom-classify.d.ts.map +0 -1
- package/dist/observability/oom-classify.js +0 -268
- package/dist/observability/oom-discriminator.d.ts +0 -176
- package/dist/observability/oom-discriminator.d.ts.map +0 -1
- package/dist/observability/oom-discriminator.js +0 -272
- package/src/_shared/install-phase.ts +0 -56
- package/src/_shared/rpc-dispose.ts +0 -31
- package/src/_shared/w7-frame.ts +0 -1013
- package/src/_shared/weighted-credit-pool.ts +0 -244
- package/src/observability/diag-counters.ts +0 -343
- package/src/observability/heap-estimate.ts +0 -331
- package/src/observability/heavy-alloc-coord.ts +0 -241
- package/src/observability/oom-classify.ts +0 -260
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
175
|
+
while (cur.i < cur.expr.length) {
|
|
176
|
+
const ch = peek(cur);
|
|
20
177
|
if (escaped) {
|
|
21
|
-
|
|
178
|
+
part += ch;
|
|
22
179
|
escaped = false;
|
|
180
|
+
cur.i++;
|
|
181
|
+
continue;
|
|
23
182
|
}
|
|
24
|
-
|
|
183
|
+
if (ch === '\\') {
|
|
25
184
|
escaped = true;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
parts.push(current);
|
|
30
|
-
current = '';
|
|
185
|
+
part += '\\';
|
|
186
|
+
cur.i++;
|
|
187
|
+
continue;
|
|
31
188
|
}
|
|
32
|
-
|
|
33
|
-
|
|
189
|
+
if (ch === delim) {
|
|
190
|
+
cur.i++;
|
|
191
|
+
return part;
|
|
34
192
|
}
|
|
193
|
+
part += ch;
|
|
194
|
+
cur.i++;
|
|
35
195
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
output.push(line);
|
|
121
|
-
}
|
|
443
|
+
return exitCode;
|
|
122
444
|
}
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
493
|
+
throw e;
|
|
135
494
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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 = {
|