@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/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
- /** @type {assert} */
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} [key: string] - Any string key maps to a string value
38
- * @property {string[]} _ - Array of strings, unnamed parameters
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
- * Creates a new SHDispatch object that represents a command to be executed.
51
- *
52
- * @typedef {Function} Shell
53
- * @property {function(Array, ...*): ProcessPromise} execute - The function to execute the command.
54
- *
55
- * @param {Array} pieces - An array of string literals from a template literal.
56
- * @param {...*} args - The values to be interpolated into the string literals.
57
- * @returns {SHDispatch} Trigger for the command.
58
- * @throws {Error} Throws an error if any of the string literals in `pieces` is undefined.
59
- *
60
- * @example
61
- * const command = await SH`echo 'Hello, world!'`.run();
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
- * Determine a javascript type
66
- *
67
- * @param {any} fn - Any let type
68
- * @returns {string} The "real" object / typeof name
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
- * 'Code Safe' has own prop
78
- *
79
- * @param {any} o - object to examine
80
- * @param {string} p - property to look for
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
- * 4ms, 5s || 5
89
- * @param {number|string} d
90
- * @returns {number}
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
- * bash escape a string suitable as an argument on the commandline
108
- * for javascript code
109
- * @param {string} x
110
- * @returns {string}
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; // Allow setting on target
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
- * A template-tag that returns an SHDispatch.
149
- * @typedef {(pieces: TemplateStringsArray, ...args: unknown[]) => SHDispatch} SHTag
150
- */
151
- /**
152
- * SH template tag
153
- *
154
- * Interpolation rules:
155
- * - Arrays: each element is String(x).trim(), with newlines/carriage returns/tabs escaped; if an element contains shell metacharacters or spaces, it is wrapped in single-quotes and internal single-quotes are escaped as '\''.
156
- * - Non-array values: currently coerced with String(value) and inserted as-is (NOT shell-escaped). Be careful when interpolating untrusted input.
157
- *
158
- * @type {Shell & SHTag & defaultOptions}
159
- * Returns an SHDispatch that can be configured via .options() and executed with .run() / .runSync().
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
- * Create a async/sync context in new execution callstack
194
- * @param {function} callback - async/sync function
195
- * @example
196
- * const p = within(async () => {
197
- * const res = await Promise.all([
198
- * SH`sleep 1; echo 1`.run(),
199
- * SH`sleep 2; echo 2`.run(),
200
- * sleep(2),
201
- * SH`sleep 3; echo 3`.run()
202
- * ]);
203
- * return 'res';
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
- * This function reads the standard input (stdin) from the current process.
212
- * @returns {Promise<string>}
213
- * @example
214
- * const content = await readIn();
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; // nothing was piped
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
- * Get user input from the command line (stdin)
233
- * void when input has been aborted
234
- * @param {string} prompt - prompt or question
235
- * @returns {{input: Promise<string|void>, abort: function(): void}}
236
- * @example
237
- * const user = userIn('Question: ');
238
- * const content = await user.input
239
- * // user.abort()
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 a given asynchronous function a specified number of times with optional delays between attempts.
290
- *
291
- * @param {number} count - The number of retry attempts.
292
- * @param {string|number|expBackoff|Function} a - Either a delay duration as a string, a delay generator object, or the callback function.
293
- * @param {Function} [b] - The callback function to retry, required if `a` is not a function.
294
- * @returns {Promise<*>} - The result of the callback function if it succeeds within the retry attempts.
295
- * @throws {Error} - The last error encountered if all retry attempts fail.
296
- *
297
- * @example
298
- * // Retry a command 3 times
299
- * const p = await retry(3, () => SH`curl -s https://flipwrsi`);
300
- *
301
- * // Retry a command 3 times with an interval of 1 second between each try
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
- * This function pauses or "sleeps" code execution for a specified duration.
350
- * @param {string|number} duration - The duration to pause execution for, e.g., '100ms' or '3s'.
351
- *
352
- * @example
353
- *
354
- * await sleep('5s');
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
- * Change working directory
363
- * @param {string} dir
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
- * Generates an exponential backoff time with a random jitter.
386
+ * Generator for exponential backoff delays with jitter.
371
387
  *
372
388
  * @generator
373
- * @param {string} [max='60s'] - The maximum backoff time in a human-readable format (e.g., '60s' for 60 seconds).
374
- * @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
375
- * @yields {number} The backoff time in milliseconds.
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 command-line arguments into an object.
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
- * Supported:
405
- * - --key value, -k value (no grouped short flags)
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
- * Not supported:
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);