使用 Node.js 編寫 shell script
你可以購買此書的離線版本(HTML、PDF、EPUB、MOBI),並支持免費的線上版本。
(廣告,請不要封鎖。)

14 建立跨平台 shell script



在本章中,我們將學習如何透過 Node.js ESM 模組實作殼層腳本。有兩種常見的方法可以執行此操作

14.1 必要的知識

您應該大致熟悉以下兩個主題

14.1.1 本章接下來的內容

Windows 並不真正支援以 JavaScript 撰寫的獨立殼層腳本。因此,我們將先探討如何為 Unix 撰寫具有檔名副檔名的獨立腳本。這項知識將有助於我們建立包含殼層腳本的套件。稍後,我們將學習

透過套件安裝殼層腳本的主題,請參閱§13「安裝 npm 套件和執行 bin 腳本」

14.2 Unix 上的 Node.js ESM 模組作為獨立殼層腳本

讓我們將 ESM 模組轉換為 Unix 殼層腳本,我們可以在套件內部執行它。原則上,我們可以在 ESM 模組中選擇兩個檔名副檔名

然而,由於我們要建立獨立腳本,因此我們無法依賴 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

./hello.mjs

這些事情是

14.2.2 Unix 上的 Hashbang

在 Unix shell script 中,第一行是一個 hashbang – 告訴 shell 如何執行檔案的元資料。例如,這是 Node.js 腳本最常見的 hashbang

#!/usr/bin/env node

這行之所以稱為「hashbang」,是因為它以雜湊符號和驚嘆號開頭。它也常被稱為「shebang」。

如果一行以雜湊開頭,在大部分 Unix shell(sh、bash、zsh 等)中會被視為註解。因此,那些 shell 會忽略 hashbang。Node.js 也會忽略它,但僅限於它是第一行時。

為什麼我們不使用這個 hashbang?

#!/usr/bin/node

並非所有 Unix 都會在該路徑安裝 Node.js 二進位檔。那這個路徑呢?

#!node

唉,並非所有 Unix 都允許相對路徑。這就是為什麼我們透過絕對路徑參考 env,並使用它為我們執行 node

有關 Unix hashbang 的更多資訊,請參閱 Alex Ewerlöf 的「Node.js shebang」。

14.2.2.1 傳遞引數給 Node.js 二進位檔

如果我們想要傳遞引數(例如命令列選項)給 Node.js 二進位檔,該怎麼辦?

一個在許多 Unix 上可行的解決方案是使用 env 的選項 -S,它可以防止 env 將其所有引數都解釋為二進位檔的單一名稱

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

在 macOS 上,即使沒有 -S,前一個指令也行得通;在 Linux 上通常不行。

14.2.2.2 Hashbang 陷阱:在 Windows 上建立 hashbang

如果我們在 Windows 上使用文字編輯器建立一個 ESM 模組,這個模組應該可以在 Unix 或 Windows 上以腳本執行,我們必須新增一個 hashbang。如果我們這麼做,第一行將以 Windows 行終結符 \r\n 結尾

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

在 Unix 上執行有這種 hashbang 的檔案會產生以下錯誤

env: node\r: No such file or directory

也就是說,env 認為可執行檔的名稱是 node\r。有兩種方法可以解決這個問題。

首先,有些編輯器會自動檢查檔案中已使用的行終結符,並持續使用它們。例如,Visual Studio Code 會在右下方的狀態列中顯示目前的行終結符(它稱之為「行尾序列」)

我們可以按一下該狀態資訊來選擇行終結符。

其次,我們可以在 Windows 上建立一個只有 Unix 行終結符的最小檔案 my-script.mjs,我們永遠不會在 Windows 上編輯它

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

14.2.3 在 Unix 上讓檔案可執行

要成為 shell 程式碼,除了要有 hashbang 之外,hello.mjs 還必須可執行(檔案的權限)

chmod u+x hello.mjs

請注意,我們讓建立檔案的使用者(u)可以執行檔案(x),而不是所有人。

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 使用 shell 程式碼建立 npm 套件

在本節中,我們將使用 shell 程式碼建立 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

一種選擇是建立套件,而不將其發布到 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 新增相依性

接下來,我們安裝一個相依性,我們想要在我們的其中一個程式碼中使用它 - 套件 lodash-esLodash 的 ESM 版本)

npm install lodash-es

此命令

如果我們只在開發期間使用套件,我們可以將其新增到 "devDependencies" 而不是 "dependencies",npm 將只會在我們於套件目錄中執行 npm install 時安裝它,但如果我們將其安裝為依賴項,則不會安裝它。單元測試程式庫是典型的開發依賴項。

以下兩種方式可以安裝開發依賴項

第二種方式表示我們可以輕鬆地延後決定套件是依賴項還是開發依賴項。

14.3.3 將內容新增到套件

讓我們新增自述檔案和兩個 shell 腳本模組 homedir.mjsversions.mjs

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

我們必須告知 npm 這兩個 shell 腳本,以便它可以為我們安裝它們。這就是 package.json"bin" 屬性的用途

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

如果我們安裝此套件,將會有兩個名為 homedirversions 的 shell 腳本可用。

您可能偏好 shell 腳本的檔案名稱副檔名 .js。然後,您必須將下列兩個屬性新增到 package.json,而不是前一個屬性

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

第一個屬性告訴 Node.js 它應該將 .js 檔案解釋為 ESM 模組(而不是 CommonJS 模組,這是預設值)。

以下是 homedir.mjs 的外觀

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

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

如果我們要在 Unix 上使用此模組,此模組會以前面提到的 hashbang 開頭。它會從內建模組 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 的三個屬性。

14.3.4 在未安裝的情況下執行 shell 腳本

我們可以像這樣執行,例如,homedir.mjs

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

14.4 npm 如何安裝 shell 腳本

14.4.1 在 Unix 上安裝

homedir.mjs 這樣的腳本在 Unix 上不需要可執行,因為 npm 會透過可執行符號連結來安裝它

14.4.2 在 Windows 上安裝

要在 Windows 上安裝 homedir.mjs,npm 會建立三個檔案

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 在全球安裝套件 – 不上傳

我們可以使用以下兩個指令中的任何一個,在全球安裝我們的套件,而不將其發佈到 npm 註冊表

npm link
npm install . -g

要查看是否成功,我們可以開啟一個新的 shell,並檢查這兩個指令是否可用。我們也可以列出所有在全球安裝的套件

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

現在,我們可以使用以下兩個指令中的任何一個來執行,例如,homedir

npx homedir
./node_modules/.bin/homedir

14.5.3 npm publish:將套件上傳到 npm 註冊表

在我們上傳套件之前,我們需要建立一個 npm 使用者帳戶。npm 文件 說明如何執行此操作

然後,我們終於可以發佈我們的套件

npm publish --access public

我們必須指定公開存取,因為預設為

選項 --access 僅在我們第一次發佈時有效。之後,我們可以省略它,並需要使用 npm access 來變更存取層級。

我們可以透過 package.json 中的 publishConfig.access 來變更初始 npm publish 的預設值

"publishConfig": {
  "access": "public"
}
14.5.3.1 每次上傳都需要一個新版本

一旦我們上傳了一個具有特定版本的套件,我們就不能再使用該版本,我們必須增加版本的三個組成部分中的任何一個

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 shell 指令碼

14.6.1 Unix:透過自訂執行檔使用任意檔案名稱副檔名

Node.js 二進位檔 node 使用檔案名稱副檔名來偵測檔案是哪種類型的模組。目前沒有命令列選項可以覆寫它。而且預設值是 CommonJS,這不是我們想要的。

不過,我們可以建立自己的執行檔來執行 Node.js,例如,將它命名為 node-esm。然後,如果我們將第一行變更為

#!/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 會告訴 Node.js 它接收到的文字是 ESM 模組。

我們也會使用以下 Unix shell 功能

在我們可以使用 node-esm 之前,我們必須確保它可執行,而且可以在 $PATH 中找到。稍後會說明如何執行此操作。

14.6.2 Unix:透過 shell 前導碼指定任意檔案副檔名

我們已經看到我們無法為檔案指定模組類型,只能為標準輸入指定。因此,我們可以撰寫一個 Unix shell 腳本 hello,它使用 Node.js 以 ESM 模組的身分執行它自己(根據 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}!`);

我們在此使用的 shell 功能大部分都說明於本章的開頭。$? 包含最後執行的 shell 命令的結束代碼。這讓 hello 能夠以與 node 相同的代碼結束。

此腳本使用的關鍵技巧是第二行同時是 Unix shell 腳本程式碼和 JavaScript 程式碼

將 shell 程式碼隱藏起來以避免 JavaScript 看到的另一個好處是,在處理和顯示語法時,JavaScript 編輯器不會感到困惑。

14.7 Windows 上的獨立 Node.js shell 腳本

14.7.1 Windows:設定檔案副檔名 .mjs

在 Windows 上建立獨立 Node.js shell 腳本的一個選項是使用檔案副檔名 .mjs,並設定它讓具有此副檔名的檔案透過 node 執行。唉,這僅適用於命令提示字元,不適用於 PowerShell。

另一個缺點是我們無法以這種方式將引數傳遞給腳本

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

我們要如何設定 Windows,讓命令提示字元直接執行像是 args.mjs 的檔案?

檔案關聯性 會指定當我們在 shell 中輸入檔案名稱時,要使用哪個應用程式開啟該檔案。如果我們將檔案副檔名 .mjs 與 Node.js 二進位檔關聯起來,我們就可以在 shell 中執行 ESM 模組。執行此操作的方法之一是透過設定應用程式,如 Tim Fisher 在 “如何在 Windows 中變更檔案關聯性” 中所說明的。

如果我們另外將 .MJS 加入變數 %PATHEXT%,我們甚至可以在參照 ESM 模組時省略檔案副檔名。此環境變數可以透過設定應用程式永久變更,請搜尋「變數」。

14.7.2 Windows 命令殼層:透過殼層序言執行 Node.js 腳本

在 Windows 上,我們面臨的挑戰是沒有像 hashbangs 那樣的機制。因此,我們必須使用一個解決方法,類似於我們在 Unix 上對沒有副檔名的檔案所使用的解決方法:我們建立一個腳本,透過 Node.js 在其自身內部執行 JavaScript 程式碼。

命令殼層腳本的檔案副檔名為 .bat。我們可以透過 script.batscript 來執行名為 script.bat 的腳本。

如果我們將 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 以檔案形式執行此程式碼需要兩個不存在的功能

因此,我們別無選擇,只能將檔案內容導向 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 Shell 路徑:確保 shell 找到腳本

在大部分 shell 中,我們可以在不直接參照檔案的情況下輸入檔名,它們會在幾個目錄中搜尋具有該名稱的檔案並執行它。這些目錄通常會列在特殊 shell 變數中

我們需要 PATH 變數來達成兩個目的

14.9.1 Unix:$PATH

大部分 Unix shell 都具有變數 $PATH,其中列出 shell 在我們輸入指令時尋找可執行檔的所有路徑。其值可能如下所示

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

下列指令適用於大部分 shell (來源),並會變更 $PATH,直到我們離開目前的 shell

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

如果兩個 shell 變數之一包含空白,則需要引號。

14.9.1.1 永久變更 $PATH

在 Unix 中,$PATH 的組態方式取決於 shell。您可以透過下列方式找出您正在執行的 shell

echo $0

MacOS 使用 Zsh,其中永久組態 $PATH 的最佳位置是啟動腳本 $HOME/.zprofile像這樣

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

14.9.2 變更 Windows 上的 PATH 變數(命令提示字元、PowerShell)

在 Windows 上,命令提示字元和 PowerShell 的預設環境變數可以透過設定應用程式(搜尋「變數」)進行組態(永久)。