@j-o-r/sh 1.0.7 → 1.1.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.
- package/README.md +29 -1
- package/lib/SH.js +18 -2
- package/lib/SHDispatch.js +2 -0
- package/lib/SHExecute.js +13 -2
- package/lib/Test.js +213 -0
- package/package.json +4 -3
- package/types/SH.d.ts +9 -0
- package/types/SHDispatch.d.ts +4 -0
- package/types/SHExecute.d.ts +1 -0
- package/types/Test.d.ts +64 -0
package/README.md
CHANGED
|
@@ -66,6 +66,9 @@ The module also provides additional utilities for common tasks:
|
|
|
66
66
|
- `readIn()`: Read from standard input.
|
|
67
67
|
- `within(callback)`: Create an async context in a sync block.
|
|
68
68
|
- `expBackoff(max, rand)`: Generate intervals for exponential backoff.
|
|
69
|
+
- `jsType(any)`: Get the 'real' javascript variable type
|
|
70
|
+
- `assert.`: Node assert library
|
|
71
|
+
- `new Test()`: A small sync/async minimal test framework
|
|
69
72
|
|
|
70
73
|
## SHDispatch
|
|
71
74
|
|
|
@@ -125,7 +128,7 @@ This class is returned by the `SH` function. Here's a summary of its methods and
|
|
|
125
128
|
/**
|
|
126
129
|
* Copy text to the clipboard
|
|
127
130
|
* @param {string} text
|
|
128
|
-
* @
|
|
131
|
+
* @returns {Promise<string>}
|
|
129
132
|
*/
|
|
130
133
|
const copyToClipboard = async (text) => {
|
|
131
134
|
const prams = [
|
|
@@ -140,6 +143,31 @@ This class is returned by the `SH` function. Here's a summary of its methods and
|
|
|
140
143
|
```javascript
|
|
141
144
|
SH`vim`.options({stdio: 'inherit'}).runSync();
|
|
142
145
|
```
|
|
146
|
+
|
|
147
|
+
- Create and run a test
|
|
148
|
+
```javascript
|
|
149
|
+
import { assert, jsType, Test} from '@j-o-r/sh';
|
|
150
|
+
|
|
151
|
+
const test = new Test();
|
|
152
|
+
test.add('Test is test in sync', () => {
|
|
153
|
+
assert.strictEqual(jsType(test), 'Test');
|
|
154
|
+
});
|
|
155
|
+
test.add('Test an Array in sync', () => {
|
|
156
|
+
assert.strictEqual(jsType([]), 'Array');
|
|
157
|
+
});
|
|
158
|
+
test.add('Test is test in async', async () => {
|
|
159
|
+
assert.strictEqual(jsType(test), 'Test');
|
|
160
|
+
});
|
|
161
|
+
test.add('Test is test in a promise',
|
|
162
|
+
new Promise((resolve, _reject) => {
|
|
163
|
+
assert.strictEqual(jsType(test), 'Test');
|
|
164
|
+
resolve();
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
await test.run();
|
|
169
|
+
```
|
|
170
|
+
|
|
143
171
|
## License
|
|
144
172
|
|
|
145
173
|
This project is licensed under the Apache License, Version 2.0.
|
package/lib/SH.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// Modified by: jorrit.duin+sh[AT]gmail.com
|
|
29
29
|
import assert from 'node:assert';
|
|
30
30
|
import SHDispatch from './SHDispatch.js';
|
|
31
|
-
|
|
31
|
+
import Test from './Test.js'
|
|
32
32
|
/**
|
|
33
33
|
* Creates a new SHDispatch object that represents a command to be executed.
|
|
34
34
|
*
|
|
@@ -238,6 +238,19 @@ const parseArgs = (args) => {
|
|
|
238
238
|
}
|
|
239
239
|
return result;
|
|
240
240
|
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Determine a javascript type
|
|
244
|
+
*
|
|
245
|
+
* @param {any} fn - Any let type
|
|
246
|
+
* @returns {string} The "real" object / typeof name
|
|
247
|
+
*/
|
|
248
|
+
const jsType = (fn) => {
|
|
249
|
+
if (fn === undefined) return 'undefined';
|
|
250
|
+
const type = Object.prototype.toString.call(fn).slice(8, -1);
|
|
251
|
+
return type === 'Object' ? fn.constructor.name : type;
|
|
252
|
+
};
|
|
253
|
+
|
|
241
254
|
export {
|
|
242
255
|
SH,
|
|
243
256
|
cd,
|
|
@@ -246,5 +259,8 @@ export {
|
|
|
246
259
|
readIn,
|
|
247
260
|
within,
|
|
248
261
|
expBackoff,
|
|
249
|
-
parseArgs
|
|
262
|
+
parseArgs,
|
|
263
|
+
jsType,
|
|
264
|
+
Test,
|
|
265
|
+
assert
|
|
250
266
|
}
|
package/lib/SHDispatch.js
CHANGED
|
@@ -18,6 +18,7 @@ import SHExec from './SHExecute.js';
|
|
|
18
18
|
* @property {string} [prefix] - The prefix commands to ensure a safe execution environment. e.g: prefix: 'set -euo pipefail;/usr/bin/env',
|
|
19
19
|
* @property {StdioOptions|StdioOption} [stdio] - The stdio configuration.
|
|
20
20
|
* @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
|
|
21
22
|
*/
|
|
22
23
|
/**
|
|
23
24
|
* @typedef {('pipe' | 'ignore' | 'inherit' | number)} StdioOption
|
|
@@ -86,6 +87,7 @@ const defaultOptions = {
|
|
|
86
87
|
cwd: process.cwd(),
|
|
87
88
|
env: process.env,
|
|
88
89
|
shell: 'bash',
|
|
90
|
+
detached: false,
|
|
89
91
|
prefix: '/usr/bin/env',
|
|
90
92
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
91
93
|
timeout: 10000 // when 0 there is no timeout
|
package/lib/SHExecute.js
CHANGED
|
@@ -73,7 +73,7 @@ class SHExecute {
|
|
|
73
73
|
if (payload && typeof payload !== 'string') {
|
|
74
74
|
throw new Error('Argument is not a string');
|
|
75
75
|
}
|
|
76
|
-
let { cwd, shell, env, stdio } = this.#options;
|
|
76
|
+
let { cwd, shell, env, stdio, detached } = this.#options;
|
|
77
77
|
// pipe need to be set on stdin when posting a payload
|
|
78
78
|
if (payload) stdio[0] = 'pipe';
|
|
79
79
|
const input = payload || undefined;
|
|
@@ -81,6 +81,7 @@ class SHExecute {
|
|
|
81
81
|
cwd,
|
|
82
82
|
shell,
|
|
83
83
|
stdio,
|
|
84
|
+
detached,
|
|
84
85
|
windowsHide: true,
|
|
85
86
|
env,
|
|
86
87
|
input
|
|
@@ -146,6 +147,7 @@ class SHExecute {
|
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
/**
|
|
150
|
+
* Kill this process and possible child processes
|
|
149
151
|
* @param {string} signal - kill signal
|
|
150
152
|
* @returns {Promise<number[]>}
|
|
151
153
|
*/
|
|
@@ -153,7 +155,16 @@ class SHExecute {
|
|
|
153
155
|
if (!this.#proc) throw new Error('Trying to kill a process without creating one.');
|
|
154
156
|
if (!this.#proc.pid) throw new Error('The process pid is undefined.');
|
|
155
157
|
this.#forcedKill = true;
|
|
156
|
-
|
|
158
|
+
let res = [];
|
|
159
|
+
try {
|
|
160
|
+
// Try to kill pid 'childs'
|
|
161
|
+
res = await killProcesses(this.#proc.pid, signal);
|
|
162
|
+
} catch (_e) { }
|
|
163
|
+
if (!res.includes(this.#proc.pid)) {
|
|
164
|
+
// Kill self if I am not allready killed
|
|
165
|
+
res.push(this.#proc.pid);
|
|
166
|
+
this.#proc.kill(signal);
|
|
167
|
+
}
|
|
157
168
|
this.#proc = undefined;
|
|
158
169
|
return res;
|
|
159
170
|
}
|
package/lib/Test.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { jsType } from './SH.js'
|
|
3
|
+
const FNC = ['Function', 'AsyncFunction', 'Promise'];
|
|
4
|
+
// Settle async calls in a SYNC function
|
|
5
|
+
const SETTLE_ASYNC = 100;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} testDefinition
|
|
9
|
+
* @prop {string} description
|
|
10
|
+
* @prop {function|asyncfunction} callback
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} testReport
|
|
15
|
+
* @prop {string} description
|
|
16
|
+
* @prop {number} duration - start time in MS
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} Report
|
|
21
|
+
* @prop {number} tests
|
|
22
|
+
* @prop {number} duration - start time in MS
|
|
23
|
+
* @prop {number} errors - number of errors
|
|
24
|
+
* @prop {number} executed - number of tests executed
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Get the current time
|
|
28
|
+
* @returns {number}
|
|
29
|
+
*/
|
|
30
|
+
function getNow() {
|
|
31
|
+
return new Date().getTime();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the duration
|
|
36
|
+
* @param {number} start - start time
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
function getDuration(start) {
|
|
40
|
+
return new Date().getTime() - start;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class Test {
|
|
44
|
+
#catchErrors = false;
|
|
45
|
+
#currentTest = -1;
|
|
46
|
+
/** @type {testDefinition[]} */
|
|
47
|
+
#tests = [];
|
|
48
|
+
/** @type {testReport[]} */
|
|
49
|
+
#reports = [];
|
|
50
|
+
#errors = [];
|
|
51
|
+
/** verbosed **/
|
|
52
|
+
#quite = false;
|
|
53
|
+
/** Timeout in ms to settle async code blocks called from sync methods */
|
|
54
|
+
#TO = SETTLE_ASYNC;
|
|
55
|
+
/**
|
|
56
|
+
* @param {boolean} [quiet] - does not output a report when true, default `false`
|
|
57
|
+
*/
|
|
58
|
+
constructor(quiet = false) {
|
|
59
|
+
if (quiet) {
|
|
60
|
+
this.#quite = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Set the timeout when a synced function is called.
|
|
65
|
+
* This settles async code used in a sync function
|
|
66
|
+
* and give some time to catch errors (#HACK)
|
|
67
|
+
* @param {number} timeout - in MS, default 100
|
|
68
|
+
*/
|
|
69
|
+
syncTimeout(timeout) {
|
|
70
|
+
this.#TO = timeout;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Activate a listener on errors outside the call stack scope
|
|
75
|
+
* @param {boolean} active - register / unregister
|
|
76
|
+
*/
|
|
77
|
+
#detectErrors(active = true) {
|
|
78
|
+
const errListener = (err) => {
|
|
79
|
+
this._handleError(err)
|
|
80
|
+
}
|
|
81
|
+
if (active) {
|
|
82
|
+
if (!this.#catchErrors) {
|
|
83
|
+
process.on('uncaughtException', errListener);
|
|
84
|
+
this.catchErrors = true;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
if (this.#catchErrors) {
|
|
88
|
+
process.removeListener('uncaughtException', errListener);
|
|
89
|
+
this.catchErrors = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
*
|
|
97
|
+
* @param {string} description
|
|
98
|
+
* @param {Function|AsyncFunction|Promise} callback
|
|
99
|
+
* @throws Error when conditions are not met
|
|
100
|
+
*/
|
|
101
|
+
add(description, callback) {
|
|
102
|
+
if (jsType(description) !== 'String') {
|
|
103
|
+
throw new Error(`'description' should be a string`)
|
|
104
|
+
}
|
|
105
|
+
if (!FNC.includes(jsType(callback))) {
|
|
106
|
+
throw new Error(`'callback' should be a (async) function or Promise`)
|
|
107
|
+
}
|
|
108
|
+
this.#tests.push({ description, callback });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Execute tests
|
|
112
|
+
* @param {number[]} [execute] - limit the execution tests
|
|
113
|
+
* @returns {Promise<Report>}
|
|
114
|
+
*/
|
|
115
|
+
async run(execute) {
|
|
116
|
+
// Detect errors outside the call stack
|
|
117
|
+
this.#detectErrors(true);
|
|
118
|
+
let errors = false;
|
|
119
|
+
let i = 0;
|
|
120
|
+
const len = this.#tests.length;
|
|
121
|
+
for (; i < len; i++) {
|
|
122
|
+
this.#currentTest = i;
|
|
123
|
+
if (execute && !execute.includes(i)) continue;
|
|
124
|
+
let start = getNow();
|
|
125
|
+
const cb = this.#tests[i];
|
|
126
|
+
const type = jsType(cb.callback);
|
|
127
|
+
let error;
|
|
128
|
+
try {
|
|
129
|
+
if (type === 'Function') {
|
|
130
|
+
cb.callback();
|
|
131
|
+
// To settle async calls in sync functions
|
|
132
|
+
// (catching errors outside this call stack, that may throw later)
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, this.#TO)); // This will pause the the current loop.
|
|
134
|
+
// add timeout for an honest execution time
|
|
135
|
+
start = start + this.#TO;
|
|
136
|
+
} else if (type === 'Promise') {
|
|
137
|
+
await Promise.all([cb.callback]);
|
|
138
|
+
} else {
|
|
139
|
+
await cb.callback();
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (!errors) {
|
|
143
|
+
errors = true
|
|
144
|
+
}
|
|
145
|
+
error = e;
|
|
146
|
+
}
|
|
147
|
+
const duration = getDuration(start);
|
|
148
|
+
if (!this.#quite) process.stdout.write(`${i}. ${cb.description} : ${duration} ms\n`);
|
|
149
|
+
if (error) {
|
|
150
|
+
this._handleError(error);
|
|
151
|
+
}
|
|
152
|
+
this.#reports[i] = { description: cb.description, duration };
|
|
153
|
+
}
|
|
154
|
+
this.#currentTest = -1;
|
|
155
|
+
this.#detectErrors(false);
|
|
156
|
+
return this.#report();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @returns {Report}
|
|
160
|
+
*/
|
|
161
|
+
#report() {
|
|
162
|
+
let duration = 0;
|
|
163
|
+
let executed = 0;
|
|
164
|
+
let errors = 0;
|
|
165
|
+
const tests = this.#tests.length;
|
|
166
|
+
let i = 0;
|
|
167
|
+
const len = this.#reports.length;
|
|
168
|
+
for (; i < len; i++) {
|
|
169
|
+
const r = this.#reports[i];
|
|
170
|
+
if (r) {
|
|
171
|
+
duration = r.duration + duration;
|
|
172
|
+
executed = 1 + executed;
|
|
173
|
+
const E = this.#errors[i];
|
|
174
|
+
if (E) {
|
|
175
|
+
errors = 1 + errors;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
}
|
|
180
|
+
if (tests !== executed) {
|
|
181
|
+
if (!this.#quite) console.log('** Not all tests have been executed **');
|
|
182
|
+
}
|
|
183
|
+
if (!this.#quite) {
|
|
184
|
+
console.log('--------------------------------------------------');
|
|
185
|
+
console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`);
|
|
186
|
+
}
|
|
187
|
+
return {tests, executed, duration, errors};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Empty tests
|
|
192
|
+
*/
|
|
193
|
+
reset() {
|
|
194
|
+
this.#tests = [];
|
|
195
|
+
this.#reports = [];
|
|
196
|
+
this.#errors = [];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Handle an error outside the callstack
|
|
200
|
+
* for the current test
|
|
201
|
+
*/
|
|
202
|
+
_handleError(err) {
|
|
203
|
+
if (this.#currentTest > -1) {
|
|
204
|
+
// Register this error
|
|
205
|
+
// Always print out errors
|
|
206
|
+
// Despite #quite
|
|
207
|
+
console.error(err);
|
|
208
|
+
this.#errors[this.#currentTest] = err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default Test;
|
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.0
|
|
5
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"license": "Apache License, Version 2.0",
|
|
24
24
|
"dependencies": {},
|
|
25
|
-
"devDependencies": {},
|
|
26
25
|
"bugs": {
|
|
27
26
|
"url": "https://codeberg.org/duin/sh/issues"
|
|
28
27
|
},
|
|
29
28
|
"homepage": "https://codeberg.org/duin",
|
|
30
29
|
"keywords": [
|
|
31
30
|
"shell",
|
|
31
|
+
"typeof",
|
|
32
|
+
"test",
|
|
32
33
|
"posix",
|
|
33
34
|
"linux",
|
|
34
35
|
"command-line",
|
|
@@ -46,4 +47,4 @@
|
|
|
46
47
|
"process-promise",
|
|
47
48
|
"process-output"
|
|
48
49
|
]
|
|
49
|
-
}
|
|
50
|
+
}
|
package/types/SH.d.ts
CHANGED
|
@@ -83,4 +83,13 @@ export function expBackoff(max?: string | undefined, rand?: string | undefined):
|
|
|
83
83
|
* - The `_` property contains an array of unbound arguments.
|
|
84
84
|
*/
|
|
85
85
|
export function parseArgs(args: string[]): object;
|
|
86
|
+
/**
|
|
87
|
+
* Determine a javascript type
|
|
88
|
+
*
|
|
89
|
+
* @param {any} fn - Any let type
|
|
90
|
+
* @returns {string} The object / let type name
|
|
91
|
+
*/
|
|
92
|
+
export function jsType(fn: any): string;
|
|
93
|
+
import Test from './Test.js';
|
|
86
94
|
import SHDispatch from './SHDispatch.js';
|
|
95
|
+
export { Test, assert };
|
package/types/SHDispatch.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export type SHOptions = {
|
|
|
53
53
|
* - default 20000: a timeout error is triggerd when a process execution time is exceeded, 0 is no timeout
|
|
54
54
|
*/
|
|
55
55
|
timeout?: number | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* - default false, when true it runnes as a background process
|
|
58
|
+
*/
|
|
59
|
+
detached?: boolean | undefined;
|
|
56
60
|
};
|
|
57
61
|
export type StdioOption = ("pipe" | "ignore" | "inherit" | number);
|
|
58
62
|
export type StdioOptions = Array<StdioOption> | StdioOption;
|
package/types/SHExecute.d.ts
CHANGED
package/types/Test.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export default Test;
|
|
2
|
+
export type testDefinition = {
|
|
3
|
+
description: string;
|
|
4
|
+
callback: Function | asyncfunction;
|
|
5
|
+
};
|
|
6
|
+
export type testReport = {
|
|
7
|
+
description: string;
|
|
8
|
+
/**
|
|
9
|
+
* - start time in MS
|
|
10
|
+
*/
|
|
11
|
+
duration: number;
|
|
12
|
+
};
|
|
13
|
+
export type Report = {
|
|
14
|
+
tests: number;
|
|
15
|
+
/**
|
|
16
|
+
* - start time in MS
|
|
17
|
+
*/
|
|
18
|
+
duration: number;
|
|
19
|
+
/**
|
|
20
|
+
* - number of errors
|
|
21
|
+
*/
|
|
22
|
+
errors: number;
|
|
23
|
+
/**
|
|
24
|
+
* - number of tests executed
|
|
25
|
+
*/
|
|
26
|
+
executed: number;
|
|
27
|
+
};
|
|
28
|
+
declare class Test {
|
|
29
|
+
/**
|
|
30
|
+
* @param {boolean} [quiet] - does not output a report when true, default `false`
|
|
31
|
+
*/
|
|
32
|
+
constructor(quiet?: boolean | undefined);
|
|
33
|
+
/**
|
|
34
|
+
* Set the timeout when a synced function is called.
|
|
35
|
+
* This settles async code used in a sync function
|
|
36
|
+
* and give some time to catch errors (#HACK)
|
|
37
|
+
* @param {number} timeout - in MS, default 100
|
|
38
|
+
*/
|
|
39
|
+
syncTimeout(timeout: number): void;
|
|
40
|
+
catchErrors: boolean | undefined;
|
|
41
|
+
/**
|
|
42
|
+
*
|
|
43
|
+
* @param {string} description
|
|
44
|
+
* @param {Function|AsyncFunction|Promise} callback
|
|
45
|
+
* @throws Error when conditions are not met
|
|
46
|
+
*/
|
|
47
|
+
add(description: string, callback: Function | AsyncFunction | Promise<any>): void;
|
|
48
|
+
/**
|
|
49
|
+
* Execute tests
|
|
50
|
+
* @param {number[]} [execute] - limit the execution tests
|
|
51
|
+
* @returns {Promise<Report>}
|
|
52
|
+
*/
|
|
53
|
+
run(execute?: number[] | undefined): Promise<Report>;
|
|
54
|
+
/**
|
|
55
|
+
* Empty tests
|
|
56
|
+
*/
|
|
57
|
+
reset(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Handle an error outside the callstack
|
|
60
|
+
* for the current test
|
|
61
|
+
*/
|
|
62
|
+
_handleError(err: any): void;
|
|
63
|
+
#private;
|
|
64
|
+
}
|