npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@jordanrickman/b

v0.5.4

Published

b.js ===

Downloads

3

Readme

b.js

b is a DSL for calling shell commands from Node, powered by the tagged template literal syntax. Instead of writing lots of calls to spawnSync(), you can just write

const runTestSuite = (env, noCoverage) => {
  b`npm install`
  b`docker-compose up -d test_services`
  b`./scripts/start_test_database --env=${env}`
  b`jest ${noCoverage ?? '--coverage=false'}`
  b`./scripts/stop_test_database`
  if (!noCoverage) {
    b`./upload_coverage.sh`
  }
}

b gives you all the expressive power of Bash one-liners, right in your JavaScript code!

Contents

Get it

npm i @jordanrickman/b

The module exports a single function.

const b = require('@jordanrickman/b')
// Or, using ESM syntax
import * as b from '@jordanrickman/b'

Async, but Sequential

b wouldn't be much fun if you had to write this:

await b`my first command`
await b`a second command`
await b`the third, all in order`

However, if we used child_process.spawnSync, there'd be lots of fancy things we couldn't do. Such as, we couldn't echo child process stdout/stderr, while also capturing it for you to save to a variable.

b chooses a hybrid approach. Each b tag returns a Promise, and if you want access to command results, you have to await that promise (or use then()).

const { status, stdout } = await b`ls -la`
if (status === 0) {
  console.log(stdout)
} else {
  console.log(`ls failed with exit code ${status}`)
}

However, b secretly chains all those Promises together, such that one command won't start until the previous one finishes. So for most purposes, you can still treat b calls as sequential.

Just remember to wait for them all to finish before you do anything that depends on their side effects.

b`unzip big_archive.zip big_archive`
const text = readFileSync('big_archive/data.txt') // Oops!
                                                  // The file isn't there yet.

You can just await your last b command; it will happen after the ones before it.

b`cp a.zip b.zip`
b`unzip b.zip c/`
await b`cat c/data.txt | grep ${searchText} > matched.txt`
const matches = readFileSync('matched.txt', 'utf8')

Or, you can use b.waitAll(), whose promise resolves once b's queue has finished.

Parallelization

If you need to, you can run things in parallel instead of sequentially with b.fork, which "forks" off a new queue of b commands.

// command 3 will wait for command 1, but not for command 2
b`command 1`
b.fork`command 2`
b`command 3`

// Save a forked `b` instance, whose shell commands will run
// in sequence with themselves, but parallel to the main instance
// (or to other forked instances).
const b2 = b.fork()
b2`command a`
b2`command b`
b2`command c`
await b2.waitAll() // wait for b2's queue to finish

// Or, you can pass fork() a sequence of commands to run in a parallel "thread"
const parallelTask = b.fork((b_) => {
  b_`command theFirst`
  b_`command theSecond`
  b_`command theThird`
})

// It returns a new `b` object, just like `fork()` does.
parallelTask`command oneMoreThingAtTheEnd`
await parallelTask.waitAll()

You can also use b.bg to start a command that will keep running after your JS code exits.

Exception Handling

Also, unlike spawnSync() and other child_process APIs, which just set the error attribute, b raises an exception when a process fails to spawn.

By default, b also raises an exception if a shell command produces a non-zero exit code. This can be relaxed with b.mayfail.

When trying to catch these, keep in mind they will come in as Promise rejections; a call to b will not itself throw any of these (unless awaited).

Return Signature

b's Promise resolves with the same object structure as that returned by child_process.spawnSync(), except that it converts the I/O from Buffers into Strings. The specific type signature is

{
  pid: number; // PID of the child process
  output: [string, string, string]; // [stdin, stdout, stderr]
  stdout: string; // Identical to output[1]
  stderr: string; // Identical to output[2]
  status?: number; // Exit code, or undefined if the process was terminated by a signal
  signal?: string; // If it was terminated by a signal, the signal used to terminate the process
  error?: Error; // The error object if the child process failed or timed out
}

As it is so common, there is a convenience method b.stdoutof that just gets the stdout of a command. Unlike most other b methods (see Options), this will have a different return signature, so you will lose access to all other return values. Use when you need a command's output, and nothing else.

const result = await b.stdoutof`echo Hello`
console.log(result) // Hello

Template Parsing

b applies some smart processing to your template literals. To see all the possibilities, look in test/b.test.js, at the test cases for the _interpolate function. Some highlights include:

  • Strings are wrapped in double quotes.
b`cat ${'My File Name.txt'}` // > cat "My File Name.txt"
  • Use b.raw to avoid quoting strings.
b`cp ${b.raw('../source ./destination')}` // > cp ../source ./destination
b`ls /path/to/${b.raw('directory')}` // > ls /path/to/directory
  • Use b.squote to single quote / (aka "strong quote") strings
b`echo ${b.squote('these variables are $not $read by Bash')}`
// > echo 'these variables are $not $read by Bash'
  • Arrays are converted to space-separated lists.
b`git add ${['file1', 'file 2', 'file3']}` // > git add "file1" "file 2" "file3"
  • Null and undefined are ignored.
b`echo Hello ${undefined}World` // > echo Hello World
b`git add ${['file1', null, 'file 2']}` // > git add "file1" "file 2"
  • Functions are executed before running the command, but after previous commands have finished. Async functions can be used.
b`doSomething > tempfile`
b`echo file contents: ${() => readFileSync('tempfile')}`

b`uploadDocument --id=${docId} myDocument.json`
b`echo uploaded contents: ${async () => docServer.get('/document/'+docId)}`

Stdio

Because b was written to help with scripting / automation, it prints stdout and stderr by default, piping them to the stdout/stderr of the parent process. It also pipes the parent process stdin to the child stdin, allowing you to run interactive commands.

You can override this behavior with b.quiet and b.silent

b.quiet`cat averylongfile.txt` // stdout is ignored
b.silent`echo "Don't show this error" >&2` // stdout AND stderr are ignored

For extra information, if you use b.echo (or b.with({ echo: true })), b will print each command string before it runs them.

b.echo`echo "Hello, World!"`
// echo "Hello, World!"
// Hello, World!

Be careful when echoing commands that might contain passwords or other sensitive data. Obfuscation of data in echo might be a feature in the future, but is not supported right now.

Options

You can configure b behavior with various options. All options can be set in a few different ways.

// Apply one option to a single command
b.optionName('value')`command ${template} as normal`
b.optionName`command` // for a boolean option

// Apply multiple options to a single command
b.with({ optionA: true, optionB: 'value', optionC: true })
// ... or chain them, fluent-style
b.optionA.optionB('value').optionC`command`

// Set options globally
b.config({ optionA: true, optionB: 'value', optionC: true })

// Because you can chain options, you can also save configured instances.
const silentB = b.with({ silent: true })
silentB`command 1`
silentB`command 2`
silentB`command 3`

// Note that unlike fork(), these will still run in the same sequence
// (or, in the sequence of a forked `b` instance they were made from).

// Finally, you can pass a function to with(), which will receive a
// configured `b` instance.
b.with({ optionA: true, optionB: 'value' }, b_ => {
  b_`command 1`
  b_`command 2`
  b_`command 3`
})

// b.with({ ... }).fork(<function>) can be a useful combination.

Set current working directory

b.cd('/path/to/directory')`command string`
b.with({ cd: '/path/to/directory' })`command string`

Set environment variables

b.env({ VARIABLE_NAME: 'value' })`command string`
b.with({ env: { VARIABLE_NAME: 'value' }})`command string`

Note that the environment of the parent process is automatically inherited - this is necessary to, for instance, retain the same PATH. If you want to ensure an environment variable is not set, try setting it to null (NOT undefined).

Don't raise an exception on failure

b.mayfail`command string`
b.with({ mayfail: true })`command string`

Default behavior is to raise an exception (Promise rejection) either on a non-zero exit code, or if the child process is terminated by a signal (e.g. Ctrl-C / SIGINT). Setting this flag to true disables both cases. Errors will still be raised if the child process fails to start.

Echo command input

b.echo`command string`
b.with({ echo: true })

Print each command string to stdout before executing.

Suppress command output

b.quiet`command string` // Don't echo stdout
b.silent`command string` // Don't echo stderr OR stdout
b.with({ quiet: true, silent: true }) // (redundant; silent implies quiet)

Default behavior is to pipe stdout and stderr from the child process to the parent process, meaning the parent will emit these as the child does (and on the same channels). These flags prevent that. You can still access the stdout and stderr attributes in the Promise result.

Run a process in the background

WARNING! This currently has some bugs, and will leave b in a bad state, so don't use it.

b.bg`long-running command`
b.with({ bg: true })`long-running command`

The process will be fully detached, and will keep running after your JS code quits. We capture no process information apart from PID, and the returned Promise will resolve immediately.

Set user or group of the child process

b.user('username')`command string`
b.group('groupname')`command string`
b.uid(n)`command string`
b.gid(n)`command string`

Timeout process after n milliseconds

b.timeout(n)`command string`
b.with({ timeout: n })`command string`

After the timeout, we attempt to kill the process using SIGTERM. This may not work (the process may catch it).

When not to Use b

I hope primarily to give you a new possibility for flexible, JavaScript-based task automation.

I am not a systems programmer, or an inter-process communication expert. You shouldn't use b to manage subprocesses in production code, and you probably shouldn't use it in things like production build chains, where you need a guarantee that certain commands were run with certain exact arguments.

Also, I have made only minimal effort to support non-Unix systems. You're welcome to try it on Windows, but... good luck.

Security

b is a shell command runner. Strings passed into b can do anything. Don't pass user input into b. Ever.

Why the name?

A single-letter tag to keep it short. b as in Bash, a good mnemonic for what it does with your string templates.