@j-o-r/sh 1.1.28 → 1.1.31
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 +145 -151
- package/TODO.md +8 -0
- package/lib/AsyncTracker.js +126 -107
- package/lib/SH.js +281 -224
- package/lib/SHDispatch.js +122 -94
- package/lib/SHExecute.js +83 -41
- package/lib/Test.js +153 -123
- package/package.json +2 -2
- package/types/AsyncTracker.d.ts +65 -31
- package/types/SH.d.ts +190 -168
- package/types/SHDispatch.d.ts +81 -66
- package/types/SHExecute.d.ts +44 -23
- package/types/Test.d.ts +66 -30
package/README.md
CHANGED
|
@@ -1,195 +1,189 @@
|
|
|
1
|
-
# @j-o-r/sh
|
|
1
|
+
# @j-o-r/sh [](https://www.npmjs.com/package/@j-o-r/sh) [](https://codeberg.org/duin/sh/src/branch/main/LICENSE) [](https://codeberg.org/duin/sh)
|
|
2
2
|
|
|
3
|
-
Execute shell commands from JavaScript.
|
|
3
|
+
Execute shell commands from JavaScript on Linux.
|
|
4
4
|
|
|
5
5
|
## Introduction
|
|
6
6
|
|
|
7
|
-
`@j-o-r/sh` is a Node.js module
|
|
7
|
+
`@j-o-r/sh` is a lightweight Node.js module for running shell commands with a clean, zx-inspired API. It fixes namespace pollution, adds **rolling timeouts** (reset on stdout/stderr), **no-shell mode** (`/usr/bin/env -S`), process tree kills, buffer limits (1MB default), and a built-in **Test** framework + **AsyncTracker** for leaks.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Key Features**:
|
|
10
|
+
- Template tag `SH`cmd`` → `SHDispatch` for `.options().run()`.
|
|
11
|
+
- Global options: `SH.timeout = '5s'; SH.cwd = '/tmp';`.
|
|
12
|
+
- Sync/async `run()`/`runSync()`; stdin payload; detached mode.
|
|
13
|
+
- Utils: `sleep`, `retry`, `expBackoff`, `cd`, `parseArgs`, `userIn`, `readIn`.
|
|
14
|
+
- Testing: `new Test().add('name', () => assert(...)).run()`.
|
|
15
|
+
- Async leak detection: `AsyncTracker` via `async_hooks`.
|
|
16
|
+
- Raw-by-default interpolation with explicit `bashEscape` quoting for untrusted argument values; full JSDoc.
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
No runtime deps. ESM-only (ES2020+).
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
## Quick Install
|
|
14
21
|
|
|
15
|
-
```
|
|
16
|
-
npm
|
|
22
|
+
```bash
|
|
23
|
+
npm i @j-o-r/sh
|
|
17
24
|
```
|
|
18
25
|
|
|
19
26
|
## Usage
|
|
20
27
|
|
|
21
|
-
###
|
|
28
|
+
### Basics
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
```js
|
|
31
|
+
import { SH, cd, sleep } from '@j-o-r/sh';
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
SH`your_shell_command`.run()
|
|
29
|
-
.then(output => {
|
|
30
|
-
console.log('Output:', output);
|
|
31
|
-
})
|
|
32
|
-
.catch(error => {
|
|
33
|
-
console.error('Error:', error);
|
|
34
|
-
});
|
|
33
|
+
cd('/tmp');
|
|
34
|
+
const out = await SH`ls -la`.run();
|
|
35
|
+
console.log(out); // Captured stdout (trimmed)
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
###
|
|
38
|
+
### Interpolation: raw by default
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
Interpolated values are inserted into the shell command as raw shell source. This
|
|
41
|
+
is convenient for trusted shell fragments such as flags, pipes, redirects, and
|
|
42
|
+
compound expressions, but it is **not safe for untrusted input**.
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
SH`sleep 3; echo 3`.run()
|
|
49
|
-
]);
|
|
50
|
-
});
|
|
51
|
-
```
|
|
44
|
+
```js
|
|
45
|
+
import { SH, bashEscape } from '@j-o-r/sh';
|
|
46
|
+
|
|
47
|
+
const flags = ['-l', '-a'];
|
|
48
|
+
await SH`ls ${flags}`.run(); // Executes: ls -l -a
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const fragment = 'printf ok; printf done';
|
|
51
|
+
await SH`${fragment}`.run(); // Raw shell syntax is intentional
|
|
52
|
+
|
|
53
|
+
const userInput = 'name; rm -rf /';
|
|
54
|
+
await SH`printf '%s\n' ${bashEscape(userInput)}`.run();
|
|
55
|
+
// Prints the value literally instead of executing `rm`.
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
`bashEscape(value)` returns one POSIX-shell-quoted argument. It preserves empty
|
|
59
|
+
strings, whitespace, quotes, semicolons, glob characters, tabs, and newlines.
|
|
60
|
+
For arrays of untrusted values, quote each element yourself:
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
```js
|
|
63
|
+
const args = ['two words', 'semi;colon', "quote's"];
|
|
64
|
+
await SH`printf '%s\n' ${args.map(bashEscape)}`.run();
|
|
65
|
+
```
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
- `parseArgs(process.args)`: Transform an array of strings into an object
|
|
63
|
-
- `cd(dir)`: Change the working directory.
|
|
64
|
-
- `sleep(duration)`: Pause execution for a specified duration.
|
|
65
|
-
- `retry(count, interval, callback)`: Retry a command a specified number of times with an optional interval.
|
|
66
|
-
- `readIn()`: Read from standard input.
|
|
67
|
-
- `userIn(prompt)`: Prompt the user for input and return an object with `input` (Promise resolving to the user input) and `abort()` method to cancel.
|
|
68
|
-
- `within(callback)`: Create an async context in a sync block.
|
|
69
|
-
- `expBackoff(max, rand)`: Generate intervals for exponential backoff.
|
|
70
|
-
- `jsType(any)`: Get the 'real' javascript variable type
|
|
71
|
-
- `hasProp(object, property)`: Safely check if an object has its own property (handles null/undefined objects).
|
|
72
|
-
- `assert.`: Node assert library
|
|
73
|
-
- `new Test()`: A small sync/async minimal test framework
|
|
74
|
-
- `AsyncTracker`: A class for tracking asynchronous operations using Node.js async hooks.
|
|
67
|
+
### Chaining & Options
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
```js
|
|
70
|
+
SH`curl -s ip.js.org`
|
|
71
|
+
.options({ timeout: '2s', shell: false }) // No-shell: /usr/bin/env -S
|
|
72
|
+
.run()
|
|
73
|
+
.catch(e => console.error(e.message)); // "Command failed with code 1: ..."
|
|
74
|
+
```
|
|
77
75
|
|
|
78
|
-
|
|
76
|
+
### Parallel & Context
|
|
79
77
|
|
|
80
|
-
|
|
78
|
+
```js
|
|
79
|
+
import { within } from '@j-o-r/sh';
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
const results = await within(async () => Promise.all([
|
|
82
|
+
SH`sleep 1; echo ok`.run(),
|
|
83
|
+
sleep('500ms'),
|
|
84
|
+
SH`uname`.run()
|
|
85
|
+
]));
|
|
86
|
+
```
|
|
86
87
|
|
|
87
|
-
###
|
|
88
|
+
### Retry & Backoff
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const res = await SH`ls -FLa | grep package.json | wc -l`.run();
|
|
92
|
-
console.log(res);
|
|
93
|
-
```
|
|
90
|
+
```js
|
|
91
|
+
import { retry, expBackoff } from '@j-o-r/sh';
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
try {
|
|
94
|
+
const res = await retry(3, expBackoff('10s'), () =>
|
|
95
|
+
SH`curl -s unreachable`.run()
|
|
96
|
+
);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// Last error
|
|
99
|
+
}
|
|
100
|
+
```
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
```javascript
|
|
103
|
-
const command = "uname -r";
|
|
104
|
-
const content = await SH`${command}`.run();
|
|
105
|
-
console.log(content);
|
|
106
|
-
```
|
|
102
|
+
### Interactive / Stdin
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
within(async () => {
|
|
111
|
-
const res = await Promise.all([
|
|
112
|
-
SH`sleep 1; echo 1`.run(),
|
|
113
|
-
SH`sleep 2; echo 2`.run(),
|
|
114
|
-
sleep(2),
|
|
115
|
-
SH`sleep 3; echo 3`.run()
|
|
116
|
-
]);
|
|
117
|
-
console.log(res);
|
|
118
|
-
});
|
|
119
|
-
```
|
|
104
|
+
```js
|
|
105
|
+
import { userIn, readIn } from '@j-o-r/sh';
|
|
120
106
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const p = await retry(3, expBackoff(), () => SH`curl -s https://unreachable`.run());
|
|
125
|
-
} catch (e) {
|
|
126
|
-
console.error('Retry failed:', e);
|
|
127
|
-
}
|
|
128
|
-
```
|
|
107
|
+
// User prompt (abortable)
|
|
108
|
+
const { input, abort } = userIn('Password: ');
|
|
109
|
+
const pw = await input;
|
|
129
110
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const name = await user.input;
|
|
134
|
-
console.log('Hello,', name);
|
|
135
|
-
```
|
|
111
|
+
// Piped stdin
|
|
112
|
+
const out = await SH`grep secret`.run(await readIn());
|
|
113
|
+
```
|
|
136
114
|
|
|
137
|
-
|
|
138
|
-
```javascript
|
|
139
|
-
const obj = { a: 1 };
|
|
140
|
-
console.log(hasProp(obj, 'a')); // true
|
|
141
|
-
console.log(hasProp(obj, 'b')); // false
|
|
142
|
-
console.log(hasProp(null, 'a')); // false
|
|
143
|
-
```
|
|
115
|
+
### Vim / TTY (Sync)
|
|
144
116
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
* Copy text to the clipboard
|
|
149
|
-
* @param {string} text
|
|
150
|
-
* @returns {Promise<string>}
|
|
151
|
-
*/
|
|
152
|
-
const copyToClipboard = async (text) => {
|
|
153
|
-
const prams = [
|
|
154
|
-
'-selection',
|
|
155
|
-
'clipboard'
|
|
156
|
-
]
|
|
157
|
-
return SH`xclip ${prams}`.options({stdio: 'inherit'}).run(text);
|
|
158
|
-
}
|
|
159
|
-
```
|
|
117
|
+
```js
|
|
118
|
+
SH`vim`.options({ stdio: 'inherit' }).runSync();
|
|
119
|
+
```
|
|
160
120
|
|
|
161
|
-
|
|
162
|
-
```javascript
|
|
163
|
-
SH`vim`.options({stdio: 'inherit'}).runSync();
|
|
164
|
-
```
|
|
121
|
+
## Testing
|
|
165
122
|
|
|
166
|
-
-
|
|
167
|
-
```javascript
|
|
168
|
-
import { assert, jsType, Test} from '@j-o-r/sh';
|
|
123
|
+
Full-featured tester with async support, error reporting, unresolved Promise detection.
|
|
169
124
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
assert.strictEqual(jsType(test), 'Test');
|
|
179
|
-
});
|
|
180
|
-
test.add('Test is test in async, returning a promise', async () => {
|
|
181
|
-
return new Promise((resolve, _reject) => {
|
|
182
|
-
assert.strictEqual(jsType(test), 'Test');
|
|
183
|
-
resolve();
|
|
184
|
-
});
|
|
125
|
+
```js
|
|
126
|
+
import { Test, assert, jsType } from '@j-o-r/sh';
|
|
127
|
+
|
|
128
|
+
const t = new Test(true); // quiet: no console
|
|
129
|
+
t.add('sync assert', () => assert.strictEqual(1 + 1, 2));
|
|
130
|
+
t.add('async', async () => {
|
|
131
|
+
await sleep('100ms');
|
|
132
|
+
assert.strictEqual(jsType([]), 'Array');
|
|
185
133
|
});
|
|
186
|
-
const report = await
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
134
|
+
const report = await t.run();
|
|
135
|
+
console.log(report); // { tests: 2, executed: 2, duration: 150, errors: 0 }
|
|
136
|
+
|
|
137
|
+
t.unresolved(); // Logs leaks if any
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Advanced
|
|
141
|
+
|
|
142
|
+
- **SSH Example** (interactive; use keys/sshpass for automation):
|
|
143
|
+
```js
|
|
144
|
+
// TTY/inherit for prompts
|
|
145
|
+
SH`ssh user@host`.options({ stdio: 'inherit' }).runSync();
|
|
146
|
+
// Or sshpass: SH`sshpass -p pw ssh user@host`.run()
|
|
147
|
+
```
|
|
148
|
+
See `scenarios/` for full demos.
|
|
149
|
+
|
|
150
|
+
- **CLI Args**: `parseArgs()` → `{ port: '8080', _: ['file'] }`.
|
|
151
|
+
- **Global Defaults**: Set global options on the `SH` object to apply to all subsequent commands. These can be overridden per-command via `.options()`. Examples:
|
|
152
|
+
- `SH.timeout = '5s';` – Default timeout for all runs.
|
|
153
|
+
- `SH.cwd = '/tmp';` – Default working directory.
|
|
154
|
+
- `SH.shell = false;` – Disable shell mode (uses `/usr/bin/env -S`).
|
|
155
|
+
- `SH.maxBuffer = 1 * 1024 * 1024;` – Override default buffer limit (500kb) to 1MB per stream (stdout/stderr). Buffering captures output up to this limit; excess is truncated with markers.
|
|
156
|
+
- **Kill Tree**: `dispatch.kill('SIGKILL')` → children via `pgrep -P`.
|
|
157
|
+
|
|
158
|
+
## API
|
|
159
|
+
|
|
160
|
+
Full JSDoc in `lib/*.js`. Key exports:
|
|
161
|
+
|
|
162
|
+
| Utility | Description |
|
|
163
|
+
|---------|-------------|
|
|
164
|
+
| `SH`cmd`` | Template → {@link SHDispatch} |
|
|
165
|
+
| `cd(dir)` | `process.chdir()` |
|
|
166
|
+
| `sleep('1s')` | Promise delay |
|
|
167
|
+
| `retry(3, '1s', fn)` | Retry w/ delay/gen |
|
|
168
|
+
| `userIn(prompt)` | `{ input: Promise, abort() }` |
|
|
169
|
+
| `Test` | Test runner |
|
|
170
|
+
| `AsyncTracker` | Async leak detector |
|
|
171
|
+
| `parseArgs(argv)` | CLI parser |
|
|
172
|
+
| `bashEscape(value)` | Quote one value as a POSIX shell argument for safe interpolation |
|
|
173
|
+
|
|
174
|
+
See [types/SH.d.ts](types/SH.d.ts) for TS defs.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm run types # Generate types/
|
|
180
|
+
npm test # Run scenarios/sh.js
|
|
181
|
+
npm run release # Pack for publish
|
|
182
|
+
npm run publish # npm publish
|
|
191
183
|
```
|
|
192
184
|
|
|
185
|
+
Repo: [Codeberg](https://codeberg.org/duin/sh) | Issues: [Codeberg Issues](https://codeberg.org/duin/sh/issues)
|
|
186
|
+
|
|
193
187
|
## License
|
|
194
188
|
|
|
195
|
-
|
|
189
|
+
Apache-2.0 © [Jorrit Duin](mailto:jorrit.duin+sh@gmail.com)
|