Created
April 7, 2021 17:31
-
-
Save Qix-/2e58b362904ea066967d4a4b1b6e4db2 to your computer and use it in GitHub Desktop.
A thing I spent a few hours writing and ended up not needing.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
sh`emulator` (shell emulator) | |
by Josh Junon | |
Dual license: MIT License OR public domain | |
const [code, out, err] = await sh | |
`cat ${process.argv[2]}` | |
`wc -w` | |
`rev` | |
`tee /tmp/rev-word-count.txt` | |
console.log({code, out, err}) | |
{ | |
code: 0, | |
out: ' 42', | |
err: '' | |
} | |
Interpolation works as you'd expect; values are | |
considered contiguous strings (as though they | |
were double-quoted in shellscript). | |
*/ | |
const child_process = require('child_process'); | |
class StreamSink { | |
constructor() { | |
this.chunks = []; | |
this.totalLength = 0; | |
this.finishedBuffer = null; | |
this.endCbs = []; | |
this.streamCounts = 0; | |
} | |
attach(stream) { | |
++this.streamCounts; | |
stream.on('data', buf => { | |
this.chunks.push(buf); | |
this.totalLength += buf.length; | |
}); | |
stream.on('end', () => { | |
if (--this.streamCounts === 0) { | |
this.finishedBuffer = Buffer.concat(this.chunks, this.totalLength); | |
this.endCbs.forEach(cb => cb(this.finishedBuffer)); | |
delete this.endCbs; // Make any misuse or bugs fail loudly | |
} | |
}); | |
return this; | |
} | |
/*async*/ getBuffer() { | |
return new Promise(resolve => { | |
if (this.finishedBuffer) { | |
return resolve(this.finishedBuffer); | |
} | |
this.endCbs.push(resolve); | |
}); | |
} | |
}; | |
const makeSh = (ctx = {}) => { | |
if (!('stderr' in ctx)) { | |
ctx.stderr = new StreamSink(); | |
} | |
const newSh = (strings, ...args) => { | |
if (!Array.isArray(strings)) { | |
// strings is really a context; | |
// apply whatever changes are given | |
// and then return a new context. | |
return makeSh(Object.assign( | |
{}, ctx, strings | |
)); | |
} | |
const argv = []; | |
let chunks = []; | |
const commit = () => { | |
if (chunks.length > 0) { | |
argv.push(chunks.join('')); | |
chunks = []; | |
} | |
}; | |
strings[0] = strings[0].trimStart(); | |
strings[strings.length - 1] = strings[strings.length - 1].trimEnd(); | |
for (let i = 0, len = strings.length; i < len; i++) { | |
if (i > 0) { | |
const arg = args[i - 1]; | |
chunks.push(arg.toString()); | |
} | |
const string = strings[i]; | |
const pattern = /([^ \t\r\n]+)|([ \t\r\n]+)/g; | |
let match; | |
while ((match = pattern.exec(string))) { | |
if (match[1]) { | |
chunks.push(match[1]); | |
} else { | |
commit(); | |
} | |
} | |
} | |
commit(); | |
if (argv.length === 0) { | |
throw new Error('no arguments given'); | |
} | |
const argv0 = argv.shift(); | |
const newCtx = Object.assign({}, ctx); | |
/* | |
A little note on the implementation: | |
Technically speaking, we could pass | |
the preceding process's standard | |
output here to the `stdio` property. | |
However, we'd have to wait for the | |
`spawn` event since there wouldn't be | |
a file descriptor guaranteed to back | |
the stream, thus the call to spawn | |
would fail. | |
Thus, we opt just to eat the copy cost | |
and pipe directly to the next process. | |
In theory, we could wait to spawn | |
processes upon `await` but that's a | |
refactor for another day. | |
Feel free to PR. | |
*/ | |
const child = child_process.spawn( | |
argv0, | |
argv, | |
{ | |
cwd: ctx.cwd || process.cwd(), | |
env: ctx.env || process.env, | |
argv0: ctx.argv0 || argv0, | |
stdio: [ | |
(ctx.stdout ? 'pipe' : process.stdin), | |
'pipe', | |
'pipe' | |
] | |
} | |
); | |
let resolve; | |
let reject; | |
const promise = new Promise((resolve_, reject_) => { | |
resolve = resolve_; | |
reject = reject_; | |
}); | |
newCtx.stdout = child.stdout; | |
newCtx.statusPromise = promise; | |
if (ctx.stdout) ctx.stdout.pipe(child.stdin); // otherwise, we use process.stdin above, which CAN be duped | |
const stdoutSink = new StreamSink().attach(child.stdout); | |
ctx.stderr.attach(child.stderr); | |
child.on('error', reject); | |
child.on('exit', code => resolve(code)); | |
const result = makeSh(newCtx); | |
result.then = (...args) => ( | |
promise | |
/* | |
If Javascript's array destructuring ignored | |
extra elements, we'd also pass the signal | |
as the fourth element. However I can't see it | |
being needed frequently enough to justify having | |
it in there otherwise. | |
*/ | |
.then(async code => ([ | |
(code || (ctx.statusPromise && await ctx.statusPromise) || 0), | |
await stdoutSink.getBuffer(), | |
await ctx.stderr.getBuffer() | |
])) | |
.then(...args) | |
); | |
result.catch = (...args) => promise.catch(...args); | |
return result; | |
}; | |
newSh.out = (...args) => { | |
const shres = newSh(...args); | |
const result = (...args) => shres.out(...args); | |
result.then = (...args) => shres | |
.then(([code, stdout, stderr]) => { | |
process.stderr.write(stderr); | |
if (code !== 0) process.exit(code); | |
return stdout; | |
}) | |
.then(...args); | |
result.catch = (...args) => shres.catch(...args); | |
return result; | |
}; | |
return newSh; | |
}; | |
module.exports = makeSh(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment