@j-o-r/sh 1.1.28 → 1.1.29
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 +119 -154
- package/TODO.md +18 -0
- package/lib/AsyncTracker.js +126 -107
- package/lib/SH.js +158 -154
- package/lib/SHDispatch.js +105 -91
- package/lib/SHExecute.js +83 -41
- package/lib/Test.js +153 -123
- package/package.json +1 -1
- package/types/AsyncTracker.d.ts +65 -31
- package/types/SH.d.ts +157 -164
- package/types/SHDispatch.d.ts +55 -78
- package/types/SHExecute.d.ts +44 -23
- package/types/Test.d.ts +66 -30
package/lib/SH.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// - Some methods were added and existing ones were modified or deleted to enhance usability.
|
|
26
26
|
// - The namespace has been changed from '$' to 'SH'.
|
|
27
27
|
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
import assert from 'node:assert';
|
|
30
30
|
import readline from 'node:readline/promises';
|
|
31
31
|
import SHDispatch from './SHDispatch.js';
|
|
@@ -33,40 +33,47 @@ import Test from './Test.js'
|
|
|
33
33
|
import AsyncTracker from './AsyncTracker.js'
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* @typedef {Object.<string, string>} ArgsObject
|
|
37
|
-
* @property {string}
|
|
38
|
-
* @property {string[]
|
|
39
|
-
* @description Parsed parameters result.
|
|
40
|
-
*/
|
|
36
|
+
* @typedef {Object.<string, string>} ArgsObject
|
|
37
|
+
* @property {string[]} _ - Array of strings, unnamed parameters
|
|
38
|
+
* @property {string} [key: string] - Any string key maps to a string value
|
|
39
|
+
* @description Parsed parameters result.
|
|
40
|
+
*/
|
|
41
|
+
|
|
41
42
|
/**
|
|
42
|
-
* @typedef {Function} RejectCallback
|
|
43
|
-
* @param {Error} error - The error object passed to the callback.
|
|
44
|
-
*/
|
|
43
|
+
* @typedef {Function} RejectCallback
|
|
44
|
+
* @param {Error} error - The error object passed to the callback.
|
|
45
|
+
*/
|
|
46
|
+
|
|
45
47
|
/**
|
|
46
|
-
* @typedef {Function} ResolveCallback
|
|
47
|
-
* @param {any} [param] - Optional callback any value
|
|
48
|
-
*/
|
|
48
|
+
* @typedef {Function} ResolveCallback
|
|
49
|
+
* @param {any} [param] - Optional callback any value
|
|
50
|
+
*/
|
|
51
|
+
|
|
49
52
|
/**
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* @
|
|
56
|
-
* @
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* @
|
|
61
|
-
*
|
|
62
|
-
|
|
53
|
+
* @typedef {import('./SHDispatch.js').SHOptions} SHOptions
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} AbortableInput
|
|
58
|
+
* @property {Promise<string|void>} input - Promise that resolves to user input or void if aborted.
|
|
59
|
+
* @property {() => void} abort - Function to abort input collection.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} ExpBackoffGenerator
|
|
64
|
+
* @generator
|
|
65
|
+
* @yields {number} Backoff time in ms.
|
|
66
|
+
*/
|
|
63
67
|
|
|
64
68
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* @param {any} fn - Any
|
|
68
|
-
* @returns {string} The "real" object
|
|
69
|
-
|
|
69
|
+
* Utility to determine the JavaScript type of a value.
|
|
70
|
+
*
|
|
71
|
+
* @param {any} fn - Any value to inspect.
|
|
72
|
+
* @returns {string} The "real" object type name (e.g., 'Array', 'Promise') or primitive typeof.
|
|
73
|
+
* @example
|
|
74
|
+
* jsType([]); // 'Array'
|
|
75
|
+
* jsType(Promise.resolve()); // 'Promise'
|
|
76
|
+
*/
|
|
70
77
|
const jsType = (fn) => {
|
|
71
78
|
if (fn === undefined) return 'undefined';
|
|
72
79
|
const type = Object.prototype.toString.call(fn).slice(8, -1);
|
|
@@ -74,21 +81,26 @@ const jsType = (fn) => {
|
|
|
74
81
|
};
|
|
75
82
|
|
|
76
83
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* @param {any} o -
|
|
80
|
-
* @param {string} p -
|
|
81
|
-
* @returns {boolean}
|
|
82
|
-
*/
|
|
84
|
+
* Checks if an object has a specific own property (code-safe).
|
|
85
|
+
*
|
|
86
|
+
* @param {any} o - Object to examine.
|
|
87
|
+
* @param {string} p - Property name to check.
|
|
88
|
+
* @returns {boolean} True if the object has the own property.
|
|
89
|
+
*/
|
|
83
90
|
const hasProp = (o, p) => {
|
|
84
91
|
if (o == null) return false;
|
|
85
92
|
return Object.prototype.hasOwnProperty.call(o, p);
|
|
86
93
|
};
|
|
94
|
+
|
|
87
95
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
96
|
+
* Parses a human-readable duration string or number into milliseconds.
|
|
97
|
+
*
|
|
98
|
+
* Supports: '5s' (5000ms), '100ms' (100ms), plain numbers (ms).
|
|
99
|
+
*
|
|
100
|
+
* @param {number|string} d - Duration as number (ms) or string ('5s', '100ms').
|
|
101
|
+
* @returns {number} Duration in milliseconds.
|
|
102
|
+
* @throws {Error} If invalid duration format.
|
|
103
|
+
*/
|
|
92
104
|
const parseDuration = (d) => {
|
|
93
105
|
if (typeof d == 'number') {
|
|
94
106
|
if (isNaN(d) || d < 0)
|
|
@@ -103,12 +115,15 @@ const parseDuration = (d) => {
|
|
|
103
115
|
}
|
|
104
116
|
throw new Error(`Unknown duration: "${d}".`);
|
|
105
117
|
}
|
|
118
|
+
|
|
106
119
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
|
|
120
|
+
* Escapes a string for safe use as a bash command-line argument.
|
|
121
|
+
*
|
|
122
|
+
* Handles quotes, backticks, $, newlines, etc.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} x - Input string to escape.
|
|
125
|
+
* @returns {string} Bash-escaped string.
|
|
126
|
+
*/
|
|
112
127
|
const bashEscape = (x) => {
|
|
113
128
|
let str = String(x).trim();
|
|
114
129
|
// the trick is to double escape escape vars first
|
|
@@ -125,8 +140,7 @@ const bashEscape = (x) => {
|
|
|
125
140
|
return str;
|
|
126
141
|
}
|
|
127
142
|
|
|
128
|
-
|
|
129
|
-
/** @type {import('./SHDispatch.js').SHOptions} */
|
|
143
|
+
/** Default options applied to all SH commands unless overridden. */
|
|
130
144
|
const defaultOptions = {
|
|
131
145
|
cwd: process.cwd(),
|
|
132
146
|
env: process.env,
|
|
@@ -135,30 +149,36 @@ const defaultOptions = {
|
|
|
135
149
|
timeout: 0, // when 0 there is no timeout
|
|
136
150
|
};
|
|
137
151
|
|
|
152
|
+
/** Proxy set trap for dynamically updating defaultOptions via SH.prop = value */
|
|
138
153
|
const setHandler = {
|
|
139
154
|
set(target, prop, value) {
|
|
140
|
-
defaultOptions[prop] = value;
|
|
141
|
-
// console.log(`Set ${String(prop)} = ${value}`);
|
|
142
|
-
// console.log(defaultOptions);
|
|
155
|
+
defaultOptions[prop] = value;
|
|
143
156
|
return true;
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* @
|
|
159
|
-
*
|
|
160
|
-
* @returns {SHDispatch}
|
|
161
|
-
|
|
161
|
+
* Template tag for building and executing shell commands.
|
|
162
|
+
*
|
|
163
|
+
* Returns an {@link SHDispatch} instance for configuration (.options()) and execution (.run(), .runSync()).
|
|
164
|
+
*
|
|
165
|
+
* Interpolation rules:
|
|
166
|
+
* - Arrays: Elements trimmed, newlines/tabs escaped, shell-special chars single-quoted with '\'' escape.
|
|
167
|
+
* - Other values: String(value) inserted as-is (NOT auto-escaped; use {@link bashEscape} for safety).
|
|
168
|
+
*
|
|
169
|
+
* SH acts as both template tag and options setter: `SH.timeout = 5000; SH`cmd``
|
|
170
|
+
*
|
|
171
|
+
* @param {TemplateStringsArray} pieces - String literals from template.
|
|
172
|
+
* @param {...unknown[]} args - Values to interpolate.
|
|
173
|
+
* @returns {SHDispatch} Command dispatcher.
|
|
174
|
+
* @throws {Error} If pieces contain undefined.
|
|
175
|
+
* @example
|
|
176
|
+
* const cmd = SH`echo ${'Hello'}`;
|
|
177
|
+
* await cmd.run(); // Executes: echo Hello
|
|
178
|
+
*
|
|
179
|
+
* // Array interpolation
|
|
180
|
+
* SH`ls ${['-la', '/']}`.run(); // ls '-la' '/'
|
|
181
|
+
*/
|
|
162
182
|
const SH = new Proxy(function(pieces, ...args) {
|
|
163
183
|
if (pieces.some((p) => p == undefined)) {
|
|
164
184
|
throw new Error(`Malformed command ${pieces}`);
|
|
@@ -188,37 +208,31 @@ const SH = new Proxy(function(pieces, ...args) {
|
|
|
188
208
|
return new SHDispatch(cmd, defaultOptions);
|
|
189
209
|
}, setHandler);
|
|
190
210
|
|
|
191
|
-
|
|
192
211
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
|
|
204
|
-
* });
|
|
205
|
-
*/
|
|
212
|
+
* Executes a callback in a new async context (fresh callstack).
|
|
213
|
+
*
|
|
214
|
+
* Useful for parallel operations without nesting.
|
|
215
|
+
*
|
|
216
|
+
* @param {() => Promise<any>} callback - Async function to execute.
|
|
217
|
+
* @returns {Promise<any>} Result of callback.
|
|
218
|
+
* @example
|
|
219
|
+
* const results = await within(async () => {
|
|
220
|
+
* return Promise.all([SH`sleep 1; echo 1`.run(), sleep(2)]);
|
|
221
|
+
* });
|
|
222
|
+
*/
|
|
206
223
|
const within = async (callback) => {
|
|
207
224
|
return await callback();
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
/**
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* @
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Read entire stdin as UTF-8. If stdin is a TTY, resolves to an empty string.
|
|
218
|
-
* @returns {Promise<string|undefined>}
|
|
228
|
+
* Reads entire stdin as UTF-8 string. Resolves to empty string if TTY (no pipe).
|
|
229
|
+
*
|
|
230
|
+
* @returns {Promise<string|undefined>} Stdin content or undefined if TTY.
|
|
231
|
+
* @example
|
|
232
|
+
* const input = await readIn(); // Use in piped scripts
|
|
219
233
|
*/
|
|
220
234
|
const readIn = async () => {
|
|
221
|
-
if (process.stdin.isTTY) return;
|
|
235
|
+
if (process.stdin.isTTY) return;
|
|
222
236
|
let buf = '';
|
|
223
237
|
process.stdin.setEncoding('utf8');
|
|
224
238
|
for await (const chunk of process.stdin) {
|
|
@@ -227,17 +241,18 @@ const readIn = async () => {
|
|
|
227
241
|
return buf;
|
|
228
242
|
}
|
|
229
243
|
|
|
230
|
-
|
|
231
244
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* @
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
|
|
245
|
+
* Prompts user for input on stdin with custom prompt.
|
|
246
|
+
*
|
|
247
|
+
* Supports aborting input collection.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} prompt - Prompt text to display.
|
|
250
|
+
* @returns {AbortableInput} Object with input Promise and abort function.
|
|
251
|
+
* @example
|
|
252
|
+
* const {input, abort} = userIn('Enter name: ');
|
|
253
|
+
* const name = await input;
|
|
254
|
+
* // abort(); // Cancel anytime
|
|
255
|
+
*/
|
|
241
256
|
const userIn = (prompt) => {
|
|
242
257
|
let resolvePromise;
|
|
243
258
|
const input = new Promise((resolve) => { resolvePromise = resolve; });
|
|
@@ -285,27 +300,22 @@ const userIn = (prompt) => {
|
|
|
285
300
|
}
|
|
286
301
|
};
|
|
287
302
|
};
|
|
303
|
+
|
|
288
304
|
/**
|
|
289
|
-
* Retries
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* @param {
|
|
294
|
-
* @
|
|
295
|
-
* @
|
|
296
|
-
*
|
|
297
|
-
* @
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
|
|
302
|
-
* const p = await retry(3, '1s', () => SH`curl -s https://flipwrsi`);
|
|
303
|
-
*
|
|
304
|
-
* // Retry a command 3 times with irregular intervals using exponential backoff
|
|
305
|
-
* const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
|
|
306
|
-
*/
|
|
305
|
+
* Retries an async function up to N times with optional delays.
|
|
306
|
+
*
|
|
307
|
+
* Delay can be fixed ('1s'), generator (expBackoff()), or none.
|
|
308
|
+
*
|
|
309
|
+
* @param {number} count - Number of attempts.
|
|
310
|
+
* @param {string|number|ExpBackoffGenerator|Function} delayOrFn - Delay or callback if no separate fn.
|
|
311
|
+
* @param {Function} [fn] - Callback to retry (if delayOrFn not function).
|
|
312
|
+
* @returns {Promise<any>} Successful result.
|
|
313
|
+
* @throws {Error} Last error if all attempts fail.
|
|
314
|
+
* @example
|
|
315
|
+
* await retry(3, '1s', () => SH`curl http://unreliable`.run());
|
|
316
|
+
* await retry(3, expBackoff(), () => flakyOp());
|
|
317
|
+
*/
|
|
307
318
|
const retry = async (count, a, b) => {
|
|
308
|
-
// const total = count;
|
|
309
319
|
let callback;
|
|
310
320
|
let delayStatic = 0;
|
|
311
321
|
let delayGen;
|
|
@@ -345,34 +355,43 @@ const retry = async (count, a, b) => {
|
|
|
345
355
|
}
|
|
346
356
|
throw lastErr;
|
|
347
357
|
}
|
|
358
|
+
|
|
348
359
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* @
|
|
353
|
-
*
|
|
354
|
-
* await sleep('
|
|
355
|
-
*/
|
|
360
|
+
* Sleeps for a specified duration.
|
|
361
|
+
*
|
|
362
|
+
* @param {string|number} duration - Duration as '5s', '100ms', or ms number.
|
|
363
|
+
* @returns {Promise<void>}
|
|
364
|
+
* @example
|
|
365
|
+
* await sleep('2s');
|
|
366
|
+
*/
|
|
356
367
|
const sleep = (duration) => {
|
|
357
368
|
return new Promise((resolve) => {
|
|
358
369
|
setTimeout(resolve, parseDuration(duration));
|
|
359
370
|
});
|
|
360
371
|
}
|
|
372
|
+
|
|
361
373
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
|
|
374
|
+
* Changes the current working directory.
|
|
375
|
+
*
|
|
376
|
+
* @param {string} dir - Path to new directory.
|
|
377
|
+
* @example
|
|
378
|
+
* cd('/tmp');
|
|
379
|
+
*/
|
|
365
380
|
const cd = (dir) => {
|
|
366
381
|
// @ts-ignore
|
|
367
382
|
process.chdir(dir);
|
|
368
383
|
}
|
|
384
|
+
|
|
369
385
|
/**
|
|
370
|
-
*
|
|
386
|
+
* Generator for exponential backoff delays with jitter.
|
|
371
387
|
*
|
|
372
388
|
* @generator
|
|
373
|
-
* @param {string} [max='60s'] -
|
|
374
|
-
* @param {string} [rand='100ms'] -
|
|
375
|
-
* @yields {number}
|
|
389
|
+
* @param {string} [max='60s'] - Max backoff duration.
|
|
390
|
+
* @param {string} [rand='100ms'] - Max jitter.
|
|
391
|
+
* @yields {number} Next backoff ms.
|
|
392
|
+
* @example
|
|
393
|
+
* const backoff = expBackoff();
|
|
394
|
+
* await sleep(backoff.next().value);
|
|
376
395
|
*/
|
|
377
396
|
function* expBackoff(max = '60s', rand = '100ms') {
|
|
378
397
|
const maxMs = parseDuration(max);
|
|
@@ -383,35 +402,20 @@ function* expBackoff(max = '60s', rand = '100ms') {
|
|
|
383
402
|
yield Math.min(2 ** n++, maxMs) + ms;
|
|
384
403
|
}
|
|
385
404
|
}
|
|
405
|
+
|
|
386
406
|
/**
|
|
387
|
-
* Parses
|
|
388
|
-
*
|
|
389
|
-
* The function recognizes arguments that start with two dashes (`--`) or one dash (`-`) as keys,
|
|
390
|
-
* and the subsequent value (if not another key) as the corresponding value.
|
|
391
|
-
* If a key does not have a value, it defaults to `true`.
|
|
392
|
-
* All unrecognized arguments are collected in an array under the `_` property.
|
|
393
|
-
*
|
|
394
|
-
* @param {string[]} [args] - An array of command-line arguments (process.argv.slice(2)).
|
|
395
|
-
* @returns {ArgsObject} An object where:
|
|
396
|
-
* - If args is not passed, process.argv.slice(2) will be the default
|
|
397
|
-
* - Each key corresponds to an argument that starts with `--` or `-`,
|
|
398
|
-
* - The value is either the next argument or `true` if no value is provided,
|
|
399
|
-
* - The `_` property contains an array of unbound arguments.
|
|
400
|
-
*/
|
|
401
|
-
/**
|
|
402
|
-
* Parse command-line args into an object.
|
|
407
|
+
* Parses CLI arguments into an object.
|
|
403
408
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* - Bare values collected under `_.`
|
|
407
|
-
* - Duplicate keys throw an error; keys without a following value become true.
|
|
409
|
+
* Supports --key value, -k value (no shorts grouped).
|
|
410
|
+
* Bares go to _[]. Duplicates error. No = syntax.
|
|
408
411
|
*
|
|
409
|
-
*
|
|
410
|
-
* - --key=value syntax
|
|
411
|
-
* - Grouped short flags like -abc
|
|
412
|
+
* Defaults to process.argv.slice(2).
|
|
412
413
|
*
|
|
413
|
-
* @param {string[]} [args]
|
|
414
|
+
* @param {string[]} [args=process.argv.slice(2)] - Args array.
|
|
414
415
|
* @returns {ArgsObject}
|
|
416
|
+
* @throws {Error} On invalid/dupe args.
|
|
417
|
+
* @example
|
|
418
|
+
* parseArgs(['--port', '8080', 'file.txt']); // { port: '8080', _: ['file.txt'] }
|
|
415
419
|
*/
|
|
416
420
|
const parseArgs = (args) => {
|
|
417
421
|
if (!args) args = process.argv.slice(2);
|