Chapter 5
Input: Letting the Reader Talk Back
Reading a line from the terminal, no async required.
Every program you’ve written types its own answers. Chapter 4 gave you
strings to work with, but you were the one who wrote "Ada Lovelace" into
the file. Let’s hand that job to whoever is running the program.
Borrowing a tool from Node
Reading from the keyboard isn’t something console.log does. It lives in a
different part of Node, one built for reading and writing files, because to
a program, text arriving from the keyboard and text arriving from a file
are close enough to the same thing:
const fs = require("fs");
require pulls in a piece of Node you haven’t asked for yet, here, the
file system module. It’s a genuinely different piece of syntax from
anything in this book so far, and it comes with a small toolchain step
before it’ll compile.
Reading everything at once
/// <reference types="node" />
const fs = require("fs");
const input = fs.readFileSync(0, "utf-8").trim();
console.log(`You typed: ${input}`);
0 is the keyboard, the same way 1 would mean the screen if you were
writing to it this way. .trim() is there because whatever you type
arrives with the newline from pressing return still attached, print that
raw and you get a blank line sitting in your output, so stripping it off
is a habit worth starting on day one.
Compile that, then run it and type something. What appears first is your own typing, echoed back by the terminal itself, nothing to do with the program yet:
npx tsc listen.ts && node listen.js
Ada
You typed: Ada
That first Ada is you. Nothing the program actually printed shows up
until you’ve told it there’s no more coming, readFileSync doesn’t return
after one line, it keeps waiting.
Piping input in sidesteps this entirely, which is how every example in this book gets verified, and it’s worth knowing for testing your own programs too:
printf 'Ada\n' | node listen.js
You typed: Ada
printf writes Ada\n and then closes its end of the pipe the moment it’s
done, which is end-of-file. Node never has to wait, because the signal
already arrived.
Asking more than one question
Here’s the part that doesn’t work the way you’d guess. Try reading twice:
/// <reference types="node" />
const fs = require("fs");
const first = fs.readFileSync(0, "utf-8");
console.log(`first: ${first}`);
const second = fs.readFileSync(0, "utf-8");
console.log(`second: ${second}`);
printf 'one\ntwo\n' | node twice.js
first: one
two
second:
The first call took everything, both lines, the whole input, right up to the end-of-file signal. By the time the second call runs, there’s nothing left, so it gets an empty string back immediately.
readFileSync isn’t “read the next bit that’s available.” It’s “read
until the input is completely finished,” every single time you call it.
Call it twice and the second call is reading from a well that’s already
dry.
So the real pattern for more than one answer is: ask every question you have, then read everything in one call, then pull the answers apart yourself.
Reading several answers out of one call
console.log("A name:");
console.log("How many?");
const lines = fs.readFileSync(0, "utf-8").split("\n");
const answerName = lines[0];
const answerCount = lines[1];
console.log(`${answerName}, ${answerCount}`);
printf 'Ada Lovelace\n12\n' | node twoQuestions.js
A name:
How many?
Ada Lovelace, 12
.split("\n") breaks one long string into an array wherever a newline
sits, chapter 10 covers arrays properly, for now, think of it as a numbered
row of strings you reach into with [0], [1], and so on, same square
brackets you already used on a string in chapter 4.
Numbers arrive as text
Every answer out of readFileSync is a string, always, even one that’s
nothing but digits:
const count = lines[1];
console.log(typeof count);
string
To do arithmetic with it, convert it on purpose:
const count = Number(lines[1]);
console.log(typeof count);
console.log(count + 1);
number
13
Number() turns a string into, well, a number. Feed it something that
isn’t one and it doesn’t stop your program, it hands back a special value:
console.log(Number("banana"));
NaN
NaN means “not a number”, and it happily prints right through a template
literal without complaint, `You typed: ${Number("banana")}` reads
You typed: NaN, which looks like an answer but isn’t one. Actually
noticing a bad NaN and doing something about it needs if, chapter 7,
and chapter 19 comes back and handles input properly. For now, just
recognise it on sight.
Exercise 1 · Break it on purpose
Write a program that reads one line and prints Number() of it plus 1.
Feed it 41 (you should get 42) and then feed it hello (you should
get NaN). Watch it happen once, so the shape is familiar the next time
a number comes out wrong from somewhere you didn’t expect.
Check yourself
Project
Mad-libs
Roughly 30 minutes
Ask for a few words without telling the reader what they’re for, then print the result. The comedy is in the gap between the two.
Print all your questions first, then read everything in one call:
A name:
How many?
A plural noun:Collect at least:
- a name (call the variable something other than
name, chapter 3’s gotcha about the global of that name is still in effect) - a number, converted with
Number() - a plural noun
Then print a short story with all of them in it, something like:
Ada Lovelace woke up and found 12 rubber ducks in the kitchen.Rules: exactly one call to fs.readFileSync, everything else comes
from .split("\n") and indexing into what it gives you. Test it with
printf, the way this chapter did:
printf 'Ada Lovelace\n12\nrubber ducks\n' | node story.jsWatch for the trap on purpose. Compile and run it, then type your three answers by hand instead of piping them in, and forget to press Ctrl-D. Watch it hang. That’s not a bug you introduced, it’s the program correctly waiting for a signal you haven’t sent. Send it and watch the story print.
Stretch: ask for two more words and make the story longer. Notice every question is the same two lines, print a prompt, read a line, with a different piece of text. That repetition is the itch chapter 9 scratches.