Talk to your computer in Javascript via the repl console

alexeydc

Alexey

Posted on April 18, 2021

Talk to your computer in Javascript via the repl console

Premise

I often want to run ad hoc Javascript commands that rely on npm packages and custom classes I've written that work with a database/filesystem or wrap common logic.

Node comes with repl (Read-Eval-Print Loop), and you can launch a simple repl console by just running node with no arguments - the commands for it are documented in e.g. https://nodejs.org/api/repl.html#repl_design_and_features. That's quite handy - but falls short of a full-featured interactive shell that has access to all necessary packages.

The solution

Luckily, repl is available in node as a package ( https://nodejs.org/api/repl.html#repl_repl ) - so all that's necessary is to write a small script that starts a repl instance and pulls in everything you need.

You'll need to inject all the packages you want to use interactively into the repl console via a launcher script. It's also handy to configure repl in the script, and I show some examples below:

/*
  Opinionated example on how
  to make the repl console environment aware.
*/
require("dotenv").config()
/*
  If you intend to launch the console
  across multiple environments (development/production/staging) -
  it's helpful print the environment
  to avoid unfortunate mistakes.
*/
console.log(`Starting console - ${process.env.NODE_ENV}`)

const repl = require("repl")
const util = require("util")

const startConsole = async () => {
  /*
    The lines below configure output formatting for repl.

    W/o specifying any output options, you'd get
    formatting like
    > a = {a: {b: {c: {d: {e: {f: {g: {h: 1}}}}}}}}
    { a: { b: { c: [Object] } } }

    With these options, you'd get
    > a = {a: {b: {c: {d: {e: {f: {g: {h: 1}}}}}}}}
    { a: { b: { c: { d: { e: { f: { g: { h: 1 } } } } } } } }

    Note these options are the same as the options passed to inspect
    https://nodejs.org/api/util.html#util_util_inspect_object_options
  */
  util.inspect.defaultOptions.depth = 20
  util.inspect.defaultOptions.colors = true
  util.inspect.defaultOptions.getters = true
  util.inspect.defaultOptions.compact = true

  /*
    repl is supposed to use util.inspect to format by default.
    However, w/o explicitly specifying {writer: util.inspect},
    I was not able to get the options above to be successfully applied
    for eval results formatting. They _do_ get applied to
    console.log formatting, though, in either case.

    You may want to specify other options - see
    https://nodejs.org/api/repl.html#repl_repl_start_options
    for a comprehensive list - e.g. {prompt: "xyz>"} is a handy one.
  */
  const replServer = repl.start({writer: util.inspect})
  /*
    Pull in any number of modules here - these are the
    modules that will be available to you in the repl instance.
  */
  const modules = ["util", "fs"]
  modules.forEach((moduleName) => {
    replServer.context[moduleName] = require(moduleName)
  })
  /*
    This is not necessary in newer versions of node,
    but in older versions I wasn't able to pull in
    ad-hoc modules to a running repl instance w/o it.
  */
  replServer.context.require = require
}

startConsole()
Enter fullscreen mode Exit fullscreen mode

The way I personally set it up is by having all the things my application cares about available as a single module defined in my application - including both npm packages and my own library/reusable code.

I use this single module in application code, scripts, background jobs, and also in the repl console - that way accessing functionality looks the same in all contexts, and I can easily memorize commands and have them at my fingertips.

My script ends up looking more like this:

require("dotenv").config()
console.log(`Starting console - ${process.env.NODE_ENV}`)

const repl = require("repl")
const util = require("util")
/*
  This contains all the modules I want to pull in
*/
const lib = require("../lib.js")

const startConsole = async () => {
  /*
    E.g. establish connections to various databases...
  */
  await lib.init()

  util.inspect.defaultOptions.depth = 20
  util.inspect.defaultOptions.colors = true
  util.inspect.defaultOptions.getters = true
  util.inspect.defaultOptions.compact = true
  const replServer = repl.start({writer: util.inspect})

  for(key of Object.keys(lib)) {
    replServer.context[key] = lib[key]
  }
}

startConsole()
Enter fullscreen mode Exit fullscreen mode

Starting the console

I usually start the script through npm/yarn, via package.json:

...
  "scripts": {
    ...
    "console": "node --experimental-repl-await ./scripts/console.js"
    ...
  },
...
Enter fullscreen mode Exit fullscreen mode

I love --experimental-repl-await (https://nodejs.org/api/cli.html#cli_experimental_repl_await - added in Node.js 10.0.0), and I hope it makes its way out of experimental soon. It allows awaiting on async commands in the repl console. Without it, working with promises is quite annoying.

After that's in, it's just yarn run console or npm run console.

Working with the console

yarn run console
> console.log("Hello world")
Hello world
undefined
Enter fullscreen mode Exit fullscreen mode

Note how console.log("...") produces 2 lines as output. It performs its side effect of printing and returns a value - and repl will print the result of each expression it evaluates. For example, variable declarations return undefined, but variable assignments return the assigned value:

> let a = 1
undefined
> a = 2
2
Enter fullscreen mode Exit fullscreen mode

That's handy to know if you want to skip printing the output of some expression.

In most cases, I tend to avoid using variable declarations in repl, since you can assign a variable without declaring it. The reason is that I often copy-paste sections of code from a text editor, and variable declarations are not re-runnable. In application code I'll usually use const, but in repl that locks you out from fixing mistakes, especially with e.g. function declarations.

> let a = 1
undefined
> let a = 1
Uncaught SyntaxError: Identifier 'a' has already been declared
> b = 1
1
> b = 1
1
Enter fullscreen mode Exit fullscreen mode

Persistent history

Repl supports bi-directional reverse-i-search similar to zsh. I.e. you can search back through your history by pressing ctrl+r (or ctrl+s to search forward) - which makes preserving history between runs potentially very worth it.

History is preserved in a file, so you'll need to choose where to store it. I store it in a .gitignored folder in my project. E.g. the default node.js repl console stores history by default, in your home folder in .node_repl_history ( https://nodejs.org/api/repl.html#repl_persistent_history ).

Here's the code for enabling persistent command history - the path is relative to the root of the project ( https://nodejs.org/api/repl.html#repl_replserver_setuphistory_historypath_callback ):

replServer.setupHistory("./no_commit/repl_history", () => {
  console.log("Loaded history!")
})
Enter fullscreen mode Exit fullscreen mode

I add this at the end of the startConsole() function above, adding the environment as the filename suffix:

require("dotenv").config()
console.log(`Starting console - ${process.env.NODE_ENV}`)

const repl = require("repl")
const lib = require("../index.js")
const util = require("util")

const startConsole = async () => {
  await lib.init()

  util.inspect.defaultOptions.depth = 20
  util.inspect.defaultOptions.colors = true
  util.inspect.defaultOptions.getters = true
  util.inspect.defaultOptions.compact = true
  const replServer = repl.start({
    writer: util.inspect,
    prompt: "> "
  })

  for(key of Object.keys(lib)) {
    replServer.context[key] = lib[key]
  } 

  const historyPath = `./no_commit/repl_history_${process.env.NODE_ENV}`
  replServer.setupHistory(historyPath, () => {})
}

startConsole()
Enter fullscreen mode Exit fullscreen mode

Conclusion

It's quite easy to set up an interactive Javascript shell based on Node's REPL module. It can be configured flexibly, have access to application logic, and any installed npm modules.

Unlike a Chrome console, it can be used to run arbitrary commands on your computer (or a remote computer), and not just for working with a particular application - hence the title of this article.

💖 💪 🙅 🚩
alexeydc
Alexey

Posted on April 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related