@getvim/execute
TypeScript icon, indicating that this package has built-in type declarations

1.0.0 • Public • Published

@getvim/execute

This is an extremely opinionated, zero dependencies, tiny module for executing command line programs from Node.js using a Bash shell.

The design philosophy behind this module aims to abstract away many of the Bash pitfalls most of us never knew existed.

Requirements

You must be working in an environment where /bin/bash is available.

Minimal Node.js version is 8.0.0.

Usage

Installation

npm install --save @getvim/execute
yarn add @getvim/execute

Basic usage

const { execute } = require('@getvim/execute');
execute('ls')
    .then(console.log); // result: "feature.js\nfeature.spec.js"

Environment variables

const { execute } = require('@getvim/execute');
execute('echo $MY_NAME', { env: { MY_NAME: 'Jake Chambers' } })
    .then(console.log); // result: "Jake Chambers"

Piping

Piping allows you to interleave shell commands with modern JS features that are safer and easier to use than a lot of shell alternatives like using external modules, concurrency, loops or string formatters.

const { execute, pipe } = require('@getvim/execute');
const mailer = require('@fictional/mail-client')
execute('ls | grep temp')
    .then(tempFiles => {
        mailer('admin@website.com', 'Temp files being deleted', tempFiles);
        return tempFiles;
    })
    .then(pipe('xargs rm'));

Errors

const { execute } = require('@getvim/execute');
execute('npm install')
    .then(() => execute('npm test')) // Has a failing test
    .then(() => execute('npm publish'))
    .catch(error => console.error('Failed CI/CD!', error));
    /* result:
        Failed CI/CD! 
        { 
            killed: false, 
            code: 1, 
            signal: null, 
            cmd: "set -euo pipefail\nnpm test" 
            stdout: "... test results ..."
            stderr: "... failing test ..."
        } 
    */

Behavior

  • execute's Promise will reject in the following cases:
    • Usage of an undefined environment variable
    • A command, in a pipe or otherwise, returned an exit code other than 0 (even greping without a result will do this)
  • Results are always a string.
  • Processes are not interactive and do not print to the screen. If a process is waiting for user input, (like a prompt for a Y/N confirmation) the Promise will never resolve/reject.

Why?

Bash has a lot of pitfalls. As code organizations grow their reliance on CI/CD scripts or internal tooling for day-to-day work grow as well. Most organizations write those scripts in pure Bash. Most of the people reading/writing these scripts will find themselves victims of those counter intuitive Bash pitfalls. This module aims to take some very opinionated decisions that will guard its users from 80% of these common mistakes.

Every problem that this tool purports to solve can be solved using Bash only solutions, but it requires learning time most developers will not invest. Using this module you can use language features you're already familiar with instead of learning the archaic and unintuitive Bash solutions.

Common Bash Pitfalls

Only the last error counts

Problem:

npm install
npm test # this contains a failing test, the exit code here is nonzero
npm publish # Bash continues to run the script and npm publish will still happen

Solution: Any command run with execute will be run with the set -e flag and the script will immediately exit with a Promise rejection on any line that returns a non zero exit code.

Failure mid-pipe doesn't count

Problem:

failingCommand | echo "a" # failingCommand does not throw an error because it's in a pipe
echo "b"
# result:
# a
# b

Solution: Any command run with execute will be run with the set -o pipefail flag and the script will immediately exit with a Promise rejection on any command in a pipe chain that returns a non zero exit code.

Using unset variables does not throw errors

Problem:

rm -rf $BUILD_FOLDER/* # If $BUILD_FOLDER is unset by mistake, this line would delete the entire filesystem

Solution: Any command run with execute will be run with the set -u flag and the script will immediately exit with a Promise rejection on any usage of an unset environment variable.

Basic built-in language features like string manipulation are easy to get wrong

Problem:

# Trying to extract the Nth word in a string
echo $STRING | awk -v N=$N '{print $N}' # You have to learn awk or other tools

Solution: Usage of execute.pipe allows to easily jump between a Bash context and a JS context.

const { execute, pipe } = require('@getvim/execute');
const N = 5;
execute('echo $STRING', { env: { STRING:'My name is Inigo Montoya'}})
    .then(result => result.split(' ')[N - 1])
    .then(pipe('rev'))
    .then(console.log); // Result: "ayotnoM"

Basic built-in language constructs like IF-ELSE / Conditionals are easy to get wrong

Problem: It very easy to get Syntax error or unintended behavior from Bash's language constructs.

# Checking if a variable is greater than 0
if [$VAR > 0]; then ... # Syntax error, forgot spaces

if [ $VAR > 0 ]; then ... # ">" Means stream redirection

if [[ $VAR > 0 ]]; then ... # ">" Means comparison, but of strings (lexicographs)

if [[ $VAR -gt 0 ]]; then ... # Finally works!

if (( $VAR > 0 )); then ... # Confusingly, this also works...

Solution: Execute allows to easily jump between a Bash context and a JS context.

const { execute, pipe } = require('@getvim/execute');
execute('wc -l file.txt')
    .then(lineCount => lineCount > 0 ? "Success" : "Failure")
    .then(pipe("tee SomeFile.txt"))

Readme

Keywords

Package Sidebar

Install

npm i @getvim/execute

Weekly Downloads

1,516

Version

1.0.0

License

ISC

Unpacked Size

9.22 kB

Total Files

5

Last publish

Collaborators

  • peledyuval