πŸ§‘πŸΎβ€πŸ’» prep

Overview description of the prep work for the sprint

❓ NodeJS

Learning Objectives

We know that that JavaScript is an interpreted language. Running it needs some interpreter to read our lines of code and execute them.

We’ve already seen that web browsers can run JavaScript. Web browsers provide a runtime environment for JavaScript.

NodeJS is another runtime environment for running JavaScript. It allows us to run JavaScript files from a terminal.

There are some similarities and differences between how NodeJS runs JavaScript, and how web browsers run JavaScript. For instance:

  • Both support the same core language (e.g. defining variables, if statements, for loops, etc).
  • Web browsers expose 🧢 🧢 expose To expose an API means to provide functions or values to the programmer. Sometimes we expose these over the internet, using HTTP+JSON. Other times we expose them directly as symbols you can import into your program. extra APIs that can be used from JavaScript, e.g. the DOM.
  • NodeJS exposes extra APIs that can be used from JavaScript, e.g. reading and writing files in the filesystem.
  • Some APIs are implemented differently, e.g. if you call console.log in a web browser it will log to the web inspector console (hidden by default), whereas in NodeJS it will log to stdout (the default output of a program).

People use NodeJS so that they can run code they’ve written in a terminal. Some example reasons:

  • Because they want to use NodeJS’s extra capabilities in their code (e.g. reading files).
  • Because they want to use a JavaScript as part of a shell pipeline.
  • Because they want their program to run for a long time on a server.

You’ve already written JavaScript programs and run them in the NodeJS runtime environment - every time you run a command like node index.js or npm test you’re running JavaScript with NodeJS.

Most of the programs you wrote and ran like this in the Introduction to Programming course were short-lived experiments (learning a concept and trying it out), or tests.

We’re going to start thinking about writing programs intended to be run like this.

πŸ› οΈ Writing a NodeJS program

Learning Objectives

Below we have a small NodeJS program. It is a bit like wc. It counts words in a file which contain the letter e.

Our program accepts one command line argument - the path of the file to read and count.

Our program’s output to stdout is just the number of words which contain an e.

Our program uses the same language (JavaScript) as we’ve written before, but uses some different APIs.

import process from "node:process";
import { promises as fs } from "node:fs";

const argv = process.argv.slice(2);
if (argv.length != 1) {
    console.error(`Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`);
    process.exit(1);
}
const path = argv[0];

const content = await fs.readFile(path, "utf-8");
const countOfWordsContainingEs = content
  .split(" ")
  .filter((word) => word.includes("e"))
  .length;
console.log(countOfWordsContainingEs);

Let’s play computer with this program - line by line:

import process from "node:process";

This import is loading some code from somewhere that isn’t this file.

We’ve seen import before. Here, instead of importing from a file we’ve written, we’re importing the process module which is built into NodeJS.

This is an example of the same language features (import) being used slightly differently (the "node:" is a special prefix to say “specially from node”).

The process module is built into NodeJS for managing our process 🧢 🧢 Process The running instance of our program: the code, state, memory, and system resources. . We can use it to find out what arguments were passed to the process when it started, find out what user ran the process, exit the process, and more.

import { promises as fs } from "node:fs";

We’re importing another module.

The fs module is built into NodeJS for interacting with the filesystem.

This time, we’re not importing the whole module. We are destructuring 🧢 🧢 Destructuring Destructuring is a form of variable assignment where we give variables values based on where we can find them structurally in another value. Examples:

We can write const [first, second] = [3, 1]; to assign first = 3 and second = 1.

We can write const {name, age} = {name: "Amir", age: 34}; to assign name = "Amir" and age = 34.
. The node:fs module exposes an object, and we are saying “import the promises property from the fs module, and bind it to the name fs”.

It is like writing import { promises } from "node:fs"; const fs = promises;.

The fs module exposes two alternate APIs with functions with the same name (like readFile):

  • The default fs module was written before async/await was added to JavaScript, and requires using callbacks, which can be annoying.
  • fs has a submodule called promises which can be used with async/await, and is generally much more convenient.

We want to use the promises submodule, because it’s much more convenient for us to use async/await. But if we just wrote import { promises } from "node:fs";, we’d be binding the submodule to the name promises, and everywhere we used it we’d need to write promises.readFile. This is less clear than fs.readFile, because promises is a very general name. So we rename promises to fs, and can use it like fs.readFile.

We are really doing this because we wish the async/await APIs were the default APIs exposed by the fs module, and this lets us pretend that they are in the rest of our code.

🧠 Think

fs uses callbacks or promises because its operations are asynchronous.

Why would interacting with the filesystem (e.g. reading a file) be an asynchronous operation?

Explain on a Slack thread why you think this is. If you’re not sure, ask about it on Slack.

const argv = process.argv.slice(2);

We’re getting the argv array from the process module, and slicing it. We can see in the process.argv documentation that process.argv[0] will be the path to node, and process.argv[1] will be the path to this file. We don’t care about those, so we’ll skip them - as far as we’re concerned the arguments start at index 2.

Again, Array.slice is exactly the same as we know from JavaScript, but process.argv is a new API we can use to get the array we need.

Play computer with the rest of the program - read each line, and explain what you think that line does. After you make your predictions, expand the explanations below and compare them to your predictions.

if (argv.length != 1) {
    console.error(`Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`);
    process.exit(1);
}
We always expect our program to be given exactly one argument. Here we check this using an `if` statement, just like we've seen before.

console.error writes a message to stderr (which is where error messages should go).

process.exit is a function which, when called, will stop our program running. Passing a non-zero number to it indicates that our program did not succeed. We can read more about it in the official NodeJS documentation for the process module.

const path = argv[0];

Giving a useful name to our argument.

const content = await fs.readFile(path, "utf-8");

Reading the file at the path passed as an argument. We’re using the fs module here from node, but everything else is just JavaScript - declaring a variable, using await because fs.promises.readFile is an async function, calling a function.

You can read more about this in the documentation for fs.promises.readFile.

const countOfWordsContainingEs = content
  .split(" ")
  .filter((word) => word.includes("e"))
  .length;

Just some regular JavaScript. Taking a string, splitting it into an array, filtering the array, searching strings to see if they contain any e characters, and getting the length of an array.

console.log(countOfWordsContainingEs);

console.log in a NodeJS environment logs to stdout, so this outputs our result to stdout.

Exercise

Save the above program into a file. Run the file with node, and count how many words contain “e"s in a few different files.

If you run into problems, ask for help.

πŸ“š Using dependencies from npm

Learning Objectives

We’ve seen that we can use code that was built into NodeJS - we don’t need to write everything ourselves.

We can also use code that other people have written, which isn’t built into NodeJS. You’ve probably seen this before, e.g. using jest for testing.

This can be really useful - it means we can benefit from work others have already done, and focus on just solving the part of a problem which is unique to us. It’s like making shell pipelines - instead of having to solve every problem from scratch, we can plug together different tools that other people have already made.

Let’s expand the functionality of our program. Rather than always searching for words containing the letter e, let’s allow the user to specify what character they’re searching for.

This means we want to introduce a flag. And programs that accept flags, should also document themselves. One common convention is that if you run a program with the flag --help, it will tell you how to use it.

But writing all of this code to parse flags, to output information about the flags, and so on, is a lot of work.

So let’s use a library 🧢 🧢 library A library is a collection of code that we can use, but which isn’t part of our project. for this. We will use a library called commander.

Exercise

Add import { program } from "commander"; to the top of your e-word-counting program.

This line imports the program property from the object which is the commander library (using object destructuring).

Try running your program. What happens? What does the output mean?

πŸ“š Installing dependencies with npm

Learning Objectives

To use a library, we need to fetch the code we’re going to use. When using NodeJS, we use a tool called npm for this.

First we need a package.json file - this a file that npm will read to understand your project. This is the same as the package.json file you’ve seen when using npm in the past.

Make this package.json file in the same directory as your e-word-counting program:

{
    "type": "module"
}

The package.json contains a JSON object with information about your project. For now, we’re just telling npm that our project is a module - this means we are allowed to use import in our program.

From a terminal which is cd’d to the same directory as your package.json file, run npm install commander.

This command does two things:

  1. Look in your package.json file - notice that now has a dependencies section listing commander. This means that if someone else downloads your program, they know they need to install commander to use it.
  2. There’s now a node_modules directory alongside your package.json. Inside that is a directory named commander which contains the code for the commander library. This means node now knows how to find the code when you try to import it.

Exercise

Try running your program again.

What has changed since the last time you tried to run it (and it didn’t work)?

What has changed since the last time you successfully ran it?

Now that we have commander installed, let’s try using it in our program:

import { program } from "commander";
import { promises as fs } from "node:fs";
import process from "node:process";

program
    .name("count-containing-words")
    .description("Counts words in a file that contain a particular character")
    .option("-c, --char <char>", "The character to search for", "e");

program.parse();

const argv = program.args;
if (argv.length != 1) {
    console.error(`Expected exactly 1 argument (a path) to be passed but got ${argv.length}.`);
    process.exit(1);
}
const path = argv[0];
const char = program.opts().char;

const content = await fs.readFile(path, "utf-8");
const countOfWordsContainingChar = content
  .split(" ")
  .filter((word) => word.includes(char))
  .length;
console.log(countOfWordsContainingChar);

Exercise

Try running this program with the --help flag.

What do you see? Where do you think this behaviour and text came from?

We didn’t have to write all the code for this functionality - commander did most of it for us.

Exercise

Try running the program with different values of the -c flag. Try also specifying some other flags, like --count.

Make sure you understand how it’s behaving, and why.

Let’s run through what we changed:

program
    .name("count-containing-words")
    .description("Counts words in a file that contain a particular character")
    .option("-c, --char <char>", "The character to search for", "e");

We told commander information about our program. We gave it a name, a description, and told it that it should allow a user to pass a flag name -c (or equivalently --char), and use a default value of - for that flag if it’s not specified.

program.parse();

We asked commander to interpret the command line arguments our program was given, based on what options we wanted to allow. If it sees something it doesn’t understand, it will error.

const argv = program.args;

Instead of asking NodeJS’s process module for all of the program’s arguments, we’re asking commander to tell us “after you understood and removed all the flags, what arguments were left?”

Then our if check about the number of arguments is exactly the same as before.

const char = program.opts().char;

We are getting the char flag that commander interpreted and storing it in a variable.

const countOfWordsContainingChar = content
  .split(" ")
  .filter((word) => word.includes(char))
  .length;
console.log(countOfWordsContainingChar);

We have renamed our countOfWordsContainingEs variable to countOfWordsContainingChar because we’re no longer always looking for hyphens, and changed the includes call to look for the value of the char variable instead of always an e.

We only needed to make a few small changes to get all of this new functionality:

  • Support for accepting a new command line flag.
  • --help support explaining how to use the program.
  • Detection for if someone passes flags that aren’t known, and warning them about this (and even suggesting what they maybe meant).

We could have written all of this code ourselves. But using a library meant we could focus on what’s unique about our problem, rather than spending time implementing flag parsing.

This is a very common task in software development in the real world, joining together libraries (written by other people) to create some new unique solution.

πŸ“Note

We also could have used the builtin util.parseArgs function from NodeJS for most of this functionality, but it doesn’t support --help like commander does.

πŸ’» Operating systems

Learning Objectives

Reading

Read chapter 10 of How Computers Really Work.

Do every exercise listed in the chapters.

You only need to do the projects listed below (though are welcome to try any others that you want!)

Check you have achieved each learning objective listed on this page.

Project

Do project 23 from How Computers Really Work.

You can do this on any Unix-family OS 🧢 🧢 Unix-family OS Linux and macOS are both Unix-family operating systems. Windows is not.

  • you do not need a Raspberry Pi.

Project

Do project 20 from How Computers Really Work.

You can do this on any Unix-family OS - you don’t need a Raspberry Pi.

Note: If you’re on macOS, ps -eH doesn’t exist. You can use ps or ps aux to get a list of processes. To get parent-child relationships, you’ll need to install pstree using brew (brew install pstree), then run pstree.

Note: If you’re on macOS, process 1 will probably be launchd not init.

Project

If you’re on a Linux machine, do projects 21, 22, and 24.

If you’re on macOS, pair up with someone who has a Linux machine to do these projects.

Note: Several of these projects may not work inside Docker or virtual machines, you need to actually be using Linux.