Zum Inhalt springen
aviral gupta

// B5.2 · ca. 32 Min. · Einstieg

Eingaben lesen mit readline

Nach dieser Lektion können Sie mit readline/promises Fragen stellen, jede Eingabezeile bis zum Ende lesen, Antworten in einer Schleife prüfen und einem Programm Eingaben per Pipe oder Datei geben.

Lektion 2 von 5 in B5 Ereignisse, Eingaben und erste Tests

Danach können Sie

  • Mit rl.question eine Frage stellen und das Interface schließen, damit das Programm enden kann
  • Jede Eingabezeile mit for await lesen und wissen, wann und wie die Eingabe endet
  • Antworten in einer Schleife prüfen und einem Programm die Eingabe per Pipe oder Datei geben
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus B5.1: Ein readline-Interface ist ein EventEmitter. Die Eingabe sind die zwei Zeilen a und b, per Pipe. Was gibt das Programm aus?

    import {createInterface} from 'node:readline/promises';
    
    const rl = createInterface({input: process.stdin});
    rl.on('line', (line) => console.log('line', line));
    rl.on('close', () => console.log('closed'));
    console.log('ready');
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen. Sie starten dieses Programm im Terminal, tippen Ada und drücken Enter. Was passiert?

    import {createInterface} from 'node:readline/promises';
    
    const rl = createInterface({input: process.stdin, output: process.stdout});
    const name = await rl.question('Name? ');
    console.log('Hi ' + name + '!');
  3. Üben · Aufgabe 3 von 7

    rl stammt aus node:readline/promises. Ergänzen Sie das Schlüsselwort, damit name die eingegebene Zeile ist und kein Promise.

    const name = ____ rl.question('Name? ');
    const name = rl.question('Name? ');
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Stück readline-Code zu, was es tut.

  5. Üben · Aufgabe 5 von 7

    Die Eingabe ist x, eine leere Zeile, y und last, ohne Zeilenumbruch nach last: printf 'x\n\ny\nlast' | node main.js. Was gibt das Programm aus?

    import {createInterface} from 'node:readline/promises';
    
    const rl = createInterface({input: process.stdin});
    let count = 0;
    for await (const line of rl) count++;
    console.log(count + ' lines');
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Wenn ein Mensch tippt, funktioniert dieses Programm. Jetzt kommen die zwei Antworten per Pipe: printf '3\n4\n' | node main.js. Was passiert?

    import {createInterface} from 'node:readline/promises';
    
    const rl = createInterface({input: process.stdin, output: process.stdout});
    const first = await rl.question('First? ');
    console.log('got ' + first);
    const second = await rl.question('Second? ');
    console.log('got ' + second);
    rl.close();
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie eine main.js, die mit rl.question 'Temperature in °C? ' fragt, das Interface schließt und die Temperatur in °F ausgibt (°C × 9 / 5 + 32). Bei einer Antwort, die keine Zahl ist, schreiben Sie eine Meldung nach stderr und setzen Exit-Code 1 (B1.4). Starten Sie es auf drei Arten: Antwort tippen, dann echo 21 | node main.js, dann node main.js < temp.txt mit einer Datei temp.txt, die 21 enthält.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Fragen, bis die Antwort gültig ist

Das Programm fragt, wie viele Tickets gekauft werden sollen, und fragt weiter, bis die Antwort eine ganze Zahl von 1 bis 10 ist. parseTickets übernimmt die Prüfung und weiß nichts von readline. Die Schleife liest eine Zeile pro Prompt; eine gültige Antwort beendet sie mit break. Hier kommen die Antworten abc, 0 und 4 per Pipe. Weil die Eingabe kein Terminal ist, werden die Antworten nicht angezeigt, also folgt die Rückmeldung jeweils in derselben Zeile wie ihr Prompt.

main.js

import {createInterface} from 'node:readline/promises';

// Returns the number of tickets, or null when the answer is not 1 to 10.
function parseTickets(answer) {
  const text = answer.trim();
  const n = Number(text);
  return text !== '' && Number.isInteger(n) && n >= 1 && n <= 10 ? n : null;
}

const rl = createInterface({input: process.stdin, output: process.stdout});
rl.setPrompt('Tickets (1-10)? ');
rl.prompt();

let tickets = null;
for await (const line of rl) {
  tickets = parseTickets(line);
  if (tickets !== null) break; // leaving the loop closes rl
  console.log('"' + line + '" is not a whole number from 1 to 10');
  rl.prompt(); // ask again
}

if (tickets === null) console.log('The input ended without a valid answer.');
else console.log(tickets + ' tickets cost ' + tickets * 12 + ' euros');

input.txt

abc
0
4

Ausführen mit

node main.js

Ausgabe

Tickets (1-10)? "abc" is not a whole number from 1 to 10
Tickets (1-10)? "0" is not a whole number from 1 to 10
Tickets (1-10)? 4 tickets cost 48 euros
  • rl.prompt() schrieb Tickets (1-10)? vor jede Antwort; es hängt keinen Zeilenumbruch an.
  • Jede ungültige Antwort bekam eine Meldung und einen neuen Prompt; die Schleife lief weiter.
  • Die gültige Antwort 4 beendete die Schleife mit break, was auch das Interface schloss, also endete das Programm.
  • Wäre die Eingabe ohne gültige Antwort zu Ende gegangen, hätte die Schleife ebenfalls geendet, mit tickets noch null.

Übungen

Übung 1 von 2

Eine Antwort prüfen

Schreiben Sie den prüfenden Teil einer Frageschleife; er läuft auch im Browser. parseNumber(answer, min, max) entfernt Leerraum am Rand. Leer: {error: 'Please type a number'}. Keine ganze Zahl (nur ein optionales Minus und Ziffern, also nicht 2.5 oder 1e1): {error: '"2.5" is not a whole number'}, mit der bereinigten Antwort in Anführungszeichen. Unter min: {error: 'The smallest is 1'}; über max: {error: 'The largest is 10'}, mit den echten Grenzen. Sonst {value: n}, eine Zahl.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Beginnen Sie mit const text = answer.trim(); und geben Sie {error: 'Please type a number'} zurück, wenn text '' ist.

  2. Hinweis 2

    Prüfen Sie die Form mit einem regulären Ausdruck: /^-?[0-9]+$/.test(text). Number allein würde 2.5, 1e1 und 0x10 akzeptieren.

  3. Hinweis 3

    Dann const n = Number(text); vergleichen Sie mit min und max und geben Sie zuletzt {value: n} zurück.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

// Checks one typed answer: a whole number from min to max.
// Returns {value: n} or {error: text}.
export function parseNumber(answer, min, max) {
  const text = answer.trim();
  if (text === '') return {error: 'Please type a number'};
  if (!/^-?[0-9]+$/.test(text)) return {error: '"' + text + '" is not a whole number'};
  const n = Number(text);
  if (n < min) return {error: 'The smallest is ' + min};
  if (n > max) return {error: 'The largest is ' + max};
  return {value: n};
}

console.log(parseNumber(' 4 ', 1, 10));
console.log(parseNumber('2.5', 1, 10));
console.log(parseNumber('11', 1, 10));
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

// Checks one typed answer: a whole number from min to max.
// Returns {value: n} or {error: text}.
export function parseNumber(answer, min, max) {
  return {value: Number(answer)};
}

console.log(parseNumber(' 4 ', 1, 10));
console.log(parseNumber('2.5', 1, 10));
console.log(parseNumber('11', 1, 10));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parseNumber} from './main.js';

test('eine ganze Zahl im Bereich ergibt ihren Wert, Leerzeichen entfernt', () => {
  assert.deepEqual(parseNumber(' 4 ', 1, 10), {value: 4}, `parseNumber(' 4 ', 1, 10) ergab ${JSON.stringify(parseNumber(' 4 ', 1, 10))}`);
  assert.deepEqual(parseNumber('-3', -5, 5), {value: -3}, `parseNumber('-3', -5, 5) ergab ${JSON.stringify(parseNumber('-3', -5, 5))}`);
});

test('eine leere Antwort bittet um eine Zahl', () => {
  assert.deepEqual(parseNumber('   ', 1, 10), {error: 'Please type a number'}, `parseNumber('   ', 1, 10) ergab ${JSON.stringify(parseNumber('   ', 1, 10))}`);
});

test('Text, Dezimalzahlen und Exponenten sind keine ganzen Zahlen', () => {
  for (const answer of ['abc', '2.5', '1e1', '4 tickets']) {
    const got = parseNumber(answer, 1, 10);
    assert.deepEqual(got, {error: `"${answer}" is not a whole number`}, `parseNumber('${answer}', 1, 10) ergab ${JSON.stringify(got)}`);
  }
});

test('Zahlen außerhalb des Bereichs nennen die Grenze', () => {
  assert.deepEqual(parseNumber('0', 1, 10), {error: 'The smallest is 1'}, `parseNumber('0', 1, 10) ergab ${JSON.stringify(parseNumber('0', 1, 10))}`);
  assert.deepEqual(parseNumber('11', 1, 10), {error: 'The largest is 10'}, `parseNumber('11', 1, 10) ergab ${JSON.stringify(parseNumber('11', 1, 10))}`);
  assert.deepEqual(parseNumber('120', 0, 99), {error: 'The largest is 99'}, `parseNumber('120', 0, 99) ergab ${JSON.stringify(parseNumber('120', 0, 99))}`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 2

Eine Einkaufsliste von der Standardeingabe

Lesen Sie eine Einkaufsliste von der Standardeingabe, ein Eintrag pro Zeile, mit for await. Entfernen Sie Leerraum am Rand jeder Zeile und überspringen Sie leere. Eine Zeile end beendet das Lesen; spätere Zeilen werden ignoriert. Geben Sie dann die Einträge nummeriert aus, 1. milk, 2. bread, und zuletzt N items (0 items, wenn keine). Probieren Sie printf 'milk\n\n bread \nend\ntea\n' | node main.js, dann node --test.

Diese Übung braucht Node.js auf Ihrem Computer (die Browser-Version kann sie nicht ausführen). Dateien und Befehle stehen unten.

Hinweise
  1. Hinweis 1

    In der Schleife: const item = line.trim(); dann if (item === "end") break; und nur nicht leere Einträge mit push aufnehmen.

  2. Hinweis 2

    Nach der Schleife nummeriert items.forEach((item, i) => console.log(i + 1 + '. ' + item)); die Liste ab 1.

  3. Hinweis 3

    Geben Sie zuletzt items.length + ' items' aus; ohne Einträge gibt forEach nichts aus, und Sie erhalten 0 items.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import {createInterface} from 'node:readline/promises';

const rl = createInterface({input: process.stdin});
const items = [];
for await (const line of rl) {
  const item = line.trim();
  if (item === 'end') break;
  if (item !== '') items.push(item);
}

items.forEach((item, i) => console.log(i + 1 + '. ' + item));
console.log(items.length + ' items');
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

import {createInterface} from 'node:readline/promises';

const rl = createInterface({input: process.stdin});
const items = [];
for await (const line of rl) {
  items.push(line);
}

items.forEach((item) => console.log(item));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';

test('Einträge werden bereinigt und nummeriert, leere Zeilen übersprungen', async () => {
  const printed = await runMain({stdin: 'milk\n\n bread \n'});
  assert.equal(printed, '1. milk\n2. bread\n2 items\n', `für milk, eine leere Zeile und " bread " gab das Programm aus:\n${printed}`);
});

test('eine Zeile end beendet das Lesen', async () => {
  const printed = await runMain({stdin: 'milk\nend\ntea\n'});
  assert.equal(printed, '1. milk\n1 items\n', `für milk, end, tea gab das Programm aus:\n${printed}`);
});

test('die letzte Zeile zählt auch ohne Zeilenumbruch', async () => {
  const printed = await runMain({stdin: 'eggs\nflour'});
  assert.equal(printed, '1. eggs\n2. flour\n2 items\n', `für eggs und flour ohne abschließenden Zeilenumbruch gab das Programm aus:\n${printed}`);
});

test('ohne Einträge wird 0 items ausgegeben', async () => {
  const printed = await runMain({stdin: ''});
  assert.equal(printed, '0 items\n', `für eine leere Eingabe gab das Programm aus:\n${printed}`);
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

Eine Frage nach der for-await-Schleife stellen

import {createInterface} from 'node:readline/promises';

const rl = createInterface({input: process.stdin, output: process.stdout});
let total = 0;
for await (const line of rl) total += Number(line);
const again = await rl.question('Add more? ');
console.log(total, again);

Was Node.js ausgibt

Error [ERR_USE_AFTER_CLOSE]: readline was closed

Warum, und die Lösung

Eine for-await-Schleife über rl endet erst, wenn die Eingabe zu Ende ist, und dann ist das Interface geschlossen. Eine Frage danach lehnt ab, also stürzt das Programm ab. Stellen Sie Ihre Fragen vor der Schleife, oder behandeln Sie die zusätzliche Antwort als weitere Zeile in der Schleife, etwa eine Zeile done, die sie mit break beendet.

stdin benutzen, ohne es zu importieren

import {createInterface} from 'node:readline/promises';

const rl = createInterface({input: stdin, output: stdout});
const name = await rl.question('Name? ');
console.log('Hi ' + name + '!');
rl.close();

Was Node.js ausgibt

ReferenceError: stdin is not defined

Warum, und die Lösung

Die readline-Doku schreibt {input: stdin, output: stdout} nach import {stdin, stdout} from 'node:process'. Kopieren Sie nur die Zeile mit createInterface, gibt es diese Namen nicht. Fügen Sie entweder den Import hinzu oder schreiben Sie process.stdin und process.stdout: process ist global, stdin nicht.

Zwei Fragen an Eingaben aus einer Pipe stellen

// $ printf 'Ada\nBerlin\n' | node main.js
import {createInterface} from 'node:readline/promises';

const rl = createInterface({input: process.stdin, output: process.stdout});
const name = await rl.question('Name? ');
const city = await rl.question('City? ');
console.log(name + ' lives in ' + city);
rl.close();

Was Node.js ausgibt

Warning: Detected unsettled top-level await

Warum, und die Lösung

Eingaben aus einer Pipe (und aus einer Datei) kommen auf einmal. Die erste Frage nimmt ihre Zeile; die nächste wird gelesen, bevor die zweite Frage gestellt ist, und geht verloren. Die zweite Frage wartet auf eine Zeile, die nie kommt, die Eingabe ist zu Ende, und Node.js bricht mit Exit-Code 13 ab. Ganz ohne Eingabe passiert das schon bei der ersten Frage. Für mehrere Antworten lesen Sie die Zeilen mit einer for-await-Schleife.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Fragen, abwarten, schließen

Importieren Sie createInterface aus 'node:readline/promises' und erzeugen Sie rl = createInterface({input: process.stdin, output: process.stdout}). await rl.question('Name? ') schreibt die Frage in die Ausgabe und erfüllt sich mit der nächsten eingegebenen Zeile, ohne Zeilenumbruch. Die Antwort ist immer ein String: Wandeln Sie sie selbst mit Number um. Solange das Interface offen ist, wartet es auf weitere Eingaben; im Terminal endet das Programm also nicht, auch wenn seine letzte Zeile gelaufen ist. rl.close() gibt die Eingabe frei, dann kann das Programm enden. Eine Frage nach rl.close() lehnt mit ERR_USE_AFTER_CLOSE ab. node:readline ohne /promises enthält die ältere Callback-Version von question.

Jede Zeile, bis die Eingabe endet

for await (const line of rl) liefert Ihnen eine Zeile nach der anderen, leere Zeilen eingeschlossen, und die letzte auch ohne Zeilenumbruch am Ende. Die Schleife endet, wenn die Eingabe endet: wenn Sie im Terminal Strg+D (Ctrl+D) drücken oder wenn eine Pipe oder Datei keine Daten mehr hat. Dann emittiert das Interface sein close-Event; rl ist ein EventEmitter wie in B5.1, mit den Events line und close. break verlässt die Schleife vorzeitig und schließt das Interface. Beginnen Sie die Schleife direkt nach createInterface: Zeilen, die ankommen, während das Programm auf etwas anderes wartet, etwa eine Frage, gehen verloren.

In einer Schleife prüfen, Eingaben hineinleiten

Menschen tippen alles Mögliche. Legen Sie die Prüfung in eine kleine Funktion, die einen Wert oder null liefert, und arbeiten Sie in einer Schleife: rl.setPrompt('Age? ') und rl.prompt() schreiben den Prompt, die for-await-Schleife holt die Antwort, eine gültige beendet die Schleife mit break, eine ungültige bekommt eine Meldung und ein neues rl.prompt(). Eingaben müssen nicht getippt sein: echo 3 | node main.js oder node main.js < answers.txt verbindet stdin mit einer Pipe oder Datei; in process.argv kommt nichts an. Per Pipe kommt alles auf einmal, also findet ein zweites rl.question seine Zeile schon verbraucht vor; Node.js bricht dann mit Warning: Detected unsettled top-level await ab (Exit-Code 13). Mehrere Antworten: mit einer for-await-Schleife lesen.

Quellen

Zuletzt geprüft am 4. Oktober 2026