teaching machines

CS 347: Lab 13 – Node Shell

October 6, 2021 by . Filed under fall-2021, labs, webdev.

Dear students:

Welcome to lab. Now’s your chance to apply the ideas you read about. Find a partner and complete as much of the task below as you can. At the end of our time together, paste your HTML files into Crowdsource in order to receive credit.

Converting a Callback to a Promise

Imagine you wanted to write a REPL for a programming language. In Java, you’d write code like this:

Scanner in = new Scanner(System.in);
while (true) {
  System.out.print("> ");
  String line = in.nextLine();
  // process line
}

This is harder to write in JavaScript. Many JavaScript libraries are designed around callbacks to handle asynchronous operations like getting user input. That’s sad. However, if you had promises, you could write code pretty close to the Java code above. Sometimes an alternative version of the library based on promises is available. But not always. The good news is that you can convert a callback scheme into a promise scheme. Consider this delayed console message:

setTimeout(() => {
  console.log('Happy birthday!');
}, 1000);

To frame this synchronously, you’d write it by awaiting an asynchronous delay function:

await delay(1000);
console.log('Happy birthday!');

There is no delay function. But you can write one:

function delay(millis) {
  return new Promise(resolve => {
    setTimeout(() -> resolve(), millis);
  });
}

This function creates a promise. The promise constructor expects a function as its parameter. It hands that function another function named resolve, which must be called when the asynchronous operation finishes. The callback that we provide our asynchronous function will call resolve instead of logging a message.

That rounds out the conversion. There’s just one gotcha. Node.js doesn’t allow awaiting at the top-level. So we wrap the sequential code up in a function:

function delay(millis) {
  return new Promise(resolve => {
    setTimeout(() => resolve(), millis);
  });
}

async function main() {
  await delay(1000);
  console.log('Happy birthday!');
}

main();

If our asynchronous operation had produced a result, then we pass it along through the call to resolve, and it becomes the value produced by the await command:

function somePromiseOp(...) {
  return new Promise(resolve => {
    someCallbackOp(..., result => resolve(result));
  });
}

async function main() {
  const result = await somePromiseOp(...);
  // process result
}

main();

Task 1

When you open up a terminal on your computer, you are given a shell. The shell prompts the user for a line of text. The first word in the line is a shell command or the name of an executable file. Any other words are passed as command-line arguments to the command or executable. Your task in this lab is to write your own shell using Node.js.

Support the following commands, listed in the order of recommended implementation:

Include the following dependencies in your solution:

const fsPromises = require('fs').promises;
const path = require('path');
const http = require('http');

If you are using a lab computer, you’ll need to install NVM as described in All Over the Web. You’ll additionally need to consult the Node.js documentation.

Getting User Input

Use the builtin readline to get user input. It is imported and configured to read from standard input and write to standard output with this statement:

const readline = require('readline').createInterface({
  input: process.stdin,
  output: process.stdout
});

Getting user input with readline is awkward because of its orientation toward asynchonicity. Its question method expects you provide a callback to handle the answer:

readline.question(prompt, answer => {
  console.log(`You answered: ${answer}`);
});

However, you can translate this function into one that yields a promise. Then you can use async and await to write sequential code.

TODO

The next step of your learning is to complete the following tasks:

See you next time.

Sincerely,