@j-o-r/sh 0.0.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +55 -63
  2. package/lib/sh.d.ts +95 -170
  3. package/lib/sh.js +257 -858
  4. package/package.json +2 -2
package/lib/sh.js CHANGED
@@ -1,34 +1,4 @@
1
- import assert from 'node:assert';
2
- import { AsyncLocalStorage } from 'node:async_hooks';
3
- import { exec, spawn } from 'node:child_process';
4
- import { inspect } from 'node:util';
5
-
6
- // Copyright 2021 Google LLC
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
1
+ import { spawnSync, spawn, exec } from 'node:child_process';
32
2
 
33
3
  /**
34
4
  * Kills a process and all child processes of a given process ID in Linux/Posix.
@@ -36,7 +6,7 @@ import { inspect } from 'node:util';
36
6
  * @param {string} signal - Signal to send.
37
7
  * @retruns {Promise<number[]>} array with killed pid numbers
38
8
  */
39
- function killProcesses(processPid, signal) {
9
+ const killProcesses = (processPid, signal) => {
40
10
  const killed = [];
41
11
  return new Promise((resolve, reject) => {
42
12
  // Command to get child PIDs of the given process
@@ -72,487 +42,104 @@ function killProcesses(processPid, signal) {
72
42
  resolve(killed);
73
43
  });
74
44
  });
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;
45
+ };
46
+
47
+ class SHExecute {
48
+ #proc;
49
+ #command = '';
50
+ #options = {};
51
+ #stdout = '';
52
+ #stderr = '';
53
+ constructor(command, options = {}) {
54
+ this.#command = command;
55
+ this.#options = options;
56
+ this.#proc = null;
57
+ this.#stdout = '';
58
+ this.#stderr = '';
59
+ }
60
+ runSync () {
61
+ let { cwd, shell, env, stdio } = this.#options;
62
+ return spawnSync(this.#options.prefix, [this.#command], {
63
+ cwd,
64
+ shell,
65
+ stdio,
66
+ windowsHide: true,
67
+ env,
68
+ });
400
69
  }
401
- return out + '\n';
402
- }
70
+ /**
71
+ * @param {string} [payload] - data to write
72
+ * @retuns {Promise<string>}
73
+ */
74
+ run(payload) {
75
+ if (payload && typeof payload !== 'string') {
76
+ throw new Error('Argument is not a string');
77
+ }
78
+ let { cwd, shell, env, stdio } = this.#options;
79
+ if (payload) stdio = ['pipe', 'pipe', 'pipe'];
80
+ this.#proc = spawn(this.#options.prefix, [this.#command], {
81
+ cwd,
82
+ shell,
83
+ stdio,
84
+ windowsHide: true,
85
+ env,
86
+ });
403
87
 
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
88
+ this.#proc.stdout?.on('data', (data) => {
89
+ this.#stdout += data;
90
+ });
430
91
 
92
+ this.#proc.stderr?.on('data', (data) => {
93
+ this.#stderr += data;
94
+ });
95
+ if (payload) {
96
+ this.#proc.stdin.write(payload);
97
+ this.#proc.stdin.end();
98
+ }
99
+ return new Promise((resolve, reject) => {
100
+ this.#proc.on('close', (code) => {
101
+ if (code === 0) {
102
+ resolve(this.#stdout.trim());
103
+ } else {
104
+ reject(new Error(`${code}: ${this.#command} "${this.#stderr.trim()}"`));
105
+ }
106
+ });
431
107
 
432
- /**
433
- * The ProcessPromise returns a ProcessOutput even when it fails, by rejecting it.
434
- * The extension of the Error class is implemented to ensure compatibility when an Error is expected upon rejection.
435
- */
436
- class ProcessOutput extends Error {
437
- #code = 0;
438
- #signal;
439
- #stdout = '';
440
- #stderr = '';
441
- #combined = '';
442
- /**
443
- * @param {number} code - exit code
444
- * @param {string} signal - SIGTERM ...
445
- * @param {string} stdout - std reponse string
446
- * @param {string} stderr - error reponse string
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';
458
- }
459
- /**
460
- * This string represents the standard output (stdout) from the child process.
461
- * @returns {string}
462
- */
463
- get stdout() {
464
- return this.#stdout;
465
- }
466
- /**
467
- * This string represents the error output (stderr) from the child process.
468
- * @returns {string}
469
- */
470
- get stderr() {
471
- return this.#stderr;
108
+ this.#proc.on('error', (err) => {
109
+ reject(err);
110
+ });
111
+ });
472
112
  }
113
+ /**
114
+ * @returns {Promise<number[]>}
115
+ */
116
+ async kill(signal = 'SIGTERM') {
117
+ if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
118
+ if (!this.#proc.pid) throw new Error('The process pid is undefined.');
473
119
 
474
- /**
475
- * This is an internal method that is often invoked automatically
476
- * by various JavaScript methods.
477
- * It consolidates the entire output for completeness and facilitates further processing.
478
- *
479
- * @returns {string} The consolidated output as a
480
- */
481
- toString() {
482
- return this.#combined.trim();
120
+ return killProcesses(this.#proc.pid, signal);
483
121
  }
484
- /**
485
- * This represents the exit code returned by the external process.
486
- * @returns {number} The exit
487
- */
488
- get exitCode() {
489
- return this.#code;
490
- }
491
- /**
492
- * This represents the exit signal, for example, "SIGTERM", received from the child process.
493
- * @returns {string} The exit signal from the child
494
- */
495
- get signal() {
496
- return this.#signal;
497
- }
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
122
  }
514
123
 
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
124
  /**
552
- * @typedef {Function} PromiseConstruct
553
- * @param {resolver} resolve
554
- * @param {rejecter} reject
555
- */
125
+ * @typedef {Object} SpawnSyncResponse
126
+ * @property {number} status - The exit code of the child process. A value of `0` indicates success.
127
+ * @property {Buffer|null} signal - The signal used to terminate the process, if any.
128
+ * @property {Array<string|null>} output - An array containing the standard output and standard error of the child process.
129
+ * @property {number} pid - The process ID of the child process.
130
+ * @property {Buffer|null} stdout - The standard output of the child process.
131
+ * @property {Buffer|null} stderr - The standard error of the child process.
132
+ */
133
+ /**
134
+ * Default options for the execution environment.
135
+ *
136
+ * @typedef {Object} SHOptions
137
+ * @property {string} [cwd] - The current working directory.
138
+ * @property {NodeJS.ProcessEnv} [env] - The environment variables.
139
+ * @property {string} [shell] - The shell to use for execution.
140
+ * @property {string} [prefix] - The prefix commands to ensure a safe execution environment.
141
+ * @property {StdioOption|StdioOptions} [stdio] - The stdio configuration.
142
+ */
556
143
  /**
557
144
  * @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
558
145
  * @description Defines the stdio configuration for each of the standard streams.
@@ -564,7 +151,6 @@ class ProcessOutput extends Error {
564
151
  * - Stream object to be used for the stdio stream.
565
152
  * - Positive integer representing a file descriptor to be used for the stdio stream.
566
153
  */
567
-
568
154
  /**
569
155
  * @typedef {Array<StdioOption>|StdioOption} StdioOptions
570
156
  * @description
@@ -578,287 +164,96 @@ class ProcessOutput extends Error {
578
164
  * - ['pipe', 'pipe', 'ignore']: Pipe stdin and stdout, ignore stderr.
579
165
  * - 'inherit': Inherit all stdio streams from the parent.
580
166
  */
167
+ /**
168
+ * 'Code Safe' has own prop
169
+ *
170
+ * @param {any} o - object to examine
171
+ * @param {string} p - property to look for
172
+ * @returns {boolean}
173
+ */
174
+ const hasProp = (o, p) => {
175
+ if (typeof o === 'undefined') {
176
+ return false;
177
+ }
178
+ return Object.prototype.hasOwnProperty.call(o, p);
179
+ };
581
180
 
582
181
  /**
583
- * class extends promise
182
+ * Merge property values while maintaining the fixed set of props in the original object
183
+ * @param {object} predefined - original object
184
+ * @param {object} options - object with new values
185
+ * @retuns {object}
584
186
  */
585
- class ProcessPromise extends Promise {
586
- #command = '';
587
- #from = '';
588
- /** @type {resolver} */
589
- #resolve = () => { };
590
- /** @type {rejecter} */
591
- #reject = () => { };
592
- #snapshot = {};
593
- /** @type {StdioOptions} */
594
- #stdio = ['inherit', 'pipe', 'pipe'];
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;
187
+ const mergeOptions = (predefined, options) => {
188
+ // Extract the keys from the predefined object
189
+ const keys = Object.keys(predefined);
190
+
191
+ // Use reduce to accumulate only the predefined properties from sourceObj
192
+ const mergedObj = keys.reduce((acc, key) => {
193
+ if (hasProp(options, key)) {
194
+ acc[key] = options[key];
195
+ } else {
196
+ acc[key] = predefined[key];
780
197
  }
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.');
198
+ return acc;
199
+ }, {});
791
200
 
792
- return await killProcesses(this.child.pid, signal)
793
- }
794
- stdio(stdin, stdout = 'pipe', stderr = 'pipe') {
795
- this.#stdio = [stdin, stdout, stderr];
796
- return this;
797
- }
798
- /**
799
- * Do not throw
800
- */
801
- nothrow() {
802
- this.#nothrow = true;
803
- return this;
804
- }
805
- /**
806
- * supress log output
807
- * SH.verbose = false; does the same
808
- */
809
- quiet() {
810
- this.#quiet = true;
811
- return this;
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
- }
201
+ return mergedObj;
202
+ };
203
+
204
+
205
+ /** @type {SHOptions} */
206
+ const defaultOptions = {
207
+ cwd: process.cwd(),
208
+ env: process.env,
209
+ shell: 'bash',
210
+ prefix: 'set -euo pipefail;/usr/bin/env',
211
+ stdio: ['inherit', 'pipe', 'pipe']
212
+ };
213
+
214
+
215
+
216
+ class SHDispatch {
217
+ #cmd = '';
218
+ #options = {};
219
+ #proc;
831
220
  /**
832
- * stop execution for the next step
221
+ * @param {string} cmd - cmd to execute
833
222
  */
834
- halt() {
835
- this.#halted = true;
223
+ constructor(cmd) {
224
+ this.#cmd = cmd;
225
+ this.#options = defaultOptions;
226
+ }
227
+ /**
228
+ * @param {SHOptions} options
229
+ * @returns {SHDispatch}
230
+ */
231
+ options(options) {
232
+ this.#options = mergeOptions(defaultOptions, options);
836
233
  return this;
837
234
  }
838
- /**
839
- * @private
840
- * Set a prerun action, internal use only
841
- * @param {function} f
842
- */
843
- set _prerun(f) {
844
- // @ts-ignore
845
- this.#prerun = f;
235
+ /**
236
+ * @param {string} [payload]
237
+ * @returns {Promise<string>}
238
+ */
239
+ run(payload) {
240
+ this.#proc = new SHExecute(this.#cmd, this.#options);
241
+ return this.#proc.run(payload);
846
242
  }
847
- /**
848
- * @private
849
- * Set a postrun action, internal use only
850
- * @param {function} f
851
- */
852
- set _postrun(f) {
243
+
244
+ /**
245
+ * Works for screen takeovers like editors
246
+ * @returns {SpawnSyncResponse}
247
+ */
248
+ runSync() {
853
249
  // @ts-ignore
854
- this.#postrun = f;
250
+ return new SHExecute(this.#cmd, this.#options).runSync();
855
251
  }
856
- /**
857
- * Is this promise halted?
858
- * @returns {boolean}
859
- */
860
- get isHalted() {
861
- return this.#halted;
252
+ async kill() {
253
+ try {
254
+ await this.#proc.kill();
255
+ } catch (_e) {}
256
+ this.#proc = undefined;
862
257
  }
863
258
  }
864
259
 
@@ -883,60 +278,85 @@ class ProcessPromise extends Promise {
883
278
  // The core functionality of this code is highly beneficial. However, certain parts of the original code
884
279
  // were overwriting the global namespace with core libraries and variables. This was causing conflicts with
885
280
  // other packages (for instance, fetch) and introducing unexpected elements into my code base.
281
+ // The main $/SH method is all there is left, with barebone Promises and readable code.
886
282
  // Changes Made:
887
283
  // - 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.
284
+ // - Some methods were added and existing ones were modified or deleted to enhance usability.
285
+ // - Most methods were deleted,
889
286
  // - The namespace has been changed from '$' to 'SH'.
890
287
  // Modified by: jorrit.duin+sh[AT]gmail.com
891
288
 
892
- const storage = new AsyncLocalStorage();
893
289
 
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
290
 
903
291
  /**
904
- * Escape CLI arguments
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.
292
+ * Creates a new SHDispatch object that represents a command to be executed.
921
293
  *
922
294
  * @typedef {Function} Shell
923
295
  * @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
924
- * @property {boolean} verbose - A property to control verbosity.
925
296
  *
926
297
  * @param {Array} pieces - An array of string literals from a template literal.
927
298
  * @param {...*} args - The values to be interpolated into the string literals.
928
- * @returns {ProcessPromise} A ProcessPromise object that represents the command.
299
+ * @returns {SHDispatch} Trigger for the command.
929
300
  * @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
930
301
  *
931
302
  * @example
932
- * const command = await SH`echo 'Hello, world!'`;
303
+ * const command = await SH`echo 'Hello, world!'`.run();
933
304
  */
934
- /** @type {Shell & { (pieces: TemplateStringsArray, ...args: any[]): ProcessPromise }} */
935
- // @ts-ignore
305
+
306
+ /**
307
+ * escape paramater commands
308
+ * @param {string} arg
309
+ * @returns {string}
310
+ */
311
+ const quote = (arg) => {
312
+ if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
313
+ return arg;
314
+ }
315
+ return (`'` +
316
+ arg
317
+ .replace(/\\/g, '\\\\')
318
+ .replace(/'/g, "\\'")
319
+ .replace(/\f/g, '\\f')
320
+ .replace(/\n/g, '\\n')
321
+ .replace(/\r/g, '\\r')
322
+ .replace(/\t/g, '\\t')
323
+ .replace(/\v/g, '\\v')
324
+ .replace(/\0/g, '\\0') +
325
+ `'`);
326
+ };
327
+ /**
328
+ * Escape CLI arguments
329
+ * @param {string[]} arg
330
+ * @retruns {string}
331
+ */
332
+ const sanitizeArg = (arg) => {
333
+ const s = `${arg}`;
334
+ return quote(s);
335
+ };
336
+
337
+ /**
338
+ * 4ms, 5s || 5
339
+ * @param {number|string} d
340
+ * @returns {number}
341
+ */
342
+ const parseDuration = (d) => {
343
+ if (typeof d == 'number') {
344
+ if (isNaN(d) || d < 0)
345
+ throw new Error(`Invalid duration: "${d}".`);
346
+ return d;
347
+ }
348
+ else if (/\d+s/.test(d)) {
349
+ return +d.slice(0, -1) * 1000;
350
+ }
351
+ else if (/\d+ms/.test(d)) {
352
+ return +d.slice(0, -2);
353
+ }
354
+ throw new Error(`Unknown duration: "${d}".`);
355
+ };
356
+ /** @type {Shell & { (pieces: TemplateStringsArray, ...args: *): SHDispatch }} */
936
357
  const SH = new Proxy(function(pieces, ...args) {
937
- const from = new Error().stack.split(/^\s*at\s/m)[2].trim();
938
358
  if (pieces.some((p) => p == undefined)) {
939
- throw new Error(`Malformed command at ${from}`);
359
+ throw new Error(`Malformed command ${pieces}`);
940
360
  }
941
361
  let cmd = pieces[0], i = 0;
942
362
  while (i < args.length) {
@@ -949,29 +369,8 @@ const SH = new Proxy(function(pieces, ...args) {
949
369
  }
950
370
  cmd += s + pieces[++i];
951
371
  }
952
- let resolve, reject;
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
- });
372
+ return new SHDispatch(cmd);
373
+ }, {});
975
374
 
976
375
  /**
977
376
  * Create a async context in an sync block
@@ -979,23 +378,23 @@ const SH = new Proxy(function(pieces, ...args) {
979
378
  * @example
980
379
  * const p = within(async () => {
981
380
  * const res = await Promise.all([
982
- * SH`sleep 1; echo 1`,
983
- * SH`sleep 2; echo 2`,
381
+ * SH`sleep 1; echo 1`.run(),
382
+ * SH`sleep 2; echo 2`.run(),
984
383
  * sleep(2),
985
- * SH`sleep 3; echo 3`
384
+ * SH`sleep 3; echo 3`.run()
986
385
  * ]);
987
386
  */
988
387
  const within = (callback) => {
989
- // @ts-ignore
990
- return storage.run({ ...getStore() }, callback);
388
+ (async () => {
389
+ return await callback()
390
+ })();
991
391
  };
992
392
  /**
993
- * This function reads the standard input (stdin) for the current process.
994
- * It is used to get piped content into a script.
393
+ * This function reads the standard input (stdin) from the current process.
995
394
  * @example
996
395
  * const content = await stdin();
997
396
  */
998
- const stdin = async () => {
397
+ const readIn = async () => {
999
398
  let buf = '';
1000
399
  process.stdin.setEncoding('utf8');
1001
400
  for await (const chunk of process.stdin) {
@@ -1003,8 +402,16 @@ const stdin = async () => {
1003
402
  }
1004
403
  return buf;
1005
404
  };
405
+
1006
406
  /**
1007
- * This function retries a command a specified number of times.
407
+ * Retries a given asynchronous function a specified number of times with optional delays between attempts.
408
+ *
409
+ * @param {number} count - The number of retry attempts.
410
+ * @param {string|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
411
+ * @param {Function} [b] - The callback function to retry, required if `a` is not a function.
412
+ * @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
413
+ * @throws {Error} - The last error encountered if all retry attempts fail.
414
+ *
1008
415
  * @example
1009
416
  * // Retry a command 3 times
1010
417
  * const p = await retry(3, () => SH`curl -s https://flipwrsi`);
@@ -1016,12 +423,11 @@ const stdin = async () => {
1016
423
  * const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
1017
424
  */
1018
425
  const retry = async (count, a, b) => {
1019
- const total = count;
426
+ // const total = count;
1020
427
  let callback;
1021
428
  let delayStatic = 0;
1022
429
  let delayGen;
1023
430
  // @ts-ignore
1024
- const verbose = SH.verbose;
1025
431
  if (typeof a == 'function') {
1026
432
  callback = a;
1027
433
  }
@@ -1036,9 +442,7 @@ const retry = async (count, a, b) => {
1036
442
  callback = b;
1037
443
  }
1038
444
  let lastErr;
1039
- let attempt = 0;
1040
445
  while (count-- > 0) {
1041
- attempt++;
1042
446
  try {
1043
447
  return await callback();
1044
448
  }
@@ -1047,13 +451,6 @@ const retry = async (count, a, b) => {
1047
451
  if (delayStatic > 0)
1048
452
  delay = delayStatic;
1049
453
  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
454
  lastErr = err;
1058
455
  if (count == 0)
1059
456
  break;
@@ -1069,11 +466,7 @@ const retry = async (count, a, b) => {
1069
466
  *
1070
467
  * @example
1071
468
  *
1072
- * const res = await Promise.all([
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
- * ]);
469
+ * const res = await sleep('5s');
1077
470
  */
1078
471
  const sleep = (duration) => {
1079
472
  return new Promise((resolve) => {
@@ -1086,10 +479,16 @@ const sleep = (duration) => {
1086
479
  */
1087
480
  const cd = (dir) => {
1088
481
  // @ts-ignore
1089
- const verbose = SH.verbose;
1090
- log({ kind: 'cd', dir, verbose });
1091
482
  process.chdir(dir);
1092
483
  };
484
+ /**
485
+ * Generates an exponential backoff time with a random jitter.
486
+ *
487
+ * @generator
488
+ * @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
489
+ * @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
490
+ * @yields {number} The backoff time in milliseconds.
491
+ */
1093
492
  function* expBackoff(max = '60s', rand = '100ms') {
1094
493
  const maxMs = parseDuration(max);
1095
494
  const randMs = parseDuration(rand);
@@ -1100,4 +499,4 @@ function* expBackoff(max = '60s', rand = '100ms') {
1100
499
  }
1101
500
  }
1102
501
 
1103
- export { SH, cd, expBackoff, retry, sleep, stdin, within };
502
+ export { SH, cd, expBackoff, readIn, retry, sleep, within };