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