Shell-Skripting mit Node.js
Sie können die Offline-Version dieses Buches (HTML, PDF, EPUB, MOBI) kaufen und damit die kostenlose Online-Version unterstützen.
(Werbung, bitte nicht blockieren.)

14 Erstellung plattformübergreifender Shell-Skripte



In diesem Kapitel lernen wir, wie man Shell-Skripte über Node.js ESM-Module implementiert. Es gibt zwei gängige Möglichkeiten, dies zu tun:

14.1 Erforderliche Kenntnisse

Sie sollten mit den folgenden beiden Themen einigermaßen vertraut sein:

14.1.1 Was kommt als Nächstes in diesem Kapitel

Windows unterstützt keine eigenständigen Shell-Skripte in JavaScript wirklich. Daher werden wir uns zuerst ansehen, wie man eigenständige Skripte *mit* Dateinamenserweiterungen für Unix schreibt. Dieses Wissen wird uns bei der Erstellung von Paketen, die Shell-Skripte enthalten, helfen. Später werden wir lernen:

Die Installation von Shell-Skripten über Pakete ist Thema von §13 „npm-Pakete installieren und Binärskripte ausführen“.

14.2 Node.js ESM-Module als eigenständige Shell-Skripte unter Unix

Lassen Sie uns ein ESM-Modul in ein Unix-Shell-Skript umwandeln, das wir ausführen können, ohne dass es sich in einem Paket befindet. Grundsätzlich können wir zwischen zwei Dateinamenserweiterungen für ESM-Module wählen:

Da wir jedoch ein eigenständiges Skript erstellen möchten, können wir uns nicht auf das Vorhandensein von package.json verlassen. Daher müssen wir die Dateinamenserweiterung .mjs verwenden (Workarounds werden später behandelt).

Die folgende Datei hat den Namen hello.mjs:

import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);

Wir können diese Datei bereits ausführen:

node hello.mjs

14.2.1 Node.js Shell-Skripte unter Unix

Wir müssen zwei Dinge tun, damit wir hello.mjs wie folgt ausführen können:

./hello.mjs

Diese Dinge sind:

14.2.2 Hashbangs unter Unix

In einem Unix-Shell-Skript ist die erste Zeile ein Hashbang – Metadaten, die der Shell mitteilen, wie die Datei ausgeführt werden soll. Zum Beispiel ist dies der gebräuchlichste Hashbang für Node.js-Skripte:

#!/usr/bin/env node

Diese Zeile wird „Hashbang“ genannt, da sie mit einem Hash-Symbol und einem Ausrufezeichen beginnt. Sie wird auch oft „Shebang“ genannt.

Wenn eine Zeile mit einem Hash beginnt, ist sie in den meisten Unix-Shells (sh, bash, zsh usw.) ein Kommentar. Daher wird der Hashbang von diesen Shells ignoriert. Node.js ignoriert ihn ebenfalls, aber nur, wenn es die erste Zeile ist.

Warum verwenden wir nicht diesen Hashbang?

#!/usr/bin/node

Nicht alle Unix-Systeme installieren das Node.js-Binärprogramm unter diesem Pfad. Wie wäre es mit diesem Pfad?

#!node

Leider erlauben nicht alle Unix-Systeme relative Pfade. Deshalb verweisen wir über einen absoluten Pfad auf env und verwenden es, um node für uns auszuführen.

Weitere Informationen zu Unix-Hashbangs finden Sie unter „Node.js shebang“ von Alex Ewerlöf.

14.2.2.1 Argumente an das Node.js-Binärprogramm übergeben

Was ist, wenn wir Argumente wie Kommandozeilenoptionen an das Node.js-Binärprogramm übergeben möchten?

Eine Lösung, die auf vielen Unix-Systemen funktioniert, ist die Verwendung der Option -S für env, die verhindert, dass env alle seine Argumente als einen einzigen Namen eines Binärprogramms interpretiert:

#!/usr/bin/env -S node --disable-proto=throw

Auf macOS funktioniert der vorherige Befehl auch ohne -S; unter Linux normalerweise nicht.

14.2.2.2 Hashbang-Fallstrick: Erstellung von Hashbangs unter Windows

Wenn wir einen Texteditor unter Windows verwenden, um ein ESM-Modul zu erstellen, das auf Unix oder Windows als Skript ausgeführt werden soll, müssen wir einen Hashbang hinzufügen. Wenn wir dies tun, endet die erste Zeile mit dem Windows-Zeilenabschlusszeichen \r\n.

#!/usr/bin/env node\r\n

Das Ausführen einer Datei mit einem solchen Hashbang unter Unix erzeugt den folgenden Fehler:

env: node\r: No such file or directory

Das bedeutet, env hält den Namen der ausführbaren Datei für node\r. Es gibt zwei Möglichkeiten, dies zu beheben.

Erstens prüfen einige Editoren automatisch, welche Zeilenabschlüsse bereits in einer Datei verwendet werden, und behalten sie bei. Zum Beispiel zeigt Visual Studio Code den aktuellen Zeilenabschluss (es nennt ihn „end of line sequence“) in der Statusleiste unten rechts an:

Wir können einen Zeilenabschluss auswählen, indem wir auf diese Statusinformation klicken.

Zweitens können wir eine minimale Datei my-script.mjs mit ausschließlich Unix-Zeilenabschlüssen erstellen, die wir niemals unter Windows bearbeiten werden:

#!/usr/bin/env node
import './main.mjs';

14.2.3 Dateien unter Unix ausführbar machen

Um ein Shell-Skript zu werden, muss hello.mjs zusätzlich zu einem Hashbang auch ausführbar sein (eine Berechtigung von Dateien):

chmod u+x hello.mjs

Beachten Sie, dass wir die Datei für den Benutzer, der sie erstellt hat (u), ausführbar (x) gemacht haben, nicht für alle.

14.2.4 hello.mjs direkt ausführen

hello.mjs ist nun ausführbar und sieht wie folgt aus:

#!/usr/bin/env node

import * as os from 'node:os';

const {username} = os.userInfo();
console.log(`Hello ${username}!`);

Wir können es daher wie folgt ausführen:

./hello.mjs

Leider gibt es keine Möglichkeit, node mitzuteilen, eine Datei mit einer beliebigen Erweiterung als ESM-Modul zu interpretieren. Deshalb müssen wir die Erweiterung .mjs verwenden. Workarounds sind möglich, aber kompliziert, wie wir später sehen werden.

14.3 Erstellen eines npm-Pakets mit Shell-Skripten

In diesem Abschnitt erstellen wir ein npm-Paket mit Shell-Skripten. Anschließend untersuchen wir, wie wir ein solches Paket installieren können, damit seine Skripte auf der Befehlszeile unseres Systems (Unix oder Windows) verfügbar werden.

Das fertige Paket ist hier verfügbar:

14.3.1 Einrichten des Verzeichnisses des Pakets

Diese Befehle funktionieren sowohl unter Unix als auch unter Windows:

mkdir demo-shell-scripts
cd demo-shell-scripts
npm init --yes

Nun gibt es die folgenden Dateien:

demo-shell-scripts/
  package.json
14.3.1.1 package.json für unveröffentlichte Pakete

Eine Möglichkeit besteht darin, ein Paket zu erstellen und es nicht im npm-Registry zu veröffentlichen. Wir können ein solches Paket immer noch auf unserem System installieren (wie später erklärt wird). In diesem Fall sieht unser package.json wie folgt aus:

{
  "private": true,
  "license": "UNLICENSED"
}

Erläuterungen

14.3.1.2 package.json für veröffentlichte Pakete

Wenn wir unser Paket im npm-Registry veröffentlichen möchten, sieht unser package.json wie folgt aus:

{
  "name": "@rauschma/demo-shell-scripts",
  "version": "1.0.0",
  "license": "MIT"
}

Für Ihre eigenen Pakete müssen Sie den Wert von "name" durch einen Paketnamen ersetzen, der für Sie funktioniert:

14.3.2 Abhängigkeiten hinzufügen

Als Nächstes installieren wir eine Abhängigkeit, die wir in einem unserer Skripte verwenden möchten – das Paket lodash-es (die ESM-Version von Lodash):

npm install lodash-es

Dieser Befehl:

Wenn wir ein Paket nur während der Entwicklung verwenden, können wir es zu "devDependencies" anstelle von "dependencies" hinzufügen, und npm wird es nur installieren, wenn wir npm install in unserem Paketverzeichnis ausführen, aber nicht, wenn wir es als Abhängigkeit installieren. Eine Unit-Testing-Bibliothek ist eine typische Dev-Abhängigkeit.

Dies sind zwei Möglichkeiten, wie wir eine Dev-Abhängigkeit installieren können:

Der zweite Weg bedeutet, dass wir die Entscheidung, ob ein Paket eine Abhängigkeit oder eine Dev-Abhängigkeit ist, leicht aufschieben können.

14.3.3 Inhalt zum Paket hinzufügen

Fügen wir eine README-Datei und zwei Module hinzu: homedir.mjs und versions.mjs, die Shell-Skripte sind:

demo-shell-scripts/
  package.json
  package-lock.json
  README.md
  src/
    homedir.mjs
    versions.mjs

Wir müssen npm über die beiden Shell-Skripte informieren, damit es sie für uns installieren kann. Dafür ist die Eigenschaft "bin" in package.json da:

"bin": {
  "homedir": "./src/homedir.mjs",
  "versions": "./src/versions.mjs"
}

Wenn wir dieses Paket installieren, werden zwei Shell-Skripte mit den Namen homedir und versions verfügbar.

Möglicherweise bevorzugen Sie die Dateinamenserweiterung .js für die Shell-Skripte. Dann müssen Sie anstelle der vorherigen Eigenschaft die folgenden beiden Eigenschaften zu package.json hinzufügen:

"type": "module",
"bin": {
  "homedir": "./src/homedir.js",
  "versions": "./src/versions.js"
}

Die erste Eigenschaft teilt Node.js mit, dass es .js-Dateien als ESM-Module interpretieren soll (und nicht als CommonJS-Module – was der Standard ist).

So sieht homedir.mjs aus:

#!/usr/bin/env node
import {homedir} from 'node:os';

console.log('Homedir: ' + homedir());

Dieses Modul beginnt mit dem oben genannten Hashbang, der erforderlich ist, wenn wir es unter Unix verwenden möchten. Es importiert die Funktion homedir() aus dem integrierten Modul node:os, ruft sie auf und gibt das Ergebnis auf der Konsole (d. h. Standardausgabe) aus.

Beachten Sie, dass homedir.mjs nicht ausführbar sein muss; npm stellt die Ausführbarkeit von "bin"-Skripten sicher, wenn es sie installiert (wie wir gleich sehen werden).

versions.mjs hat folgenden Inhalt:

#!/usr/bin/env node

import {pick} from 'lodash-es';

console.log(
  pick(process.versions, ['node', 'v8', 'unicode'])
);

Wir importieren die Funktion pick() aus Lodash und verwenden sie, um drei Eigenschaften des Objekts process.versions anzuzeigen.

14.3.4 Shell-Skripte ausführen, ohne sie zu installieren

Wir können z. B. homedir.mjs wie folgt ausführen:

cd demo-shell-scripts/
node src/homedir.mjs

14.4 Wie npm Shell-Skripte installiert

14.4.1 Installation unter Unix

Ein Skript wie homedir.mjs muss unter Unix nicht ausführbar sein, da npm es über einen symbolischen Link installiert:

14.4.2 Installation unter Windows

Um homedir.mjs unter Windows zu installieren, erstellt npm drei Dateien:

npm fügt diese Dateien einem Verzeichnis hinzu:

14.5 Veröffentlichen des Beispielpakets im npm-Registry

Lassen Sie uns das Paket @rauschma/demo-shell-scripts (das wir zuvor erstellt haben) auf npm veröffentlichen. Bevor wir npm publish verwenden, um das Paket hochzuladen, sollten wir überprüfen, ob alles richtig konfiguriert ist.

14.5.1 Welche Dateien werden veröffentlicht? Welche werden ignoriert?

Die folgenden Mechanismen werden verwendet, um Dateien beim Veröffentlichen auszuschließen und einzuschließen:

Die npm-Dokumentation enthält weitere Details darüber, was beim Veröffentlichen enthalten und was ausgeschlossen wird.

14.5.2 Überprüfen, ob ein Paket richtig konfiguriert ist

Es gibt mehrere Dinge, die wir überprüfen können, bevor wir ein Paket hochladen.

14.5.2.1 Überprüfen, welche Dateien hochgeladen werden

Ein Trockenlauf von npm install führt den Befehl aus, ohne etwas hochzuladen:

npm publish --dry-run

Dies zeigt an, welche Dateien hochgeladen würden, und liefert verschiedene Statistiken über das Paket.

Wir können auch ein Archiv des Pakets erstellen, wie es im npm-Registry vorhanden wäre:

npm pack

Dieser Befehl erstellt die Datei rauschma-demo-shell-scripts-1.0.0.tgz im aktuellen Verzeichnis.

14.5.2.2 Das Paket global installieren – ohne es hochzuladen

Wir können einen der folgenden beiden Befehle verwenden, um unser Paket global zu installieren, ohne es im npm-Registry zu veröffentlichen:

npm link
npm install . -g

Um zu sehen, ob das funktioniert hat, können wir eine neue Shell öffnen und überprüfen, ob die beiden Befehle verfügbar sind. Wir können auch alle global installierten Pakete auflisten:

npm ls -g
14.5.2.3 Das Paket lokal (als Abhängigkeit) installieren – ohne es hochzuladen

Um unser Paket als Abhängigkeit zu installieren, müssen wir die folgenden Befehle ausführen (während wir uns im Verzeichnis demo-shell-scripts befinden):

cd ..
mkdir sibling-directory
cd sibling-directory
npm init --yes
npm install ../demo-shell-scripts

Wir können nun z. B. homedir mit einem der folgenden beiden Befehle ausführen:

npx homedir
./node_modules/.bin/homedir

14.5.3 npm publish: Pakete zum npm-Registry hochladen

Bevor wir unser Paket hochladen können, müssen wir ein npm-Benutzerkonto erstellen. Die npm-Dokumentation beschreibt, wie das geht.

Dann können wir schließlich unser Paket veröffentlichen:

npm publish --access public

Wir müssen öffentlichen Zugriff angeben, da die Standardeinstellungen lauten:

Die Option --access wirkt sich nur beim ersten Veröffentlichen aus. Danach können wir sie weglassen und müssen npm access verwenden, um die Zugriffsebene zu ändern.

Wir können die Standardeinstellung für das anfängliche npm publish über publishConfig.access in package.json ändern:

"publishConfig": {
  "access": "public"
}
14.5.3.1 Für jeden Upload ist eine neue Version erforderlich

Sobald wir ein Paket mit einer bestimmten Version hochgeladen haben, können wir diese Version nicht wiederverwenden; wir müssen eine der drei Komponenten der Version erhöhen:

major.minor.patch

14.5.4 Automatisch Aufgaben ausführen, die jedes Mal vor dem Veröffentlichen erfolgen

Es kann Schritte geben, die wir jedes Mal ausführen möchten, bevor wir ein Paket hochladen – z. B.:

Dies kann automatisch über die Eigenschaft "scripts" in package.json erfolgen. Diese Eigenschaft kann wie folgt aussehen:

"scripts": {
  "build": "tsc",
  "test": "mocha --ui qunit",
  "dry": "npm publish --dry-run",
  "prepublishOnly": "npm run test && npm run build"
}

mocha ist eine Unit-Testing-Bibliothek. tsc ist der TypeScript-Compiler.

Die folgenden Paket-Skripte werden vor npm publish ausgeführt:

Weitere Informationen zu diesem Thema finden Sie in §15 „Plattformübergreifende Aufgaben über npm-Paket-Skripte ausführen“.

14.6 Eigenständige Node.js Shell-Skripte mit beliebigen Erweiterungen unter Unix

14.6.1 Unix: beliebige Dateinamenserweiterung über eine benutzerdefinierte ausführbare Datei

Das Node.js-Binärprogramm node verwendet die Dateinamenserweiterung, um den Modultyp einer Datei zu erkennen. Derzeit gibt es keine Kommandozeilenoption, um dies zu überschreiben. Und der Standard ist CommonJS, was nicht das ist, was wir wollen.

Wir können jedoch unsere eigene ausführbare Datei zum Ausführen von Node.js erstellen und sie z. B. node-esm nennen. Dann können wir unser vorheriges eigenständiges Skript hello.mjs in hello (ohne Erweiterung) umbenennen, wenn wir die erste Zeile wie folgt ändern:

#!/usr/bin/env node-esm

Zuvor war das Argument von env node.

Dies ist eine Implementierung von node-esm, die von Andrea Giammarchi vorgeschlagen wurde.

#!/usr/bin/env sh
input_file=$1
shift
exec node --input-type=module - $@ < $input_file

Diese ausführbare Datei sendet den Inhalt eines Skripts über die Standardeingabe an node. Die Kommandozeilenoption --input-type=module teilt Node.js mit, dass der empfangene Text ein ESM-Modul ist.

Wir verwenden auch die folgenden Unix-Shell-Funktionen:

Bevor wir node-esm verwenden können, müssen wir sicherstellen, dass es ausführbar ist und über $PATH gefunden werden kann. Wie das geht, wird später erklärt.

14.6.2 Unix: beliebige Dateinamenserweiterung über einen Shell-Prolog

Wir haben gesehen, dass wir den Modultyp für eine Datei nicht angeben können, nur für die Standardeingabe. Daher können wir ein Unix-Shell-Skript hello schreiben, das Node.js verwendet, um sich selbst als ESM-Modul auszuführen (basierend auf der Arbeit von sambal.org):

#!/bin/sh
':' // ; cat "$0" | node --input-type=module - $@ ; exit $?

import * as os from 'node:os';

const {username} = os.userInfo();
console.log(`Hello ${username}!`);

Die meisten der hier verwendeten Shell-Funktionen werden zu Beginn dieses Kapitels beschrieben. $? enthält den Exit-Code des zuletzt ausgeführten Shell-Befehls. Dadurch kann hello mit dem gleichen Code wie node beendet werden.

Der Schlüsseltrick, der von diesem Skript verwendet wird, ist, dass die zweite Zeile sowohl Unix-Shell-Skriptcode als auch JavaScript-Code ist:

Ein zusätzlicher Vorteil des Versteckens des Shell-Codes vor JavaScript ist, dass JavaScript-Editoren bei der Verarbeitung und Anzeige der Syntax nicht verwirrt werden.

14.7 Eigenständige Node.js Shell-Skripte unter Windows

14.7.1 Windows: Konfiguration der Dateinamenserweiterung .mjs

Eine Möglichkeit, eigenständige Node.js Shell-Skripte unter Windows zu erstellen, ist die Verwendung der Dateinamenserweiterung .mjs und deren Konfiguration, sodass Dateien mit dieser Erweiterung über node ausgeführt werden. Leider funktioniert dies nur für die Kommandozeile, nicht für PowerShell.

Ein weiterer Nachteil ist, dass wir auf diese Weise keine Argumente an ein Skript übergeben können:

>more args.mjs
console.log(process.argv);

>.\args.mjs one two
[
  'C:\\Program Files\\nodejs\\node.exe',
  'C:\\Users\\jane\\args.mjs'
]

>node args.mjs one two
[
  'C:\\Program Files\\nodejs\\node.exe',
  'C:\\Users\\jane\\args.mjs',
  'one',
  'two'
]

Wie konfigurieren wir Windows, damit die Kommandozeile Dateien wie args.mjs direkt ausführt?

Dateizuordnungen legen fest, mit welcher Anwendung eine Datei geöffnet wird, wenn wir ihren Namen in einer Shell eingeben. Wenn wir die Dateinamenserweiterung .mjs dem Node.js-Binärprogramm zuordnen, können wir ESM-Module in Shells ausführen. Eine Möglichkeit, dies zu tun, ist über die Einstellungen-App, wie in „How to Change File Associations in Windows“ von Tim Fisher beschrieben.

Wenn wir zusätzlich .MJS zur Variable %PATHEXT% hinzufügen, können wir sogar die Dateinamenserweiterung weglassen, wenn wir auf ein ESM-Modul verweisen. Diese Umgebungsvariable kann dauerhaft über die Einstellungen-App geändert werden – suchen Sie nach „variables“.

14.7.2 Windows-Kommandozeile: Node.js-Skripte über einen Shell-Prolog

Unter Windows stehen wir vor der Herausforderung, dass es keinen Mechanismus wie Hashbangs gibt. Daher müssen wir einen Workaround verwenden, der dem ähnelt, den wir für dateilose Dateien unter Unix verwendet haben: Wir erstellen ein Skript, das den JavaScript-Code darin über Node.js ausführt.

Kommandozeilen-Skripte haben die Dateinamenserweiterung .bat. Wir können ein Skript namens script.bat entweder über script.bat oder script ausführen.

So sieht hello.mjs aus, wenn wir es in ein Kommandozeilen-Skript hello.bat umwandeln:

:: /*
@echo off
more +5 %~f0 | node --input-type=module - %*
exit /b %errorlevel%
*/

import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);

Das Ausführen dieses Codes als Datei über node würde zwei Funktionen erfordern, die es nicht gibt:

Daher bleibt uns nichts anderes übrig, als den Inhalt der Datei in node zu pipeen. Wir verwenden auch die folgenden Kommandozeilenfunktionen:

14.7.3 Windows PowerShell: Node.js-Skripte über einen Shell-Prolog

Wir können einen ähnlichen Trick wie im vorherigen Abschnitt verwenden und hello.mjs in ein PowerShell-Skript hello.ps1 wie folgt umwandeln:

Get-Content $PSCommandPath | Select-Object -Skip 3 | node --input-type=module - $args
exit $LastExitCode
<#
import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);
// #>

Wir können dieses Skript über eines der folgenden ausführen:

.\hello.ps1
.\hello

Bevor wir das tun können, müssen wir jedoch eine Ausführungsrichtlinie festlegen, die uns das Ausführen von PowerShell-Skripten erlaubt (weitere Informationen zu Ausführungsrichtlinien).

Der folgende Befehl erlaubt uns, lokale Skripte auszuführen:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

14.8 Erstellung nativer Binärdateien für Linux, macOS und Windows

Das npm-Paket pkg wandelt ein Node.js-Paket in eine native Binärdatei um, die auch auf Systemen läuft, auf denen Node.js nicht installiert ist. Es unterstützt die folgenden Plattformen: Linux, macOS und Windows.

14.9 Shell-Pfade: Sicherstellen, dass Shells Skripte finden

In den meisten Shells können wir einen Dateinamen eingeben, ohne direkt auf eine Datei zu verweisen, und sie durchsuchen mehrere Verzeichnisse nach einer Datei mit diesem Namen und führen sie aus. Diese Verzeichnisse sind normalerweise in einer speziellen Shell-Variable aufgeführt:

Wir benötigen die PATH-Variable für zwei Zwecke:

14.9.1 Unix: $PATH

Die meisten Unix-Shells haben die Variable $PATH, die alle Pfade auflistet, in denen eine Shell nach ausführbaren Dateien sucht, wenn wir einen Befehl eingeben. Ihr Wert könnte wie folgt aussehen:

$ echo $PATH
/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin

Der folgende Befehl funktioniert in den meisten Shells (Quelle) und ändert $PATH, bis wir die aktuelle Shell verlassen:

export PATH="$PATH:$HOME/bin"

Die Anführungszeichen sind erforderlich, falls eine der beiden Shell-Variablen Leerzeichen enthält.

14.9.1.1 $PATH dauerhaft ändern

Unter Unix hängt die Konfiguration von $PATH von der Shell ab. Sie können über Folgendes herausfinden, welche Shell Sie ausführen:

echo $0

MacOS verwendet Zsh, wobei der beste Ort zur dauerhaften Konfiguration von $PATH das Startskript $HOME/.zprofile ist – wie hier

path+=('/Library/TeX/texbin')
export PATH

14.9.2 Ändern der PATH-Variable unter Windows (Kommandozeile, PowerShell)

Unter Windows können die Standard-Umgebungsvariablen der Kommandozeile und von PowerShell über die Einstellungen-App (suchen Sie nach „Variablen“) konfiguriert werden (dauerhaft).