Talk to your computer in Javascript via the repl console
Alexey
Posted on April 18, 2021
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()
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()
Starting the console
I usually start the script through npm/yarn, via package.json:
...
"scripts": {
...
"console": "node --experimental-repl-await ./scripts/console.js"
...
},
...
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 await
ing 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
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
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
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 .gitignore
d 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!")
})
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()
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.
Posted on April 18, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.