@j-o-r/sh 1.1.5 → 1.1.7

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
@@ -31,12 +31,11 @@ import SHDispatch from './SHDispatch.js';
31
31
  import Test from './Test.js'
32
32
 
33
33
  /**
34
- * @typedef {Object.<string, string>} ArgsObject
35
- * @property {string} [key: string] - Any string key maps to an object
34
+ * @typedef {Object.<string, string | string[]>} ArgsObject
35
+ * @property {string} [key: string] - Any string key maps to a string value
36
36
  * @property {string[]} _ - Array of strings, unnamed parameters
37
37
  * @description Parsed parameters result.
38
38
  */
39
-
40
39
  /**
41
40
  * @typedef {Function} RejectCallback
42
41
  * @param {Error} error - The error object passed to the callback.
@@ -271,7 +270,7 @@ function* expBackoff(max = '60s', rand = '100ms') {
271
270
  * - The value is either the next argument or `true` if no value is provided,
272
271
  * - The `_` property contains an array of unbound arguments.
273
272
  */
274
- const parseArgs = (args) => {
273
+ const parseArgsOld = (args) => {
275
274
  const result = { _: [] }; // Initialize result with an empty array for unbound values
276
275
  for (let i = 0; i < args.length; i++) {
277
276
  if (args[i].startsWith('--')) {
@@ -285,7 +284,49 @@ const parseArgs = (args) => {
285
284
  }
286
285
  return result;
287
286
  }
288
-
287
+ /**
288
+ * Parses command-line arguments into an object.
289
+ *
290
+ * The function recognizes arguments that start with two dashes (`--`) or one dash (`-`) as keys,
291
+ * and the subsequent value (if not another key) as the corresponding value.
292
+ * If a key does not have a value, it defaults to `true`.
293
+ * All unrecognized arguments are collected in an array under the `_` property.
294
+ *
295
+ * @param {string[]} [args] - An array of command-line arguments (process.argv.slice(2)).
296
+ * @returns {ArgsObject} An object where:
297
+ * - If args is not passed, process.argv.slice(2) will be the default
298
+ * - Each key corresponds to an argument that starts with `--` or `-`,
299
+ * - The value is either the next argument or `true` if no value is provided,
300
+ * - The `_` property contains an array of unbound arguments.
301
+ */
302
+ const parseArgs = (args) => {
303
+ if (!args) args = process.argv.slice(2);
304
+ const result = { _: [] };
305
+ const seenKeys = new Set();
306
+ for (let i = 0; i < args.length; i++) {
307
+ if (args[i].startsWith('--') || args[i].startsWith('-')) {
308
+ if (args[i].startsWith('-') && !args[i].startsWith('--') && args[i].length > 2) {
309
+ throw new Error(`Invalid argument: ${args[i]}. Use '--' for long options.`);
310
+ }
311
+ const key = args[i].startsWith('--') ? args[i].substring(2) : args[i].substring(1);
312
+ if (seenKeys.has(key)) {
313
+ throw new Error(`Duplicate argument: ${args[i]}`);
314
+ }
315
+ seenKeys.add(key);
316
+ let value;
317
+ if (args[i + 1] && !args[i + 1].startsWith('-')) {
318
+ value = args[i + 1];
319
+ i++; // Skip next element as it is a value
320
+ } else {
321
+ value = true;
322
+ }
323
+ result[key] = value;
324
+ } else {
325
+ result._.push(args[i]);
326
+ }
327
+ }
328
+ return result;
329
+ };
289
330
 
290
331
  export {
291
332
  SH,
package/lib/SHDispatch.js CHANGED
@@ -15,10 +15,8 @@ import SHExec from './SHExecute.js';
15
15
  * @property {string} [cwd] - The current working directory.
16
16
  * @property {NodeJS.ProcessEnv} [env] - The environment variables.
17
17
  * @property {string} [shell] - The shell to use for execution.
18
- * @property {string} [prefix] - The prefix commands to ensure a safe execution environment. e.g: prefix: 'set -euo pipefail;/usr/bin/env',
19
18
  * @property {StdioOptions|StdioOption} [stdio] - The stdio configuration.
20
19
  * @property {number} [timeout] - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
21
- * @property {boolean} [detached] - default false, when true it runnes as a background process
22
20
  */
23
21
  /**
24
22
  * @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
@@ -45,39 +43,17 @@ import SHExec from './SHExecute.js';
45
43
  * - 'inherit': Inherit all stdio streams from the parent.
46
44
  */
47
45
  /**
48
- * 'Code Safe' has own prop
49
- *
50
- * @param {any} o - object to examine
51
- * @param {string} p - property to look for
52
- * @returns {boolean}
53
- */
54
- const hasProp = (o, p) => {
55
- if (typeof o === 'undefined') {
56
- return false;
57
- }
58
- return Object.prototype.hasOwnProperty.call(o, p);
59
- };
60
-
61
- /**
62
- * Merge property values while maintaining the fixed set of props in the original object
63
- * @param {SHOptions} predefined - original object
64
- * @param {SHOptions} options - object with new values
46
+ * Merge property values while maintaining the fixed set of props from the predefined object
47
+ * @param {SHOptions} predefined - options
48
+ * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
65
49
  * @returns {SHOptions}
66
50
  */
67
51
  const mergeOptions = (predefined, options) => {
68
- // Extract the keys from the predefined object
69
52
  const keys = Object.keys(predefined);
70
-
71
- // Use reduce to accumulate only the predefined properties from sourceObj
72
53
  const mergedObj = keys.reduce((acc, key) => {
73
- if (hasProp(options, key)) {
74
- acc[key] = options[key];
75
- } else {
76
- acc[key] = predefined[key];
77
- }
54
+ acc[key] = options[key] !== undefined ? options[key] : predefined[key];
78
55
  return acc;
79
56
  }, {});
80
-
81
57
  return mergedObj;
82
58
  }
83
59
 
@@ -87,8 +63,6 @@ const defaultOptions = {
87
63
  cwd: process.cwd(),
88
64
  env: process.env,
89
65
  shell: 'bash',
90
- detached: false,
91
- prefix: '/usr/bin/env',
92
66
  stdio: ['inherit', 'pipe', 'pipe'],
93
67
  timeout: 10000 // when 0 there is no timeout
94
68
  };
@@ -96,6 +70,8 @@ const defaultOptions = {
96
70
 
97
71
 
98
72
  class SHDispatch {
73
+ // #prefix = 'set -euo pipefail;/usr/bin/env'
74
+ #prefix = 'set -euo pipefail;/usr/bin/env -S'
99
75
  #cmd = '';
100
76
  #options = {};
101
77
  /**
@@ -106,14 +82,21 @@ class SHDispatch {
106
82
  * @param {string} cmd - cmd to execute
107
83
  */
108
84
  constructor(cmd) {
85
+ if (!cmd || cmd === '') {
86
+ throw new Error('Undefined command');
87
+ }
109
88
  this.#cmd = cmd;
110
89
  this.#options = defaultOptions
111
90
  }
112
91
  /**
113
- * @param {SHOptions} options
92
+ * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
93
+ * @param {string} [prefix] - command prefix e.g (default) '/usr/bin/env'
114
94
  * @returns {SHDispatch}
115
95
  */
116
- options(options) {
96
+ options(options, prefix) {
97
+ if (prefix && typeof prefix === 'string') {
98
+ this.#prefix = prefix;
99
+ }
117
100
  if (options.stdio && typeof options.stdio === 'string') {
118
101
  // convert stdio to array
119
102
  // This sets the default io values
@@ -129,18 +112,17 @@ class SHDispatch {
129
112
  * @returns {Promise<string>}
130
113
  */
131
114
  run(payload) {
132
- this.#proc = new SHExec(this.#cmd, this.#options);
115
+ this.#proc = new SHExec(this.#cmd, this.#prefix, this.#options);
133
116
  return this.#proc.run(payload);
134
117
  }
135
118
 
136
119
  /**
137
120
  * Works for screen takeovers like editors
138
121
  * @param {string} [payload]
139
- * @returns {SpawnSyncResponse}
122
+ * @returns {import('child_process').SpawnSyncReturns}
140
123
  */
141
124
  runSync(payload) {
142
- // @ts-ignore
143
- return new SHExec(this.#cmd, this.#options).runSync(payload);
125
+ return new SHExec(this.#cmd, this.#prefix, this.#options).runSync(payload);
144
126
  }
145
127
  async kill(signal = 'SIGTERM') {
146
128
  let res = [];
package/lib/SHExecute.js CHANGED
@@ -3,7 +3,7 @@ import { spawnSync, spawn, exec } from 'node:child_process';
3
3
  /**
4
4
  * Kills a process and all child processes of a given process ID in Linux/Posix.
5
5
  * @param {number} processPid - The process ID.
6
- * @param {string} signal - Signal to send.
6
+ * @param {string|number} signal - Signal to send.
7
7
  * @retruns {Promise<number[]>} array with killed pid numbers
8
8
  */
9
9
  const killProcesses = (processPid, signal) => {
@@ -45,20 +45,24 @@ const killProcesses = (processPid, signal) => {
45
45
  }
46
46
 
47
47
  class SHExecute {
48
+ #forcedKill = false;
48
49
  /**
49
50
  * @type {import('child_process').ChildProcess}
50
51
  */
51
- #forcedKill = false;
52
52
  #proc;
53
+ #prefix = '';
53
54
  #command = '';
55
+ /** @type {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} */
54
56
  #options = {};
55
57
  #stdout = '';
56
58
  #stderr = '';
57
59
  /**
58
60
  * @param {string} command - linux command to be executed
59
- * @param {import('./SHDispatch').SHOptions} [options] - ChildProcess options
61
+ * @param {string} prefix - command prefix (bash, sh etc.)
62
+ * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
60
63
  */
61
- constructor(command, options = {}) {
64
+ constructor(command, prefix, options = {}) {
65
+ this.#prefix = prefix;
62
66
  this.#command = command;
63
67
  this.#options = options;
64
68
  this.#proc = null;
@@ -73,19 +77,16 @@ class SHExecute {
73
77
  if (payload && typeof payload !== 'string') {
74
78
  throw new Error('Argument is not a string');
75
79
  }
76
- let { cwd, shell, env, stdio, detached } = this.#options;
80
+ /** @type {import('node:child_process').SpawnSyncOptions} */
81
+ const options = this.#options;
77
82
  // pipe need to be set on stdin when posting a payload
78
- if (payload) stdio[0] = 'pipe';
83
+ // @ts-ignore
84
+ if (payload) options['stdio'][0] = 'pipe';
79
85
  const input = payload || undefined;
80
- return spawnSync(this.#options.prefix, [this.#command], {
81
- cwd,
82
- shell,
83
- stdio,
84
- detached,
85
- windowsHide: true,
86
- env,
87
- input
88
- });
86
+ if (input) {
87
+ options.input = input
88
+ }
89
+ return spawnSync(this.#prefix, [this.#command], options);
89
90
  }
90
91
  /**
91
92
  * @param {string} [payload] - data to write
@@ -99,16 +100,12 @@ class SHExecute {
99
100
  if (this.#options.timeout) {
100
101
  to = this.#options.timeout;
101
102
  }
102
- let { cwd, shell, env, stdio } = this.#options;
103
+ /** @type {import('node:child_process').SpawnOptions} */
104
+ const options = this.#options;
103
105
  // pipe need to be set on stdin when posting a payload
104
- if (payload) stdio[0] = 'pipe';
105
- this.#proc = spawn(this.#options.prefix, [this.#command], {
106
- cwd,
107
- shell,
108
- stdio,
109
- windowsHide: true,
110
- env,
111
- });
106
+ // @ts-ignore
107
+ if (payload) options.stdio[0] = 'pipe';
108
+ this.#proc = spawn(this.#prefix, [this.#command], options);
112
109
  this.#proc.stdout?.on('data', (data) => {
113
110
  this.#stdout += data;
114
111
  });
@@ -148,7 +145,7 @@ class SHExecute {
148
145
  }
149
146
  /**
150
147
  * Kill this process and possible child processes
151
- * @param {string} signal - kill signal
148
+ * @param {number | string} signal - kill signal
152
149
  * @returns {Promise<number[]>}
153
150
  */
154
151
  async kill(signal = 'SIGTERM') {
@@ -163,6 +160,7 @@ class SHExecute {
163
160
  if (!res.includes(this.#proc.pid)) {
164
161
  // Kill self if I am not allready killed
165
162
  res.push(this.#proc.pid);
163
+ // @ts-ignore
166
164
  this.#proc.kill(signal);
167
165
  }
168
166
  this.#proc = undefined;
package/lib/Test.js CHANGED
@@ -1,5 +1,7 @@
1
- import assert from 'node:assert/strict';
2
1
  import { jsType } from './SH.js'
2
+ /**
3
+ * Valid jsTypes for test methods
4
+ */
3
5
  const FNC = ['Function', 'AsyncFunction'];
4
6
  // Settle async calls in a SYNC function
5
7
  const SETTLE_ASYNC = 50;
@@ -53,6 +55,9 @@ class Test {
53
55
  #tests = [];
54
56
  /** @type {testReport[]} */
55
57
  #reports = [];
58
+ /**
59
+ * @type {Error[]}
60
+ */
56
61
  #errors = [];
57
62
  /** verbosed **/
58
63
  #quite = false;
@@ -106,7 +111,7 @@ class Test {
106
111
  * @returns {Test}
107
112
  */
108
113
  add(description, callback) {
109
- if (jsType(description) !== 'String') {
114
+ if (typeof(description) !== 'string') {
110
115
  throw new Error(`'description' should be a string`)
111
116
  }
112
117
  if (!FNC.includes(jsType(callback))) {
@@ -205,9 +210,8 @@ class Test {
205
210
  this.#errors = [];
206
211
  }
207
212
  /**
208
- * @private
209
213
  * Handle an error for the current test
210
- * @param {Error}
214
+ * @param {Error} err
211
215
  * @param {boolean} [outside] default false, Error is catched ouside the callscope of the test
212
216
  */
213
217
  #handleError(err, outside = false) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@j-o-r/sh",
3
3
  "author": "Jorrit Duin <j-o-r@duin.work>",
4
4
  "type": "module",
5
- "version": "1.1.5",
5
+ "version": "1.1.7",
6
6
  "description": "Execute shell commands on Linux-based systems from javascript",
7
7
  "main": "lib/SH.js",
8
8
  "types": "types/SH.d.ts",
@@ -14,7 +14,7 @@
14
14
  "test:sh": "scenarios/sh.js",
15
15
  "publish": "npm run release && npm publish --access public",
16
16
  "release": "npm pack --pack-destination=release",
17
- "types": "tsc",
17
+ "types": "tsc -p tsc.json",
18
18
  "clear:types": "rm types/*.d.ts"
19
19
  },
20
20
  "repository": {
package/types/SH.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type ArgsObject = {
2
- [x: string]: ApiMethod;
2
+ [x: string]: string | string[];
3
3
  };
4
4
  export type RejectCallback = Function;
5
5
  export type ResolveCallback = Function;
@@ -44,7 +44,7 @@ export function sleep(duration: string | number): Promise<any>;
44
44
  * // Retry a command 3 times with irregular intervals using exponential backoff
45
45
  * const p = await retry(3, expBackoff(), () => SH`curl -s https://flipwrsi`);
46
46
  */
47
- export function retry(count: number, a: string | typeof expBackoff | Function, b?: Function | undefined): Promise<any>;
47
+ export function retry(count: number, a: string | typeof expBackoff | Function, b?: Function): Promise<any>;
48
48
  /**
49
49
  * This function reads the standard input (stdin) from the current process.
50
50
  * @example
@@ -67,7 +67,7 @@ export function readIn(): Promise<string>;
67
67
  * return 'res';
68
68
  * });
69
69
  */
70
- export function within(callback: Function, resolve?: Function | undefined, reject?: Function | undefined): void;
70
+ export function within(callback: Function, resolve?: ResolveCallback, reject?: RejectCallback): void;
71
71
  /**
72
72
  * Generates an exponential backoff time with a random jitter.
73
73
  *
@@ -76,27 +76,28 @@ export function within(callback: Function, resolve?: Function | undefined, rejec
76
76
  * @param {string} [rand='100ms'] - The maximum random jitter time in a human-readable format (e.g., '100ms' for 100 milliseconds).
77
77
  * @yields {number} The backoff time in milliseconds.
78
78
  */
79
- export function expBackoff(max?: string | undefined, rand?: string | undefined): Generator<number, void, unknown>;
79
+ export function expBackoff(max?: string, rand?: string): Generator<number, void, unknown>;
80
80
  /**
81
81
  * Parses command-line arguments into an object.
82
82
  *
83
- * The function recognizes arguments that start with two dashes (`--`) as keys,
83
+ * The function recognizes arguments that start with two dashes (`--`) or one dash (`-`) as keys,
84
84
  * and the subsequent value (if not another key) as the corresponding value.
85
85
  * If a key does not have a value, it defaults to `true`.
86
86
  * All unrecognized arguments are collected in an array under the `_` property.
87
87
  *
88
- * @param {string[]} args - An array of command-line arguments.
88
+ * @param {string[]} [args] - An array of command-line arguments (process.argv.slice(2)).
89
89
  * @returns {ArgsObject} An object where:
90
- * - Each key corresponds to an argument that starts with `--`,
90
+ * - If args is not passed, process.argv.slice(2) will be the default
91
+ * - Each key corresponds to an argument that starts with `--` or `-`,
91
92
  * - The value is either the next argument or `true` if no value is provided,
92
93
  * - The `_` property contains an array of unbound arguments.
93
94
  */
94
- export function parseArgs(args: string[]): ArgsObject;
95
+ export function parseArgs(args?: string[]): ArgsObject;
95
96
  /**
96
- * @typedef {Object.<string, ApiMethod>} ArgsObject
97
- * @property {string} [key: string] - Any string key maps to an object
97
+ * @typedef {Object.<string, string | string[]>} ArgsObject
98
+ * @property {string} [key: string] - Any string key maps to a string value
98
99
  * @property {string[]} _ - Array of strings, unnamed parameters
99
- * @description Parse parameters result.
100
+ * @description Parsed parameters result.
100
101
  */
101
102
  /**
102
103
  * @typedef {Function} RejectCallback
@@ -41,10 +41,6 @@ export type SHOptions = {
41
41
  * - The shell to use for execution.
42
42
  */
43
43
  shell?: string | undefined;
44
- /**
45
- * - The prefix commands to ensure a safe execution environment. e.g: prefix: 'set -euo pipefail;/usr/bin/env',
46
- */
47
- prefix?: string | undefined;
48
44
  /**
49
45
  * - The stdio configuration.
50
46
  */
@@ -53,10 +49,6 @@ export type SHOptions = {
53
49
  * - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
54
50
  */
55
51
  timeout?: number | undefined;
56
- /**
57
- * - default false, when true it runnes as a background process
58
- */
59
- detached?: boolean | undefined;
60
52
  };
61
53
  export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
62
54
  export type StdioOptions = Array<StdioOption> | StdioOption;
@@ -66,21 +58,22 @@ declare class SHDispatch {
66
58
  */
67
59
  constructor(cmd: string);
68
60
  /**
69
- * @param {SHOptions} options
61
+ * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
62
+ * @param {string} [prefix] - command prefix e.g (default) '/usr/bin/env'
70
63
  * @returns {SHDispatch}
71
64
  */
72
- options(options: SHOptions): SHDispatch;
65
+ options(options: import("child_process").SpawnOptions | import("child_process").SpawnSyncOptions, prefix?: string): SHDispatch;
73
66
  /**
74
67
  * @param {string} [payload]
75
68
  * @returns {Promise<string>}
76
69
  */
77
- run(payload?: string | undefined): Promise<string>;
70
+ run(payload?: string): Promise<string>;
78
71
  /**
79
72
  * Works for screen takeovers like editors
80
73
  * @param {string} [payload]
81
- * @returns {SpawnSyncResponse}
74
+ * @returns {import('child_process').SpawnSyncReturns}
82
75
  */
83
- runSync(payload?: string | undefined): SpawnSyncResponse;
76
+ runSync(payload?: string): import("child_process").SpawnSyncReturns<any>;
84
77
  kill(signal?: string): Promise<number[]>;
85
78
  #private;
86
79
  }
@@ -2,24 +2,25 @@ export default SHExecute;
2
2
  declare class SHExecute {
3
3
  /**
4
4
  * @param {string} command - linux command to be executed
5
- * @param {import('./SHDispatch').SHOptions} [options] - ChildProcess options
5
+ * @param {string} prefix - command prefix (bash, sh etc.)
6
+ * @param {import('child_process').SpawnOptions | import('child_process').SpawnSyncOptions} options
6
7
  */
7
- constructor(command: string, options?: any);
8
+ constructor(command: string, prefix: string, options?: import("child_process").SpawnOptions | import("child_process").SpawnSyncOptions);
8
9
  /**
9
10
  * @param {string} [payload] - data to write
10
11
  * @retuns {Promise<object>}
11
12
  */
12
- runSync(payload?: string | undefined): import("child_process").SpawnSyncReturns<Buffer> & import("child_process").SpawnSyncReturns<string> & import("child_process").SpawnSyncReturns<string | Buffer>;
13
+ runSync(payload?: string): import("child_process").SpawnSyncReturns<string | Buffer<ArrayBufferLike>>;
13
14
  /**
14
15
  * @param {string} [payload] - data to write
15
16
  * @retuns {Promise<string>}
16
17
  */
17
- run(payload?: string | undefined): Promise<any>;
18
+ run(payload?: string): Promise<any>;
18
19
  /**
19
20
  * Kill this process and possible child processes
20
- * @param {string} signal - kill signal
21
+ * @param {number | string} signal - kill signal
21
22
  * @returns {Promise<number[]>}
22
23
  */
23
- kill(signal?: string): Promise<number[]>;
24
+ kill(signal?: number | string): Promise<number[]>;
24
25
  #private;
25
26
  }
package/types/Test.d.ts CHANGED
@@ -37,7 +37,7 @@ declare class Test {
37
37
  /**
38
38
  * @param {boolean} [quiet] - does not output a report when true, default `false`
39
39
  */
40
- constructor(quiet?: boolean | undefined);
40
+ constructor(quiet?: boolean);
41
41
  /**
42
42
  * Set the timeout when a synced function is called.
43
43
  * This settles async code used in a sync function
@@ -58,7 +58,7 @@ declare class Test {
58
58
  * @param {number[]} [execute] - limit the execution tests
59
59
  * @returns {Promise<Report>}
60
60
  */
61
- run(execute?: number[] | undefined): Promise<Report>;
61
+ run(execute?: number[]): Promise<Report>;
62
62
  /**
63
63
  * Empty tests
64
64
  */