dollar-shell 1.0.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/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Copyright 2005-2024 Eugene Lazutkin
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # dollar-shell [![NPM version][npm-image]][npm-url]
2
+
3
+ [npm-image]: https://img.shields.io/npm/v/dollar-shell.svg
4
+ [npm-url]: https://npmjs.org/package/dollar-shell
5
+
6
+ `dollar-shell` is a micro-library for running shell commands and using them in streams with ease in Node, Deno, Bun. It is a tiny, simple, no dependency module with TypeScript typings.
7
+
8
+ The idea is to run OS/shell commands and/or use them in stream pipelines as sources, sinks,
9
+ and transformation steps using [web streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
10
+ It can be used together with [stream-chain](https://npmjs.org/package/stream-chain) and
11
+ [stream-json](https://npmjs.org/package/stream-json) to create efficient pipelines.
12
+ It helps using shell commands in utilities written in JavaScript/TypeScript running with
13
+ Node, Deno, or Bun.
14
+
15
+ Available components:
16
+
17
+ * `$` — spawn a process using a template string.
18
+ * `$.from` — spawn a process and use its `stdout` as a source stream.
19
+ * `$.to` — spawn a process and use its `stdin` as a sink stream.
20
+ * `$.io` AKA `$.through` — spawn a process and use it as
21
+ a transformation step in our pipeline.
22
+ * `$sh` — run a shell command using a template string.
23
+ * `$sh.from` — run a shell command and use its `stdout` as a source stream.
24
+ * `$sh.to` — run a shell command and use its `stdin` as a sink stream.
25
+ * `$sh.io` AKA `$sh.through` — run a shell command and use it as
26
+ a transformation step in our pipeline.
27
+ * Advanced components:
28
+ * `spawn()` — spawn a process with advanced ways to configure and control it.
29
+ * `$$` — spawn a process using a template string based on `spawn()`.
30
+ * `shell()` — a helper to spawn a shell command using a template string based on `spawn()`.
31
+ * Various helpers for them.
32
+
33
+ ## Introduction
34
+
35
+ Run a command:
36
+
37
+ ```js
38
+ import $ from 'dollar-shell';
39
+
40
+ const result = await $`echo hello`;
41
+ console.log(result.code, result.signal, result.killed);
42
+ ```
43
+
44
+ Run a shell command:
45
+
46
+ ```js
47
+ import {$sh} from 'dollar-shell';
48
+
49
+ const result = await $sh`ls .`;
50
+ console.log(result.code, result.signal, result.killed);
51
+ ```
52
+
53
+ Run a shell command (an alias or a function) and show its result:
54
+
55
+ ```js
56
+ import {$sh} from 'dollar-shell';
57
+
58
+ // custom alias that prints `stdout` and runs an interactive shell
59
+ const $p = $sh({shellArgs: ['-ic'], stdout: 'inherit'});
60
+
61
+ const result = await $p`nvm ls`;
62
+ // prints to the console the result of the command
63
+ ```
64
+
65
+ Run a pipeline:
66
+
67
+ ```js
68
+ import $ from 'dollar-shell';
69
+ import chain from 'stream-chain';
70
+ import lines from 'stream-chain/utils/lines.js';
71
+
72
+ chain([
73
+ $.from`ls -l .`,
74
+ $.io`grep LICENSE`,
75
+ $.io`wc`
76
+ lines(),
77
+ line => console.log(line)
78
+ ]);
79
+ ```
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ npm i --save dollar-shell
85
+ ```
86
+
87
+ ## Documentation
88
+
89
+ Below is the documentation for the main components: `spawn()`, `$$`, `$` and `$sh`.
90
+ Additional information can be found in the [wiki](https://github.com/uhop/dollar-shell/wiki).
91
+
92
+ ### `spawn()`
93
+
94
+ Spawn a process with advanced ways to configure and control it.
95
+
96
+ The signature: `spawn(command, options)`
97
+
98
+ Arguments:
99
+
100
+ * `command` — an array of strings. The first element is the command to run. The rest are its arguments.
101
+ * `options` — an optional object with options to configure the process:
102
+ * `cwd` — the optional current working directory as a string. Defaults to `process.cwd()`.
103
+ * `env` — the optional environment variables as an object (key-value pairs). Defaults to `process.env`.
104
+ * `stdin` — the optional source stream. Defaults to `null`.
105
+ * `stdout` — the optional destination stream. Defaults to `null`.
106
+ * `stderr` — the optional destination stream. Defaults to `null`.
107
+
108
+ `stdin`, `stdout` and `stderr` can be a string (one of `'inherit'`, `'ignore'`, `'pipe'` or `'piped'`)
109
+ or `null`. The latter is equivalent to `'ignore'`. `'piped'` is an alias of `'pipe'`:
110
+
111
+ * `'inherit'` — inherit streams from the parent process. For output steams (`stdout` and `stderr`),
112
+ it means that they will be piped to the same target, e.g., the console.
113
+ * `'ignore'` — the stream is ignored.
114
+ * `'pipe'` — the stream is available for reading or writing.
115
+
116
+ Returns a sub-process object with the following properties:
117
+
118
+ * `command` — the command that was run as an array of strings.
119
+ * `options` — the options that were passed to `spawn()`.
120
+ * `exited` — a promise that resolves to the exit code of the process. It is used to wait for the process to exit.
121
+ * `finished` — a boolean. It is `true` when the process has finished and `false` otherwise.
122
+ * `killed` — a boolean. It is `true` when the process has been killed and `false` otherwise.
123
+ * `exitCode` — the exit code of the process as a number. It is `null` if the process hasn't exited yet.
124
+ * `signalCode` — the signal code of the process as a string. It is `null` if the process hasn't exited yet.
125
+ * `stdin` — the source stream of the process if `options.stdin` was `'pipe'`. It is `null` otherwise.
126
+ * `stdout` — the destination stream of the process if `options.stdout` was `'pipe'`. It is `null` otherwise.
127
+ * `stderr` — the destination stream of the process if `options.stderr` was `'pipe'`. It is `null` otherwise.
128
+ * `kill()` — kills the process. `killed` will be `true` as soon as the process has been killed. It can be used to pipe the input and output. See `spawn()`'s `stdin` and `stdout` above for more details.
129
+
130
+ **Important:** all streams are exposed as [web streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
131
+
132
+ #### Examples
133
+
134
+ ```js
135
+ import {spawn} from 'dollar-shell';
136
+
137
+ const sp = spawn(['sleep', '5'])
138
+ await new Promise(resolve => setTimeout(resolve, 1000));
139
+ sp.kill();
140
+ await sp.exited;
141
+
142
+ sp.finished === true;
143
+ sp.killed === true;
144
+ ```
145
+
146
+ ### `$$`
147
+
148
+ The same as `spawn()`, but it returns a tag function that can be used as a template string.
149
+
150
+ The signatures:
151
+
152
+ ```js
153
+ const sp1 = $$`ls -l ${myFile}`; // runs a command the defaults
154
+
155
+ const sp2 = $$(options)`ls -l .`; // runs a command with custom spawn options
156
+
157
+ const $tag = $$(options); // returns a tag function
158
+ const sp3 = $tag`ls -l .`; // runs a command with custom spawn options
159
+ ```
160
+
161
+ This function is effectively a helper for `spawn()`. It parses the template string
162
+ into an array of string arguments. Each inserted value is included
163
+ as a separate argument if it was surrounded by whitespaces.
164
+
165
+ The second signature is used to run a command with custom spawn options. See `spawn()` above for more details.
166
+
167
+ The first signature returns a sub-process object. See `spawn()` for more details. The second signature
168
+ returns a tag function that can be used as a template string.
169
+
170
+ ### `$`
171
+
172
+ This function is similar to `$$` but it uses different default spawn options related to streams and
173
+ different (simpler) return values:
174
+
175
+ * `$` — all streams are ignored. It returns a promise that resolves to an object with the following properties:
176
+ * `code` — the exit code of the process. See `spawn()`'s `exitCode` above for more details.
177
+ * `signal` — the signal code of the process. See `spawn()`'s `signalCode` above for more details.
178
+ * `killed` — a boolean. It is `true` when the process has been killed and `false` otherwise. See `spawn()`'s `killed` above for more details.
179
+ * `$.from` — sets `stdout` to `pipe` and returns `stdout` of the process. It can be used to process the output. See `spawn()`'s `stdout` above for more details.
180
+ * `$.to` — sets `stdin` to `pipe` and returns `stdin` of the process. It can be used to pipe the input. See `spawn()`'s `stdin` above for more details.
181
+ * `$.io` AKA `$.through` — sets `stdin` and `stdout` to `pipe` and returns `stdin` and `stdout` of the process as a `{readable, writable}` pair. It can be used to create a pipeline where an external process can be used as a transform step.
182
+
183
+ ### `$sh`
184
+
185
+ This function mirrors `$` but runs the command with the shell. It takes an options object that extends
186
+ the spawn options with the following properties:
187
+
188
+ * `shellPath` — the path to the shell.
189
+ * On Unix-like systems it defaults to the value of
190
+ the `SHELL` environment variable if specified. Otherwise it is `'/bin/sh'` or `'/system/bin/sh'` on Android.
191
+ * On Windows it defaults to the value of the `ComSpec` environment variable if specified.
192
+ Otherwise it is `cmd.exe`.
193
+ * `shellArgs` — an array of strings that are passed to the shell as arguments.
194
+ * On Unix-like systems it defaults to `['-c']`.
195
+ * On Windows it defaults to `['/d', '/s', '/c']` for `cmd.exe`
196
+ or `['-e']` for `pwsh.exe` or `powershell.exe`.
197
+
198
+ The rest is identical to `$`: `$sh`, `$sh.from`, `$sh.to` and `$sh.io`/`$sh.through`.
199
+
200
+ ## License
201
+
202
+ BSD-3-Clause
203
+
204
+ ## Release History
205
+
206
+ - 1.0.0 *The initial release.*
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "dollar-shell",
3
+ "description": "Run shell commands and use them in streams with ease in Node, Deno, Bun. Tiny, simple, no dependency package.",
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./*": "./src/*"
11
+ },
12
+ "scripts": {
13
+ "test": "tape6 --flags FO",
14
+ "ts-test": "tsc --noEmit"
15
+ },
16
+ "files": [
17
+ "/src",
18
+ "LICENSE",
19
+ "README.md"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/uhop/list-toolkit.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/uhop/list-toolkit/issues"
27
+ },
28
+ "homepage": "https://github.com/uhop/list-toolkit#readme",
29
+ "keywords": [
30
+ "shell",
31
+ "spawn",
32
+ "$",
33
+ "dollar",
34
+ "stream"
35
+ ],
36
+ "author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (http://www.lazutkin.com/)",
37
+ "funding": "https://github.com/sponsors/uhop",
38
+ "license": "BSD-3-Clause",
39
+ "devDependencies": {
40
+ "tape-six": "^0.9.6",
41
+ "typescript": "^5.5.4"
42
+ },
43
+ "tape6": {
44
+ "tests": [
45
+ "/tests/test-*.*js"
46
+ ]
47
+ }
48
+ }
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ import {isRawValue, getRawValue, verifyStrings} from './utils.js';
4
+
5
+ const impl =
6
+ (shellEscape, shell, options) =>
7
+ (strings, ...args) => {
8
+ const result = [];
9
+
10
+ for (let i = 0; i < strings.length; i++) {
11
+ // process a string
12
+ result.push(strings[i]);
13
+
14
+ // process an argument
15
+ if (i >= args.length) continue;
16
+ if (isRawValue(args[i])) {
17
+ const arg = String(getRawValue(args[i]));
18
+ arg && result.push(arg);
19
+ } else {
20
+ const arg = String(args[i]);
21
+ arg && result.push(shellEscape(arg, options, !i && /^\s*$/.test(strings[i])));
22
+ }
23
+ }
24
+
25
+ return shell(result.join(''), options);
26
+ };
27
+
28
+ const bqShell =
29
+ (shellEscape, shell, options = {}) =>
30
+ (strings, ...args) => {
31
+ if (verifyStrings(strings)) return impl(shellEscape, shell, options)(strings, ...args);
32
+ return bqShell(shellEscape, shell, {...options, ...strings});
33
+ };
34
+
35
+ export default bqShell;
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ import {verifyStrings, isRawValue, getRawValue} from './utils.js';
4
+
5
+ const impl =
6
+ (spawn, options) =>
7
+ (strings, ...args) => {
8
+ const result = [];
9
+
10
+ let previousSpace = true;
11
+ for (let i = 0; i < strings.length; i++) {
12
+ // process a string
13
+
14
+ let string = strings[i];
15
+ previousSpace ||= /^\s/.test(string);
16
+ if (previousSpace) string = string.trimStart();
17
+ const lastSpace = /\s$/.test(string);
18
+ if (lastSpace) string = string.trimEnd();
19
+
20
+ let parts = string.split(/\s+/g).filter(part => part);
21
+ if (parts.length) {
22
+ if (!previousSpace) {
23
+ if (result.length) {
24
+ result[result.length - 1] += parts[0];
25
+ } else {
26
+ result.push(parts[0]);
27
+ }
28
+ parts = parts.slice(1);
29
+ }
30
+ result.push(...parts);
31
+ previousSpace = lastSpace;
32
+ } else {
33
+ previousSpace ||= lastSpace;
34
+ }
35
+
36
+ // process an argument
37
+
38
+ if (i >= args.length) continue;
39
+
40
+ const arg = String(isRawValue(args[i]) ? getRawValue(args[i]) : args[i]);
41
+ if (!arg) continue;
42
+
43
+ if (previousSpace) {
44
+ result.push(arg);
45
+ } else {
46
+ if (result.length) {
47
+ result[result.length - 1] += arg;
48
+ } else {
49
+ result.push(arg);
50
+ }
51
+ }
52
+ previousSpace = false;
53
+ }
54
+
55
+ return spawn(result, options);
56
+ };
57
+
58
+ const bqSpawn =
59
+ (spawn, options = {}) =>
60
+ (strings, ...args) => {
61
+ if (verifyStrings(strings)) return impl(spawn, options)(strings, ...args);
62
+ return bqSpawn(spawn, {...options, ...strings});
63
+ };
64
+
65
+ export default bqSpawn;
package/src/index.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ export type SpawnStreamState = 'pipe' | 'ignore' | 'inherit' | 'piped' | null;
2
+
3
+ export interface SpawnOptions {
4
+ cwd?: string;
5
+ env?: {[key: string]: string | undefined};
6
+ stdin?: SpawnStreamState;
7
+ stdout?: SpawnStreamState;
8
+ stderr?: SpawnStreamState;
9
+ }
10
+
11
+ export interface Subprocess<R = string> {
12
+ readonly command: string[];
13
+ readonly options: SpawnOptions | undefined;
14
+ readonly exited: Promise<number>;
15
+ readonly finished: boolean;
16
+ readonly killed: boolean;
17
+ readonly exitCode: number | null;
18
+ readonly signalCode: string | null;
19
+ readonly stdin: WritableStream<R> | null;
20
+ readonly stdout: ReadableStream<R> | null;
21
+ readonly stderr: ReadableStream<R> | null;
22
+ kill(): void;
23
+ }
24
+
25
+ export declare function spawn(command: string[], options?: SpawnOptions): Subprocess;
26
+
27
+ export declare function cwd(): string;
28
+ export declare function currentExecPath(): string;
29
+ export declare const runFileArgs: string[];
30
+
31
+ export declare function raw(value: unknown): object;
32
+ export declare function winCmdEscape(value: unknown): object | string;
33
+
34
+ export interface ShellEscapeOptions {
35
+ shellPath?: string;
36
+ }
37
+
38
+ export declare function shellEscape(s: {toString(): string}, options?: ShellEscapeOptions): string;
39
+
40
+ export declare function currentShellPath(): string;
41
+ export declare function buildShellCommand(shell: string | undefined, args: string[] | undefined, command: string): string[];
42
+
43
+ type Backticks<R> = (strings: TemplateStringsArray, ...args: unknown[]) => R;
44
+
45
+ interface Dollar<R, O = SpawnOptions> extends Backticks<R> {
46
+ (options: O): Dollar<R, O>;
47
+ }
48
+
49
+ export declare const $$: Dollar<Subprocess>;
50
+
51
+ export interface DollarResult {
52
+ code: number | null;
53
+ signal: string | null;
54
+ killed: boolean;
55
+ }
56
+
57
+ interface DuplexPair<R = string> {
58
+ readable: ReadableStream<R>;
59
+ writable: WritableStream<R>;
60
+ }
61
+
62
+ interface DollarImpl<R = string> extends Dollar<Promise<DollarResult>> {
63
+ from: Dollar<ReadableStream<R>>;
64
+ to: Dollar<WritableStream<R>>;
65
+ through: Dollar<DuplexPair<R>>;
66
+ io: Dollar<DuplexPair<R>>;
67
+ }
68
+
69
+ export declare const $: DollarImpl;
70
+
71
+ export interface ShellOptions extends SpawnOptions {
72
+ shellPath?: string;
73
+ shellArgs?: string[];
74
+ }
75
+
76
+ export declare const shell: Dollar<Subprocess, ShellOptions>;
77
+ export declare const sh = shell;
78
+
79
+ interface ShellImpl<R = string> extends Dollar<Promise<DollarResult>, ShellOptions> {
80
+ from: Dollar<ReadableStream<R>, ShellOptions>;
81
+ to: Dollar<WritableStream<R>, ShellOptions>;
82
+ through: Dollar<DuplexPair<R>, ShellOptions>;
83
+ io: Dollar<DuplexPair<R>, ShellOptions>;
84
+ }
85
+
86
+ export declare const $sh: ShellImpl;
87
+
88
+ export default $;
package/src/index.js ADDED
@@ -0,0 +1,89 @@
1
+ // @ts-self-types="./index.d.ts"
2
+
3
+ 'use strict';
4
+
5
+ // load dependencies
6
+
7
+ import {isWindows, raw, winCmdEscape} from './utils.js';
8
+
9
+ import bqSpawn from './bq-spawn.js';
10
+ import bqShell from './bq-shell.js';
11
+
12
+ export {isWindows, raw, winCmdEscape};
13
+
14
+ let modSpawn;
15
+ if (typeof Deno !== 'undefined') {
16
+ modSpawn = await import('./spawn/deno.js');
17
+ } else if (typeof Bun !== 'undefined') {
18
+ modSpawn = await import('./spawn/bun.js');
19
+ } else {
20
+ modSpawn = await import('./spawn/node.js');
21
+ }
22
+ export const {spawn, cwd, currentExecPath, runFileArgs} = modSpawn;
23
+
24
+ let modShell;
25
+ if (isWindows) {
26
+ modShell = await import('./shell/windows.js');
27
+ } else {
28
+ modShell = await import('./shell/unix.js');
29
+ }
30
+ export const {shellEscape, currentShellPath, buildShellCommand} = modShell;
31
+
32
+ // define spawn functions
33
+
34
+ export const $$ = bqSpawn(spawn);
35
+
36
+ export const $ = bqSpawn((command, options) => {
37
+ const sp = spawn(command, options);
38
+ return sp.exited.then(() => ({code: sp.exitCode, signal: sp.signalCode, killed: sp.killed}));
39
+ });
40
+
41
+ const fromProcess = bqSpawn((command, options) => {
42
+ const sp = spawn(command, Object.assign({}, options, {stdout: 'pipe'}));
43
+ return sp.stdout;
44
+ });
45
+
46
+ const toProcess = bqSpawn((command, options) => {
47
+ const sp = spawn(command, Object.assign({}, options, {stdin: 'pipe'}));
48
+ return sp.stdin;
49
+ });
50
+
51
+ const throughProcess = bqSpawn((command, options) => {
52
+ const sp = spawn(command, Object.assign({}, options, {stdin: 'pipe', stdout: 'pipe'}));
53
+ return {readable: sp.stdout, writable: sp.stdin};
54
+ });
55
+
56
+ $.from = fromProcess;
57
+ $.to = toProcess;
58
+ $.through = $.io = throughProcess;
59
+
60
+ // define shell functions
61
+
62
+ export const shell = bqShell(shellEscape, (command, options) => spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), options));
63
+ export {shell as sh};
64
+
65
+ export const $sh = bqShell(shellEscape, (command, options) => {
66
+ const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), options);
67
+ return sp.exited.then(() => ({code: sp.exitCode, signal: sp.signalCode, killed: sp.killed}));
68
+ });
69
+
70
+ const fromShell = bqShell(shellEscape, (command, options) => {
71
+ const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), Object.assign({}, options, {stdout: 'pipe'}));
72
+ return sp.stdout;
73
+ });
74
+
75
+ const toShell = bqShell(shellEscape, (command, options) => {
76
+ const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), Object.assign({}, options, {stdin: 'pipe'}));
77
+ return sp.stdin;
78
+ });
79
+
80
+ const throughShell = bqShell(shellEscape, (command, options) => {
81
+ const sp = spawn(buildShellCommand(options?.shellPath, options?.shellArgs, command), Object.assign({}, options, {stdin: 'pipe', stdout: 'pipe'}));
82
+ return {readable: sp.stdout, writable: sp.stdin};
83
+ });
84
+
85
+ $sh.from = fromShell;
86
+ $sh.to = toShell;
87
+ $sh.through = $sh.io = throughShell;
88
+
89
+ export default $;
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ import {getEnv, isAndroid} from '../utils.js';
4
+
5
+ export const shellEscape = s => "'" + String(s).replace(/'/g, "'\\''") + "'";
6
+
7
+ export const currentShellPath = () => getEnv('SHELL') || (isAndroid ? '/system/bin/sh' : '/bin/sh');
8
+
9
+ // derived from https://github.com/nodejs/node/blob/43f699d4d2799cfc17cbcad5770e1889075d5dbe/lib/child_process.js#L620
10
+ export const buildShellCommand = (shell, args, command) => [shell || currentShellPath(), ...(args || ['-c']), command];
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ import {getEnv, toBase64} from '../utils.js';
4
+
5
+ export const currentShellPath = () => getEnv('ComSpec') || 'cmd.exe';
6
+
7
+ const isCmd = s => /^(?:.*\\)?cmd(?:\.exe)?$/i.test(s);
8
+ const isPwsh = s => /^(?:.*\\)?(?:pwsh|powershell)(?:\.exe)?$/i.test(s);
9
+
10
+ const mapControls = {
11
+ '\b': '`b',
12
+ '\t': '`t',
13
+ '\r': '`r',
14
+ '\n': '`n',
15
+ '\f': '`f',
16
+ '\x00': '`0',
17
+ '\x07': '`a',
18
+ '\x1B': '`e',
19
+ '\x0B': '`v',
20
+ '"': '`\\"'
21
+ };
22
+
23
+ const escapePowerShell = (_, controls, nonAlphas, theRest) => {
24
+ if (controls) return mapControls[controls] || '';
25
+ if (nonAlphas) return '`' + nonAlphas;
26
+ return theRest;
27
+ };
28
+
29
+ const escapeCmd = (_, doubleQuote, nonAlphas, theRest) => {
30
+ if (doubleQuote) return '""';
31
+ if (nonAlphas) return '^' + nonAlphas;
32
+ return theRest;
33
+ };
34
+
35
+ export const shellEscape = (s, options, isFirst) => {
36
+ s = String(s);
37
+ const shell = options?.shellPath || currentShellPath();
38
+ if (isCmd(shell)) {
39
+ // based on https://github.com/nodejs/node/blob/dc74f17f6c37b1bb2d675216066238f33790ed29/deps/uv/src/win/process.c#L449
40
+ if (!s) return '""';
41
+ if (!/\s|[\t\"]/.test(s)) return s;
42
+ if (!/[\"\\]/.test(s)) return `"${s}"`;
43
+
44
+ let quoteHit = true;
45
+ const result =
46
+ '"' +
47
+ [...s]
48
+ .map(c => {
49
+ if (quoteHit && c === '\\') return '\\\\';
50
+ if (c === '"') {
51
+ quoteHit = true;
52
+ return '\\"';
53
+ }
54
+ quoteHit = false;
55
+ return c;
56
+ })
57
+ .join('') +
58
+ '"';
59
+ return isFirst ? result.replace(/(0xFF)|([\W])|(\w+)/g, escapeCmd) : result;
60
+ }
61
+ if (isPwsh(shell)) return s.replace(/([\t\r\n\f\x00\x1B\x07\x08\x0B\"])|([\W])|(\w+)/g, escapePowerShell);
62
+ return s;
63
+ };
64
+
65
+ export const buildShellCommand = (shell, args, command) => {
66
+ shell ||= currentShellPath();
67
+ if (isCmd(shell)) {
68
+ args ||= ['/d', '/s', '/c'];
69
+ } else if (isPwsh(shell)) {
70
+ args ||= ['-c'];
71
+ if (args.includes('-e')) command = toBase64(command);
72
+ } else {
73
+ args ||= ['-c'];
74
+ }
75
+ return [shell, ...args, command];
76
+ };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const sanitize = (value, defaultValue = 'ignore') => {
4
+ switch (value) {
5
+ case 'pipe':
6
+ case 'ignore':
7
+ case 'inherit':
8
+ return value;
9
+ case 'piped':
10
+ return 'pipe';
11
+ case null:
12
+ return 'ignore';
13
+ }
14
+ return defaultValue;
15
+ };
16
+
17
+ class Subprocess {
18
+ constructor(command, options) {
19
+ this.command = command;
20
+ this.options = options;
21
+
22
+ this.killed = false;
23
+
24
+ const spawnOptions = {windowsVerbatimArguments: true};
25
+ options.cwd && (spawnOptions.cwd = options.cwd);
26
+ options.env && (spawnOptions.env = options.env);
27
+
28
+ spawnOptions.stdin = sanitize(options.stdin);
29
+ spawnOptions.stdout = sanitize(options.stdout);
30
+ spawnOptions.stderr = sanitize(options.stderr);
31
+
32
+ this.spawnOptions = spawnOptions;
33
+
34
+ this.childProcess = Bun.spawn(command, spawnOptions);
35
+
36
+ this.stdin = this.childProcess.stdin ? new WritableStream(this.childProcess.stdin) : null;
37
+ this.stdout = this.childProcess.stdout || null;
38
+ this.stderr = this.childProcess.stderr || null;
39
+ }
40
+
41
+ get exited() {
42
+ return this.childProcess.exited;
43
+ }
44
+
45
+ get finished() {
46
+ return this.childProcess.killed;
47
+ }
48
+
49
+ get exitCode() {
50
+ return this.childProcess.exitCode;
51
+ }
52
+
53
+ get signalCode() {
54
+ return this.childProcess.signalCode;
55
+ }
56
+
57
+ kill() {
58
+ this.killed = true;
59
+ this.childProcess.kill();
60
+ }
61
+ }
62
+
63
+ export const currentExecPath = () => process.execPath;
64
+ export const runFileArgs = ['run'];
65
+
66
+ export const cwd = () => process.cwd();
67
+
68
+ const bunSpawn = (command, options = {}) => new Subprocess(command, options);
69
+ export {bunSpawn as spawn};
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const sanitize = (value, defaultValue = 'null') => {
4
+ switch (value) {
5
+ case 'pipe':
6
+ case 'piped':
7
+ return 'piped';
8
+ case 'ignore':
9
+ case null:
10
+ return 'null';
11
+ case 'inherit':
12
+ return value;
13
+ }
14
+ return defaultValue;
15
+ };
16
+
17
+ class Subprocess {
18
+ constructor(command, options) {
19
+ this.command = command;
20
+ this.options = options;
21
+
22
+ this.exitCode = null;
23
+ this.signalCode = null;
24
+ this.killed = false;
25
+ this.finished = false;
26
+
27
+ this.controller = new AbortController();
28
+
29
+ const spawnOptions = {signal: this.controller.signal, args: command.slice(1), windowsRawArguments: true};
30
+ options.cwd && (spawnOptions.cwd = options.cwd);
31
+ options.env && (spawnOptions.env = options.env);
32
+
33
+ spawnOptions.stdin = sanitize(options.stdin);
34
+ spawnOptions.stdout = sanitize(options.stdout);
35
+ spawnOptions.stderr = sanitize(options.stderr);
36
+
37
+ this.spawnOptions = spawnOptions;
38
+
39
+ this.childProcess = new Deno.Command(command[0], spawnOptions).spawn();
40
+
41
+ this.exited = this.childProcess.status
42
+ .then(status => {
43
+ this.finished = true;
44
+ this.exitCode = status.code;
45
+ this.signalCode = status.signal;
46
+ return status.code;
47
+ })
48
+ .catch(error => {
49
+ this.finished = true;
50
+ return Promise.reject(error);
51
+ });
52
+ }
53
+
54
+ get stdin() {
55
+ return this.spawnOptions.stdin === 'piped' ? this.childProcess.stdin : null;
56
+ }
57
+
58
+ get stdout() {
59
+ return this.spawnOptions.stdout === 'piped' ? this.childProcess.stdout : null;
60
+ }
61
+
62
+ get stderr() {
63
+ return this.spawnOptions.stderr === 'piped' ? this.childProcess.stderr : null;
64
+ }
65
+
66
+ kill() {
67
+ this.killed = true;
68
+ this.controller.abort();
69
+ }
70
+ }
71
+
72
+ export const currentExecPath = () => Deno.execPath();
73
+ export const runFileArgs = ['run'];
74
+
75
+ export const cwd = () => Deno.cwd();
76
+
77
+ const denoSpawn = (command, options = {}) => new Subprocess(command, options);
78
+ export {denoSpawn as spawn};
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ import {spawn} from 'node:child_process';
4
+ import {Readable, Writable} from 'node:stream';
5
+
6
+ const setStdio = (stdio, fd, value) => {
7
+ switch (value) {
8
+ case 'pipe':
9
+ case 'ignore':
10
+ case 'inherit':
11
+ stdio[fd] = value;
12
+ break;
13
+ case 'piped':
14
+ stdio[fd] = 'pipe';
15
+ break;
16
+ case null:
17
+ stdio[fd] = 'ignore';
18
+ break;
19
+ }
20
+ };
21
+
22
+ class Subprocess {
23
+ constructor(command, options) {
24
+ this.command = command;
25
+ this.options = options;
26
+
27
+ this.exitCode = null;
28
+ this.signalCode = null;
29
+ this.killed = false;
30
+ this.finished = false;
31
+
32
+ const spawnOptions = {stdio: ['ignore', 'ignore', 'ignore'], windowsVerbatimArguments: true};
33
+ options.cwd && (spawnOptions.cwd = options.cwd);
34
+ options.env && (spawnOptions.env = options.env);
35
+
36
+ setStdio(spawnOptions.stdio, 0, options.stdin);
37
+ setStdio(spawnOptions.stdio, 1, options.stdout);
38
+ setStdio(spawnOptions.stdio, 2, options.stderr);
39
+
40
+ this.spawnOptions = spawnOptions;
41
+
42
+ this.childProcess = spawn(command[0], command.slice(1), spawnOptions);
43
+ this.childProcess.on('exit', (code, signal) => {
44
+ this.finished = true;
45
+ this.exitCode = code;
46
+ this.signalCode = signal;
47
+ this.resolve(code);
48
+ });
49
+ this.childProcess.on('error', error => {
50
+ this.finished = true;
51
+ this.reject(error);
52
+ });
53
+
54
+ this.exited = new Promise((resolve, reject) => {
55
+ this.resolve = resolve;
56
+ this.reject = reject;
57
+ });
58
+
59
+ this.stdin = this.childProcess.stdin && Writable.toWeb(this.childProcess.stdin);
60
+ this.stdout = this.childProcess.stdout && Readable.toWeb(this.childProcess.stdout);
61
+ this.stderr = this.childProcess.stderr && Readable.toWeb(this.childProcess.stderr);
62
+ }
63
+
64
+ kill() {
65
+ this.killed = true;
66
+ this.childProcess.kill();
67
+ }
68
+ }
69
+
70
+ export const currentExecPath = () => process.execPath;
71
+ export const runFileArgs = [];
72
+
73
+ export const cwd = () => process.cwd();
74
+
75
+ const nodeSpawn = (command, options = {}) => new Subprocess(command, options);
76
+ export {nodeSpawn as spawn};
package/src/utils.js ADDED
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ let getEnv, isWindows, isAndroid;
4
+ if (typeof Deno !== 'undefined') {
5
+ getEnv = name => Deno.env.get(name);
6
+ isWindows = Deno.build.os === 'windows';
7
+ isAndroid = Deno.build.os === 'android';
8
+ } else if (typeof Bun !== 'undefined') {
9
+ getEnv = name => Bun.env[name];
10
+ isWindows = process.platform === 'win32';
11
+ isAndroid = process.platform === 'android';
12
+ } else {
13
+ getEnv = name => process.env[name];
14
+ isWindows = process.platform === 'win32';
15
+ isAndroid = process.platform === 'android';
16
+ }
17
+ export {getEnv, isWindows, isAndroid};
18
+
19
+ export const verifyStrings = strings => Array.isArray(strings) && strings.every(s => typeof s === 'string');
20
+
21
+ export const toBase64 = s => {
22
+ // const buf = new TextEncoder().encode(s),
23
+ // bytes = Array.from(buf, b => String.fromCharCode(b >> 8) + String.fromCharCode(b & 0xFF)).join('');
24
+ const bytes = Array.from(s, c => {
25
+ const code = c.charCodeAt(0);
26
+ return String.fromCharCode(code & 0xff) + String.fromCharCode((code >> 8) & 0xff);
27
+ }).join('');
28
+ return btoa(bytes);
29
+ };
30
+
31
+ const rawValueSymbol = Symbol.for('object-stream.raw-value');
32
+
33
+ export const raw = value => ({[rawValueSymbol]: value});
34
+ export const isRawValue = value => value && typeof value === 'object' && rawValueSymbol in value;
35
+ export const getRawValue = value => value[rawValueSymbol];
36
+
37
+ export const winCmdEscape = isWindows ? value => raw(String(value).replace(/./g, '^$&')) : value => String(value);