Contents

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

1. A program calls fs.readFileSync(0, "utf-8") twice in a row. What does the second call return?

Not quite. The first call already consumed everything up to end-of-file. There is nothing left by the time the second call runs.

Yes. readFileSync reads until end-of-file every time it is called. The first call already reached it, so the second finds nothing.

Not quite. Reading is not repeatable like that. Once the input is consumed, it is gone.

2. You run a program that calls readFileSync(0), type an answer, and press return. Nothing happens. Why?

Not quite. It has not crashed. It is still running, and still waiting.

Yes. readFileSync keeps reading until it gets an explicit end-of-file signal, Ctrl-D on Mac and Linux, Ctrl-Z then return on Windows.

Not quite. Nothing is broken. Sending the end-of-file signal the program is actually waiting for finishes it normally.

3. lines[1] holds "banana" and you write Number(lines[1]). What do you get?

Not quite. Number() never throws. It hands back a value instead, one that is worth recognising.

Not quite. TypeScript does not invent a fallback number. It tells you the conversion did not work instead.

Yes. "Not a number." It prints right through a template literal, which is exactly why it is worth learning to spot.

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.js

Watch 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.