1.0.0-beta.2 • Published 4 years ago

@atweel/intershell v1.0.0-beta.2

Weekly downloads
-
License
ISC
Repository
github
Last release
4 years ago

Validation on master

Intershell

Intershell is a utility package that allows for linux shell scripts to be embedded into and executed directly from Javascript/TypeScript code of your Node.js applications by leveraging the power of ES2015 tagged template literals. Intershell exports a function called shell that can serve both as a tag function that turns template string literals into executable script functions; and a factory function for producing specialized tag functions. The example below deminstrates how shell can be used as a tag function to wrap a shell script into a function.

// Source code:
import { shell } from '@atweel/intershell';

const name = 'Robby the Robot';

const script = shell`
    echo "Hello from ${ name }!"
`;

script((error, stdout) => {
    if (error) {
        console.error(`Intershell script execution failed. ${ error }`);
    } else {
        console.log(stdout);
    }
});

// Output:
// Hello from Robby the Robot!

When shell is applied to a template literal as in the example above, it produces a function that will start a separate interpreter process to execute the specified script once invoked. By default, intershell uses /bin/sh as the interpreter and the script is executed asynchronously, however both custom interpreters and synchronous execution are supported. The function produced by shell (aka intershell script function) accepts zero to two arguments. These arguments can be a parameters object discussed below, a callback, both, or none. When a callback is specified, it will be called once the script exits with two arguments: an error which can be of any type and an stdout (string or Buffer) containing the output produced by the script. If the script exits with a zero, error is null, otherwise error contains information about the error.

Using a custom shell interpreter

As noted previously, when intershell executes scripts, it uses /bin/sh as the interpreter by default. Althouth /bin/sh seems to be a reasonable choice in most situations, sometimes you might want to specify an interpreter explicitly. For that case, intershell's shell function can be invoked as a "regular" (non-tag) function and be provided with a a full path to an interpretere or a command name that can be resolved by the system. The example below demonstrates this. Here, we call shell to create a tag function that is bound to the specified interpreter which is /bin/bash in this case. Then, we use this tag function in place of shell to compile a script function. As seen from the output, the script in this example is actually executed under /bin/bash (variable $0 within a script refers to the executable).

// Source code:
import { shell } from '@atweel/intershell';

const bashShell = shell('/bin/bash');

const script = bashShell`
    echo "$0"
`;

script((error, stdout) => {
    if (error) {
        console.error(`Intershell script execution failed. ${ error }`);
    } else {
        console.log(stdout);
    }
});

// Output:
// /bin/bash

For convenience, intershell package provides shortcuts for the four most used interpreters, namely /bin/bash, /bin/dash, /bin/zsh, and /bin/sh.

// Source code:
import { sh, bash, dash, zsh } from '@atweel/intershell';

sh`echo "Hello from $0"`((error, stdout) => {
    console.log(`${ stdout } (sh)`);
});

bash`echo "Hello from $0"`((error, stdout) => {
    console.log(`${ stdout } (bash)`);
});

dash`echo "Hello from $0"`((error, stdout) => {
    console.log(`${ stdout } (dash)`);
});

zsh`echo "Hello from $0"`((error, stdout) => {
    console.log(`${ stdout } (zsh)`);
});

// Output:
// Hello from /bin/dash
//  (dash)
// Hello from /bin/bash
//  (bash)
// Hello from /bin/sh
//  (sh)
// Hello from /bin/zsh
//  (zsh)

Synchronous vs. asynchronous execution

As already stated, Intershell scripts are executed asynchronously by default. To support synchronous execution, Intershell script function exposes a method called execSync which starts the interpreter in s separate process, waits for it to finish execution, and returns the output as a Buffer or a string similarly to the execSync API from the standard Node.js child_process package.

// Source code:
import { shell } from '@atweel/intershell';

const name = 'Robby';

const script = shell`
    echo "Hello ${ name }!"
`;

const output = script.execSync().toString();

console.log(output);

// Output:
// Hello Robby!

For the sake of API symmetry, Intershell script functions also expose the execAsync method which does exactly the same as the direct invocation.

// Source code:
import { shell } from '@atweel/intershell';

const name = 'Robby';

const script = shell`
    echo "Hello ${ name }!"
`;

script.execAsync((error, stdout) => {
    if (error) {
        console.error(`Intershell script execution failed. ${ error }`);
    } else {
        console.log(stdout);
    }
});

// Output:
// Hello Robby!

Support for promises

Intershell scripts support promises via the standard promisify mechanism from the util package for Node.js.

// Source code:
import { promisify } from 'util';

import { shell } from '@atweel/intershell';

const name = 'Robby';

const script = promisify(shell`echo "Hello from ${ name }!"`);

script().then((stdout) => {
    console.log(stdout);
});

// Output:
// Hello from Robby!

Parameterised scripts

As with regular interpolated template literals, one can use variables that are in the scope of the template to customise the script that is being generated by intershell. But what if we wanted to make a script that is callable with different parameters? To achieve this, one can use the gneric form of the shell function as shown below.

// Source code:
import { shell } from '@atweel/intershell';

const script = shell<{
    name: string;
}>`
    echo "Hello from ${ ({ name }) => name }!"
`;

console.log(script.execSync({ name: 'Richie' }).toString());
console.log(script.execSync({ name: 'Megan' }).toString());

// Output:
// Hello from Richie!
// 
// Hello from Megan!

Dependencies

As of version 1.0.0, intershell has only two runtime dependencies: debug and reflect-metadata. Please note, that reflect-metadata API is not yet standardized and may change in the future.

Contributing

Contributions in the form of issues and PRs are always welcome. For additional information on the topic, please refer to contributing.md.

License

This software is licensed under the MIT license as detailed below.


Copyright 2020 Atweel Inc.

Copyright 2020 Eduard Malakhov

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.