A einfaches Kommandozeilen-Tool in Node.js schreibn
De meisten internen CLI-Tools brauchn ka Framework. A Skript, des a Charge Dateien umbenennt, a interne API aufruaft oda a repetitive git-Sequenz einpackt, kann a einzelne Node-Datei mit ana Shebang-Zeiln und am package.json-Eintrag sei - ka Commander, ka Yargs, ka Build-Schritt. Greif zu am Framework, sobois du echte Subcommands host und generierten --help-Output brauchst; bis dahin is es Overhead für a Problem, des du no ned hast.
Des minimal brauchbare CLI
Jedes Node-CLI fangt gleich o: a Shebang, damit die Shell woaß, wia sie's ausführn soi, und process.argv für die rohen Argumente.
#!/usr/bin/env node
const args = process.argv.slice(2);
console.log('You passed:', args);
chmod +x rename-files.js
./rename-files.js --dry-run photos/
# You passed: [ '--dry-run', 'photos/' ]
process.argv fangt imma mit dem Pfad zum Node-Binary und dem Pfad zum Skript söba o - .slice(2) wirft beides weg und lasst dir des, wos da Nutzer tatsächlich eingetippt hot.
Flags ohne Abhängigkeit parsn
Bei mehr ois oam oda zwoa Flags wird a selbstgschriebener Parser schnö mühsam, aber du brauchst ka vollständige Argument-Parsing-Bibliothek, um's sauber hinzukriagn. Nodes eingebautes util.parseArgs (stabil seit Node 20) deckt de gängigen Fälle ab, ohne a Abhängigkeit dazuzunehmen:
#!/usr/bin/env node
import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
'dry-run': { type: 'boolean', default: false },
output: { type: 'string', short: 'o', default: './out' },
verbose: { type: 'boolean', short: 'v', default: false },
},
allowPositionals: true,
});
console.log(values); // { 'dry-run': true, output: './out', verbose: false }
console.log(positionals); // ['photos/']
./rename-files.js --dry-run -o ./renamed photos/
Damit kriagst benannte Flags, kurze Aliase, Defaults und Typ-Umwandlung (--dry-run wird a echtes Boolean, ned der String "true") ohne externe Abhängigkeiten - gut zum Wissen, bevor du aus Gewohnheit zu ana Bibliothek greifst.
Dateien lesn und die eigentliche Orbeit machn
A Umbenennungs-Tool muass a Verzeichnis lesn und auf jeden Eintrag reagiern. Nodes fs/promises-API hoit des mit async/await statt verschachtelter Callbacks lesbar:
import { readdir, rename } from 'node:fs/promises';
import { join, extname } from 'node:path';
async function renamePhotos(dir, { dryRun }) {
const entries = await readdir(dir);
const photos = entries.filter((name) => ['.jpg', '.png'].includes(extname(name).toLowerCase()));
for (const [index, name] of photos.entries()) {
const ext = extname(name);
const newName = `photo-${String(index + 1).padStart(3, '0')}${ext}`;
const from = join(dir, name);
const to = join(dir, newName);
if (dryRun) {
console.log(`${name} -> ${newName} (dry run)`);
} else {
await rename(from, to);
console.log(`${name} -> ${newName}`);
}
}
}
Beachte den Dry-Run-Zweig: Für olles, wos des Dateisystem verändert, zahlt si a Vorschau-Modus aus, der genau zeigt, wos passieren würd, ohne es zu tun - die zehn zusätzlichen Zeiln sand's wert. Es is der Unterschied zwischn am Tool, dem Leit vertrauen und es gegn a echtes Verzeichnis laufn lossn, und oam, das sie sich nur zuerst an an Backup trauen.
Nützliche Exit-Codes und Fehler liefern
A Skript, des imma mit 0 endet, is für Shell-Skripte und CI unsichtbar - &&-Ketten und if-Checks hängen vom Exit-Code ab, ned vom Parsen deiner Log-Ausgabe.
async function main() {
const dir = positionals[0];
if (!dir) {
console.error('Error: missing target directory');
console.error('Usage: rename-files.js [--dry-run] [-o <dir>] <directory>');
process.exit(1);
}
try {
await renamePhotos(dir, { dryRun: values['dry-run'] });
} catch (err) {
console.error('Failed:', err.message);
process.exit(1);
}
}
main();
process.exit(1) sowohl beim Usage-Fehler ois a bei da abgfangenen Exception heißt, dass rename-files.js photos/ && echo done nur dann "done" ausgibt, wenn die Umbenennung tatsächlich geklappt hot - genau des, wos du in ana Pipeline oda am Pre-Commit-Hook willst.
Ois echtes Kommando installierbar machn
A Skript, des in oam Repo lebt und mit node scripts/rename-files.js ausgführt wird, is für was Einmaliges in Ordnung. Damit's si wia a echtes Kommando anfühlt - von überoi als rename-photos aufrufbar -, füg an bin-Eintrag in die package.json ein:
{
"name": "rename-photos",
"version": "1.0.0",
"type": "module",
"bin": {
"rename-photos": "./rename-files.js"
}
}
npm link
rename-photos --dry-run photos/
npm link legt an Symlink in dein globales node_modules/.bin, sodass des Kommando während der Entwicklung systemweit verfügbar wird. Auf npm veröffentlicht, ist dasselbe bin-Feld das, wos npx rename-photos oda a globales npm install -g für jeden ondern funktionieren lasst.
Testn ohne s'Dateisystem zu mockn
Weil renamePhotos an Verzeichnispfad ois Parameter nimmt, statt oan fest zu codiern, kannst es in Tests auf a echtes temporäres Verzeichnis zeigen lossn, statt fs zu mockn:
import { mkdtemp, writeFile, readdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { test, expect } from 'vitest';
test('renames jpg and png files sequentially', async () => {
const dir = await mkdtemp(join(tmpdir(), 'rename-test-'));
await writeFile(join(dir, 'holiday.jpg'), '');
await writeFile(join(dir, 'notes.txt'), '');
await renamePhotos(dir, { dryRun: false });
const files = await readdir(dir);
expect(files).toContain('photo-001.jpg');
expect(files).toContain('notes.txt'); // untouched — not a photo extension
await rm(dir, { recursive: true });
});
A echtes (temporäres) Verzeichnis fangt Pfad-Verknüpfungs- und Dateiendungs-Bugs ab, de a gemocktes fs-Modul fröhlich zudecken würd.
Wann ma wirklich zu am Framework greift
Sobois a Tool echte Subcommands kriagt (mytool deploy, mytool rollback, mytool status), jedes mit eigenen Flags und Hilfetext, zahlt si händisch gschriebene Dispatch-Logik nimma aus - do is der Punkt, Commander oda a ähnliche Bibliothek fürs Subcommand-Routing und generierten --help zu holn. Für a Skript mit oam Zweck gibt dir parseArgs plus bin-Eintrag aber a echtes, teilbares Kommando, ohne a Abhängigkeit dazuzunehmen, de du sonst aktuell hoitn müsstest.
Fazit
A nützliches CLI braucht ka Framework - es braucht a Shebang-Zeiln, Argument-Parsing (im modernen Node über parseArgs eingebaut), aussagekräftige Exit-Codes und an Dry-Run-Modus, wenn's des Dateisystem anfasst. Füg an bin-Eintrag dazu, wenn's Zeit is, aufzuhörn, node scripts/thing.js zu tippen, und den Kommandonamen zu tippen anzufangen.