@j-o-r/sh 0.0.4 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -64
- package/lib/sh.d.ts +100 -170
- package/lib/sh.js +284 -850
- package/package.json +2 -2
package/lib/sh.js
CHANGED
|
@@ -1,34 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
-
import { exec, spawn } from 'node:child_process';
|
|
4
|
-
import { inspect } from 'node:util';
|
|
1
|
+
import { spawnSync, spawn, exec } from 'node:child_process';
|
|
5
2
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
-
// you may not use this file except in compliance with the License.
|
|
10
|
-
// You may obtain a copy of the License at
|
|
11
|
-
//
|
|
12
|
-
// https://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
-
//
|
|
14
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
15
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
-
// See the License for the specific language governing permissions and
|
|
18
|
-
// limitations under the License.
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// Original Source: zx
|
|
22
|
-
// Link to Original Source: https://github.com/google/zx
|
|
23
|
-
// Reason for Using This Code:
|
|
24
|
-
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
25
|
-
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
26
|
-
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
27
|
-
// Changes Made:
|
|
28
|
-
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
29
|
-
// - Some methods were added and existing ones were modified to enhance usability.
|
|
30
|
-
// - The namespace has been changed from '$' to 'SH'.
|
|
31
|
-
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
3
|
+
// timeout when a process becomes inresponsive
|
|
32
4
|
|
|
33
5
|
/**
|
|
34
6
|
* Kills a process and all child processes of a given process ID in Linux/Posix.
|
|
@@ -36,7 +8,7 @@ import { inspect } from 'node:util';
|
|
|
36
8
|
* @param {string} signal - Signal to send.
|
|
37
9
|
* @retruns {Promise<number[]>} array with killed pid numbers
|
|
38
10
|
*/
|
|
39
|
-
|
|
11
|
+
const killProcesses = (processPid, signal) => {
|
|
40
12
|
const killed = [];
|
|
41
13
|
return new Promise((resolve, reject) => {
|
|
42
14
|
// Command to get child PIDs of the given process
|
|
@@ -72,487 +44,128 @@ function killProcesses(processPid, signal) {
|
|
|
72
44
|
resolve(killed);
|
|
73
45
|
});
|
|
74
46
|
});
|
|
75
|
-
}
|
|
76
|
-
function noop() { }
|
|
77
|
-
function quote(arg) {
|
|
78
|
-
if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
|
|
79
|
-
return arg;
|
|
80
|
-
}
|
|
81
|
-
return (`$'` +
|
|
82
|
-
arg
|
|
83
|
-
.replace(/\\/g, '\\\\')
|
|
84
|
-
.replace(/'/g, "\\'")
|
|
85
|
-
.replace(/\f/g, '\\f')
|
|
86
|
-
.replace(/\n/g, '\\n')
|
|
87
|
-
.replace(/\r/g, '\\r')
|
|
88
|
-
.replace(/\t/g, '\\t')
|
|
89
|
-
.replace(/\v/g, '\\v')
|
|
90
|
-
.replace(/\0/g, '\\0') +
|
|
91
|
-
`'`);
|
|
92
|
-
}
|
|
93
|
-
function quotePowerShell(arg) {
|
|
94
|
-
if (/^[a-z0-9/_.\-]+$/i.test(arg) || arg === '') {
|
|
95
|
-
return arg;
|
|
96
|
-
}
|
|
97
|
-
return `'` + arg.replace(/'/g, "''") + `'`;
|
|
98
|
-
}
|
|
99
|
-
function log(entry) {
|
|
100
|
-
switch (entry.kind) {
|
|
101
|
-
case 'cmd':
|
|
102
|
-
if (!entry.verbose) return;
|
|
103
|
-
process.stderr.write(formatCmd(entry.cmd));
|
|
104
|
-
break;
|
|
105
|
-
case 'stdout':
|
|
106
|
-
case 'stderr':
|
|
107
|
-
if (!entry.verbose) return;
|
|
108
|
-
process.stderr.write(entry.data);
|
|
109
|
-
break;
|
|
110
|
-
case 'retry':
|
|
111
|
-
if (!entry.verbose) return;
|
|
112
|
-
process.stderr.write(entry.error + '\n');
|
|
113
|
-
break;
|
|
114
|
-
case 'cd':
|
|
115
|
-
if (!entry.verbose) return;
|
|
116
|
-
process.stderr.write(`cd ${entry.dir}\n`);
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function exitCodeInfo(exitCode) {
|
|
121
|
-
return {
|
|
122
|
-
2: 'Misuse of shell builtins',
|
|
123
|
-
126: 'Invoked command cannot execute',
|
|
124
|
-
127: 'Command not found',
|
|
125
|
-
128: 'Invalid exit argument',
|
|
126
|
-
129: 'Hangup',
|
|
127
|
-
130: 'Interrupt',
|
|
128
|
-
131: 'Quit and dump core',
|
|
129
|
-
132: 'Illegal instruction',
|
|
130
|
-
133: 'Trace/breakpoint trap',
|
|
131
|
-
134: 'Process aborted',
|
|
132
|
-
135: 'Bus error: "access to undefined portion of memory object"',
|
|
133
|
-
136: 'Floating point exception: "erroneous arithmetic operation"',
|
|
134
|
-
137: 'Kill (terminate immediately)',
|
|
135
|
-
138: 'User-defined 1',
|
|
136
|
-
139: 'Segmentation violation',
|
|
137
|
-
140: 'User-defined 2',
|
|
138
|
-
141: 'Write to pipe with no one reading',
|
|
139
|
-
142: 'Signal raised by alarm',
|
|
140
|
-
143: 'Termination (request to terminate)',
|
|
141
|
-
145: 'Child process terminated, stopped (or continued*)',
|
|
142
|
-
146: 'Continue if stopped',
|
|
143
|
-
147: 'Stop executing temporarily',
|
|
144
|
-
148: 'Terminal stop signal',
|
|
145
|
-
149: 'Background process attempting to read from tty ("in")',
|
|
146
|
-
150: 'Background process attempting to write to tty ("out")',
|
|
147
|
-
151: 'Urgent data available on socket',
|
|
148
|
-
152: 'CPU time limit exceeded',
|
|
149
|
-
153: 'File size limit exceeded',
|
|
150
|
-
154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
|
|
151
|
-
155: 'Profiling timer expired',
|
|
152
|
-
157: 'Pollable event',
|
|
153
|
-
159: 'Bad syscall',
|
|
154
|
-
}[exitCode || -1];
|
|
155
|
-
}
|
|
156
|
-
function errnoMessage(errno) {
|
|
157
|
-
if (errno === undefined) {
|
|
158
|
-
return 'Unknown error';
|
|
159
|
-
}
|
|
160
|
-
return ({
|
|
161
|
-
0: 'Success',
|
|
162
|
-
1: 'Not super-user',
|
|
163
|
-
2: 'No such file or directory',
|
|
164
|
-
3: 'No such process',
|
|
165
|
-
4: 'Interrupted system call',
|
|
166
|
-
5: 'I/O error',
|
|
167
|
-
6: 'No such device or address',
|
|
168
|
-
7: 'Arg list too long',
|
|
169
|
-
8: 'Exec format error',
|
|
170
|
-
9: 'Bad file number',
|
|
171
|
-
10: 'No children',
|
|
172
|
-
11: 'No more processes',
|
|
173
|
-
12: 'Not enough core',
|
|
174
|
-
13: 'Permission denied',
|
|
175
|
-
14: 'Bad address',
|
|
176
|
-
15: 'Block device required',
|
|
177
|
-
16: 'Mount device busy',
|
|
178
|
-
17: 'File exists',
|
|
179
|
-
18: 'Cross-device link',
|
|
180
|
-
19: 'No such device',
|
|
181
|
-
20: 'Not a directory',
|
|
182
|
-
21: 'Is a directory',
|
|
183
|
-
22: 'Invalid argument',
|
|
184
|
-
23: 'Too many open files in system',
|
|
185
|
-
24: 'Too many open files',
|
|
186
|
-
25: 'Not a typewriter',
|
|
187
|
-
26: 'Text file busy',
|
|
188
|
-
27: 'File too large',
|
|
189
|
-
28: 'No space left on device',
|
|
190
|
-
29: 'Illegal seek',
|
|
191
|
-
30: 'Read only file system',
|
|
192
|
-
31: 'Too many links',
|
|
193
|
-
32: 'Broken pipe',
|
|
194
|
-
33: 'Math arg out of domain of func',
|
|
195
|
-
34: 'Math result not representable',
|
|
196
|
-
35: 'File locking deadlock error',
|
|
197
|
-
36: 'File or path name too long',
|
|
198
|
-
37: 'No record locks available',
|
|
199
|
-
38: 'Function not implemented',
|
|
200
|
-
39: 'Directory not empty',
|
|
201
|
-
40: 'Too many symbolic links',
|
|
202
|
-
42: 'No message of desired type',
|
|
203
|
-
43: 'Identifier removed',
|
|
204
|
-
44: 'Channel number out of range',
|
|
205
|
-
45: 'Level 2 not synchronized',
|
|
206
|
-
46: 'Level 3 halted',
|
|
207
|
-
47: 'Level 3 reset',
|
|
208
|
-
48: 'Link number out of range',
|
|
209
|
-
49: 'Protocol driver not attached',
|
|
210
|
-
50: 'No CSI structure available',
|
|
211
|
-
51: 'Level 2 halted',
|
|
212
|
-
52: 'Invalid exchange',
|
|
213
|
-
53: 'Invalid request descriptor',
|
|
214
|
-
54: 'Exchange full',
|
|
215
|
-
55: 'No anode',
|
|
216
|
-
56: 'Invalid request code',
|
|
217
|
-
57: 'Invalid slot',
|
|
218
|
-
59: 'Bad font file fmt',
|
|
219
|
-
60: 'Device not a stream',
|
|
220
|
-
61: 'No data (for no delay io)',
|
|
221
|
-
62: 'Timer expired',
|
|
222
|
-
63: 'Out of streams resources',
|
|
223
|
-
64: 'Machine is not on the network',
|
|
224
|
-
65: 'Package not installed',
|
|
225
|
-
66: 'The object is remote',
|
|
226
|
-
67: 'The link has been severed',
|
|
227
|
-
68: 'Advertise error',
|
|
228
|
-
69: 'Srmount error',
|
|
229
|
-
70: 'Communication error on send',
|
|
230
|
-
71: 'Protocol error',
|
|
231
|
-
72: 'Multihop attempted',
|
|
232
|
-
73: 'Cross mount point (not really error)',
|
|
233
|
-
74: 'Trying to read unreadable message',
|
|
234
|
-
75: 'Value too large for defined data type',
|
|
235
|
-
76: 'Given log. name not unique',
|
|
236
|
-
77: 'f.d. invalid for this operation',
|
|
237
|
-
78: 'Remote address changed',
|
|
238
|
-
79: 'Can access a needed shared lib',
|
|
239
|
-
80: 'Accessing a corrupted shared lib',
|
|
240
|
-
81: '.lib section in a.out corrupted',
|
|
241
|
-
82: 'Attempting to link in too many libs',
|
|
242
|
-
83: 'Attempting to exec a shared library',
|
|
243
|
-
84: 'Illegal byte sequence',
|
|
244
|
-
86: 'Streams pipe error',
|
|
245
|
-
87: 'Too many users',
|
|
246
|
-
88: 'Socket operation on non-socket',
|
|
247
|
-
89: 'Destination address required',
|
|
248
|
-
90: 'Message too long',
|
|
249
|
-
91: 'Protocol wrong type for socket',
|
|
250
|
-
92: 'Protocol not available',
|
|
251
|
-
93: 'Unknown protocol',
|
|
252
|
-
94: 'Socket type not supported',
|
|
253
|
-
95: 'Not supported',
|
|
254
|
-
96: 'Protocol family not supported',
|
|
255
|
-
97: 'Address family not supported by protocol family',
|
|
256
|
-
98: 'Address already in use',
|
|
257
|
-
99: 'Address not available',
|
|
258
|
-
100: 'Network interface is not configured',
|
|
259
|
-
101: 'Network is unreachable',
|
|
260
|
-
102: 'Connection reset by network',
|
|
261
|
-
103: 'Connection aborted',
|
|
262
|
-
104: 'Connection reset by peer',
|
|
263
|
-
105: 'No buffer space available',
|
|
264
|
-
106: 'Socket is already connected',
|
|
265
|
-
107: 'Socket is not connected',
|
|
266
|
-
108: "Can't send after socket shutdown",
|
|
267
|
-
109: 'Too many references',
|
|
268
|
-
110: 'Connection timed out',
|
|
269
|
-
111: 'Connection refused',
|
|
270
|
-
112: 'Host is down',
|
|
271
|
-
113: 'Host is unreachable',
|
|
272
|
-
114: 'Socket already connected',
|
|
273
|
-
115: 'Connection already in progress',
|
|
274
|
-
116: 'Stale file handle',
|
|
275
|
-
122: 'Quota exceeded',
|
|
276
|
-
123: 'No medium (in tape drive)',
|
|
277
|
-
125: 'Operation canceled',
|
|
278
|
-
130: 'Previous owner died',
|
|
279
|
-
131: 'State not recoverable',
|
|
280
|
-
}[-errno] || 'Unknown error');
|
|
281
|
-
}
|
|
282
|
-
function parseDuration(d) {
|
|
283
|
-
if (typeof d == 'number') {
|
|
284
|
-
if (isNaN(d) || d < 0)
|
|
285
|
-
throw new Error(`Invalid duration: "${d}".`);
|
|
286
|
-
return d;
|
|
287
|
-
}
|
|
288
|
-
else if (/\d+s/.test(d)) {
|
|
289
|
-
return +d.slice(0, -1) * 1000;
|
|
290
|
-
}
|
|
291
|
-
else if (/\d+ms/.test(d)) {
|
|
292
|
-
return +d.slice(0, -2);
|
|
293
|
-
}
|
|
294
|
-
throw new Error(`Unknown duration: "${d}".`);
|
|
295
|
-
}
|
|
296
|
-
function formatCmd(cmd) {
|
|
297
|
-
if (cmd == undefined)
|
|
298
|
-
return 'undefined';
|
|
299
|
-
const chars = [...cmd];
|
|
300
|
-
let out = '$ ';
|
|
301
|
-
let buf = '';
|
|
302
|
-
let ch;
|
|
303
|
-
let state = root;
|
|
304
|
-
while (state) {
|
|
305
|
-
ch = chars.shift() || 'EOF';
|
|
306
|
-
if (ch == '\n') {
|
|
307
|
-
out += style(state, buf) + '\n> ';
|
|
308
|
-
buf = '';
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
const next = ch == 'EOF' ? undefined : state();
|
|
312
|
-
if (next != state) {
|
|
313
|
-
out += style(state, buf);
|
|
314
|
-
buf = '';
|
|
315
|
-
}
|
|
316
|
-
state = next == root ? next() : next;
|
|
317
|
-
buf += ch;
|
|
318
|
-
}
|
|
319
|
-
function style(state, s) {
|
|
320
|
-
// if (s == '')
|
|
321
|
-
// return '';
|
|
322
|
-
// if (reservedWords.includes(s)) {
|
|
323
|
-
// return chalk.cyanBright(s);
|
|
324
|
-
// }
|
|
325
|
-
// if (state == word && wordCount == 0) {
|
|
326
|
-
// wordCount++;
|
|
327
|
-
// return chalk.greenBright(s);
|
|
328
|
-
// }
|
|
329
|
-
// if (state == syntax) {
|
|
330
|
-
// wordCount = 0;
|
|
331
|
-
// return chalk.cyanBright(s);
|
|
332
|
-
// }
|
|
333
|
-
// if (state == dollar)
|
|
334
|
-
// return chalk.yellowBright(s);
|
|
335
|
-
// if (state?.name.startsWith('str'))
|
|
336
|
-
// return chalk.yellowBright(s);
|
|
337
|
-
return s;
|
|
338
|
-
}
|
|
339
|
-
function isSyntax(ch) {
|
|
340
|
-
return '()[]{}<>;:+|&='.includes(ch);
|
|
341
|
-
}
|
|
342
|
-
function root() {
|
|
343
|
-
if (/\s/.test(ch))
|
|
344
|
-
return space;
|
|
345
|
-
if (isSyntax(ch))
|
|
346
|
-
return syntax;
|
|
347
|
-
if (/[$]/.test(ch))
|
|
348
|
-
return dollar;
|
|
349
|
-
if (/["]/.test(ch))
|
|
350
|
-
return strDouble;
|
|
351
|
-
if (/[']/.test(ch))
|
|
352
|
-
return strSingle;
|
|
353
|
-
return word;
|
|
354
|
-
}
|
|
355
|
-
function space() {
|
|
356
|
-
if (/\s/.test(ch))
|
|
357
|
-
return space;
|
|
358
|
-
return root;
|
|
359
|
-
}
|
|
360
|
-
function word() {
|
|
361
|
-
if (/[0-9a-z/_.]/i.test(ch))
|
|
362
|
-
return word;
|
|
363
|
-
return root;
|
|
364
|
-
}
|
|
365
|
-
function syntax() {
|
|
366
|
-
if (isSyntax(ch))
|
|
367
|
-
return syntax;
|
|
368
|
-
return root;
|
|
369
|
-
}
|
|
370
|
-
function dollar() {
|
|
371
|
-
if (/[']/.test(ch))
|
|
372
|
-
return str;
|
|
373
|
-
return root;
|
|
374
|
-
}
|
|
375
|
-
function str() {
|
|
376
|
-
if (/[']/.test(ch))
|
|
377
|
-
return strEnd;
|
|
378
|
-
if (/[\\]/.test(ch))
|
|
379
|
-
return strBackslash;
|
|
380
|
-
return str;
|
|
381
|
-
}
|
|
382
|
-
function strBackslash() {
|
|
383
|
-
return strEscape;
|
|
384
|
-
}
|
|
385
|
-
function strEscape() {
|
|
386
|
-
return str;
|
|
387
|
-
}
|
|
388
|
-
function strDouble() {
|
|
389
|
-
if (/["]/.test(ch))
|
|
390
|
-
return strEnd;
|
|
391
|
-
return strDouble;
|
|
392
|
-
}
|
|
393
|
-
function strSingle() {
|
|
394
|
-
if (/[']/.test(ch))
|
|
395
|
-
return strEnd;
|
|
396
|
-
return strSingle;
|
|
397
|
-
}
|
|
398
|
-
function strEnd() {
|
|
399
|
-
return root;
|
|
400
|
-
}
|
|
401
|
-
return out + '\n';
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Copyright 2021 Google LLC
|
|
405
|
-
//
|
|
406
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
407
|
-
// you may not use this file except in compliance with the License.
|
|
408
|
-
// You may obtain a copy of the License at
|
|
409
|
-
//
|
|
410
|
-
// https://www.apache.org/licenses/LICENSE-2.0
|
|
411
|
-
//
|
|
412
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
413
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
414
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
415
|
-
// See the License for the specific language governing permissions and
|
|
416
|
-
// limitations under the License.
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// Original Source: zx
|
|
420
|
-
// Link to Original Source: https://github.com/google/zx
|
|
421
|
-
// Reason for Using This Code:
|
|
422
|
-
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
423
|
-
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
424
|
-
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
425
|
-
// Changes Made:
|
|
426
|
-
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
427
|
-
// - Some methods were added and existing ones were modified to enhance usability.
|
|
428
|
-
// - The namespace has been changed from '$' to 'SH'.
|
|
429
|
-
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
430
|
-
|
|
47
|
+
};
|
|
431
48
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
class ProcessOutput extends Error {
|
|
437
|
-
#code = 0;
|
|
438
|
-
#signal;
|
|
49
|
+
class SHExecute {
|
|
50
|
+
#proc;
|
|
51
|
+
#command = '';
|
|
52
|
+
#options = {};
|
|
439
53
|
#stdout = '';
|
|
440
54
|
#stderr = '';
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
* @param {string} combined - stderr + stdout
|
|
448
|
-
* @param {string} message - Error message
|
|
449
|
-
*/
|
|
450
|
-
constructor(code, signal, stdout = '', stderr = '', combined = '', message = '') {
|
|
451
|
-
super(message);
|
|
452
|
-
this.#code = code;
|
|
453
|
-
this.#signal = signal;
|
|
454
|
-
this.#stdout = stdout;
|
|
455
|
-
this.#stderr = stderr;
|
|
456
|
-
this.#combined = combined;
|
|
457
|
-
this.name = 'ProcessOutput';
|
|
55
|
+
constructor(command, options = {}) {
|
|
56
|
+
this.#command = command;
|
|
57
|
+
this.#options = options;
|
|
58
|
+
this.#proc = null;
|
|
59
|
+
this.#stdout = '';
|
|
60
|
+
this.#stderr = '';
|
|
458
61
|
}
|
|
459
62
|
/**
|
|
460
|
-
*
|
|
461
|
-
* @
|
|
63
|
+
* @param {string} [payload] - data to write
|
|
64
|
+
* @retuns {Promise<object>}
|
|
462
65
|
*/
|
|
463
|
-
|
|
464
|
-
|
|
66
|
+
runSync(payload) {
|
|
67
|
+
if (payload && typeof payload !== 'string') {
|
|
68
|
+
throw new Error('Argument is not a string');
|
|
69
|
+
}
|
|
70
|
+
let { cwd, shell, env, stdio } = this.#options;
|
|
71
|
+
// pipe need to be set on stdin when posting a payload
|
|
72
|
+
if (payload) stdio[0] = 'pipe';
|
|
73
|
+
const input = payload || undefined;
|
|
74
|
+
return spawnSync(this.#options.prefix, [this.#command], {
|
|
75
|
+
cwd,
|
|
76
|
+
shell,
|
|
77
|
+
stdio,
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
env,
|
|
80
|
+
input
|
|
81
|
+
});
|
|
465
82
|
}
|
|
466
83
|
/**
|
|
467
|
-
*
|
|
468
|
-
* @
|
|
84
|
+
* @param {string} [payload] - data to write
|
|
85
|
+
* @retuns {Promise<string>}
|
|
469
86
|
*/
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
87
|
+
run(payload) {
|
|
88
|
+
let to = 0;
|
|
89
|
+
if (payload && typeof payload !== 'string') {
|
|
90
|
+
throw new Error('Argument is not a string');
|
|
91
|
+
}
|
|
92
|
+
if (this.#options.timeout) {
|
|
93
|
+
to = this.#options.timeout;
|
|
94
|
+
}
|
|
95
|
+
let { cwd, shell, env, stdio } = this.#options;
|
|
96
|
+
// pipe need to be set on stdin when posting a payload
|
|
97
|
+
if (payload) stdio[0] = 'pipe';
|
|
98
|
+
this.#proc = spawn(this.#options.prefix, [this.#command], {
|
|
99
|
+
cwd,
|
|
100
|
+
shell,
|
|
101
|
+
stdio,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
env,
|
|
104
|
+
});
|
|
473
105
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
106
|
+
this.#proc.stdout?.on('data', (data) => {
|
|
107
|
+
this.#stdout += data;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.#proc.stderr?.on('data', (data) => {
|
|
111
|
+
this.#stderr += data;
|
|
112
|
+
});
|
|
113
|
+
if (payload) {
|
|
114
|
+
this.#proc.stdin.end(payload);
|
|
115
|
+
}
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
let timeout;
|
|
118
|
+
if (to > 0) {
|
|
119
|
+
timeout = setTimeout(async () => {
|
|
120
|
+
await this.#proc.kill();
|
|
121
|
+
reject(new Error('Process timed out'));
|
|
122
|
+
}, to); // options.timeout
|
|
123
|
+
}
|
|
124
|
+
this.#proc.on('close', (code) => {
|
|
125
|
+
if (timeout) clearTimeout(timeout);
|
|
126
|
+
if (code === 0) {
|
|
127
|
+
resolve(this.#stdout.trim());
|
|
128
|
+
} else {
|
|
129
|
+
reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.#proc.on('error', (err) => {
|
|
134
|
+
reject(err);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
490
137
|
}
|
|
491
138
|
/**
|
|
492
|
-
*
|
|
493
|
-
* @returns {string} The exit signal from the child
|
|
139
|
+
* @returns {Promise<number[]>}
|
|
494
140
|
*/
|
|
495
|
-
|
|
496
|
-
|
|
141
|
+
async kill(signal = 'SIGTERM') {
|
|
142
|
+
if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
|
|
143
|
+
if (!this.#proc.pid) throw new Error('The process pid is undefined.');
|
|
144
|
+
|
|
145
|
+
return killProcesses(this.#proc.pid, signal);
|
|
497
146
|
}
|
|
498
|
-
/**
|
|
499
|
-
* This method is used for debugging purposes. It displays the current state of the object
|
|
500
|
-
* when passed to the console.log function.
|
|
501
|
-
*/
|
|
502
|
-
[inspect.custom]() {
|
|
503
|
-
let stringify = (s) => s.length === 0 ? "''" : inspect(s);
|
|
504
|
-
return `ProcessOutput {
|
|
505
|
-
stdout: ${stringify(this.stdout)},
|
|
506
|
-
stderr: ${stringify(this.stderr)},
|
|
507
|
-
signal: ${inspect(this.signal)},
|
|
508
|
-
exitCode: ${(this.exitCode)}${exitCodeInfo(this.exitCode)
|
|
509
|
-
? ' (' + exitCodeInfo(this.exitCode) + ')'
|
|
510
|
-
: ''}
|
|
511
|
-
}`;
|
|
512
|
-
}
|
|
513
147
|
}
|
|
514
148
|
|
|
515
|
-
// Copyright 2021 Google LLC
|
|
516
|
-
//
|
|
517
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
518
|
-
// you may not use this file except in compliance with the License.
|
|
519
|
-
// You may obtain a copy of the License at
|
|
520
|
-
//
|
|
521
|
-
// https://www.apache.org/licenses/LICENSE-2.0
|
|
522
|
-
//
|
|
523
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
524
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
525
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
526
|
-
// See the License for the specific language governing permissions and
|
|
527
|
-
// limitations under the License.
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
// Original Source: zx
|
|
531
|
-
// Link to Original Source: https://github.com/google/zx
|
|
532
|
-
// Reason for Using This Code:
|
|
533
|
-
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
534
|
-
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
535
|
-
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
536
|
-
// Changes Made:
|
|
537
|
-
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
538
|
-
// - Some methods were added and existing ones were modified to enhance usability.
|
|
539
|
-
// - The namespace has been changed from '$' to 'SH'.
|
|
540
|
-
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* @typedef {Function} resolver
|
|
545
|
-
* @param {ProcessOutput} value
|
|
546
|
-
*/
|
|
547
|
-
/**
|
|
548
|
-
* @typedef {Function} rejecter
|
|
549
|
-
* @param {ProcessOutput} value
|
|
550
|
-
*/
|
|
551
149
|
/**
|
|
552
|
-
* @typedef {
|
|
553
|
-
* @
|
|
554
|
-
* @
|
|
555
|
-
|
|
150
|
+
* @typedef {Object} SpawnSyncResponse
|
|
151
|
+
* @property {number} status - The exit code of the child process. A value of `0` indicates success.
|
|
152
|
+
* @property {Buffer|null} signal - The signal used to terminate the process, if any.
|
|
153
|
+
* @property {Array<string|null>} output - An array containing the standard output and standard error of the child process.
|
|
154
|
+
* @property {number} pid - The process ID of the child process.
|
|
155
|
+
* @property {Buffer|null} stdout - The standard output of the child process.
|
|
156
|
+
* @property {Buffer|null} stderr - The standard error of the child process.
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* Default options for the execution environment.
|
|
160
|
+
*
|
|
161
|
+
* @typedef {Object} SHOptions
|
|
162
|
+
* @property {string} [cwd] - The current working directory.
|
|
163
|
+
* @property {NodeJS.ProcessEnv} [env] - The environment variables.
|
|
164
|
+
* @property {string} [shell] - The shell to use for execution.
|
|
165
|
+
* @property {string} [prefix] - The prefix commands to ensure a safe execution environment.
|
|
166
|
+
* @property {StdioOptions|StdioOption} [stdio] - The stdio configuration.
|
|
167
|
+
* @property {number} [timeout] - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
|
|
168
|
+
*/
|
|
556
169
|
/**
|
|
557
170
|
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
558
171
|
* @description Defines the stdio configuration for each of the standard streams.
|
|
@@ -564,7 +177,6 @@ class ProcessOutput extends Error {
|
|
|
564
177
|
* - Stream object to be used for the stdio stream.
|
|
565
178
|
* - Positive integer representing a file descriptor to be used for the stdio stream.
|
|
566
179
|
*/
|
|
567
|
-
|
|
568
180
|
/**
|
|
569
181
|
* @typedef {Array<StdioOption>|StdioOption} StdioOptions
|
|
570
182
|
* @description
|
|
@@ -578,287 +190,105 @@ class ProcessOutput extends Error {
|
|
|
578
190
|
* - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
|
|
579
191
|
* - 'inherit': Inherit all stdio streams from the parent.
|
|
580
192
|
*/
|
|
193
|
+
/**
|
|
194
|
+
* 'Code Safe' has own prop
|
|
195
|
+
*
|
|
196
|
+
* @param {any} o - object to examine
|
|
197
|
+
* @param {string} p - property to look for
|
|
198
|
+
* @returns {boolean}
|
|
199
|
+
*/
|
|
200
|
+
const hasProp = (o, p) => {
|
|
201
|
+
if (typeof o === 'undefined') {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return Object.prototype.hasOwnProperty.call(o, p);
|
|
205
|
+
};
|
|
581
206
|
|
|
582
207
|
/**
|
|
583
|
-
*
|
|
208
|
+
* Merge property values while maintaining the fixed set of props in the original object
|
|
209
|
+
* @param {SHOptions} predefined - original object
|
|
210
|
+
* @param {SHOptions} options - object with new values
|
|
211
|
+
* @returns {SHOptions}
|
|
584
212
|
*/
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
#nothrow = false;
|
|
596
|
-
#quiet = false;
|
|
597
|
-
#resolved = false;
|
|
598
|
-
#halted = false;
|
|
599
|
-
#piped = false;
|
|
600
|
-
#prerun = noop;
|
|
601
|
-
#postrun = noop;
|
|
602
|
-
/**
|
|
603
|
-
* @param {PromiseConstruct} p - A function that takes two arguments, resolve and reject.
|
|
604
|
-
*/
|
|
605
|
-
constructor(p) {
|
|
606
|
-
// @ts-ignore
|
|
607
|
-
super(p);
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* Set the environment
|
|
611
|
-
* and the
|
|
612
|
-
* @param {string} cmd - Command to execute
|
|
613
|
-
* @param {string} from - Position in the codfe where this is triggred from
|
|
614
|
-
* @param {function} resolve - Promise resolve method
|
|
615
|
-
* @param {function} reject - Reject method
|
|
616
|
-
* @param {object} options - Settings (options default)
|
|
617
|
-
*/
|
|
618
|
-
_bind(cmd, from, resolve, reject, options) {
|
|
619
|
-
this.#command = cmd;
|
|
620
|
-
this.#from = from;
|
|
621
|
-
this.#resolve = resolve;
|
|
622
|
-
this.#reject = reject;
|
|
623
|
-
this.#snapshot = { ...options };
|
|
624
|
-
}
|
|
625
|
-
/**
|
|
626
|
-
* Run the promise
|
|
627
|
-
*/
|
|
628
|
-
run() {
|
|
629
|
-
const ENV = this.#snapshot;
|
|
630
|
-
if (this.child) return this; // The _run() can be called from a few places.
|
|
631
|
-
this.#prerun(); // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
|
|
632
|
-
log({
|
|
633
|
-
kind: 'cmd',
|
|
634
|
-
cmd: this.#command,
|
|
635
|
-
verbose: ENV.verbose && !this.#quiet,
|
|
636
|
-
});
|
|
637
|
-
const cwd = ENV['processCwd'];
|
|
638
|
-
const shell = ENV['shell'];
|
|
639
|
-
this.child = spawn(ENV.prefix, [this.#command], {
|
|
640
|
-
cwd,
|
|
641
|
-
shell,
|
|
642
|
-
// @ts-ignore
|
|
643
|
-
stdio: this.#stdio,
|
|
644
|
-
windowsHide: true,
|
|
645
|
-
env: ENV.env,
|
|
646
|
-
});
|
|
647
|
-
this.child.on('close', (code, signal) => {
|
|
648
|
-
let message = `exit code: ${code}`;
|
|
649
|
-
if (code != 0 || signal != null) {
|
|
650
|
-
message = `${stderr || '\n'} at ${this.#from}`;
|
|
651
|
-
message += `\n exit code: ${code}${exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''}`;
|
|
652
|
-
if (signal != null) {
|
|
653
|
-
message += `\n signal: ${signal}`;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
let output = new ProcessOutput(code, signal, stdout, stderr, combined, message);
|
|
657
|
-
if (code === 0 || this.#nothrow) {
|
|
658
|
-
this.#resolve(output);
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
this.#reject(output);
|
|
662
|
-
}
|
|
663
|
-
this.#resolved = true;
|
|
664
|
-
});
|
|
665
|
-
this.child.on('error', (err) => {
|
|
666
|
-
const message = `${err.message}\n` +
|
|
667
|
-
// @ts-ignore
|
|
668
|
-
` errno: ${err.errno} (${errnoMessage(err.errno)})\n` +
|
|
669
|
-
// @ts-ignore
|
|
670
|
-
` code: ${err.code}\n` +
|
|
671
|
-
` at ${this.#from}`;
|
|
672
|
-
this.#reject(new ProcessOutput(null, null, stdout, stderr, combined, message));
|
|
673
|
-
this.#resolved = true;
|
|
674
|
-
});
|
|
675
|
-
let stdout = '', stderr = '', combined = '';
|
|
676
|
-
/** @param {Blob} data */
|
|
677
|
-
const onStdout = (data) => {
|
|
678
|
-
log({ kind: 'stdout', data, verbose: ENV.verbose && !this.#quiet });
|
|
679
|
-
stdout += data;
|
|
680
|
-
combined += data;
|
|
681
|
-
};
|
|
682
|
-
/** @param {Blob} data */
|
|
683
|
-
const onStderr = (data) => {
|
|
684
|
-
log({ kind: 'stderr', data, verbose: ENV.verbose && !this.#quiet });
|
|
685
|
-
stderr += data;
|
|
686
|
-
combined += data;
|
|
687
|
-
};
|
|
688
|
-
if (!this.#piped)
|
|
689
|
-
// @ts-ignore
|
|
690
|
-
this.child.stdout?.on('data', onStdout); // If process is piped, don't collect or print output.
|
|
691
|
-
// @ts-ignore
|
|
692
|
-
this.child.stderr?.on('data', onStderr); // Stderr should be printed regardless of piping.
|
|
693
|
-
this.#postrun(); // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
|
|
694
|
-
if (this._timeout && this._timeoutSignal) {
|
|
695
|
-
const t = setTimeout(() => this.kill(this._timeoutSignal), this._timeout);
|
|
696
|
-
this.finally(() => clearTimeout(t)).catch(noop);
|
|
697
|
-
}
|
|
698
|
-
return this;
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* stdin child stream
|
|
702
|
-
* @retruns {Writeable}
|
|
703
|
-
*/
|
|
704
|
-
get stdin() {
|
|
705
|
-
this.stdio('pipe');
|
|
706
|
-
this.run();
|
|
707
|
-
assert(this.child);
|
|
708
|
-
if (this.child.stdin == null)
|
|
709
|
-
throw new Error('The stdin of subprocess is null.');
|
|
710
|
-
return this.child.stdin;
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* stdout child stream
|
|
714
|
-
* @retruns {Readable}
|
|
715
|
-
*/
|
|
716
|
-
get stdout() {
|
|
717
|
-
this.run();
|
|
718
|
-
assert(this.child);
|
|
719
|
-
if (this.child.stdout == null)
|
|
720
|
-
throw new Error('The stdout of subprocess is null.');
|
|
721
|
-
return this.child.stdout;
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* stderr child stream
|
|
725
|
-
* @retruns {Readable}
|
|
726
|
-
*/
|
|
727
|
-
get stderr() {
|
|
728
|
-
this.run();
|
|
729
|
-
assert(this.child);
|
|
730
|
-
if (this.child.stderr == null)
|
|
731
|
-
throw new Error('The stderr of subprocess is null.');
|
|
732
|
-
return this.child.stderr;
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* process exit code
|
|
736
|
-
* @returns {Promise<number>}
|
|
737
|
-
*/
|
|
738
|
-
get exitCode() {
|
|
739
|
-
return this.#then((p) => p.exitCode, (p) => p.exitCode);
|
|
740
|
-
}
|
|
741
|
-
#then(onfulfilled, onrejected) {
|
|
742
|
-
if (this.isHalted && !this.child) {
|
|
743
|
-
throw new Error('The process is halted!');
|
|
744
|
-
}
|
|
745
|
-
return super.then(onfulfilled, onrejected);
|
|
746
|
-
}
|
|
747
|
-
catch(onrejected) {
|
|
748
|
-
return super.catch(onrejected);
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* Pipe the output to the input to the next Promise
|
|
752
|
-
* @example
|
|
753
|
-
* const res = await SH`ls -FLa`.pipe(SH`grep package.json`);
|
|
754
|
-
*/
|
|
755
|
-
pipe(dest) {
|
|
756
|
-
if (typeof dest == 'string')
|
|
757
|
-
throw new Error('The pipe() method does not take strings. Forgot SH?');
|
|
758
|
-
if (this.#resolved) {
|
|
759
|
-
if (dest instanceof ProcessPromise)
|
|
760
|
-
// @ts-ignore
|
|
761
|
-
dest.stdin.end(); // In case of piped stdin, we may want to close stdin of dest as well.
|
|
762
|
-
throw new Error("The pipe() method shouldn't be called after promise is already resolved!");
|
|
763
|
-
}
|
|
764
|
-
this.#piped = true;
|
|
765
|
-
if (dest instanceof ProcessPromise) {
|
|
766
|
-
dest.stdio('pipe');
|
|
767
|
-
dest._prerun = this.run.bind(this);
|
|
768
|
-
dest._postrun = () => {
|
|
769
|
-
if (!dest.child)
|
|
770
|
-
throw new Error('Access to stdin of pipe destination without creation a subprocess.');
|
|
771
|
-
// @ts-ignore
|
|
772
|
-
this.stdout.pipe(dest.stdin);
|
|
773
|
-
};
|
|
774
|
-
return dest;
|
|
775
|
-
}
|
|
776
|
-
else {
|
|
777
|
-
// @ts-ignore
|
|
778
|
-
this._postrun = () => this.stdout.pipe(dest);
|
|
779
|
-
return this;
|
|
213
|
+
const mergeOptions = (predefined, options) => {
|
|
214
|
+
// Extract the keys from the predefined object
|
|
215
|
+
const keys = Object.keys(predefined);
|
|
216
|
+
|
|
217
|
+
// Use reduce to accumulate only the predefined properties from sourceObj
|
|
218
|
+
const mergedObj = keys.reduce((acc, key) => {
|
|
219
|
+
if (hasProp(options, key)) {
|
|
220
|
+
acc[key] = options[key];
|
|
221
|
+
} else {
|
|
222
|
+
acc[key] = predefined[key];
|
|
780
223
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
* Send a KILL signal to the child process
|
|
784
|
-
* @returns {Promise<number[]>} the pid numbers that has been killed
|
|
785
|
-
*/
|
|
786
|
-
async kill(signal = 'SIGTERM') {
|
|
787
|
-
if (!this.child)
|
|
788
|
-
throw new Error('Trying to kill a process without creating one.');
|
|
789
|
-
if (!this.child.pid)
|
|
790
|
-
throw new Error('The process pid is undefined.');
|
|
224
|
+
return acc;
|
|
225
|
+
}, {});
|
|
791
226
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* Show log output in the console
|
|
815
|
-
*/
|
|
816
|
-
verbose() {
|
|
817
|
-
this._quiet = false;
|
|
818
|
-
return this;
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Set a timeout to kill a process
|
|
822
|
-
*
|
|
823
|
-
* @param {string} d - 10s, 1000ms
|
|
824
|
-
* @param {string} [signal] - default "SIGTERM" Signal to send to kill the proces
|
|
825
|
-
*/
|
|
826
|
-
timeout(d, signal = 'SIGTERM') {
|
|
827
|
-
this._timeout = parseDuration(d);
|
|
828
|
-
this._timeoutSignal = signal;
|
|
829
|
-
return this;
|
|
830
|
-
}
|
|
227
|
+
return mergedObj;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
/** @type {SHOptions} */
|
|
232
|
+
const defaultOptions = {
|
|
233
|
+
cwd: process.cwd(),
|
|
234
|
+
env: process.env,
|
|
235
|
+
shell: 'bash',
|
|
236
|
+
prefix: 'set -euo pipefail;/usr/bin/env',
|
|
237
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
238
|
+
timeout: 10000 // when 0 there is no timeout
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class SHDispatch {
|
|
244
|
+
#cmd = '';
|
|
245
|
+
#options = {};
|
|
246
|
+
#proc;
|
|
831
247
|
/**
|
|
832
|
-
*
|
|
248
|
+
* @param {string} cmd - cmd to execute
|
|
833
249
|
*/
|
|
834
|
-
|
|
835
|
-
this.#
|
|
250
|
+
constructor(cmd) {
|
|
251
|
+
this.#cmd = cmd;
|
|
252
|
+
this.#options = defaultOptions;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* @param {SHOptions} options
|
|
256
|
+
* @returns {SHDispatch}
|
|
257
|
+
*/
|
|
258
|
+
options(options) {
|
|
259
|
+
if (options.stdio && typeof options.stdio === 'string') {
|
|
260
|
+
// convert stdio to array
|
|
261
|
+
// This sets the default io values
|
|
262
|
+
// but can be overwritten when having a payload
|
|
263
|
+
const io = options.stdio;
|
|
264
|
+
options.stdio = Array(3).fill(io);
|
|
265
|
+
}
|
|
266
|
+
this.#options = mergeOptions(defaultOptions, options);
|
|
836
267
|
return this;
|
|
837
268
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
this.#prerun = f;
|
|
269
|
+
/**
|
|
270
|
+
* @param {string} [payload]
|
|
271
|
+
* @returns {Promise<string>}
|
|
272
|
+
*/
|
|
273
|
+
run(payload) {
|
|
274
|
+
this.#proc = new SHExecute(this.#cmd, this.#options);
|
|
275
|
+
return this.#proc.run(payload);
|
|
846
276
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Works for screen takeovers like editors
|
|
280
|
+
* @param {string} [payload]
|
|
281
|
+
* @returns {SpawnSyncResponse}
|
|
282
|
+
*/
|
|
283
|
+
runSync(payload) {
|
|
853
284
|
// @ts-ignore
|
|
854
|
-
this.#
|
|
285
|
+
return new SHExecute(this.#cmd, this.#options).runSync(payload);
|
|
855
286
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return this.#halted;
|
|
287
|
+
async kill() {
|
|
288
|
+
try {
|
|
289
|
+
await this.#proc.kill();
|
|
290
|
+
} catch (_e) {}
|
|
291
|
+
this.#proc = undefined;
|
|
862
292
|
}
|
|
863
293
|
}
|
|
864
294
|
|
|
@@ -883,60 +313,85 @@ class ProcessPromise extends Promise {
|
|
|
883
313
|
// The core functionality of this code is highly beneficial. However, certain parts of the original code
|
|
884
314
|
// were overwriting the global namespace with core libraries and variables. This was causing conflicts with
|
|
885
315
|
// other packages (for instance, fetch) and introducing unexpected elements into my code base.
|
|
316
|
+
// The main $/SH method is all there is left, with barebone Promises and readable code.
|
|
886
317
|
// Changes Made:
|
|
887
318
|
// - The code has been or is being reformatted to comply with ES2020 standards.
|
|
888
|
-
// - Some methods were added and existing ones were modified to enhance usability.
|
|
319
|
+
// - Some methods were added and existing ones were modified or deleted to enhance usability.
|
|
320
|
+
// - Most methods were deleted,
|
|
889
321
|
// - The namespace has been changed from '$' to 'SH'.
|
|
890
322
|
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
891
323
|
|
|
892
|
-
const storage = new AsyncLocalStorage();
|
|
893
324
|
|
|
894
|
-
const defaults = {
|
|
895
|
-
processCwd: '',
|
|
896
|
-
verbose: false,
|
|
897
|
-
env: {},
|
|
898
|
-
shell: 'bash',
|
|
899
|
-
prefix: '',
|
|
900
|
-
};
|
|
901
|
-
defaults.prefix = 'set -euo pipefail;/usr/bin/env';
|
|
902
325
|
|
|
903
326
|
/**
|
|
904
|
-
*
|
|
905
|
-
* @param {string[]} arg
|
|
906
|
-
* @retruns {string}
|
|
907
|
-
*/
|
|
908
|
-
const sanitizeArg = (arg) => {
|
|
909
|
-
const s = `${arg}`;
|
|
910
|
-
if (process.platform == 'win32') {
|
|
911
|
-
return quotePowerShell(s)
|
|
912
|
-
}
|
|
913
|
-
return quote(s);
|
|
914
|
-
};
|
|
915
|
-
|
|
916
|
-
const getStore = () => {
|
|
917
|
-
return storage.getStore() || defaults;
|
|
918
|
-
};
|
|
919
|
-
/**
|
|
920
|
-
* Creates a new ProcessPromise object that represents a command to be executed.
|
|
327
|
+
* Creates a new SHDispatch object that represents a command to be executed.
|
|
921
328
|
*
|
|
922
329
|
* @typedef {Function} Shell
|
|
923
330
|
* @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
|
|
924
|
-
* @property {boolean} verbose - A property to control verbosity.
|
|
925
331
|
*
|
|
926
332
|
* @param {Array} pieces - An array of string literals from a template literal.
|
|
927
333
|
* @param {...*} args - The values to be interpolated into the string literals.
|
|
928
|
-
* @returns {
|
|
334
|
+
* @returns {SHDispatch} Trigger for the command.
|
|
929
335
|
* @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
|
|
930
336
|
*
|
|
931
337
|
* @example
|
|
932
|
-
* const command = await SH`echo 'Hello, world!'
|
|
338
|
+
* const command = await SH`echo 'Hello, world!'`.run();
|
|
933
339
|
*/
|
|
934
|
-
|
|
935
|
-
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* escape paramater commands
|
|
343
|
+
* @param {string} arg
|
|
344
|
+
* @returns {string}
|
|
345
|
+
*/
|
|
346
|
+
const quote = (arg) => {
|
|
347
|
+
if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
|
|
348
|
+
return arg;
|
|
349
|
+
}
|
|
350
|
+
return (`'` +
|
|
351
|
+
arg
|
|
352
|
+
.replace(/\\/g, '\\\\')
|
|
353
|
+
.replace(/'/g, "\\'")
|
|
354
|
+
.replace(/\f/g, '\\f')
|
|
355
|
+
.replace(/\n/g, '\\n')
|
|
356
|
+
.replace(/\r/g, '\\r')
|
|
357
|
+
.replace(/\t/g, '\\t')
|
|
358
|
+
.replace(/\v/g, '\\v')
|
|
359
|
+
.replace(/\0/g, '\\0') +
|
|
360
|
+
`'`);
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Escape CLI arguments
|
|
364
|
+
* @param {string[]} arg
|
|
365
|
+
* @retruns {string}
|
|
366
|
+
*/
|
|
367
|
+
const sanitizeArg = (arg) => {
|
|
368
|
+
const s = `${arg}`;
|
|
369
|
+
return quote(s);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 4ms, 5s || 5
|
|
374
|
+
* @param {number|string} d
|
|
375
|
+
* @returns {number}
|
|
376
|
+
*/
|
|
377
|
+
const parseDuration = (d) => {
|
|
378
|
+
if (typeof d == 'number') {
|
|
379
|
+
if (isNaN(d) || d < 0)
|
|
380
|
+
throw new Error(`Invalid duration: "${d}".`);
|
|
381
|
+
return d;
|
|
382
|
+
}
|
|
383
|
+
else if (/\d+s/.test(d)) {
|
|
384
|
+
return +d.slice(0, -1) * 1000;
|
|
385
|
+
}
|
|
386
|
+
else if (/\d+ms/.test(d)) {
|
|
387
|
+
return +d.slice(0, -2);
|
|
388
|
+
}
|
|
389
|
+
throw new Error(`Unknown duration: "${d}".`);
|
|
390
|
+
};
|
|
391
|
+
/** @type {Shell & { (pieces: TemplateStringsArray, ...args: *): SHDispatch }} */
|
|
936
392
|
const SH = new Proxy(function(pieces, ...args) {
|
|
937
|
-
const from = new Error().stack.split(/^\s*at\s/m)[2].trim();
|
|
938
393
|
if (pieces.some((p) => p == undefined)) {
|
|
939
|
-
throw new Error(`Malformed command
|
|
394
|
+
throw new Error(`Malformed command ${pieces}`);
|
|
940
395
|
}
|
|
941
396
|
let cmd = pieces[0], i = 0;
|
|
942
397
|
while (i < args.length) {
|
|
@@ -949,29 +404,8 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
949
404
|
}
|
|
950
405
|
cmd += s + pieces[++i];
|
|
951
406
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const promise = new ProcessPromise((...args) => ([resolve, reject] = args));
|
|
955
|
-
// re-add the environment
|
|
956
|
-
defaults.processCwd = process.cwd();
|
|
957
|
-
defaults.env = process.env,
|
|
958
|
-
promise._bind(cmd, from, resolve, reject, getStore());
|
|
959
|
-
// Postpone run to allow promise configuration.
|
|
960
|
-
setImmediate(() => promise.isHalted || promise.run());
|
|
961
|
-
return promise;
|
|
962
|
-
}, {
|
|
963
|
-
// this will get and set from:
|
|
964
|
-
// defaults OR storage (@see within());
|
|
965
|
-
set(_, key, value) {
|
|
966
|
-
const target = key in Function.prototype ? _ : getStore();
|
|
967
|
-
Reflect.set(target, key, value);
|
|
968
|
-
return true;
|
|
969
|
-
},
|
|
970
|
-
get(_, key) {
|
|
971
|
-
const target = key in Function.prototype ? _ : getStore();
|
|
972
|
-
return Reflect.get(target, key);
|
|
973
|
-
},
|
|
974
|
-
});
|
|
407
|
+
return new SHDispatch(cmd);
|
|
408
|
+
}, {});
|
|
975
409
|
|
|
976
410
|
/**
|
|
977
411
|
* Create a async context in an sync block
|
|
@@ -979,23 +413,23 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
979
413
|
* @example
|
|
980
414
|
* const p = within(async () => {
|
|
981
415
|
* const res = await Promise.all([
|
|
982
|
-
* SH`sleep 1; echo 1
|
|
983
|
-
* SH`sleep 2; echo 2
|
|
416
|
+
* SH`sleep 1; echo 1`.run(),
|
|
417
|
+
* SH`sleep 2; echo 2`.run(),
|
|
984
418
|
* sleep(2),
|
|
985
|
-
* SH`sleep 3; echo 3
|
|
419
|
+
* SH`sleep 3; echo 3`.run()
|
|
986
420
|
* ]);
|
|
987
421
|
*/
|
|
988
422
|
const within = (callback) => {
|
|
989
|
-
|
|
990
|
-
|
|
423
|
+
(async () => {
|
|
424
|
+
return await callback()
|
|
425
|
+
})();
|
|
991
426
|
};
|
|
992
427
|
/**
|
|
993
|
-
* This function reads the standard input (stdin)
|
|
994
|
-
* It is used to get piped content into a script.
|
|
428
|
+
* This function reads the standard input (stdin) from the current process.
|
|
995
429
|
* @example
|
|
996
430
|
* const content = await stdin();
|
|
997
431
|
*/
|
|
998
|
-
const
|
|
432
|
+
const readIn = async () => {
|
|
999
433
|
let buf = '';
|
|
1000
434
|
process.stdin.setEncoding('utf8');
|
|
1001
435
|
for await (const chunk of process.stdin) {
|
|
@@ -1003,8 +437,16 @@ const stdin = async () => {
|
|
|
1003
437
|
}
|
|
1004
438
|
return buf;
|
|
1005
439
|
};
|
|
440
|
+
|
|
1006
441
|
/**
|
|
1007
|
-
*
|
|
442
|
+
* Retries a given asynchronous function a specified number of times with optional delays between attempts.
|
|
443
|
+
*
|
|
444
|
+
* @param {number} count - The number of retry attempts.
|
|
445
|
+
* @param {string|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
|
|
446
|
+
* @param {Function} [b] - The callback function to retry, required if `a` is not a function.
|
|
447
|
+
* @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
|
|
448
|
+
* @throws {Error} - The last error encountered if all retry attempts fail.
|
|
449
|
+
*
|
|
1008
450
|
* @example
|
|
1009
451
|
* // Retry a command 3 times
|
|
1010
452
|
* const p = await retry(3, () => SH`curl -s https://flipwrsi`);
|
|
@@ -1016,12 +458,11 @@ const stdin = async () => {
|
|
|
1016
458
|
* const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
|
|
1017
459
|
*/
|
|
1018
460
|
const retry = async (count, a, b) => {
|
|
1019
|
-
const total = count;
|
|
461
|
+
// const total = count;
|
|
1020
462
|
let callback;
|
|
1021
463
|
let delayStatic = 0;
|
|
1022
464
|
let delayGen;
|
|
1023
465
|
// @ts-ignore
|
|
1024
|
-
const verbose = SH.verbose;
|
|
1025
466
|
if (typeof a == 'function') {
|
|
1026
467
|
callback = a;
|
|
1027
468
|
}
|
|
@@ -1036,9 +477,7 @@ const retry = async (count, a, b) => {
|
|
|
1036
477
|
callback = b;
|
|
1037
478
|
}
|
|
1038
479
|
let lastErr;
|
|
1039
|
-
let attempt = 0;
|
|
1040
480
|
while (count-- > 0) {
|
|
1041
|
-
attempt++;
|
|
1042
481
|
try {
|
|
1043
482
|
return await callback();
|
|
1044
483
|
}
|
|
@@ -1047,13 +486,6 @@ const retry = async (count, a, b) => {
|
|
|
1047
486
|
if (delayStatic > 0)
|
|
1048
487
|
delay = delayStatic;
|
|
1049
488
|
if (delayGen) delay = delayGen.next().value;
|
|
1050
|
-
log({
|
|
1051
|
-
verbose,
|
|
1052
|
-
kind: 'retry',
|
|
1053
|
-
error: ' FAIL ' +
|
|
1054
|
-
` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` +
|
|
1055
|
-
(delay > 0 ? `; next in ${delay}ms` : ''),
|
|
1056
|
-
});
|
|
1057
489
|
lastErr = err;
|
|
1058
490
|
if (count == 0)
|
|
1059
491
|
break;
|
|
@@ -1069,11 +501,7 @@ const retry = async (count, a, b) => {
|
|
|
1069
501
|
*
|
|
1070
502
|
* @example
|
|
1071
503
|
*
|
|
1072
|
-
* const res = await
|
|
1073
|
-
* SH`sleep 2; echo 2`, // Sleep for 2 seconds
|
|
1074
|
-
* sleep(2), // Sleep for 2 seconds
|
|
1075
|
-
* SH`sleep 3; echo 3` // Sleep for 3 seconds
|
|
1076
|
-
* ]);
|
|
504
|
+
* const res = await sleep('5s');
|
|
1077
505
|
*/
|
|
1078
506
|
const sleep = (duration) => {
|
|
1079
507
|
return new Promise((resolve) => {
|
|
@@ -1086,10 +514,16 @@ const sleep = (duration) => {
|
|
|
1086
514
|
*/
|
|
1087
515
|
const cd = (dir) => {
|
|
1088
516
|
// @ts-ignore
|
|
1089
|
-
const verbose = SH.verbose;
|
|
1090
|
-
log({ kind: 'cd', dir, verbose });
|
|
1091
517
|
process.chdir(dir);
|
|
1092
518
|
};
|
|
519
|
+
/**
|
|
520
|
+
* Generates an exponential backoff time with a random jitter.
|
|
521
|
+
*
|
|
522
|
+
* @generator
|
|
523
|
+
* @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
|
|
524
|
+
* @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
|
|
525
|
+
* @yields {number} The backoff time in milliseconds.
|
|
526
|
+
*/
|
|
1093
527
|
function* expBackoff(max = '60s', rand = '100ms') {
|
|
1094
528
|
const maxMs = parseDuration(max);
|
|
1095
529
|
const randMs = parseDuration(rand);
|
|
@@ -1100,4 +534,4 @@ function* expBackoff(max = '60s', rand = '100ms') {
|
|
|
1100
534
|
}
|
|
1101
535
|
}
|
|
1102
536
|
|
|
1103
|
-
export { SH, cd, expBackoff, retry, sleep,
|
|
537
|
+
export { SH, cd, expBackoff, readIn, retry, sleep, within };
|