Warm-up · Activity 1 of 7
Warm-up: you run node main.js Ada 3. Which expression in main.js is the string "Ada"?
node main.js Ada 3// B1.3 · ~33 min · Beginner
After this lesson you can read what was typed after node main.js, describe the options of your command with util.parseArgs, and turn a wrong argument into a clear message instead of a crash.
You will be able to
Warm-up · Activity 1 of 7
node main.js Ada 3Predict · Activity 2 of 7
const [a, b] = process.argv.slice(2);
console.log(a + b);Practice · Activity 3 of 7
const args = process.argv.____(2);
console.log(args);Practice · Activity 4 of 7
Practice · Activity 5 of 7
import {parseArgs} from 'node:util';
const {values} = parseArgs({
args: ['--name', 'Ada', '--name', 'Grace'],
options: {name: {type: 'string'}}
});
console.log(values.name);Brain teaser · Activity 6 of 7
import {parseArgs} from 'node:util';
const {values, positionals} = parseArgs({
options: {verbose: {type: 'boolean', short: 'v'}}
});
console.log(values.verbose, positionals);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
This program first shows what process.argv holds when you run plain node main.js: two entries, the node program and the script, and nothing after them. Then it describes two options, a switch --loud (-l) and --times (-t) with a default, and gives parseArgs three command lines as arrays of strings, the way process.argv.slice(2) would deliver them. Look at how each value arrives: as true, as a string, or as the default.
main.js
import {basename} from 'node:path';
import {parseArgs} from 'node:util';
// Run as plain node main.js: no arguments after the script.
console.log(process.argv.length, basename(process.argv[1]), process.argv.slice(2));
const options = {
loud: {type: 'boolean', short: 'l'},
times: {type: 'string', short: 't', default: '1'}
};
// The same parser, given three command lines as arrays of strings.
for (const args of [['Ada'], ['-l', '--times', '3', 'Ada'], ['--times=2', 'Ada', 'Grace']]) {
const {values, positionals} = parseArgs({args, options, allowPositionals: true});
console.log(args.join(' '), '->', values, positionals);
}
Run it with
node main.jsOutput
2 main.js []
Ada -> [Object: null prototype] { times: '1' } [ 'Ada' ]
-l --times 3 Ada -> [Object: null prototype] { loud: true, times: '3' } [ 'Ada' ]
--times=2 Ada Grace -> [Object: null prototype] { times: '2' } [ 'Ada', 'Grace' ]Exercise 1 of 2
A command takes a text and an optional count: node main.js hello 3. Write readArgs(args), where args is what process.argv.slice(2) gives. Return {text, times} with times as a number, 1 when it is missing. Throw an Error whose message starts with Usage when no text is given, and an Error when times is not a whole number of at least 1. The function takes an array instead of reading process.argv itself, so the tests can try any command line, and it runs in the browser too.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Destructuring can give a default: const [text, count = '1'] = args;
Number(count) turns "3" into 3; Number.isInteger tells you whether the result is a whole number.
An argument nobody typed is undefined. Check text === undefined before you use it.
One way to solve it. Yours can look different and still pass the checks.
// args is what process.argv.slice(2) gives: an array of strings.
export function readArgs(args) {
const [text, count = '1'] = args;
if (text === undefined) throw new Error('Usage: node main.js <text> [times]');
const times = Number(count);
if (!Number.isInteger(times) || times < 1) throw new Error('times must be a whole number from 1, got "' + count + '"');
return {text, times};
}
console.log(readArgs(['hello', '3']));
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// args is what process.argv.slice(2) gives: an array of strings.
export function readArgs(args) {
const text = args[0];
const times = args[1];
return {text, times};
}
console.log(readArgs(['hello', '3']));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readArgs} from './main.js';
test('readArgs(["hello", "3"]) is {text: "hello", times: 3}', () => {
const got = readArgs(['hello', '3']);
assert.deepEqual(got, {text: 'hello', times: 3}, `readArgs(["hello", "3"]) returned ${JSON.stringify(got)}`);
});
test('times is 1 when only the text is given', () => {
const got = readArgs(['hi']);
assert.deepEqual(got, {text: 'hi', times: 1}, `readArgs(["hi"]) returned ${JSON.stringify(got)}`);
});
test('no arguments: throws an Error starting with Usage', () => {
assert.throws(() => readArgs([]), {message: /^Usage/}, 'readArgs([]) should throw an Error whose message starts with Usage');
});
test('times that is not a whole number from 1 throws', () => {
assert.throws(() => readArgs(['hi', 'two']), Error, 'readArgs(["hi", "two"]) should throw');
assert.throws(() => readArgs(['hi', '0']), Error, 'readArgs(["hi", "0"]) should throw');
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 2
Complete echo(args), which returns the text the command prints. Describe two options for parseArgs: --upper (-u), a switch that turns the words into capitals, and --times (-t), a string with the default '1' that repeats the line, one per line. Allow positionals: they are the words, joined with spaces. When parseArgs throws, do not let it crash: return its message, a newline and USAGE. Try node main.js -u -t 2 hello world, then node --test.
This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.
Describe each option by its long name: upper: {type: 'boolean', short: 'u'}.
times is a string option with default: '1'. Array(Number(values.times)).fill(line).join('\n') repeats the line.
Wrap the parseArgs call in try/catch and return error.message + '\n' + USAGE from the catch.
One way to solve it. Yours can look different and still pass the checks.
import {parseArgs} from 'node:util';
export const USAGE = 'Usage: node main.js [--upper] [--times N] <words...>';
// Returns what the command prints for these arguments.
export function echo(args) {
try {
const {values, positionals} = parseArgs({
args,
options: {
upper: {type: 'boolean', short: 'u'},
times: {type: 'string', short: 't', default: '1'}
},
allowPositionals: true
});
const text = positionals.join(' ');
const line = values.upper ? text.toUpperCase() : text;
return Array(Number(values.times)).fill(line).join('\n');
} catch (error) {
return error.message + '\n' + USAGE;
}
}
console.log(echo(process.argv.slice(2)));
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
import {parseArgs} from 'node:util';
export const USAGE = 'Usage: node main.js [--upper] [--times N] <words...>';
// Returns what the command prints for these arguments.
export function echo(args) {
const {values, positionals} = parseArgs({args, options: {}, allowPositionals: true});
return positionals.join(' ');
}
console.log(echo(process.argv.slice(2)));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runMain} from './learnrun.js';
import {echo, USAGE} from './main.js';
const attempt = (args) => {
try {
return echo(args);
} catch (error) {
return `it threw ${error.message}`;
}
};
test('echo(["hello", "world"]) is "hello world"', () => {
const got = attempt(['hello', 'world']);
assert.equal(got, 'hello world', `echo(["hello", "world"]) returned ${JSON.stringify(got)}`);
});
test('--upper and -u turn the words into capitals', () => {
assert.equal(attempt(['--upper', 'hi']), 'HI', `echo(["--upper", "hi"]) returned ${JSON.stringify(attempt(['--upper', 'hi']))}`);
assert.equal(attempt(['-u', 'hi']), 'HI', `echo(["-u", "hi"]) returned ${JSON.stringify(attempt(['-u', 'hi']))}`);
});
test('--times 3 repeats the line three times, one per line', () => {
const got = attempt(['--times', '3', 'hi']);
assert.equal(got, 'hi\nhi\nhi', `echo(["--times", "3", "hi"]) returned ${JSON.stringify(got)}`);
});
test('an unknown option returns its message and the usage line', () => {
const got = attempt(['--colour', 'hi']);
assert.ok(got.startsWith("Unknown option '--colour'") && got.endsWith('\n' + USAGE), `echo(["--colour", "hi"]) returned ${JSON.stringify(got)}`);
});
test('node main.js -u -t 2 hello world prints HELLO WORLD twice', async () => {
const got = (await runMain({args: ['-u', '-t', '2', 'hello', 'world']})).trimEnd();
assert.equal(got, 'HELLO WORLD\nHELLO WORLD', `the program printed ${JSON.stringify(got)}`);
});
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsconst name = process.argv[2];
console.log("Hello, " + name.toUpperCase() + "!");
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'toUpperCase')Why, and the fix
Run with node main.js and nothing after it, process.argv[2] is undefined, and calling a method on undefined throws. Check before you use an argument: if (name === undefined), print a usage line such as Usage: node main.js <name> and stop, or give a default with process.argv[2] ?? "world".
import {parseArgs} from 'node:util';
const {values} = parseArgs({options: {port: {type: 'number'}}});
console.log(values.port);
What Node.js prints
TypeError [ERR_INVALID_ARG_TYPE]: The "options.port.type" property must be ('string|boolean'). Received type string ('number')Why, and the fix
parseArgs knows only two types: 'boolean' for a switch and 'string' for an option with a value. Describe the port as type: 'string', then convert it yourself: const port = Number(values.port), and check it with Number.isInteger before you use it. The error comes before any argument is read, because the description itself is wrong.
import {parseArgs} from 'node:util';
// What node main.js --lines 5 notes.txt passes to the program:
const args = ['--lines', '5', 'notes.txt'];
const {values, positionals} = parseArgs({args, options: {lines: {type: 'string'}}});
console.log(values.lines, positionals);
What Node.js prints
TypeError [ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL]: Unexpected argument 'notes.txt'. This command does not take positional argumentsWhy, and the fix
In strict mode, which is the default, allowPositionals is false: every argument must be an option you described. A file name such as notes.txt is a positional, so add allowPositionals: true. Then values.lines is '5' and positionals is [ 'notes.txt' ].
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.