Node.jsを使ったシェルスクリプト
この書籍のオフライン版(HTML、PDF、EPUB、MOBI)を購入して、無料のオンライン版をサポートできます。
(広告、ブロックしないでください。)

14 クロスプラットフォームのシェルスクリプトを作成する



この章では、Node.js ESMモジュールを介してシェルスクリプトを実装する方法を学びます。これを行うには、2つの一般的な方法があります。

14.1 必要な知識

次の2つのトピックについて、ある程度精通している必要があります。

14.1.1 この章の次のステップ

Windowsは、JavaScriptで記述されたスタンドアロンのシェルスクリプトを実際にはサポートしていません。したがって、最初に、Unix用のファイル名拡張子付きのスタンドアロンスクリプトを作成する方法を見ていきます。その知識は、シェルスクリプトを含むパッケージの作成に役立ちます。後で、以下について学びます。

パッケージを介したシェルスクリプトのインストールは、§13「npmパッケージのインストールとbinスクリプトの実行」のトピックです。

14.2 UnixでのスタンドアロンシェルスクリプトとしてのNode.js ESMモジュール

パッケージ内になくても実行できるUnixシェルスクリプトにESMモジュールを変換してみましょう。原則として、ESMモジュールのファイル名拡張子は2つから選択できます。

ただし、スタンドアロンスクリプトを作成する場合は、package.jsonが存在することに頼ることはできません。したがって、ファイル名拡張子.mjsを使用する必要があります(後で回避策について説明します)。

次のファイルの名前はhello.mjsです。

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

このファイルはすでに実行できます。

node hello.mjs

14.2.1 UnixでのNode.jsシェルスクリプト

hello.mjsをこのように実行できるようにするには、2つのことを行う必要があります。

./hello.mjs

これらのことは次のとおりです。

14.2.2 Unixでのハッシュバン

Unixシェルスクリプトでは、最初の行はハッシュバンです。これは、シェルにファイルの実行方法を指示するメタデータです。たとえば、これはNode.jsスクリプトで最も一般的なハッシュバンです。

#!/usr/bin/env node

この行は、ハッシュ記号と感嘆符で始まるため、「ハッシュバン」という名前です。「シーバン」とも呼ばれることがよくあります。

行がハッシュで始まる場合、ほとんどのUnixシェル(sh、bash、zshなど)ではコメントです。したがって、ハッシュバンはこれらのシェルによって無視されます。Node.jsも無視しますが、最初の行である場合に限ります。

このハッシュバンを使用しないのはなぜですか?

#!/usr/bin/node

すべてのUnixで、そのパスにNode.jsバイナリがインストールされているわけではありません。では、このパスはどうですか?

#!node

悲しいかな、すべてのUnixが相対パスを許可しているわけではありません。そのため、絶対パスでenvを参照し、それを使用してnodeを実行します。

Unixのハッシュバンの詳細については、Alex Ewerlöfによる「Node.js shebang」を参照してください。

14.2.2.1 Node.jsバイナリに引数を渡す

コマンドラインオプションなどの引数をNode.jsバイナリに渡したい場合はどうすればよいですか?

多くのUnixで機能する1つの解決策は、envにオプション-Sを使用することです。これにより、引数すべてをバイナリの単一の名前として解釈しなくなります。

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

macOSでは、前のコマンドは-Sなしでも機能します。Linuxでは通常機能しません。

14.2.2.2 ハッシュバンの落とし穴:Windowsでハッシュバンを作成する

Windowsでテキストエディターを使用して、UnixまたはWindowsのいずれかでスクリプトとして実行する必要があるESMモジュールを作成する場合は、ハッシュバンを追加する必要があります。これを行うと、最初の行はWindowsの行末記号\r\nで終わります。

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

このようなハッシュバンを含むファイルをUnixで実行すると、次のエラーが発生します。

env: node\r: No such file or directory

つまり、envは実行可能ファイルの名前がnode\rであると考えています。これを修正する方法は2つあります。

まず、一部のエディターは、ファイルですでに使用されている行末記号を自動的にチェックし、それらを使用し続けます。たとえば、Visual Studio Codeでは、右下のステータスバーに現在の行末記号(「行末シーケンス」と呼びます)が表示されます。

そのステータス情報をクリックして、行末記号を選択できます。

次に、Windowsで編集しないUnixの行末記号のみを含む最小限のファイルmy-script.mjsを作成できます。

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

14.2.3 Unixでファイルを実行可能にする

シェルスクリプトになるには、hello.mjsはハッシュバンを持っていることに加えて、実行可能(ファイルのアクセス許可)である必要もあります。

chmod u+x hello.mjs

ファイルを実行可能にした(x)のは、作成したユーザー(u)に対してであり、全員に対してではないことに注意してください。

14.2.4 hello.mjsを直接実行する

hello.mjsは実行可能になり、次のようになります。

#!/usr/bin/env node

import * as os from 'node:os';

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

したがって、次のように実行できます。

./hello.mjs

悲しいかな、nodeに任意の拡張子を持つファイルをESMモジュールとして解釈するように指示する方法はありません。そのため、拡張子.mjsを使用する必要があります。後で説明するように、回避策は可能ですが複雑です。

14.3 シェルスクリプトを含むnpmパッケージを作成する

このセクションでは、シェルスクリプトを含むnpmパッケージを作成します。次に、そのようなパッケージをインストールして、そのスクリプトがシステム(UnixまたはWindows)のコマンドラインで使用できるようになる方法を調べます。

完成したパッケージはこちらで入手できます。

14.3.1 パッケージのディレクトリを設定する

これらのコマンドは、UnixとWindowsの両方で機能します。

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

これで、次のファイルができました。

demo-shell-scripts/
  package.json
14.3.1.1 公開されていないパッケージ用のpackage.json

1つのオプションは、パッケージを作成してnpmレジストリに公開しないことです。それでも、(後で説明するように)そのようなパッケージをシステムにインストールできます。その場合、package.jsonは次のようになります。

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

説明

14.3.1.2 公開されたパッケージ用のpackage.json

パッケージをnpmレジストリに公開する場合は、package.jsonは次のようになります。

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

独自のパッケージの場合は、"name"の値を、自分に適したパッケージ名に置き換える必要があります。

14.3.2 依存関係を追加する

次に、スクリプトの1つで使用する依存関係をインストールします。パッケージlodash-esLodashのESMバージョン)です。

npm install lodash-es

このコマンドは、

開発中にのみパッケージを使用する場合は、"dependencies"ではなく"devDependencies"に追加できます。npmは、パッケージのディレクトリ内でnpm installを実行した場合にのみインストールしますが、依存関係としてインストールした場合はインストールしません。ユニットテストライブラリは、典型的な開発依存関係です。

開発依存関係をインストールする方法は2つあります。

2番目の方法は、パッケージが依存関係であるか開発依存関係であるかの決定を簡単に延期できることを意味します。

14.3.3 パッケージにコンテンツを追加する

readmeファイルと、シェルスクリプトである2つのモジュールhomedir.mjsversions.mjsを追加しましょう。

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

npmに2つのシェルスクリプトについて知らせ、インストールしてもらう必要があります。そのために、package.jsonのプロパティ"bin"を使用します。

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

このパッケージをインストールすると、homedirversionsという名前の2つのシェルスクリプトが利用可能になります。

シェルスクリプトにファイル拡張子.jsを使いたい場合もあるでしょう。その場合は、前のプロパティの代わりに、次の2つのプロパティをpackage.jsonに追加する必要があります。

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

最初のプロパティは、Node.jsに.jsファイルを(デフォルトのCommonJSモジュールではなく)ESMモジュールとして解釈するように指示します。

これがhomedir.mjsの内容です。

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

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

このモジュールは、Unixで使用する場合に必須の前述のハッシュバンで始まります。組み込みモジュールnode:osから関数homedir()をインポートし、それを呼び出して、結果をコンソール(つまり、標準出力)に出力します。

homedir.mjsは実行可能である必要はありません。npmは、インストール時に"bin"スクリプトの実行可能性を保証します(すぐに確認します)。

versions.mjsの内容は次のとおりです。

#!/usr/bin/env node

import {pick} from 'lodash-es';

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

Lodashから関数pick()をインポートし、それを使ってオブジェクトprocess.versionsの3つのプロパティを表示します。

14.3.4 インストールせずにシェルスクリプトを実行する

たとえば、次のようにしてhomedir.mjsを実行できます。

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

14.4 npmがシェルスクリプトをインストールする方法

14.4.1 Unixでのインストール

homedir.mjsのようなスクリプトは、npmが実行可能なシンボリックリンクを介してインストールするため、Unixで実行可能である必要はありません。

14.4.2 Windowsでのインストール

Windowsにhomedir.mjsをインストールするために、npmは3つのファイルを作成します。

npmはこれらのファイルをディレクトリに追加します。

14.5 npmレジストリへのサンプルパッケージの公開

(以前に作成した)パッケージ@rauschma/demo-shell-scriptsをnpmに公開しましょう。npm publishを使用してパッケージをアップロードする前に、すべてが正しく構成されていることを確認する必要があります。

14.5.1 どのファイルが公開されますか? どのファイルが無視されますか?

公開時にファイルを除外および含めるために、次のメカニズムが使用されます。

npmドキュメントには、公開時に何が含まれ、何が除外されるかについて詳細が記載されています。

14.5.2 パッケージが正しく構成されているかどうかの確認

パッケージをアップロードする前に、いくつかのことを確認できます。

14.5.2.1 アップロードされるファイルの確認

npm installドライランは、何もアップロードせずにコマンドを実行します。

npm publish --dry-run

これにより、アップロードされるファイルとパッケージに関するいくつかの統計が表示されます。

また、npmレジストリに存在する場合と同様にパッケージのアーカイブを作成できます。

npm pack

このコマンドは、現在のディレクトリにファイルrauschma-demo-shell-scripts-1.0.0.tgzを作成します。

14.5.2.2 パッケージをグローバルにインストールする – アップロードせずに

次の2つのコマンドのいずれかを使用して、パッケージをnpmレジストリに公開せずにグローバルにインストールできます。

npm link
npm install . -g

それが機能したかどうかを確認するために、新しいシェルを開き、2つのコマンドが利用可能かどうかを確認できます。また、グローバルにインストールされたすべてのパッケージをリストすることもできます。

npm ls -g
14.5.2.3 パッケージをローカルにインストールする(依存関係として) – アップロードせずに

パッケージを依存関係としてインストールするには、(ディレクトリdemo-shell-scriptsにいる間に)次のコマンドを実行する必要があります。

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

これで、たとえば、次の2つのコマンドのいずれかでhomedirを実行できます。

npx homedir
./node_modules/.bin/homedir

14.5.3 npm publish: パッケージをnpmレジストリにアップロードする

パッケージをアップロードする前に、npmユーザーアカウントを作成する必要があります。npmドキュメントで、その方法が説明されています。

そして、最後にパッケージを公開できます。

npm publish --access public

デフォルトは

オプション--accessは、最初に公開するときのみ効果があります。その後は省略でき、アクセスレベルを変更するにはnpm accessを使用する必要があります。

package.jsonpublishConfig.accessを介して、最初のnpm publishのデフォルトを変更できます。

"publishConfig": {
  "access": "public"
}
14.5.3.1 アップロードごとに新しいバージョンが必要

特定のバージョンでパッケージをアップロードしたら、そのバージョンを再度使用することはできません。バージョンの3つのコンポーネントのいずれかを増やす必要があります。

major.minor.patch

14.5.4 公開するたびにタスクを自動的に実行する

パッケージをアップロードするたびに実行したい手順がある場合があります。たとえば、

これは、package.jsonプロパティ"scripts"を介して自動的に実行できます。このプロパティは次のようになります。

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

mochaはユニットテストライブラリです。tscはTypeScriptコンパイラです。

次のパッケージスクリプトは、npm publishの前に実行されます。

このトピックの詳細については、§15「npmパッケージスクリプトによるクロスプラットフォームタスクの実行」を参照してください。

14.6 Unixでの任意の拡張子を使用したスタンドアロンNode.jsシェルスクリプト

14.6.1 Unix:カスタム実行可能ファイルによる任意のファイル拡張子

Node.jsバイナリnodeは、ファイルがどの種類のモジュールであるかを検出するためにファイル拡張子を使用します。現在、それをオーバーライドするコマンドラインオプションはありません。そして、デフォルトはCommonJSであり、これは私たちが望むものではありません。

ただし、Node.jsを実行するための独自の実行可能ファイルを作成し、たとえばnode-esmと呼ぶことができます。次に、以前のスタンドアロンスクリプトhello.mjsを(拡張子なしで)helloに名前変更できます。ただし、最初の行を次のように変更する必要があります。

#!/usr/bin/env node-esm

以前は、envの引数はnodeでした。

これは、Andrea Giammarchiが提案したnode-esmの実装です。

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

この実行可能ファイルは、標準入力を介してスクリプトの内容をnodeに送信します。コマンドラインオプション--input-type=moduleは、受信したテキストがESMモジュールであることをNode.jsに伝えます。

また、次のUnixシェル機能を使用します。

node-esmを使用する前に、実行可能であり、$PATHを介して見つけることができることを確認する必要があります。その方法は後で説明します。

14.6.2 Unix:シェルプロローグによる任意のファイル拡張子

ファイルに対してではなく、標準入力に対してのみモジュールタイプを指定できることがわかりました。したがって、Node.jsを使用して自分自身をESMモジュールとして実行するUnixシェルスクリプトhelloを作成できます(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}!`);

ここで使用しているシェル機能のほとんどは、この章の冒頭で説明されています。$?には、実行された最後のシェルコマンドの終了コードが含まれています。これにより、hellonodeと同じコードで終了できるようになります。

このスクリプトで使用される重要なトリックは、2行目がUnixシェルスクリプトコードとJavaScriptコードの両方であることです。

JavaScriptからシェルコードを隠すことの追加の利点は、JavaScriptエディターが構文を処理および表示するときに混乱しないことです。

14.7 WindowsでのスタンドアロンNode.jsシェルスクリプト

14.7.1 Windows:ファイル拡張子.mjsの構成

WindowsでスタンドアロンNode.jsシェルスクリプトを作成する1つのオプションは、ファイル拡張子.mjsを使用し、それを持つファイルがnodeを介して実行されるように構成することです。残念ながら、これはコマンドシェルでのみ機能し、PowerShellでは機能しません。

もう1つの欠点は、その方法ではスクリプトに引数を渡すことができないことです。

>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'
]

コマンドシェルでargs.mjsなどのファイルを直接実行するようにWindowsを構成するにはどうすればよいでしょうか?

ファイル関連付けは、シェルでファイル名を入力したときにファイルを開くアプリケーションを指定します。ファイル拡張子.mjsをNode.jsバイナリに関連付けると、シェルでESMモジュールを実行できます。その1つの方法は、Tim Fisherによる「Windowsでファイル関連付けを変更する方法」で説明されているように、設定アプリを使用することです。

さらに、変数%PATHEXT%.MJSを追加すると、ESMモジュールを参照する際にファイル拡張子を省略することもできます。この環境変数は、設定アプリで「変数」を検索することで永続的に変更できます。

14.7.2 Windowsコマンドシェル:シェルプロローグによるNode.jsスクリプト

Windowsでは、ハッシュバンのようなメカニズムがないという課題に直面しています。したがって、Unixで拡張子のないファイルに使用したのと同様の回避策を使用する必要があります。つまり、Node.jsを介してJavaScriptコードを内部で実行するスクリプトを作成します。

コマンドシェルスクリプトのファイル拡張子は.batです。script.batという名前のスクリプトは、script.batまたはscriptのどちらかで実行できます。

hello.mjsをコマンドシェルスクリプトhello.batに変換すると、次のようになります。

:: /*
@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}!`);

このコードをファイルとしてnodeを介して実行するには、存在しない2つの機能が必要です。

したがって、ファイルのコンテンツをnodeにパイプするしかありません。また、次のコマンドシェル機能を使用します。

14.7.3 Windows PowerShell:シェルプロローグによるNode.jsスクリプト

前のセクションで使用したのと同様のトリックを使用して、hello.mjsをPowerShellスクリプトhello.ps1に次のように変換できます。

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}!`);
// #>

このスクリプトは、次のどちらかで実行できます。

.\hello.ps1
.\hello

ただし、それを行う前に、PowerShellスクリプトを実行できるように実行ポリシーを設定する必要があります(実行ポリシーの詳細)。

次のコマンドを使用すると、ローカルスクリプトを実行できます。

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

14.8 Linux、macOS、Windows用のネイティブバイナリの作成

npmパッケージpkgは、Node.jsパッケージを、Node.jsがインストールされていないシステムでも実行できるネイティブバイナリに変換します。次のプラットフォームをサポートしています:Linux、macOS、Windows。

14.9 シェルパス:シェルがスクリプトを確実に検出できるようにする

ほとんどのシェルでは、ファイル名を入力する際にファイルを直接参照しなくても、その名前を持つファイルをいくつかのディレクトリで検索して実行できます。これらのディレクトリは通常、特別なシェル変数にリストされています。

PATH変数は2つの目的で必要です。

14.9.1 Unix:$PATH

ほとんどのUnixシェルには、コマンドを入力したときにシェルが実行可能ファイルを検索するすべてのパスをリストする変数$PATHがあります。その値は次のようになります。

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

次のコマンドはほとんどのシェルで動作し(ソース)、現在のシェルを終了するまで$PATHを変更します。

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

2つのシェル変数のいずれかにスペースが含まれる場合に備えて、引用符が必要です。

14.9.1.1 $PATHを永続的に変更する

Unixでは、$PATHの構成方法はシェルによって異なります。実行しているシェルは、次のようにして確認できます。

echo $0

MacOSはZshを使用しており、$PATHを永続的に構成するのに最適な場所は、スタートアップスクリプト$HOME/.zprofileです。 — このようになります

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

14.9.2 WindowsでのPATH変数の変更(コマンドシェル、PowerShell)

Windowsでは、コマンドシェルとPowerShellのデフォルトの環境変数を、(永続的に)設定アプリで設定できます。「変数」を検索してください。