Node.js Shell 脚本开发指南(下)

简介: Node.js Shell 脚本开发指南(下)

十四、创建跨平台 shell 脚本



  • 14.1 所需的知识
  • 14.1.1 本章的下一步是什么
  • 14.2 Node.js ESM 模块作为 Unix 上独立的 shell 脚本
  • 14.2.1 Unix 上的 Node.js shell 脚本
  • 14.2.2 Unix 上的 Hashbangs
  • 14.2.3 在 Unix 上使文件可执行
  • 14.2.4 直接运行hello.mjs
  • 14.3 使用 shell 脚本创建一个 npm 包
  • 14.3.1 设置包的目录
  • 14.3.2 添加依赖项
  • 14.3.3 向包添加内容
  • 14.3.4 在不安装的情况下运行 shell 脚本
  • 14.4 npm 如何安装 shell 脚本
  • 14.4.1 在 Unix 上安装
  • 14.4.2 在 Windows 上安装
  • 14.5 将示例包发布到 npm 注册表
  • 14.5.1 哪些文件被发布?哪些文件被忽略?
  • 14.5.2 检查包是否正确配置
  • 14.5.3 npm publish: 将包上传到 npm 注册表
  • 14.5.4 在发布之前自动执行任务
  • 14.6 Unix 上任意扩展名的独立 Node.js shell 脚本
  • 14.6.1 Unix:通过自定义可执行文件设置任意文件扩展名
  • 14.6.2 Unix:通过 shell prolog 设置任意文件扩展名
  • 14.7 Windows 上独立的 Node.js shell 脚本
  • 14.7.1 Windows:配置文件扩展名.mjs
  • 14.7.2 Windows 命令 shell:通过 shell prolog 运行 Node.js 脚本
  • 14.7.3 Windows PowerShell: 通过 shell prolog 运行 Node.js 脚本
  • 14.8 为 Linux、macOS 和 Windows 创建本机二进制文件
  • 14.9 Shell 路径:确保 shell 找到脚本
  • 14.9.1 Unix: $PATH
  • 14.9.2 在 Windows 上更改 PATH 变量(命令 shell、PowerShell)

在本章中,我们将学习如何通过 Node.js ESM 模块实现 shell 脚本。有两种常见的方法可以这样做:

  • 我们可以编写一个独立的脚本并自己安装它。
  • 我们可以把我们的脚本放在一个 npm 包中,并使用包管理器来安装它。这也给了我们选择将包发布到 npm 注册表的选项,这样其他人也可以安装它。

14.1 所需知识

你应该对以下两个主题有一定的了解:

  • ECMAScript 模块,如“JavaScript for impatient programmers”中的章节“模块”中所解释的。
  • npm 软件包,如§5“软件包:JavaScript 的软件分发单元”中所解释的。
14.1.1 本章的下一步

Windows 实际上不支持用 JavaScript 编写的独立的 shell 脚本。因此,我们首先要了解如何为 Unix 编写带有文件扩展名的独立脚本。这些知识将帮助我们创建包含 shell 脚本的软件包。后来,我们会学到:

  • 在 Windows 上编写独立的 shell 脚本的技巧。
  • 在 Unix 上编写独立的 shell 脚本不带文件扩展名的技巧。

通过软件包安装 shell 脚本是§13“安装 npm 软件包和运行 bin 脚本”的主题。

14.2 Node.js ESM 模块作为 Unix 上独立的 shell 脚本

让我们将一个 ESM 模块转换为 Unix shell 脚本,这样我们就可以在不在软件包中的情况下运行它。原则上,我们可以选择 ESM 模块的两个文件扩展名:

  • .mjs文件总是被解释为 ESM 模块。
  • 只有在最接近的package.json中有以下条目时,.js文件才会被解释为 ESM 模块:
"type": "module"

然而,由于我们想创建一个独立的脚本,我们不能依赖于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 shell 脚本

我们需要做两件事,这样我们才能像这样运行hello.mjs

./hello.mjs

这些事情是:

  • hello.mjs开头添加哈希标记
  • 使hello.mjs可执行
14.2.2 Unix 上的哈希标记

在 Unix shell 脚本中,第一行是哈希标记 - 元数据,告诉 shell 如何执行文件。例如,这是 Node.js 脚本最常见的哈希标记:

#!/usr/bin/env node

这一行被称为“哈希标记”,因为它以井号和感叹号开头。它也经常被称为“shebang”。

如果一行以井号开头,在大多数 Unix shell(sh、bash、zsh 等)中它是一个注释。因此,这些 shell 会忽略哈希标记。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 上使用env的一个解决方案是使用选项-S,这样可以防止它将其所有参数解释为一个二进制文件的名称:

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

在 macOS 上,即使没有-S,上一个命令也可以工作;在 Linux 上通常不行。

14.2.2.2 哈希标记陷阱:在 Windows 上创建哈希标记

如果我们在 Windows 上使用文本编辑器创建一个 ESM 模块,该模块应该在 Unix 或 Windows 上作为脚本运行,我们必须添加一个哈希标记。如果我们这样做,第一行将以 Windows 行终止符\r\n结束:

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

在 Unix 上运行带有这样一个哈希标记的文件会产生以下错误:

env: node\r: No such file or directory

也就是说,env认为可执行文件的名称是node\r。有两种方法可以解决这个问题。

首先,一些编辑器会自动检查文件中已经使用的行终止符,并继续使用它们。例如,Visual Studio Code 在右下角的状态栏中显示当前的行终止符(它称之为“行尾序列”):

  • LF(换行)用于 Unix 行终止符\n
  • CRLF(回车换行)用于 Windows 行终止符\r\n

我们可以通过点击状态信息来切换选择行终止符。

其次,我们可以创建一个最小的文件my-script.mjs,其中只有 Unix 行终止符,我们在 Windows 上从不编辑它:

#!/usr/bin/env node
import './main.mjs';
14.2.3 在 Unix 上使文件可执行

为了成为一个 shell 脚本,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"
}

解释:

  • 将包设为私有意味着不需要名称或版本,并且不能意外发布。
  • "UNLICENSED"拒绝他人以任何条件使用该包。
14.3.1.2 发布包的package.json

如果我们想将我们的包发布到 npm 注册表,我们的package.json如下所示:

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

对于您自己的包,您需要用适合您的包名替换"name"的值:

  • 或者一个全局唯一的名称。这样的名称应该只用于重要的包,因为我们不希望阻止其他人使用该名称。
  • 或者作用域名称:要发布一个包,你需要一个 npm 账户(如何获得一个账户将在后面解释)。你的账户名可以作为包名的作用域。例如,如果你的账户名是jane,你可以使用以下包名:
"name": "@jane/demo-shell-scripts"
14.3.2 添加依赖项

接下来,我们安装一个我们想在其中一个脚本中使用的依赖项 - 包lodash-esLodash的 ESM 版本):

npm install lodash-es

这个命令:

  • 创建目录node_modules
  • 将包lodash-es安装到其中。
  • package.json中添加以下属性:
"dependencies": {
 "lodash-es": "⁴.17.21"
}
  • 创建文件package-lock.json

如果我们只在开发过程中使用一个包,我们可以将其添加到"devDependencies"而不是"dependencies",npm 只有在我们在包的目录中运行npm install时才会安装它,而不是如果我们将它安装为一个依赖项。单元测试库是一个典型的开发依赖。

这是我们可以安装开发依赖的两种方式:

  • 通过npm install some-package
  • 我们可以使用npm install some-package --save-dev,然后手动将some-package的条目从"dependencies"移动到"devDependencies"

第二种方法意味着我们可以很容易地推迟决定一个包是一个依赖还是一个开发依赖。

14.3.3 向包添加内容

让我们添加一个 readme 文件和两个 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 脚本将变得可用。

你可能更喜欢使用.js作为 shell 脚本的文件扩展名。然后,你需要在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());

这个模块以前面提到的 hashbang 开始,这是在 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对象的三个属性。

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 通过可执行符号链接来安装它:

  • 如果我们全局安装包,链接将被添加到$PATH中列出的目录中。
  • 如果我们将包作为依赖项本地安装,链接将被添加到node_modules/.bin/
14.4.2 在 Windows 上安装

要在 Windows 上安装homedir.mjs,npm 会创建三个文件:

  • homedir.bat是一个使用node来执行homedir.mjs的命令 shell 脚本。
  • homedir.ps1对 PowerShell 也是一样的。
  • homedir对 Cygwin、MinGW 和 MSYS 也是一样的。

npm 会将这些文件添加到一个目录中:

  • 如果我们全局安装包,文件将被添加到列在%Path%中的目录中。
  • 如果我们将包作为依赖项本地安装,文件将被添加到node_modules/.bin/

14.5 将示例包发布到 npm 注册表

让我们将包@rauschma/demo-shell-scripts(之前创建的)发布到 npm。在使用npm publish上传包之前,我们应该检查一切是否配置正确。

14.5.1 发布了哪些文件?哪些文件被忽略了?

在发布时排除和包含文件时使用以下机制:

  • 顶层文件.gitignore中列出的文件会被排除。
  • 我们可以用与.gitignore相同的格式覆盖.npmignore
  • package.json属性"files"包含一个数组,其中包含要包括的文件的名称。这意味着我们可以选择列出要排除的文件(在.npmignore中)或要包括的文件。
  • 一些文件和目录默认被排除在外 - 例如:
  • node_modules
  • .*.swp
  • ._*
  • .DS_Store
  • .git
  • .gitignore
  • .npmignore
  • .npmrc
  • npm-debug.log
  • 除了这些默认值,点文件(文件名以点开头的文件)也会被包括进来。
  • 以下文件永远不会被排除:
  • package.json
  • README.md及其变体
  • CHANGELOG及其变体
  • LICENSELICENCE

npm 文档中有关于发布时包含和排除的更多细节

14.5.2 检查包是否正确配置

在上传包之前,我们可以检查几件事情。

14.5.2.1 检查将要上传的文件

npm installdry run会在不上传任何内容的情况下运行该命令:

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

我们必须指定公共访问权限,因为默认值是:

  • 对于未经范围限定的软件包,使用public
  • 对于受范围限制的软件包使用restricted。此设置使软件包private-这是一个付费的 npm 功能,主要由公司使用,并且与package.json中的"private":true不同。引用 npm:“使用 npm 私有软件包,您可以使用 npm 注册表来托管仅对您和选择的协作者可见的代码,允许您在项目中管理和使用私有代码以及公共代码。”

选项--access只在第一次发布时有效。之后,我们可以省略它,并且需要使用npm access来更改访问级别。

我们可以通过publishConfig.accesspackage.json中更改初始npm publish的默认值:

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

一旦我们使用特定版本上传了软件包,我们就不能再使用该版本,我们必须增加版本的三个组件中的任何一个:

major.minor.patch
  • 如果我们进行了重大更改,则增加major
  • 如果我们进行了向后兼容的更改,则增加minor
  • 如果我们进行了不会真正改变 API 的小修复,则增加patch
14.5.4 每次发布前自动执行任务

可能有一些步骤我们想要在上传软件包之前每次执行-例如:

  • 运行单元测试
  • 将 TypeScript 代码编译为 JavaScript 代码

这可以通过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之前运行以下软件包脚本:

  • "prepare"被运行:
  • npm pack之前
  • npm publish之前
  • 在本地npm install没有参数的情况下
  • "prepublishOnly"仅在npm publish之前运行。

有关此主题的更多信息,请参见§15“通过 npm 软件包脚本运行跨平台任务”。

14.6 在 Unix 上使用任意扩展名的独立 Node.js shell 脚本

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告诉 Node.js 它接收的文本是一个 ESM 模块。

我们还使用以下 Unix shell 功能:

  • $1包含传递给node-esm的第一个参数-脚本的路径。
  • 我们通过shift删除参数$0node-esm的路径)并将剩余的参数传递给node
  • execnode运行替换当前进程。这确保脚本以与node相同的代码退出。
  • 连字符(-)将 Node 的参数与脚本的参数分开。

在使用node-esm之前,我们必须确保它是可执行的,并且可以通过$PATH找到。如何做到这一点将在后面解释。

14.6.2 Unix:通过 shell prolog 任意文件扩展名

我们已经看到,我们无法为文件指定模块类型,只能为标准输入指定。因此,我们可以编写一个 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 脚本代码,它运行引用命令':',除了扩展其参数和执行重定向外,什么也不做。它的唯一参数是路径//。然后将当前文件的内容传递给node二进制文件。
  • 作为 JavaScript 代码,它是字符串':'(被解释为表达式语句并且什么也不做),然后是一个注释。

将 shell 代码从 JavaScript 中隐藏的另一个好处是,当处理和显示语法时,JavaScript 编辑器不会感到困惑。

14.7 在 Windows 上独立的 Node.js shell 脚本

14.7.1 Windows:配置文件扩展名.mjs

在 Windows 上创建独立的 Node.js shell 脚本的一个选项是使用文件扩展名.mjs并配置文件以便通过node运行。遗憾的是,这仅适用于命令 shell,而不适用于 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,使命令 shell 直接运行诸如args.mjs之类的文件?

文件关联指定在 shell 中输入其名称时打开文件的应用程序。如果我们将文件扩展名.mjs与 Node.js 二进制文件关联,我们可以在 shell 中运行 ESM 模块。其中一种方法是通过设置应用程序,如 Tim Fisher 在“如何更改 Windows 中的文件关联”中所解释的那样。

如果我们还将.MJS添加到变量%PATHEXT%中,甚至在引用 ESM 模块时可以省略文件扩展名。此环境变量可以通过设置应用程序永久更改-搜索“variables”。

14.7.2 Windows 命令 shell:通过 shell prolog 运行 Node.js 脚本

在 Windows 上,我们面临的挑战是没有像 hashbangs 这样的机制。因此,我们必须使用类似于我们在 Unix 上用于无扩展名文件的解决方法:创建一个通过 Node.js 在自身内部运行 JavaScript 代码的脚本。

命令 shell 脚本的文件扩展名是.bat。我们可以通过script.batscript运行名为script.bat的脚本。

如果我们将其转换为命令 shell 脚本hello.bat,则hello.mjs看起来是这样的:

:: /*
@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运行需要两个不存在的功能:

  • 使用命令行选项来覆盖默认情况下将无扩展名的文件解释为 ESM 模块。
  • 跳过文件开头的行。

因此,我们别无选择,只能将文件的内容传递给node。我们还使用以下命令 shell 功能:

  • %~f0包含当前脚本的完整路径,包括其文件扩展名。相比之下,%0包含用于调用脚本的命令。因此,前者的 shell 变量使我们能够通过hellohello.bat调用脚本。
  • %*包含命令的参数-我们将其传递给node
  • %errorlevel%包含上次执行的命令的退出代码。我们使用该值以与node指定的相同代码退出。
14.7.3 Windows PowerShell:通过 shell prolog 运行 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 脚本的执行策略(有关执行策略的更多信息):

  • Windows 客户端上的默认策略是“受限”,不允许我们运行任何脚本。
  • 策略RemoteSigned允许我们运行未签名的本地脚本。下载的脚本必须经过签名。这是 Windows 服务器上的默认设置。

以下命令让我们运行本地脚本:

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 变量中列出:

  • 在大多数 Unix shell 中,我们通过$PATH访问它。
  • 在 Windows 命令 shell 中,我们通过%Path%访问它。
  • 在 PowerShell 中,我们通过$Env:PATH访问它。

我们需要 PATH 变量有两个目的:

  • 如果我们想要安装我们自定义的 Node.js 可执行文件node-esm
  • 如果我们想要运行一个独立的 shell 脚本而不直接引用其文件。
14.9.1 Unix:$PATH

大多数 Unix shell 都有一个名为$PATH的变量,列出了当我们输入命令时 shell 查找可执行文件的所有路径。它的值可能如下所示:

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

以下命令适用于大多数 shell(来源),并且在我们离开当前 shell 之前更改$PATH

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 变量(命令 shell,PowerShell)

在 Windows 上,可以通过“设置”应用程序永久配置命令 shell 和 PowerShell 的默认环境变量-搜索“variables”。

评论

十五、通过 npm 包脚本运行跨平台任务

原文:exploringjs.com/nodejs-shell-scripting/ch_package-scripts.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 15.1 npm 包脚本
  • 15.1.1 运行包脚本的缩写 npm 命令
  • 15.1.2 用于运行包脚本的 shell
  • 15.1.3 防止自动运行包脚本
  • 15.1.4 在 Unix 上为包脚本获取选项完成
  • 15.1.5 列出和组织包脚本
  • 15.2 包脚本的种类
  • 15.2.1 预先和后置脚本
  • 15.2.2 生命周期脚本
  • 15.3 运行包脚本的 shell 环境
  • 15.3.1 当前目录
  • 15.3.2 shell 路径
  • 15.4 在包脚本中使用环境变量
  • 15.4.1 获取和设置环境变量
  • 15.4.2 通过.env文件设置环境变量
  • 15.5 包脚本的参数
  • 15.6 npm 日志级别(产生多少输出)
  • 15.6.1 日志级别和打印到终端的信息
  • 15.6.2 日志级别和写入 npm 日志的信息
  • 15.6.3 配置日志
  • 15.6.4 在npm install期间运行的生命周期脚本的输出
  • 15.6.5 观察 npm 日志的工作方式
  • 15.7 跨平台 shell 脚本
  • 15.7.1 路径和引用
  • 15.7.2 链接命令
  • 15.7.3 包脚本的退出代码
  • 15.7.4 管道和重定向输入和输出
  • 15.7.5 适用于两个平台的命令
  • 15.7.6 运行 bin 脚本和包内模块
  • 15.7.7 node --evalnode --print
  • 15.8 常见操作的辅助包
  • 15.8.1 从命令行运行包脚本
  • 15.8.2 并行或顺序运行多个脚本
  • 15.8.3 文件系统操作
  • 15.8.4 将文件或目录放入垃圾箱
  • 15.8.5 复制文件树
  • 15.8.6 监视文件
  • 15.8.7 其他功能
  • 15.8.8 HTTP 服务器
  • 15.9 扩展包脚本的功能
  • 15.9.1 per-env: 根据$NODE_ENV在脚本之间切换
  • 15.9.2 定义特定操作系统的脚本
  • 15.10 本章的来源

package.json有一个属性"scripts",让我们定义包脚本,执行与包相关的任务,如编译构件或运行测试的小型 shell 脚本。本章解释了它们以及我们如何编写它们,使它们在 Windows 和 Unix(macOS,Linux 等)上都能工作。

15.1 npm 包脚本

npm 包脚本通过package.json的属性"scripts"来定义:

{
 ···
 "scripts": {
 "tsc": "tsc",
 "tscwatch": "tsc --watch",
 "tscclean": "shx rm -rf ./dist/*"
 },
 ···
}

"scripts"的值是一个对象,其中每个属性定义了一个包脚本:

  • 属性键定义了脚本的名称。
  • 属性值定义了脚本运行时要执行的操作。

如果我们输入:

npm run <script-name>

然后 npm 在 shell 中执行名称为script-name的脚本。例如,我们可以使用:

npm run tscwatch

在 shell 中运行以下命令:

tsc --watch

在本章中,我们偶尔会使用npm选项-s,它是--silent的缩写,并告诉npm run产生更少的输出:

npm -s run <script-name>

此选项在日志部分中有更详细的介绍。

15.1.1 运行包脚本的更短的 npm 命令

一些包脚本可以通过更短的 npm 命令运行:

命令 等效
npm test, npm t npm run test
npm start npm run start
npm stop npm run stop
npm restart npm run restart
  • npm start: 如果没有包脚本"start",npm 会运行node server.js
  • npm restart: 如果没有包脚本"restart",npm 会运行"prerestart""stop""start""postrestart"
15.1.2 用于运行包脚本的 shell 是哪个?

默认情况下,npm 通过cmd.exe在 Windows 上运行包脚本,在 Unix 上通过/bin/sh运行。我们可以通过npm 配置设置script-shell来更改。

然而,这样做很少是一个好主意:许多现有的跨平台脚本都是为shcmd.exe编写的,将停止工作。

15.1.3 防止包脚本自动运行

一些脚本名称保留用于生命周期脚本,当我们执行特定的 npm 命令时,npm 会运行它们。

例如,当我们执行npm install(不带参数)时,npm 会运行脚本"postinstall"。生命周期脚本将在后面更详细地介绍。

如果配置设置ignore-scriptstrue,npm 永远不会自动运行脚本,只有在我们直接调用它们时才会运行。

15.1.4 在 Unix 上为包脚本获取 tab 补全

在 Unix 上,npm 支持通过npm completion为命令和包脚本名称进行 tab 补全。我们可以通过将以下行添加到我们的.profile / .zprofile / .bash_profile /等来安装它。

. <(npm completion)

如果您需要非 Unix 平台的 tab 补全,请搜索“npm tab completion PowerShell”等。

15.1.5 列出和组织包脚本

没有名称的npm run会列出可用的脚本。如果存在以下脚本:

"scripts": {
 "tsc": "tsc",
 "tscwatch": "tsc --watch",
 "serve": "serve ./site/"
}

然后它们会像这样列出:

% npm run
Scripts available via `npm run-script`:
  tsc
    tsc
  tscwatch
    tsc --watch
  serve
    serve ./site/
15.1.5.1 添加分隔符

如果有很多包脚本,我们可以滥用脚本名称作为分隔符(脚本 "help" 将在下一小节中解释):

"scripts": {
 "help": "scripts-help -w 40",
 "\n========== Building ==========": "",
 "tsc": "tsc",
 "tscwatch": "tsc --watch",
 "\n========== Serving ==========": "",
 "serve": "serve ./site/"
 },

现在脚本列出如下:

% npm run
Scripts available via `npm run-script`:
  help
    scripts-help -w 40
========== Building ==========
  tsc
    tsc
  tscwatch
    tsc --watch
========== Serving ==========
  serve
    serve ./site/

请注意,在 Unix 和 Windows 上都可以使用换行符(\n)的技巧。

15.1.5.2 打印帮助信息

包脚本 "help" 通过 package @rauschma/scripts-help 的 bin 脚本 scripts-help 打印帮助信息。我们通过 package.json 属性 "scripts-help" 提供描述("tscwatch" 的值被缩写以适应单行):

"scripts-help": {
 "tsc": "Compile the TypeScript to JavaScript.",
 "tscwatch": "Watch the TypeScript source code [...]",
 "serve": "Serve the generated website via a local server."
}

帮助信息如下所示:

% npm -s run help
Package “demo”
╔══════╤══════════════════════════╗
║ help │ scripts-help -w 40       ║
╚══════╧══════════════════════════╝
Building
╔══════════╤══════════════════════════════════════════╗
║ tsc      │ Compile the TypeScript to JavaScript.    ║
╟──────────┼──────────────────────────────────────────╢
║ tscwatch │ Watch the TypeScript source code and     ║
║          │ compile it incrementally when and if     ║
║          │ there are changes.                       ║
╚══════════╧══════════════════════════════════════════╝
Serving
╔═══════╤══════════════════════════════════════════╗
║ serve │ Serve the generated website via a local  ║
║       │ server.                                  ║
╚═══════╧══════════════════════════════════════════╝

15.2 包脚本的种类

如果某些名称用于脚本,则在某些情况下会自动运行:

  • Pre 脚本post 脚本 在脚本之前和之后运行。
  • 生命周期脚本 在用户执行诸如 npm install 之类的操作时运行。

所有其他脚本都被称为 直接运行脚本

15.2.1 Pre 和 post 脚本

每当 npm 运行包脚本 PS 时,它会自动运行以下脚本 - 如果它们存在的话:

  • prePS 之前(pre 脚本
  • postPS 之后(post 脚本

以下脚本包含预先脚本 prehello 和后置脚本 posthello

"scripts": {
 "hello": "echo hello",
 "prehello": "echo BEFORE",
 "posthello": "echo AFTER"
},

这是我们运行 hello 时会发生的事情:

% npm -s run hello
BEFORE
hello
AFTER
15.2.2 生命周期脚本

npm 在执行 npm publish 等命令期间运行 生命周期脚本

  • npm publish(上传包到 npm 注册表)
  • npm pack(为注册表包、包目录等创建归档)
  • npm install(无参数使用,用于安装从 npm 注册表以外的来源下载的包的依赖项)

如果任何生命周期脚本失败,整个命令将立即停止并显示错误。

生命周期脚本有哪些用例?

  • 编译 TypeScript:如果一个包包含 TypeScript 代码,我们通常会在使用之前将其编译为 JavaScript 代码。虽然后者的代码通常不会被检入版本控制,但它必须上传到 npm 注册表,以便从 JavaScript 中使用该包。生命周期脚本让我们在 npm publish 上传包之前编译 TypeScript 代码。这确保了在 npm 注册表中,JavaScript 代码始终与我们的 TypeScript 代码同步。它还确保我们的 TypeScript 代码没有静态类型错误,因为当遇到这些错误时,编译(因此发布)会停止。
  • 运行测试:我们还可以使用生命周期脚本在发布包之前运行测试。如果测试失败,包将不会被发布。

这些是最重要的生命周期脚本(有关所有生命周期脚本的详细信息,请参阅 npm 文档):

  • "prepare":
  • 在创建包归档(.tgz文件)之前运行:
  • npm publish 期间
  • npm pack 期间
  • 在从 git 或本地路径安装包时运行。
  • 在没有参数使用 npm install 或者全局安装包时运行。
  • "prepack"在创建包归档(.tgz文件)之前运行:
  • npm publish 期间
  • npm pack 期间
  • "prepublishOnly" 仅在 npm publish 期间运行。
  • "install"在没有参数使用npm install或者全局安装包时运行。
  • 请注意,我们还可以创建一个预先脚本 "preinstall" 和/或一个后置脚本 "postinstall"。它们的名称使得在 npm 运行它们时更清晰。

以下表格总结了这些生命周期脚本何时运行:

prepublishOnly prepack prepare install
npm publish
npm pack
npm install
全局安装
通过 git、路径安装

注意: 自动执行事务总是有点棘手。我通常遵循以下规则:

  • 我为自己自动化(例如通过 prepublishOnly)。
  • 我不为其他人自动化(例如通过 postinstall)。

15.3 包脚本运行的 shell 环境

在本节中,我们偶尔会使用

node -p <expr>

这个命令调用expr中的 JavaScript 代码,并将结果打印到终端 - 例如:

% node -p "'hello everyone!'.toUpperCase()" 
HELLO EVERYONE!
15.3.1 当前目录

当包脚本运行时,当前目录始终是包目录,与我们在其根目录的目录树中的位置无关。我们可以通过将以下脚本添加到package.json来确认:

"cwd": "node -p \"process.cwd()\""

让我们在 Unix 上尝试cwd

% cd /Users/robin/new-package/src/util 
% npm -s run cwd
/Users/robin/new-package

以这种方式改变当前目录有助于编写包脚本,因为我们可以使用相对于包目录的路径。

15.3.2 shell PATH

当模块M从以包P的名称开头的模块导入时,Node.js 会遍历node_modules目录,直到找到P的目录:

  • M的父目录中的第一个node_modules(如果存在)
  • M的父目录的父目录中的第二个node_modules(如果存在)
  • 依此类推,直到达到文件系统的根目录。

也就是说,M继承了其祖先目录的node_modules目录。

类似的继承方式也发生在 bin 脚本中,当我们安装一个包时,它们存储在node_modules/.bin中。npm run会临时将条目添加到 shell PATH 变量(Unix 上为$PATH,Windows 上为%Path%):

  • 包目录中的node_modules/.bin
  • 包目录的父目录中的node_modules/.bin
  • 等等。

要查看这些添加,我们可以使用以下包脚本:

"bin-dirs": "node -p \"JS\""

JS代表一行 JavaScript 代码:

(process.env.PATH ?? process.env.Path)
.split(path.delimiter)
.filter(p => p.includes('.bin'))

在 Unix 上,如果我们运行bin-dirs,我们会得到以下输出:

% npm -s run bin-dirs
[
  '/Users/robin/new-package/node_modules/.bin',
  '/Users/robin/node_modules/.bin',
  '/Users/node_modules/.bin',
  '/node_modules/.bin'
]

在 Windows 上,我们得到:

>npm -s run bin-dirs
[
  'C:\\Users\\charlie\\new-package\\node_modules\\.bin',
  'C:\\Users\\charlie\\node_modules\\.bin',
  'C:\\Users\\node_modules\\.bin',
  'C:\\node_modules\\.bin'
]

15.4 在包脚本中使用环境变量

在诸如 Make、Grunt 和 Gulp 之类的任务运行器中,变量很重要,因为它们有助于减少冗余。遗憾的是,虽然包脚本没有自己的变量,但我们可以通过使用环境变量(也称为shell 变量)来解决这个缺陷。

我们可以使用以下命令列出特定于平台的环境变量:

  • Unix:env
  • Windows 命令 shell:SET
  • 两个平台:node -p process.env

在 macOS 上,结果看起来像这样:

TERM_PROGRAM=Apple_Terminal
SHELL=/bin/zsh
TMPDIR=/var/folders/ph/sz0384m11vxf5byk12fzjms40000gn/T/
USER=robin
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
PWD=/Users/robin/new-package
HOME=/Users/robin
LOGNAME=robin
···

在 Windows 命令 shell 中,结果看起来像这样:

Path=C:\Windows;C:\Users\charlie\AppData\Roaming\npm;···
PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
PROMPT=$P$G
TEMP=C:\Users\charlie\AppData\Local\Temp
TMP=C:\Users\charlie\AppData\Local\Temp
USERNAME=charlie
USERPROFILE=C:\Users\charlie
···

此外,npm 在运行包脚本之前会临时添加更多的环境变量。为了查看最终结果是什么样子,我们可以使用以下命令:

npm run env

这个命令调用了一个内置的包脚本。让我们尝试一下这个package.json

{
 "name": "@my-scope/new-package",
 "version": "1.0.0",
 "bin": {
 "hello": "./hello.mjs"
 },
 "config": {
 "stringProp": "yes",
 "arrayProp": ["a", "b", "c"],
 "objectProp": {
 "one": 1,
 "two": 2
 }
 }
}

所有 npm 的临时变量的名称都以npm_开头。让我们按字母顺序打印这些变量:

npm run env | grep npm_ | sort

npm_变量具有分层结构。在npm_lifecycle_下,我们找到了当前运行的包脚本的名称和定义:

npm_lifecycle_event: 'env',
npm_lifecycle_script: 'env',

在 Windows 上,npm_lifecycle_script在这种情况下会SET

在前缀npm_config_下,我们可以看到一些 npm 的配置设置(在 npm 文档中有描述)。以下是一些例子:

npm_config_cache: '/Users/robin/.npm',
npm_config_global_prefix: '/usr/local',
npm_config_globalconfig: '/usr/local/etc/npmrc',
npm_config_local_prefix: '/Users/robin/new-package',
npm_config_prefix: '/usr/local'
npm_config_user_agent: 'npm/8.15.0 node/v18.7.0 darwin arm64 workspaces/false',
npm_config_userconfig: '/Users/robin/.npmrc',

前缀npm_package_让我们可以访问package.json的内容。其顶层看起来像这样:

npm_package_json: '/Users/robin/new-package/package.json',
npm_package_name: '@my-scope/new-package',
npm_package_version: '1.0.0',

npm_package_bin_下,我们可以找到package.json属性"bin"的属性:

npm_package_bin_hello: 'hello.mjs',

npm_package_config_条目让我们可以访问"config"的属性:

npm_package_config_arrayProp: 'a\n\nb\n\nc',
npm_package_config_objectProp_one: '1',
npm_package_config_objectProp_two: '2',
npm_package_config_stringProp: 'yes',

这意味着"config"让我们设置可以在包脚本中使用的变量。下一小节将进一步探讨这一点。

请注意,对象被转换为“嵌套”条目(第 2 行和第 3 行),而数组(第 1 行)和数字(第 2 行和第 3 行)被转换为字符串。

这些是剩下的npm_环境变量:

npm_command: 'run-script',
npm_execpath: '/usr/local/lib/node_modules/npm/bin/npm-cli.js',
npm_node_execpath: '/usr/local/bin/node',
15.4.1 获取和设置环境变量

以下的package.json演示了我们如何在包脚本中访问通过"config"定义的变量:

{
 "scripts": {
 "hi:unix": "echo $npm_package_config_hi",
 "hi:windows": "echo %npm_package_config_hi%"
 },
 "config": {
 "hi": "HELLO"
 }
}

遗憾的是,没有内置的跨平台方式可以从包脚本中访问环境变量。

然而,有一些带有 bin 脚本的包可以帮助我们。

Package env-var让我们获取环境变量:

"scripts": {
 "hi": "env-var echo {{npm_package_config_hi}}"
}

Package cross-env让我们设置环境变量:

"scripts": {
 "build": "cross-env FIRST=one SECOND=two node ./build.mjs"
}
15.4.2 通过.env文件设置环境变量

还有一些包可以让我们通过.env文件设置环境变量。这些文件具有以下格式:

# Comment
SECRET_HOST="https://example.com"
SECRET_KEY="123456789" # another comment

使用与package.json分开的文件使我们能够将数据排除在版本控制之外。

这些是支持.env文件的包:

  • Package dotenv支持 JavaScript 模块的.env文件。我们可以预加载它:
node -r dotenv/config app.mjs
  • 我们可以导入它:
import dotenv from 'dotenv';
dotenv.config();
console.log(process.env);
# Loads `.env` and runs an arbitrary shell script.
# If there are CLI options, we need to use `--`.
nodenv --exec node -- -p process.env.SECRET
# Loads `.env` and uses `node` to run `script.mjs`.
nodenv script.mjs
# Loads `.env` and runs an arbitrary shell script
env-cmd node -p process.env.SECRET
  • 该包还具有更多功能:在变量集之间切换,更多文件格式等。

15.5 包脚本的参数

让我们探讨如何将参数传递给我们通过包脚本调用的 shell 命令。我们将使用以下package.json

{
 ···
 "scripts": {
 "args": "log-args"
 },
 "dependencies": {
 "log-args": "¹.0.0"
 }
}

bin 脚本log-args如下所示:

for (const [key,value] of Object.entries(process.env)) {
 if (key.startsWith('npm_config_arg')) {
 console.log(`${key}=${JSON.stringify(value)}`);
 }
}
console.log(process.argv.slice(2));

位置参数按预期工作:

% npm -s run args three positional arguments
[ 'three', 'positional', 'arguments' ]

npm run使用选项并为它们创建环境变量。它们不会添加到process.argv中:

% npm -s run args --arg1='first arg' --arg2='second arg'
npm_config_arg2="second arg"
npm_config_arg1="first arg"
[]

如果我们希望选项出现在process.argv中,我们必须使用选项终结符--。该终结符通常在包脚本名称之后插入:

% npm -s run args -- --arg1='first arg' --arg2='second arg' 
[ '--arg1=first arg', '--arg2=second arg' ]

但我们也可以在该名称之前插入它:

% npm -s run -- args --arg1='first arg' --arg2='second arg' 
[ '--arg1=first arg', '--arg2=second arg' ]

15.6 npm 日志级别(产生多少输出)

npm 支持以下日志级别:

日志级别 npm选项 别名
静默 --loglevel silent -s --silent
错误 --loglevel error
警告 --loglevel warn -q --quiet
注意 --loglevel notice
http --loglevel http
时间 --loglevel timing
信息 --loglevel info -d
详细 --loglevel verbose -dd --verbose
荒谬 --loglevel silly -ddd

日志记录指的是两种活动:

  • 将信息打印到终端
  • 将信息写入 npm 日志

以下各小节描述:

  • 日志级别如何影响这些活动。原则上,silent记录最少,而silly记录最多。
  • 如何配置日志记录。前表显示了如何通过命令行选项临时更改日志级别,但还有更多设置。我们可以将它们临时或永久更改。
15.6.1 日志级别和打印到终端的信息

默认情况下,包脚本在终端输出方面相对冗长。例如,以下package.json文件:

{
 "name": "@my-scope/new-package",
 "version": "1.0.0",
 "scripts": {
 "hello": "echo Hello",
 "err": "more does-not-exist.txt"
 },
 ···
}

如果日志级别高于silent且包脚本在没有错误的情况下退出,则会发生以下情况:

% npm run hello
> @my-scope/new-package@1.0.0 hello
> echo Hello
Hello

如果日志级别高于silent且包脚本失败,则会发生以下情况:

% npm run err      
> @my-scope/new-package@1.0.0 err
> more does-not-exist.txt
does-not-exist.txt: No such file or directory

使用日志级别silent,输出变得不那么混乱:

% npm -s run hello
Hello
% npm -s run err
does-not-exist.txt: No such file or directory

一些错误被-s吞没:

% npm -s run abc
%

我们至少需要日志级别error才能看到它们:

% npm --loglevel error run abc
npm ERR! Missing script: "abc"
npm ERR! 
npm ERR! To see a list of scripts, run:
npm ERR!   npm run
npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/robin/.npm/_logs/2072-08-30T14_59_40_474Z-debug-0.log

不幸的是,日志级别silent也会抑制npm run的输出(无参数):

% npm -s run
%
15.6.2 日志级别和写入 npm 日志的信息

默认情况下,日志将被写入 npm 缓存目录,我们可以通过npm config获取其路径:

% npm config get cache
/Users/robin/.npm

日志目录的内容如下:

% ls -1 /Users/robin/.npm/_logs
2072-08-28T11_44_38_499Z-debug-0.log
2072-08-28T11_45_45_703Z-debug-0.log
2072-08-28T11_52_04_345Z-debug-0.log

日志中的每一行都以行索引和日志级别开头。这是一个使用日志级别notice写入的日志的示例。有趣的是,即使是比notice更详细的日志级别(如silly)也会显示出来:

0 verbose cli /usr/local/bin/node /usr/local/bin/npm
1 info using npm@8.15.0
···
33 silly logfile done cleaning log files
34 timing command:run Completed in 9ms
···

如果npm run返回错误,相应的日志以这种方式结束:

34 timing command:run Completed in 7ms
35 verbose exit 1
36 timing npm Completed in 28ms
37 verbose code 1

如果没有错误,相应的日志记录以这种方式结束:

34 timing command:run Completed in 7ms
35 verbose exit 0
36 timing npm Completed in 26ms
37 info ok
15.6.3 配置日志记录

npm config list --long打印各种设置的默认值。这些是与日志记录相关的设置的默认值:

% npm config list --long | grep log
loglevel = "notice"
logs-dir = null
logs-max = 10

如果logs-dir的值为null,npm 将使用 npm 缓存目录内的目录_logs(如前所述)。

  • logs-dir允许我们覆盖默认设置,使 npm 将其日志写入我们选择的目录。
  • logs-max允许我们配置 npm 在删除旧文件之前写入日志目录的文件数。如果将logs-max设置为 0,则不会写入任何日志。
  • loglevel允许我们配置 npm 的日志级别。

要永久更改这些设置,我们还可以使用npm config - 例如:

  • 获取当前的日志级别:
npm config get loglevel
  • 永久设置当前的日志级别:
npm config set loglevel silent
  • 永久重置日志级别为内置默认值:
npm config delete loglevel

我们还可以通过命令行选项临时更改设置 - 例如:

npm --loglevel silent run build

其他更改设置的方式(例如使用环境变量)由npm 文档解释。

15.6.4 在npm install期间运行的生命周期脚本的输出

npm install期间运行的生命周期脚本的输出(无参数)是隐藏的。我们可以通过(临时或永久)将foreground-scripts设置为true来更改这一点。

15.6.5 npm 日志工作的观察
  • 只有日志级别为silent时,使用npm run时才会关闭额外的输出。
  • 日志级别对于是否创建日志文件以及写入到日志文件中的内容没有影响。
  • 错误消息不会被写入日志。

15.7 跨平台 shell 脚本

用于包脚本的两个最常用的 shell 是:

  • Unix 上的sh
  • Windows 上的cmd.exe

在本节中,我们研究了在两个 shell 中都有效的构造。

15.7.1 路径和引用

提示:

  • 使用由斜杠分隔的相对路径段:Windows 接受斜杠作为分隔符,即使在该平台上通常使用反斜杠。
  • 双引号参数:虽然sh支持单引号,但 Windows 命令 shell 不支持。不幸的是,当我们在包脚本定义中使用双引号时,我们必须对其进行转义:
"dir": "mkdir \"\my dir""
15.7.2 链接命令

有两种方式可以链接在两个平台上都有效的命令:

  • &&之后的命令仅在前一个命令成功时执行(退出代码为 0)。
  • ||之后的命令仅在前一个命令失败时执行(退出代码不为 0)。

忽略退出代码的链接在不同平台上有所不同:

  • Unix:;
  • Windows 命令 shell:&

以下交互演示了在 Unix 上&&||的工作方式(在 Windows 上,我们会使用dir而不是ls):

% ls unknown && echo "SUCCESS" || echo "FAILURE"
ls: unknown: No such file or directory
FAILURE
% ls package.json && echo "SUCCESS" || echo "FAILURE"
package.json
SUCCESS
15.7.3 包脚本的退出代码

退出代码可以通过 shell 变量访问:

  • Unix:$?
  • Windows 命令 shell:%errorlevel%

npm run返回与上次执行的 shell 脚本相同的退出代码:

{
 ···
 "scripts": {
 "hello": "echo Hello",
 "err": "more does-not-exist.txt"
 }
}

以下交互发生在 Unix 上:

% npm -s run hello ; echo $?
Hello
0
% npm -s run err ; echo $?
does-not-exist.txt: No such file or directory
1
15.7.4 管道和重定向输入和输出
  • 在命令之间进行管道传输:|
  • 将输出写入文件:cmd > stdout-saved-to-file.txt
  • 从文件中读取输入:cmd < stdin-from-file.txt
15.7.5 在两个平台上都有效的命令

以下命令在两个平台上都存在(但在选项方面有所不同):

  • cd
  • echo。在 Windows 上要注意:双引号会被打印出来,而不是被忽略。
  • exit
  • mkdir
  • more
  • rmdir
  • sort
15.7.6 运行 bin 脚本和包内部模块

以下的package.json演示了在依赖项中调用 bin 脚本的三种方式:

{
 "scripts": {
 "hi1": "./node_modules/.bin/cowsay Hello",
 "hi2": "cowsay Hello",
 "hi3": "npx cowsay Hello"
 },
 "dependencies": {
 "cowsay": "¹.5.0"
 }
}

解释:

  • hi1:依赖项中的 bin 脚本安装在目录node_modules/.bin中。
  • hi2:正如我们所见,npm 在执行包脚本时会将node_modules/.bin添加到 shell PATH 中。这意味着我们可以像全局安装一样使用本地 bin 脚本。
  • hi3:当npx运行脚本时,它还会将node_modules/.bin添加到 shell PATH 中。

在 Unix 上,我们可以直接调用包本地脚本 - 如果它们有 hashbangs 并且是可执行的。然而,在 Windows 上这种方法行不通,这就是为什么最好通过node来调用它们:

"build": "node ./build.mjs"
15.7.7 node --evalnode --print

当一个包脚本的功能变得太复杂时,通常最好通过 Node.js 模块来实现它 - 这样可以轻松编写跨平台代码。

但是,我们也可以使用node命令来运行小的 JavaScript 片段,这对于以跨平台的方式执行小任务非常有用。相关的选项是:

  • 缩写:node -e
  • 缩写:node -p

以下命令在 Unix 和 Windows 上都适用(只有注释是 Unix 特定的):

# Print a string to the terminal (cross-platform echo)
node -p "'How are you?'"
# Print the value of an environment variable
# (Alas, we can’t change variables via `process.env`)
node -p process.env.USER # only Unix
node -p process.env.USERNAME # only Windows
node -p "process.env.USER ?? process.env.USERNAME"
# Print all environment variables
node -p process.env
# Print the current working directory
node -p "process.cwd()"
# Print the path of the current home directory
node -p "os.homedir()"
# Print the path of the current temporary directory
node -p "os.tmpdir()"
# Print the contents of a text file
node -p "fs.readFileSync('package.json', 'utf-8')"
# Write a string to a file
node -e "fs.writeFileSync('file.txt', 'Text content', 'utf-8')"

如果我们需要特定于平台的行终止符,我们可以使用os.EOL,例如,我们可以在前一个命令中用'Text content'替换:

`line 1${os.EOL}line2${os.EOL}`

观察:

  • 如果 JavaScript 代码包含括号,将其放在双引号中很重要,否则 Unix 会报错。
  • 所有内置模块都可以通过变量访问。这就是为什么我们不需要导入osfs
  • fs支持更多的文件系统操作。这些在§8“在 Node.js 上使用文件系统”中有文档记录。

15.8 常见操作的辅助软件包

15.8.1 从命令行运行软件包脚本

npm-quick-run提供了一个 bin 脚本nr,让我们可以使用缩写来运行软件包脚本,例如:

  • nr m -w执行"npm run mocha -- -w"(如果"mocha"是以“m”开头的第一个软件包脚本)。
  • nr c:o运行软件包脚本"cypress:open"
  • 等等。
15.8.2 同时或顺序运行多个脚本

同时运行 shell 脚本:

  • Unix:&
  • Windows 命令 shell:start

以下两个软件包为我们提供了跨平台的选项和相关功能:

concurrently "npm run clean" "npm run build"
  • 调用软件包脚本的更方便的方式。以下两个命令是等价的:
npm-run-all clean lint build
npm run clean && npm run lint && npm run build
  • 同时运行软件包脚本:
npm-run-all --parallel lint build
  • 使用通配符运行多个脚本,例如,watch:*代表所有以watch:开头的软件包脚本(watch:htmlwatch:js等):
npm-run-all "watch:*"
npm-run-all --parallel "watch:*"
15.8.3 文件系统操作

Package shx让我们可以使用“Unix 语法”来运行各种文件系统操作。它在 Unix 和 Windows 上的所有操作都有效。

创建目录:

"create-asset-dir": "shx mkdir ./assets"

删除目录:

"remove-asset-dir": "shx rm -rf ./assets"

清空目录(双引号是为了安全起见,关于通配符*):

"tscclean": "shx rm -rf \"./dist/*\""

复制文件:

"copy-index": "shx cp ./html/index.html ./out/index.html"

删除文件:

"remove-index": "shx rm ./out/index.html"

shx基于 JavaScript 库 ShellJS,其存储库列出了所有支持的命令。除了我们已经看到的 Unix 命令之外,它还模拟:catchmodechofindgrepheadlnlsmvpwdsedsorttailtouchuniq等。

15.8.4 将文件或目录放入垃圾箱

Package trash-cli适用于 macOS(10.12+)、Linux 和 Windows(8+)。它将文件和目录放入垃圾箱,并支持路径和 glob 模式。以下是使用它的示例:

trash tmp-file.txt
trash tmp-dir
trash "*.jpg"
15.8.5 复制文件树

Package copyfiles让我们可以复制文件树。

copyfiles的用例如下:在 TypeScript 中,我们可以导入非代码资产,如 CSS 和图像。TypeScript 编译器将代码编译到“dist”(输出)目录,但忽略非代码资产。这个跨平台的 shell 命令将它们复制到 dist 目录:

copyfiles --up 1 "./ts/**/*.{css,png,svg,gif}" ./dist

TypeScript 编译:

my-pkg/ts/client/picker.ts  -> my-pkg/dist/client/picker.js

copy-assets复制:

my-pkg/ts/client/picker.css -> my-pkg/dist/client/picker.css
my-pkg/ts/client/icon.svg   -> my-pkg/dist/client/icon.svg
15.8.6 监视文件

Package onchange监视文件并在每次更改时运行 shell 命令,例如:

onchange 'app/**/*.js' 'test/**/*.js' -- npm test

一个常见的替代方案(还有许多其他):

15.8.7 其他功能
  • cli-error-notifier 如果脚本失败(具有非零退出代码),则显示本机桌面通知。它支持许多操作系统。
15.8.8 HTTP 服务器

在开发过程中,通常需要一个 HTTP 服务器。以下包(以及许多其他包)可以帮助:

15.9 扩展包脚本的功能

15.9.1 per-env: 根据 $NODE_ENV 在不同脚本之间切换

The bin script per-env 允许我们运行一个包脚本 SCRIPT,并根据环境变量 NODE_ENV 的值自动在 SCRIPT:developmentSCRIPT:stagingSCRIPT:production 之间切换:

{
 "scripts": {
 // If NODE_ENV is missing, the default is "development"
 "build": "per-env",
 "build:development": "webpack -d --watch",
 "build:staging": "webpack -p",
 "build:production": "webpack -p"
 },
 // Processes spawned by `per-env` inherit environment-specific
 // variables, if defined.
 "per-env": {
 "production": {
 "DOCKER_USER": "my",
 "DOCKER_REPO": "project"
 }
 }
}
15.9.2 定义特定操作系统的脚本

The bin script cross-os 根据当前操作系统在不同脚本之间切换。

{
 "scripts": {
 "user": "cross-os user"
 },
 "cross-os": {
 "user": {
 "darwin": "echo $USER",
 "win32": "echo %USERNAME%",
 "linux": "echo $USER"
 }
 },
 ···
}

支持的属性值有:darwinfreebsdlinuxsunoswin32

15.10 本章的来源

Comments

第五部分:在脚本中处理常见任务

原文:exploringjs.com/nodejs-shell-scripting/pt_scripts.html

译者:飞龙

协议:CC BY-NC-SA 4.0

接下来:16 使用 util.parseArgs() 解析命令行参数

十六、使用 util.parseArgs()解析命令行参数

原文:exploringjs.com/nodejs-shell-scripting/ch_node-util-parseargs.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 16.1 本章中隐含的导入
  • 16.2 处理命令行参数的步骤
  • 16.3 解析命令行参数
  • 16.3.1 基础知识
  • 16.3.2 多次使用选项
  • 16.3.3 更多使用长选项和短选项的方式
  • 16.3.4 引用值
  • 16.3.5 选项终结符
  • 16.3.6 严格的parseArgs()
  • 16.4 parseArgstokens
  • 16.4.1 令牌示例
  • 16.4.2 使用令牌实现子命令

在这一章中,我们将探讨如何使用 Node.js 模块node:util中的parseArgs()函数来解析命令行参数。

16.1 本章中隐含的导入

在本章的每个示例中都隐含了以下两个导入:

import * as assert from 'node:assert/strict';
import {parseArgs} from 'node:util';

第一个导入是用于测试断言,用于检查值。第二个导入是用于本章主题parseArgs()函数。

16.2 处理命令行参数的步骤

处理命令行参数涉及以下步骤:

  1. 用户输入一个文本字符串。
  2. shell 将字符串解析为一系列单词和操作符。
  3. 如果调用一个命令,它会得到零个或多个单词作为参数。
  4. 我们的 Node.js 代码通过存储在process.argv中的数组接收这些单词。process是 Node.js 上的全局变量。
  5. 我们使用parseArgs()将数组转换为更方便处理的形式。

让我们使用以下的 shell 脚本args.mjs和 Node.js 代码来看看process.argv是什么样子的:

#!/usr/bin/env node
console.log(process.argv);

我们从一个简单的命令开始:

% ./args.mjs one two
[ '/usr/bin/node', '/home/john/args.mjs', 'one', 'two' ]

如果我们在 Windows 上通过 npm 安装命令,同样的命令在 Windows 命令 shell 上会产生以下结果:

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

无论我们如何调用 shell 脚本,process.argv始终以用于运行我们的代码的 Node.js 二进制文件的路径开始。接下来是我们脚本的路径。数组以传递给脚本的实际参数结束。换句话说:脚本的参数始终从索引 2 开始。

因此,我们改变我们的脚本,使其看起来像这样:

#!/usr/bin/env node
console.log(process.argv.slice(2));

让我们尝试更复杂的参数:

% ./args.mjs --str abc --bool home.html main.js
[ '--str', 'abc', '--bool', 'home.html', 'main.js' ]

这些参数包括:

  • 选项--str,其值是文本abc。这样的选项称为字符串选项
  • 选项--bool,它没有关联的值 - 它是一个标志,要么存在要么不存在。这样的选项称为布尔选项
  • 两个所谓的位置参数,没有名称:home.htmlmain.js

常见的使用参数的两种方式:

  • 主要参数是位置参数,选项提供额外的 - 通常是可选的 - 信息。
  • 只使用选项。

作为 JavaScript 函数调用,前面的示例看起来像这样(在 JavaScript 中,选项通常放在最后):

argsMjs('home.html', 'main.js', {str: 'abc', bool: false});

16.3 解析命令行参数

16.3.1 基础知识

如果我们希望parseArgs()解析带有参数的数组,我们首先需要告诉它我们的选项是如何工作的。假设我们的脚本有:

  • 一个布尔选项--verbose
  • 一个接收非负整数的选项--timesparseArgs()对数字没有特殊支持,所以我们必须将其作为字符串选项。
  • 一个字符串选项--color

我们将这些选项描述为 parseArgs() 如下:

const options = {
 'verbose': {
 type: 'boolean',
 short: 'v',
 },
 'color': {
 type: 'string',
 short: 'c',
 },
 'times': {
 type: 'string',
 short: 't',
 },
};

只要 options 的属性键是有效的 JavaScript 标识符,你可以选择是否引用它。两者都有利弊。在本章中,它们总是被引用。这样,具有非标识符名称的选项(如 my-new-option)看起来与具有标识符名称的选项相同。

options 中的每个条目都可以具有以下属性(通过 TypeScript 类型定义):

type Options = {
 type: 'boolean' | 'string', // required
 short?: string, // optional
 multiple?: boolean, // optional, default `false`
};
  • .type 指定选项是布尔值还是字符串。
  • .short 定义选项的短版本。必须是单个字符。我们很快将看到如何使用短版本。
  • .multiple 指示选项是否最多可以使用一次或零次或多次。稍后我们将看到这意味着什么。

以下代码使用 parseArgs()options 解析带有参数的数组:

assert.deepEqual(
 parseArgs({options, args: [
 '--verbose', '--color', 'green', '--times', '5'
 ]}),
 {
 values: {__proto__:null,
 verbose: true,
 color: 'green',
 times: '5'
 },
 positionals: []
 }
);

存储在 .values 中的对象的原型是 null。这意味着我们可以使用 in 运算符来检查属性是否存在,而不必担心继承的属性,如 .toString

如前所述,--times 的值为 5,被处理为字符串。

我们传递给 parseArgs() 的对象具有以下 TypeScript 类型:

type ParseArgsProps = {
 options?: {[key: string], Options}, // optional, default: {}
 args?: Array<string>, // optional
 // default: process.argv.slice(2)
 strict?: boolean, // optional, default `true`
 allowPositionals?: boolean, // optional, default `false`
};
  • .args:要解析的参数。如果省略此属性,parseArgs() 将使用 process.argv,从索引 2 开始。
  • .strict:如果为 true,则如果 args 不正确,将抛出异常。稍后详细介绍。
  • .allowPositionalsargs 是否包含位置参数?

这是 parseArgs() 的结果类型:

type ParseArgsResult = {
 values: {[key: string]: ValuesValue}, // an object
 positionals: Array<string>, // always an Array
};
type ValuesValue = boolean | string | Array<boolean|string>;
  • .values 包含可选参数。我们已经看到字符串和布尔值作为属性值。当我们探索具有 .multipletrue 的选项定义时,我们将看到数组值属性。
  • .positionals 包含位置参数。

两个连字符用于引用选项的长版本。一个连字符用于引用短版本:

assert.deepEqual(
 parseArgs({options, args: ['-v', '-c', 'green']}),
 {
 values: {__proto__:null,
 verbose: true,
 color: 'green',
 },
 positionals: []
 }
);

请注意,.values 包含选项的长名称。

我们通过解析混合了可选参数的位置参数来结束本小节:

assert.deepEqual(
 parseArgs({
 options,
 allowPositionals: true,
 args: [
 'home.html', '--verbose', 'main.js', '--color', 'red', 'post.md'
 ]
 }),
 {
 values: {__proto__:null,
 verbose: true,
 color: 'red',
 },
 positionals: [
 'home.html', 'main.js', 'post.md'
 ]
 }
);
16.3.2 多次使用选项

如果我们多次使用一个选项,默认情况下只有最后一次计数。它会覆盖所有先前的出现:

const options = {
 'bool': {
 type: 'boolean',
 },
 'str': {
 type: 'string',
 },
};
assert.deepEqual(
 parseArgs({
 options, args: [
 '--bool', '--bool', '--str', 'yes', '--str', 'no'
 ]
 }),
 {
 values: {__proto__:null,
 bool: true,
 str: 'no'
 },
 positionals: []
 }
);

然而,如果我们在选项的定义中将 .multiple 设置为 trueparseArgs() 将以数组形式给出所有选项值:

const options = {
 'bool': {
 type: 'boolean',
 multiple: true,
 },
 'str': {
 type: 'string',
 multiple: true,
 },
};
assert.deepEqual(
 parseArgs({
 options, args: [
 '--bool', '--bool', '--str', 'yes', '--str', 'no'
 ]
 }),
 {
 values: {__proto__:null,
 bool: [ true, true ],
 str: [ 'yes', 'no' ]
 },
 positionals: []
 }
);
16.3.3 更多使用长和短选项的方式

考虑以下选项:

const options = {
 'verbose': {
 type: 'boolean',
 short: 'v',
 },
 'silent': {
 type: 'boolean',
 short: 's',
 },
 'color': {
 type: 'string',
 short: 'c',
 },
};

以下是使用多个布尔选项的简洁方式:

assert.deepEqual(
 parseArgs({options, args: ['-vs']}),
 {
 values: {__proto__:null,
 verbose: true,
 silent: true,
 },
 positionals: []
 }
);

我们可以通过等号直接附加长字符串选项的值。这称为内联值

assert.deepEqual(
 parseArgs({options, args: ['--color=green']}),
 {
 values: {__proto__:null,
 color: 'green'
 },
 positionals: []
 }
);

短选项不能有内联值。

16.3.4 引用值

到目前为止,所有选项值和位置值都是单词。如果我们想使用包含空格的值,我们需要用双引号或单引号引起来。然而,并非所有 shell 都支持后者。

16.3.4.1 Shell 如何解析带引号的值

为了检查 shell 如何解析带引号的值,我们再次使用脚本 args.mjs

#!/usr/bin/env node
console.log(process.argv.slice(2));

在 Unix 上,双引号和单引号之间的区别如下:

  • 双引号:我们可以用反斜杠转义引号(否则原样传递),并且可以插入变量:
% ./args.mjs "say \"hi\"" "\t\n" "$USER"
[ 'say "hi"', '\\t\\n', 'rauschma' ]
  • 单引号:所有内容都原样传递,我们无法转义引号:
% ./args.mjs 'back slash\' '\t\n' '$USER' 
[ 'back slash\\', '\\t\\n', '$USER' ]

以下交互演示了双引号和单引号的选项值:

% ./args.mjs --str "two words" --str 'two words'
[ '--str', 'two words', '--str', 'two words' ]
% ./args.mjs --str="two words" --str='two words'
[ '--str=two words', '--str=two words' ]
% ./args.mjs -s "two words" -s 'two words'
[ '-s', 'two words', '-s', 'two words' ]

在 Windows 命令 shell 中,单引号没有任何特殊含义:

>node args.mjs "say \"hi\"" "\t\n" "%USERNAME%"
[ 'say "hi"', '\\t\\n', 'jane' ]
>node args.mjs 'back slash\' '\t\n' '%USERNAME%'
[ "'back", "slash\\'", "'\\t\\n'", "'jane'" ]

在 Windows 命令 shell 中引用的选项值:

>node args.mjs --str 'two words' --str "two words"
[ '--str', "'two", "words'", '--str', 'two words' ]
>node args.mjs --str='two words' --str="two words"
[ "--str='two", "words'", '--str=two words' ]
>>node args.mjs -s "two words" -s 'two words'
[ '-s', 'two words', '-s', "'two", "words'" ]

在 Windows PowerShell 中,我们可以用单引号引用,变量名不会在引号内插值,而且单引号无法转义:

> node args.mjs "say `"hi`"" "\t\n" "%USERNAME%"
[ 'say hi', '\\t\\n', '%USERNAME%' ]
> node args.mjs 'backtick`' '\t\n' '%USERNAME%'
[ 'backtick`', '\\t\\n', '%USERNAME%' ]
16.3.4.2 parseArgs() 如何处理带引号的值

这是 parseArgs() 如何处理带引号的值:

const options = {
 'times': {
 type: 'string',
 short: 't',
 },
 'color': {
 type: 'string',
 short: 'c',
 },
};
// Quoted external option values
assert.deepEqual(
 parseArgs({
 options,
 args: ['-t', '5 times', '--color', 'light green']
 }),
 {
 values: {__proto__:null,
 times: '5 times',
 color: 'light green',
 },
 positionals: []
 }
);
// Quoted inline option values
assert.deepEqual(
 parseArgs({
 options,
 args: ['--color=light green']
 }),
 {
 values: {__proto__:null,
 color: 'light green',
 },
 positionals: []
 }
);
// Quoted positional values
assert.deepEqual(
 parseArgs({
 options, allowPositionals: true,
 args: ['two words', 'more words']
 }),
 {
 values: {__proto__:null,
 },
 positionals: [ 'two words', 'more words' ]
 }
);
16.3.5 选项终结符

parseArgs() 支持所谓的选项终结符:如果 args 的一个元素是双连字符(--),那么剩余的参数都被视为位置参数。

选项终止符在哪里需要?一些可执行文件调用其他可执行文件,例如node 可执行文件。然后,可以使用选项终止符将调用者的参数与被调用者的参数分开。

这是parseArgs()处理选项终止符的方式:

const options = {
 'verbose': {
 type: 'boolean',
 },
 'count': {
 type: 'string',
 },
};
assert.deepEqual(
 parseArgs({options, allowPositionals: true,
 args: [
 'how', '--verbose', 'are', '--', '--count', '5', 'you'
 ]
 }),
 {
 values: {__proto__:null,
 verbose: true
 },
 positionals: [ 'how', 'are', '--count', '5', 'you' ]
 }
);
16.3.6 严格的parseArgs()

如果选项.stricttrue(这是默认值),那么parseArgs()如果发生以下情况之一,将抛出异常:

  • args中使用的选项名称不在options中。
  • args中的选项类型错误。目前,仅当字符串选项缺少参数时才会发生这种情况。
  • args中有位置参数,即使.allowPositionsfalse(这是默认值)。

以下代码演示了每种情况:

const options = {
 'str': {
 type: 'string',
 },
};
// Unknown option name
assert.throws(
 () => parseArgs({
 options,
 args: ['--unknown']
 }),
 {
 name: 'TypeError',
 message: "Unknown option '--unknown'",
 }
);
// Wrong option type (missing value)
assert.throws(
 () => parseArgs({
 options,
 args: ['--str']
 }),
 {
 name: 'TypeError',
 message: "Option '--str <value>' argument missing",
 }
);
// Unallowed positional
assert.throws(
 () => parseArgs({
 options,
 allowPositionals: false, // (the default)
 args: ['posarg']
 }),
 {
 name: 'TypeError',
 message: "Unexpected argument 'posarg'. " +
 "This command does not take positional arguments",
 }
);

16.4 parseArgs标记

parseArgs()在两个阶段处理args数组:

  • 阶段 1:它将args解析为标记数组:这些标记大多是带有类型信息的args元素:它是一个选项吗?它是一个位置参数吗?等等。但是,如果选项有一个值,那么标记将存储选项名称和选项值,因此包含两个args元素的数据。
  • 阶段 2:它将标记组装成通过结果属性.values返回的对象。

如果将config.tokens设置为true,则可以访问标记。然后,parseArgs()返回的对象包含一个名为.tokens的属性,其中包含标记。

这些是标记的属性:

type Token = OptionToken | PositionalToken | OptionTerminatorToken;
interface CommonTokenProperties {
 /** Where in `args` does the token start? */
 index: number;
}
interface OptionToken extends CommonTokenProperties {
 kind: 'option';
 /** Long name of option */
 name: string;
 /** The option name as mentioned in `args` */
 rawName: string;
 /** The option’s value. `undefined` for boolean options. */
 value: string | undefined;
 /** Is the option value specified inline (e.g. --level=5)? */
 inlineValue: boolean | undefined;
}
interface PositionalToken extends CommonTokenProperties {
 kind: 'positional';
 /** The value of the positional, args[token.index] */
 value: string;
}
interface OptionTerminatorToken extends CommonTokenProperties {
 kind: 'option-terminator';
}
16.4.1 标记示例

例如,考虑以下选项:

const options = {
 'bool': {
 type: 'boolean',
 short: 'b',
 },
 'flag': {
 type: 'boolean',
 short: 'f',
 },
 'str': {
 type: 'string',
 short: 's',
 },
};

布尔选项的标记如下:

assert.deepEqual(
 parseArgs({
 options, tokens: true,
 args: [
 '--bool', '-b', '-bf',
 ]
 }),
 {
 values: {__proto__:null,
 bool: true,
 flag: true,
 },
 positionals: [],
 tokens: [
 {
 kind: 'option',
 name: 'bool',
 rawName: '--bool',
 index: 0,
 value: undefined,
 inlineValue: undefined
 },
 {
 kind: 'option',
 name: 'bool',
 rawName: '-b',
 index: 1,
 value: undefined,
 inlineValue: undefined
 },
 {
 kind: 'option',
 name: 'bool',
 rawName: '-b',
 index: 2,
 value: undefined,
 inlineValue: undefined
 },
 {
 kind: 'option',
 name: 'flag',
 rawName: '-f',
 index: 2,
 value: undefined,
 inlineValue: undefined
 },
 ]
 }
);

请注意,对于选项bool,有三个标记,因为它在args中被提及三次。但是,由于解析的第二阶段,.values中只有一个bool属性。

在下一个示例中,我们将字符串选项解析为标记。.inlineValue现在具有布尔值(对于布尔选项,它始终为undefined):

assert.deepEqual(
 parseArgs({
 options, tokens: true,
 args: [
 '--str', 'yes', '--str=yes', '-s', 'yes',
 ]
 }),
 {
 values: {__proto__:null,
 str: 'yes',
 },
 positionals: [],
 tokens: [
 {
 kind: 'option',
 name: 'str',
 rawName: '--str',
 index: 0,
 value: 'yes',
 inlineValue: false
 },
 {
 kind: 'option',
 name: 'str',
 rawName: '--str',
 index: 2,
 value: 'yes',
 inlineValue: true
 },
 {
 kind: 'option',
 name: 'str',
 rawName: '-s',
 index: 3,
 value: 'yes',
 inlineValue: false
 }
 ]
 }
);

最后,这是解析位置参数和选项终止符的示例:

assert.deepEqual(
 parseArgs({
 options, allowPositionals: true, tokens: true,
 args: [
 'command', '--', '--str', 'yes', '--str=yes'
 ]
 }),
 {
 values: {__proto__:null,
 },
 positionals: [ 'command', '--str', 'yes', '--str=yes' ],
 tokens: [
 { kind: 'positional', index: 0, value: 'command' },
 { kind: 'option-terminator', index: 1 },
 { kind: 'positional', index: 2, value: '--str' },
 { kind: 'positional', index: 3, value: 'yes' },
 { kind: 'positional', index: 4, value: '--str=yes' }
 ]
 }
);
16.4.2 使用标记实现子命令

默认情况下,parseArgs()不支持git clonenpm install等子命令。但是,通过标记,相对容易实现此功能。

这是实现方式:

function parseSubcommand(config) {
 // The subcommand is a positional, allow them
 const {tokens} = parseArgs({
 ...config, tokens: true, allowPositionals: true
 });
 let firstPosToken = tokens.find(({kind}) => kind==='positional');
 if (!firstPosToken) {
 throw new Error('Command name is missing: ' + config.args);
 }
 //----- Command options
 const cmdArgs = config.args.slice(0, firstPosToken.index);
 // Override `config.args`
 const commandResult = parseArgs({
 ...config, args: cmdArgs, tokens: false, allowPositionals: false
 });
 //----- Subcommand
 const subcommandName = firstPosToken.value;
 const subcmdArgs = config.args.slice(firstPosToken.index+1);
 // Override `config.args`
 const subcommandResult = parseArgs({
 ...config, args: subcmdArgs, tokens: false
 });
 return {
 commandResult,
 subcommandName,
 subcommandResult,
 };
}

这是parseSubcommand()的示例:

const options = {
 'log': {
 type: 'string',
 },
 color: {
 type: 'boolean',
 }
};
const args = ['--log', 'all', 'print', '--color', 'file.txt'];
const result = parseSubcommand({options, allowPositionals: true, args});
const pn = obj => Object.setPrototypeOf(obj, null);
assert.deepEqual(
 result,
 {
 commandResult: {
 values: pn({'log': 'all'}),
 positionals: []
 },
 subcommandName: 'print',
 subcommandResult: {
 values: pn({color: true}),
 positionals: ['file.txt']
 }
 }
);

评论

十七、Shell 脚本配方

原文:exploringjs.com/nodejs-shell-scripting/ch_shell-scripting-recipes.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 17.1 通过 nodemon 交互式编辑代码片段
  • 17.1.1 nodemon
  • 17.1.2 尝试使用 nodemon 而不安装它
  • 17.2 检测当前模块是否为“main”(应用程序入口点)
  • 17.3 相对于当前模块访问文件

17.1 通过 nodemon 交互式编辑代码片段

本节描述了在处理 JavaScript 代码片段时使用 Node.js 的技巧。

17.1.1 nodemon

例如,假设我们想要尝试标准 Node.js 函数util.format()。我们创建文件mysnippet.mjs,内容如下:

import * as util from 'node:util';
console.log(util.format('Hello %s!', 'world'));

我们如何在处理它时运行mysnippet.mjs

首先安装npm 包nodemon

npm install -g nodemon

然后我们可以使用它来持续运行mysnippet.mjs

nodemon mysnippet.mjs

每当我们保存mysnippet.mjs,nodemon 会再次运行它。这意味着我们可以在编辑器中编辑该文件,并在保存时查看更改的结果。

17.1.2 尝试使用 nodemon 而不安装它

甚至可以通过 npx(Node.js 工具)尝试使用 nodemon 而不安装它:

npx nodemon mysnippet.mjs

17.2 检测当前模块是否为“main”(应用程序入口点)

参见§7.11.4“URL 用例:检测当前模块是否为“main”(应用程序入口点)”。

17.3 相对于当前模块访问文件

参见§7.11.3“URL 用例:访问相对于当前模块的文件”。

评论

十八、跨平台考虑

原文:exploringjs.com/nodejs-shell-scripting/ch_cross-platform-considerations.html

译者:飞龙

协议:CC BY-NC-SA 4.0


  • 18.1 文件系统路径
  • 18.2 处理行终止符
  • 18.3 检测当前平台
  • 18.4 在所有平台上运行与项目相关的任务

18.1 文件系统路径

本书其他地方的材料:

  • §7.2.1 “路径段、路径分隔符、路径分隔符”
  • §7.9 “在不同平台上使用相同的路径”
  • §7.3 “通过模块 'node:os' 获取标准目录路径”

18.2 处理行终止符

参见 §8.3 “跨平台处理行终止符”。

18.3 检测当前平台

  • process.platform包含一个标识当前平台的字符串。可能的值有:
  • 'aix'
  • 'darwin'
  • 'freebsd'
  • 'linux'
  • 'openbsd'
  • 'sunos'
  • 'win32'
  • 模块 'node:os' 包含更多与平台相关的信息(处理器架构、操作系统版本等)。

18.4 在所有平台上运行与项目相关的任务

参见 §15 “通过 npm 包脚本运行跨平台任务”。

评论

相关文章
|
3天前
|
Java 关系型数据库 MySQL
Elasticsearch【问题记录 01】启动服务&停止服务的2类方法【及 java.nio.file.AccessDeniedException: xx/pid 问题解决】(含shell脚本文件)
【4月更文挑战第12天】Elasticsearch【问题记录 01】启动服务&停止服务的2类方法【及 java.nio.file.AccessDeniedException: xx/pid 问题解决】(含shell脚本文件)
28 3
|
6天前
|
运维 监控 Shell
利用Shell脚本编写局域网监控软件:实时监测主机连接情况
本文介绍了如何使用Shell脚本创建一个局域网监控工具,以实时检查主机连接状态。脚本包括扫描IP地址范围检测主机可达性及使用`netstat`监控ESTABLISHED连接。此外,还展示了如何每60秒将连接数数据自动提交到指定网站API,以便实时跟踪网络活动。这个自动化监控系统有助于提升网络安全性和故障排查效率。
26 0
|
7天前
|
Shell
Shell脚本之流程控制语句
Shell脚本之流程控制语句
|
8天前
|
JSON 运维 监控
训练shell常用脚本练习(三)
【4月更文挑战第14天】shell代码训练(三)
29 1
|
12天前
|
存储 弹性计算 Shell
ecs服务器shell常用脚本练习(十)
【4月更文挑战第11天】shell代码训练(十)
143 0
|
12天前
|
弹性计算 Shell Go
ecs服务器shell常用脚本练习(九)
【4月更文挑战第10天】shell代码训练(八)
138 0
|
13天前
|
Web App开发 缓存 JavaScript
|
16天前
|
弹性计算 Shell Linux
ecs服务器shell常用脚本练习(六)
【4月更文挑战第4天】shell代码训练(六)
109 0
|
21天前
|
弹性计算 Shell 应用服务中间件
ecs服务器shell常用脚本练习(四)
【4月更文挑战第4天】shell代码训练(四)
96 0
|
23天前
|
Shell
【shell】实时查看网卡流量脚本
【shell】实时查看网卡流量脚本