Zum Inhalt springen
aviral gupta

// B1.5 · ca. 45 Min. · Einstieg

Build: ein Trinkgeldrechner für die Kommandozeile

Nach diesem Build schreiben Sie ein Kommandozeilenwerkzeug, das Argumente und Konfiguration liest, Ergebnisse auf stdout ausgibt, Fehler auf stderr mit einem aussagekräftigen Exit-Code meldet und aus package.json-Skripten startet.

Lektion 5 von 5 in B1 Node ausführen und Projektgrundlagen

Ende des Moduls

Danach können Sie

  • Ein Werkzeug in reine Funktionen und einen dünnen Einstiegspunkt aufteilen, der process.argv und process.env liest
  • Argumente mit util.parseArgs auswerten und Fehler auf stderr mit Exit-Code 2 oder 1 beantworten
  • Das Werkzeug über die Skripte "start" und "dev" mit --env-file-if-exists starten und vorhersagen, welche Einstellung gewinnt
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen mit Modul B1: Welche Aussagen stimmen? Wählen Sie alle zutreffenden.

    Wählen Sie alle zutreffenden aus.

  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es vorher, bevor Sie weiterlesen. Das fertige Werkzeug liest sein voreingestelltes Trinkgeld aus TIP_PERCENT. In der Shell ist TIP_PERCENT=20 gesetzt, und in .env steht TIP_PERCENT=10. Sie führen den Befehl unten aus. Was ist die erste Zeile, die er ausgibt?

    node --env-file=.env main.js 60 --tip 15
  3. Üben · Aufgabe 3 von 7

    Das Projekt hat noch keine .env-Datei, und das Skript "start" muss mit und ohne eine funktionieren. Setzen Sie das Flag ein.

    {
      "type": "module",
      "scripts": {
        "start": "node ____=.env main.js"
      }
    }
    "start": "node =.env main.js"
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jeder Kommandozeile des fertigen Werkzeugs zu, was sie bewirkt.

  5. Üben · Aufgabe 5 von 7

    Was ist die letzte Zeile, die dieser Befehl ausgibt?

    node main.js 100 -t 10 -p 3
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. run() liefert für eine unbekannte Option Exit-Code 2, und node --env-file-if-exists=.env main.js 60 --colour endet tatsächlich mit 2. Was gibt echo $? nach derselben Kommandozeile über das Skript "start" aus?

    node --run start -- 60 --colour
    echo $?
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Geben Sie dem fertigen Werkzeug eine Währung. Beschreiben Sie in run() --currency (-c), einen String, dessen Default CURRENCY aus der Umgebung ist, sonst EUR. Geben Sie sie nach jedem Betrag aus: tip: 9.00 EUR. Schreiben Sie CURRENCY=USD in .env und führen Sie node --run start -- 60 -p 3 aus, dann probieren Sie -c GBP. Starten Sie zuletzt node --run dev, ändern Sie den Default der Währung und speichern Sie: Der Beispielbefehl läuft erneut.

    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

Dieselbe Form, ein anderes Werkzeug: ein Wortzähler

Bevor Sie den Trinkgeldrechner bauen, hier ein fertiges Werkzeug mit derselben Form. node main.js the cat sat zählt die Wörter mit mindestens --min Buchstaben, und MIN_LETTERS aus der Umgebung setzt den Default. run(args, env) liefert {out, err, code}, statt auszugeben. Ein echter Einstiegspunkt endete mit vier Zeilen, die process.argv.slice(2) und process.env übergeben und das Ergebnis umsetzen; diese Demo ruft run() stattdessen mit vier Kommandozeilen auf, sodass Sie jedes Ergebnis auf einmal sehen.

main.js

import {parseArgs} from 'node:util';

const USAGE = 'Usage: node main.js [--min <letters>] <text...>';

// Counts the words of the text that have at least --min letters. Returns what
// to print on stdout and on stderr, and the exit code:
// 0 = done, 1 = a value it cannot use, 2 = wrong usage.
function run(args, env) {
  let values, positionals;
  try {
    ({values, positionals} = parseArgs({
      args,
      options: {min: {type: 'string', short: 'm', default: env.MIN_LETTERS ?? '1'}},
      allowPositionals: true
    }));
  } catch (error) {
    return {out: '', err: error.message + '\n' + USAGE + '\n', code: 2};
  }
  if (positionals.length === 0) return {out: '', err: USAGE + '\n', code: 2};
  const min = Number(values.min);
  if (!Number.isInteger(min) || min < 1) {
    return {out: '', err: 'error: --min must be a whole number from 1, got "' + values.min + '"\n', code: 1};
  }
  const words = positionals.join(' ').split(' ').filter((word) => word.length >= min);
  return {out: words.length + '\n', err: '', code: 0};
}

// A real entry point ends with four lines that touch the process:
//   const {out, err, code} = run(process.argv.slice(2), process.env);
//   process.stdout.write(out);
//   process.stderr.write(err);
//   process.exitCode = code;
// This demo calls run() with four command lines instead, and MIN_LETTERS=3
// in place of process.env, as if a .env file had set it.
const env = {MIN_LETTERS: '3'};
for (const args of [['the cat sat on a mat'], ['--min', '1', 'the cat sat on a mat'], ['-m', 'two', 'cat'], []]) {
  const {out, err, code} = run(args, env);
  console.log('args', args);
  console.log('  stdout ' + JSON.stringify(out) + ', stderr ' + JSON.stringify(err) + ', exit code ' + code);
}

Ausführen mit

node main.js

Ausgabe

args [ 'the cat sat on a mat' ]
  stdout "4\n", stderr "", exit code 0
args [ '--min', '1', 'the cat sat on a mat' ]
  stdout "6\n", stderr "", exit code 0
args [ '-m', 'two', 'cat' ]
  stdout "", stderr "error: --min must be a whole number from 1, got \"two\"\n", exit code 1
args []
  stdout "", stderr "Usage: node main.js [--min <letters>] <text...>\n", exit code 2
  • Mit MIN_LETTERS=3 als Default zählen nur the, cat, sat und mat: 4. --min 1 auf der Kommandozeile gewinnt, und alle 6 Wörter zählen.
  • -m two hat die richtige Form, aber einen unbrauchbaren Wert: eine Fehlerzeile und Exit-Code 1. Gar kein Text ist falsche Bedienung: die Usage-Zeile und 2.
  • stdout ist leer, sobald etwas schiefging, also speichert node main.js … > count.txt nie eine Meldung, als wäre sie ein Zählergebnis.
  • Die Klammern in ({values, positionals} = parseArgs(…)) sind nötig: Eine Zeile, die mit { beginnt, würde als Block gelesen.

Übungen

Übung 1 von 3

Schritt 1: die Rechnung aufteilen

Schreiben Sie splitBill({amount, tip, people}); alle drei sind Zahlen, und tip ist ein Prozentsatz. Geben Sie {tip, total, each} zurück: den Trinkgeldbetrag, den Betrag plus Trinkgeld und die Summe geteilt durch die Zahl der Personen, jeweils mit Math.round(x * 100) / 100 auf Cent gerundet. 60 mit 15 % Trinkgeld für 3 Personen ergibt also {tip: 9, total: 69, each: 23}. Die Funktion nutzt kein process und gibt nichts aus, deshalb läuft sie auch im Browser.

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

    Das Trinkgeld ist ein Prozentsatz: amount * tip / 100.

  2. Hinweis 2

    Jeder Anteil kommt aus der Summe, nicht aus dem Betrag: total / people.

  3. Hinweis 3

    Runden Sie alle drei mit einer Hilfsfunktion: const cents = (x) => Math.round(x * 100) / 100;

Eine Lösung zeigen

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

// Step 1: the arithmetic. amount, tip (a percentage) and people are numbers.
// Return the tip, the total and each share, rounded to cents.
export function splitBill({amount, tip, people}) {
  const cents = (x) => Math.round(x * 100) / 100;
  const tipAmount = cents((amount * tip) / 100);
  const total = cents(amount + tipAmount);
  return {tip: tipAmount, total, each: cents(total / people)};
}

console.log(splitBill({amount: 60, tip: 15, people: 3}));
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

// Step 1: the arithmetic. amount, tip (a percentage) and people are numbers.
// Return the tip, the total and each share, rounded to cents.
export function splitBill({amount, tip, people}) {
  return {tip: 0, total: amount, each: amount};
}

console.log(splitBill({amount: 60, tip: 15, people: 3}));

main.test.js

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

test('60 mit 15 % Trinkgeld für 3 Personen: tip 9, total 69, each 23', () => {
  const got = splitBill({amount: 60, tip: 15, people: 3});
  assert.deepEqual(got, {tip: 9, total: 69, each: 23}, `splitBill lieferte ${JSON.stringify(got)}`);
});

test('jeder Anteil wird auf Cent gerundet: 100 mit 10 % für 3 Personen ergibt je 36.67', () => {
  const got = splitBill({amount: 100, tip: 10, people: 3});
  assert.deepEqual(got, {tip: 10, total: 110, each: 36.67}, `splitBill lieferte ${JSON.stringify(got)}`);
});

test('das Trinkgeld wird auf Cent gerundet: 19.99 mit 15 % ergibt ein Trinkgeld von 3', () => {
  const got = splitBill({amount: 19.99, tip: 15, people: 1});
  assert.deepEqual(got, {tip: 3, total: 22.99, each: 22.99}, `splitBill lieferte ${JSON.stringify(got)}`);
});

test('0 % Trinkgeld: die Summe ist der Betrag', () => {
  const got = splitBill({amount: 80, tip: 0, people: 4});
  assert.deepEqual(got, {tip: 0, total: 80, each: 20}, `splitBill lieferte ${JSON.stringify(got)}`);
});

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 3

Schritt 2: die Werte prüfen

parseArgs liefert Strings. Schreiben Sie toNumbers({amount, tip, people}), das die drei Strings in Zahlen umwandelt und als {amount, tip, people} zurückgibt. Werfen Sie einen Error, der den ungültigen Wert nennt, wenn einer unbrauchbar ist: Der Betrag muss über 0 liegen, das Trinkgeld bei 0 oder mehr, und people muss eine ganze Zahl ab 1 sein. people "2.5" wirft zum Beispiel einen Error, dessen Meldung 2.5 enthält. Wie splitBill läuft es auch im Browser.

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

    Number("abc") ist NaN, und jeder Vergleich mit NaN ist false. Also ist !(n > 0) für NaN, 0 und -5 gleichermaßen true.

  2. Hinweis 2

    Number.isInteger(2.5) ist false; Number.isInteger(3) ist true.

  3. Hinweis 3

    Setzen Sie den ursprünglichen Text in die Meldung: 'people must be a whole number from 1, got "' + people + '"'.

Eine Lösung zeigen

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

// Step 2: the values from the command line are strings. Convert them, and
// throw an Error naming the bad value if one cannot be used:
// amount above 0, tip from 0, people a whole number from 1.
export function toNumbers({amount, tip, people}) {
  const bill = {amount: Number(amount), tip: Number(tip), people: Number(people)};
  if (!(bill.amount > 0)) throw new Error('amount must be a number above 0, got "' + amount + '"');
  if (!(bill.tip >= 0)) throw new Error('tip must be a number from 0, got "' + tip + '"');
  if (!Number.isInteger(bill.people) || bill.people < 1) throw new Error('people must be a whole number from 1, got "' + people + '"');
  return bill;
}

console.log(toNumbers({amount: "60", tip: "15", people: "3"}));
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

// Step 2: the values from the command line are strings. Convert them, and
// throw an Error naming the bad value if one cannot be used:
// amount above 0, tip from 0, people a whole number from 1.
export function toNumbers({amount, tip, people}) {
  return {amount: Number(amount), tip: Number(tip), people: Number(people)};
}

console.log(toNumbers({amount: "60", tip: "15", people: "3"}));

main.test.js

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

const ok = {amount: '60', tip: '15', people: '3'};

test('"60", "15" und "3" werden zu den Zahlen 60, 15 und 3', () => {
  const got = toNumbers(ok);
  assert.deepEqual(got, {amount: 60, tip: 15, people: 3}, `toNumbers lieferte ${JSON.stringify(got)}`);
});

test('ein Betrag, der nicht über 0 liegt, wirft: abc, 0, -5', () => {
  for (const amount of ['abc', '0', '-5']) {
    assert.throws(() => toNumbers({...ok, amount}), Error, `toNumbers mit amount "${amount}" sollte werfen`);
  }
});

test('ein Trinkgeld unter 0 oder keine Zahl wirft: -1, ten', () => {
  for (const tip of ['-1', 'ten']) {
    assert.throws(() => toNumbers({...ok, tip}), Error, `toNumbers mit tip "${tip}" sollte werfen`);
  }
});

test('people muss eine ganze Zahl ab 1 sein: 2.5 und 0 werfen', () => {
  for (const people of ['2.5', '0']) {
    assert.throws(() => toNumbers({...ok, people}), Error, `toNumbers mit people "${people}" sollte werfen`);
  }
});

test('die Meldung nennt den ungültigen Wert', () => {
  assert.throws(() => toNumbers({...ok, people: '2.5'}), {message: /2\.5/}, 'die Meldung für people "2.5" sollte 2.5 enthalten');
  assert.throws(() => toNumbers({...ok, amount: 'abc'}), {message: /abc/}, 'die Meldung für amount "abc" sollte abc enthalten');
});

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 3 von 3

Schritt 3: der Befehl

bill.js enthält jetzt die fertigen Schritte 1 und 2 und format. main.js funktioniert für node main.js 60 --people 3, scheitert aber still: Jede Meldung geht mit Exit-Code 0 auf stdout. Korrigieren Sie run(): Das voreingestellte Trinkgeld kommt aus env.TIP_PERCENT, sonst 15; --help (-h) liefert die Usage-Zeile auf stdout mit 0; ein parseArgs-Fehler oder ein fehlender Betrag liefert die Meldung und die Usage auf stderr mit 2; ein ungültiger Wert liefert error: und seine Meldung auf stderr mit 1. Prüfen Sie mit 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

    Der Default kann aus der Umgebung kommen: default: env.TIP_PERCENT ?? '15'. Er muss ein String sein, und Werte aus process.env sind es.

  2. Hinweis 2

    Verwenden Sie zwei try-Blöcke: einen um parseArgs (falsche Bedienung, 2) und einen um toNumbers (ein ungültiger Wert, 1). Dazwischen behandeln Sie values.help und positionals.length !== 1.

  3. Hinweis 3

    Deklarieren Sie let values, positionals; vor dem ersten try und weisen Sie in Klammern zu: ({values, positionals} = parseArgs({...}));

Eine Lösung zeigen

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

import {parseArgs} from 'node:util';
import {format, splitBill, toNumbers} from './bill.js';

const USAGE = 'Usage: node main.js <amount> [--tip <percent>] [--people <count>]';

// The whole command as a function of its arguments and environment. It returns
// what to print on stdout and on stderr, and the exit code:
// 0 = done, 1 = a value it cannot use, 2 = wrong usage.
function run(args, env) {
  let values, positionals;
  try {
    ({values, positionals} = parseArgs({
      args,
      options: {
        tip: {type: 'string', short: 't', default: env.TIP_PERCENT ?? '15'},
        people: {type: 'string', short: 'p', default: '1'},
        help: {type: 'boolean', short: 'h'}
      },
      allowPositionals: true
    }));
  } catch (error) {
    return {out: '', err: error.message + '\n' + USAGE + '\n', code: 2};
  }
  if (values.help) return {out: USAGE + '\n', err: '', code: 0};
  if (positionals.length !== 1) return {out: '', err: USAGE + '\n', code: 2};
  try {
    const bill = toNumbers({amount: positionals[0], tip: values.tip, people: values.people});
    return {out: format(splitBill(bill)), err: '', code: 0};
  } catch (error) {
    return {out: '', err: 'error: ' + error.message + '\n', code: 1};
  }
}

// The only lines that touch the real process.
const {out, err, code} = run(process.argv.slice(2), process.env);
process.stdout.write(out);
process.stderr.write(err);
process.exitCode = code;
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 {parseArgs} from 'node:util';
import {format, splitBill, toNumbers} from './bill.js';

const USAGE = 'Usage: node main.js <amount> [--tip <percent>] [--people <count>]';

// Returns what to print on stdout and on stderr, and the exit code:
// 0 = done, 1 = a value it cannot use, 2 = wrong usage.
// It works for 60 --people 3, but every mistake ends up on stdout with code 0.
function run(args, env) {
  try {
    const {values, positionals} = parseArgs({
      args,
      options: {
        tip: {type: 'string', short: 't', default: '15'},
        people: {type: 'string', short: 'p', default: '1'}
      },
      allowPositionals: true
    });
    const bill = toNumbers({amount: positionals[0], tip: values.tip, people: values.people});
    return {out: format(splitBill(bill)), err: '', code: 0};
  } catch (error) {
    return {out: error.message + '\n' + USAGE + '\n', err: '', code: 0};
  }
}

// The only lines that touch the real process.
const {out, err, code} = run(process.argv.slice(2), process.env);
process.stdout.write(out);
process.stderr.write(err);
process.exitCode = code;

main.test.js

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

test('60 --people 3 gibt das Trinkgeld, die Summe und jeden Anteil aus', async () => {
  const got = (await runMain({args: ['60', '--people', '3']})).trimEnd();
  assert.equal(got, 'tip: 9.00\ntotal: 69.00\neach: 23.00', `das Programm gab ${JSON.stringify(got)} aus`);
});

test('kurze Optionen funktionieren: -t 10 -p 3 100', async () => {
  const got = (await runMain({args: ['-t', '10', '-p', '3', '100']})).trimEnd();
  assert.equal(got, 'tip: 10.00\ntotal: 110.00\neach: 36.67', `das Programm gab ${JSON.stringify(got)} aus`);
});

test('TIP_PERCENT setzt das voreingestellte Trinkgeld, und --tip gewinnt trotzdem', async () => {
  process.env.TIP_PERCENT = '10';
  try {
    const byEnv = (await runMain({args: ['60']})).split('\n')[0];
    assert.equal(byEnv, 'tip: 6.00', `mit TIP_PERCENT=10 war die erste Zeile ${JSON.stringify(byEnv)}`);
    const byFlag = (await runMain({args: ['60', '--tip', '20']})).split('\n')[0];
    assert.equal(byFlag, 'tip: 12.00', `mit --tip 20 war die erste Zeile ${JSON.stringify(byFlag)}`);
  } finally {
    delete process.env.TIP_PERCENT;
  }
});

test('--help gibt die Usage-Zeile auf stdout aus', async () => {
  const got = await runMain({args: ['--help']});
  assert.match(got, /^Usage: node main\.js/, `--help gab ${JSON.stringify(got)} aus`);
});

test('kein Betrag: ein Exit-Code ungleich 0', async () => {
  await assert.rejects(runMain(), Error, 'node main.js ohne Betrag endete mit Exit-Code 0');
});

test('eine unbekannte Option: ein Exit-Code ungleich 0', async () => {
  await assert.rejects(runMain({args: ['60', '--colour']}), Error, 'node main.js 60 --colour endete mit Exit-Code 0');
});

test('der Betrag abc: ein Exit-Code ungleich 0', async () => {
  await assert.rejects(runMain({args: ['abc']}), Error, 'node main.js abc endete mit Exit-Code 0');
});

bill.js

// Pure functions: no process, no console, so they run anywhere.

// Turns the text values from the command line into numbers, or throws.
export function toNumbers({amount, tip, people}) {
  const bill = {amount: Number(amount), tip: Number(tip), people: Number(people)};
  if (!(bill.amount > 0)) throw new Error('amount must be a number above 0, got "' + amount + '"');
  if (!(bill.tip >= 0)) throw new Error('tip must be a number from 0, got "' + tip + '"');
  if (!Number.isInteger(bill.people) || bill.people < 1) throw new Error('people must be a whole number from 1, got "' + people + '"');
  return bill;
}

// The tip, the total and each person's share, rounded to cents.
export function splitBill({amount, tip, people}) {
  const cents = (x) => Math.round(x * 100) / 100;
  const tipAmount = cents((amount * tip) / 100);
  const total = cents(amount + tipAmount);
  return {tip: tipAmount, total, each: cents(total / people)};
}

// The result as the lines the command prints.
export function format({tip, total, each}) {
  return 'tip: ' + tip.toFixed(2) + '\ntotal: ' + total.toFixed(2) + '\neach: ' + each.toFixed(2) + '\n';
}

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 Zahl als Default einer String-Option

import {parseArgs} from 'node:util';

const {values} = parseArgs({args: ['60'], options: {tip: {type: 'string', short: 't', default: 15}}, allowPositionals: true});
console.log(values.tip);

Was Node.js ausgibt

TypeError [ERR_INVALID_ARG_TYPE]: The "options.tip.default" property must be of type string. Received type number (15)

Warum, und die Lösung

Der Default muss zum Typ der Option passen, und eine String-Option braucht einen String: default: '15'. parseArgs prüft das, bevor es ein Argument liest. Der Wert, den Sie zurückbekommen, ist ohnehin ein String, also wandelt toNumbers ihn weiterhin um.

Deklarierten Variablen ohne Klammern zuweisen

import {parseArgs} from 'node:util';

let values, positionals;
{values, positionals} = parseArgs({args: ['60'], options: {}, allowPositionals: true});
console.log(values, positionals);

Was Node.js ausgibt

SyntaxError: Unexpected token '='

Warum, und die Lösung

Eine Anweisung, die mit { beginnt, wird als Block gelesen, nicht als Muster, und das = danach ergibt keinen Sinn. Setzen Sie die ganze Zuweisung in Klammern: ({values, positionals} = parseArgs({...}));. Innerhalb eines einzigen try-Blocks können Sie stattdessen in einem Schritt deklarieren und zuweisen: const {values, positionals} = parseArgs({...});.

Einen Optionswert als Zahl verwenden

import {parseArgs} from 'node:util';

const {values} = parseArgs({args: ['--tip', '12.5'], options: {tip: {type: 'string', short: 't', default: '15'}}});
console.log('tip ' + values.tip.toFixed(1) + '%');

Was Node.js ausgibt

TypeError: values.tip.toFixed is not a function

Warum, und die Lösung

values.tip ist der String '12.5', und Strings haben kein toFixed. Wandeln Sie zuerst um und prüfen Sie das Ergebnis: toNumbers macht aus dem Text Zahlen oder wirft mit einer klaren Meldung, bevor irgendetwas damit rechnet.

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

Ein reiner Kern und eine dünne Hülle

Das Werkzeug teilt eine Rechnung auf: node main.js 60 --people 3 gibt das Trinkgeld, die Summe und jeden Anteil aus. bill.js enthält die Rechnung als reine Funktionen: toNumbers prüft die Textwerte und macht Zahlen daraus, splitBill rechnet, format baut die Zeilen. Sie lesen kein process und geben nichts aus, also kann ein Test sie mit jeder Eingabe aufrufen, sogar im Browser. main.js hat run(args, env), das eine Kommandozeile in einen Wert verwandelt: {out, err, code}. Nur die letzten vier Zeilen berühren den echten Prozess: Sie übergeben process.argv.slice(2) und process.env, schreiben out und err und setzen process.exitCode. Ein Aufruf von run() zeigt alles, was der Befehl täte.

Jedes Ergebnis hat einen Strom und einen Code

parseArgs beschreibt --tip (-t), --people (-p) und --help (-h), und allowPositionals lässt den Betrag durch. Ein Ergebnis geht auf stdout, mit Exit-Code 0. --help ist die Ausgabe, um die der Benutzer gebeten hat, also geht die Usage-Zeile auf stdout, ebenfalls mit 0. Wirft parseArgs oder fehlt der Betrag, gehen die Meldung und die Usage-Zeile auf stderr, mit Exit-Code 2: falsche Bedienung. Ein Wert, mit dem es nichts anfangen kann, etwa abc oder 2.5 Personen, bekommt eine Fehlerzeile auf stderr und Exit-Code 1. Node.js selbst endet nie mit 2: Die Doku vermerkt, dass Bash ihn für den Fehlgebrauch seiner Builtins reserviert, und dieses Werkzeug nutzt ihn genauso.

Konfiguration, Skripte und welche Einstellung gewinnt

Das Skript "start" ist node --env-file-if-exists=.env main.js, und node --run start -- 60 --people 3 hängt alles nach -- daran an. Eine .env-Zeile TIP_PERCENT=10 ändert das voreingestellte Trinkgeld, ein in der Shell gesetztes TIP_PERCENT gewinnt gegen die Datei, und --tip gewinnt gegen beide, weil die Umgebung nur den Default von parseArgs füllt. Ohne .env-Datei gibt --env-file-if-exists .env not found. Continuing without it. auf stderr aus, stdout bleibt sauber. Das Skript "dev" ergänzt --watch und eine Beispiel-Kommandozeile, sodass jedes Speichern sie neu ausführt. Ein Haken, gemessen mit Node.js 24.21.0: node --run meldet ein scheiterndes Skript als Exit-Code 1, npm start reicht die 2 durch.

Quellen

Zuletzt geprüft am 30. September 2026