终端交互命令行脚本
简述
基于nodejs环境编写的交互式命令行脚本,使用到的npm包主要有以下三个
- execa:执行脚本命令
- inquirer(核心包):用于在终端中进行提问与回答的交互操作
- detect-port:用于检测端口是否被占用
包使用详细介绍
execa
安装
npm install [email protected]
使用
import { execa } from "execa"
execa('echo', ['hello', 'world'], { cwd: "../day08-script" })
.then(result => {
console.log("stdout", result.stdout); // 输出日志: Hello, World!
console.log("exitCode", result.exitCode); // 输出子进程退出状态码
console.log("stderr", result.stderr); // 输出子进程错误信息
})
.catch(error => {
console.error('执行命令失败:', error);
});
参数
- 命令
- 各种命令,只要在系统上能跑的
- 参数
- 以字符串数组的形式自动拼接到命令后
- 配置(最常用)
- cwd:当前的命令执行路径,会先到该路径下后再执行命令
- env:环境变量对象,这些变量将传递给子进程。
- shell:是否在 shell 中执行命令。
- stdio:子进程的标准 I/O 配置。可以是 'inherit', 'pipe', 或 'ignore'
- timeout:命令执行的超时时间(毫秒)。
inquirer
安装
npm install inquirer
使用
import inquirer from "inquirer"
inquirer.prompt([
{
type: 'input', // 问题类型,这里是输入框
name: 'username', // 问题的答案将存储在这个属性中
message: 'What is your name?', // 显示给用户的问题
validate: function(value) {
if (value.length < 1) {
return 'Name cannot be empty'; // 验证函数,确保输入不为空
}
return true;
}
},
])
.then(async (answers) => {
console.log("选择了:", answers)
});
调用 inquirer.prompt() 并传入问题数组,然后使用 Promise 来处理用户的答案。
常用属性
type: 问题类型,例如 input, confirm, list, checkbox 等。
name: 用于存储用户答案的属性名。
message: 显示给用户的问题。
default: 默认答案,如果用户没有输入任何内容,将使用这个默认值。
choices: 选项列表,对于 list, rawlist, expand, checkbox 等类型的问题,这个属性定义了用户可以选择的选项。
validate: 一个函数,用于验证用户输入是否有效。如果返回 false 或者一个错误消息字符串,输入将被视为无效。
filter: 一个函数,用于在将用户输入存储到答案对象之前对输入进行处理或转换。
when: 一个函数,返回一个布尔值,用于决定是否显示这个问题。
pageSize: 用于 list 和 rawlist 类型的问题,定义了一次显示多少个选项。
prefix: 用于 list 和 rawlist 类型的问题,定义了选项前缀。
suffix: 用于 list 和 rawlist 类型的问题,定义了选项后缀。
transformer: 一个函数,用于修改 list, rawlist, 和 expand 类型问题中选项的显示方式。
loop: 用于 confirm 类型的问题,如果为 true,则用户必须输入 y 或 n 来继续。
source: 用于动态生成选项,是一个函数,返回一个 Promise,该 Promise 解析为选项数组。
store: 一个布尔值,如果为 true,则每个选项的值将被添加到最终的答案对象中。
choicesAlign: 用于 checkbox 类型的问题,定义了选项的对齐方式。
highlight: 用于 list 和 rawlist 类型的问题,定义了是否高亮显示当前选中的选项。
pointer: 用于 list 和 rawlist 类型的问题,定义了当前选中的选项的指针字符。
type
Inquirer.js 提供了多种问题类型(type),每种类型都具有不同的功能,以下是一些常见的问题类型及其功能:
- input: 标准输入框,用户可以输入任何文本。
- confirm: 确认框,用户可以选择 "yes" 或 "no"。
- list: 下拉列表,用户可以从预设的选项中选择一个。
- rawlist: 与 list 类似,但是选项以纯文本形式展示,不带高亮。
- expand: 下拉列表,带有展开/折叠选项,用户可以选择一个选项,或者展开查看更多信息。
- checkbox: 复选框,用户可以选择多个选项。
- password: 密码输入框,输入内容不会显示在屏幕上。
- editor: 编辑器,允许用户在文本编辑器中输入多行文本。
- range: 范围选择器,用户可以选择一个数值范围。
- autocomplete: 自动完成输入框,用户可以输入文本,并且得到自动完成的建议。
每种类型都有其特定的用途,以下是一些类型的详细功能:
-
input:
- 允许用户输入任何文本。
- 可以设置 validate 函数来验证输入。
inquirer.prompt([ { type: 'input', name: 'username', message: 'What is your username?', validate: value => { if (value.length) { return true; } return 'Please enter a username'; } } ]).then(answers => { console.log('Username:', answers.username); });
-
confirm:
- 通常用于需要用户确认的操作。
- 默认情况下,用户可以输入 "y" 或 "Y" 来表示 "yes",输入 "n" 或 "N" 来表示 "no"。
inquirer.prompt([ { type: 'confirm', name: 'isAgree', message: 'Do you agree to the terms and conditions?', default: false } ]).then(answers => { console.log('Terms agreed:', answers.isAgree); });
-
list:
- 提供一个下拉列表供用户选择。
- 可以设置 pageSize 来控制一次显示的选项数量。
inquirer.prompt([ { type: 'list', name: 'chocolate', message: 'What is your favorite chocolate?', choices: ['Milk', 'Dark', 'White'] } ]).then(answers => { console.log('Favorite chocolate:', answers.chocolate); });
-
rawlist:
- 类型提供了一个有序的列表供用户选择,与 list 类型不同,rawlist 不会展示一个下拉菜单,而是将所有选项以纯文本形式展示在屏幕上。
- 用户可以通过输入选项前的序号来选择答案。这种类型适用于选项数量不多,且用户需要快速浏览所有选项的场景。
inquirer.prompt([ { type: 'rawlist', name: 'iceCream', message: 'What is your favorite ice cream flavor?', choices: ['Vanilla', 'Chocolate', 'Strawberry'] } ]).then(answers => { console.log('Favorite ice cream:', answers.iceCream); });
-
expand:
- 允许用户选择一个选项,或者展开查看更多信息。
- 可以设置 expand 属性为 true 来启用展开功能。
inquirer.prompt([ { type: 'expand', name: 'os', message: 'Which operating system do you use?', choices: [ { key: 'w', name: 'Windows', value: 'windows' }, { key: 'm', name: 'MacOS', value: 'macos' }, { key: 'l', name: 'Linux', value: 'linux' } ] } ]).then(answers => { console.log('Operating system:', answers.os); });
-
checkbox:
- 允许用户选择一个或多个选项。
- 可以设置 choices 属性来定义可选的选项。
inquirer.prompt([ { type: 'checkbox', name: 'fruits', message: 'What fruits do you like?', choices: ['Apple', 'Banana', 'Cherry'] } ]).then(answers => { console.log('Fruits liked:', answers.fruits); });
-
password
- 用于输入密码,输入内容不会显示在屏幕上。
- 可以设置 mask 属性来定义显示的占位符。
inquirer.prompt([ { type: 'password', name: 'password', message: 'Please enter your password', mask: '*' } ]).then(answers => { console.log('Password entered:', answers.password); });
-
editor
- 允许用户在外部文本编辑器中输入多行文本。
- 可以设置 editor 属性为 true 来启用编辑器。
inquirer.prompt([ { type: 'editor', name: 'text', message: 'Please write some text' } ]).then(answers => { console.log('Written text:', answers.text); });
-
range:
- 允许用户选择一个数值范围。
- 可以设置 min 和 max 属性来定义范围的最小值和最大值。
inquirer.prompt([ { type: 'range', name: 'age', message: 'How old are you?', min: 18, max: 99 } ]).then(answers => { console.log('Age:', answers.age); });
-
autocomplete(需要安装 inquirer-autocomplete-prompt)
- 允许用户输入文本,并提供自动完成的建议。
- 可以设置 source 函数来定义自动完成的选项。
import Autocomplete from 'inquirer-autocomplete-prompt'; inquirer.registerPrompt('autocomplete', Autocomplete); inquirer.prompt([ { type: 'autocomplete', name: 'city', message: 'What city do you live in?', source: (answersSoFar, input) => { // 这里可以是异步操作,比如从API获取数据 return Promise.resolve(['New York', 'Los Angeles', 'Chicago'].filter(city => city.includes(input))); } } ]).then(answers => { console.log('City:', answers.city); });
额外插件
chalk: 一个用于美化终端字符串的库,可以添加颜色和样式。
cli-table: 一个用于在命令行中创建表格的库,有助于展示结构化数据。
figures: 一个提供常见特殊字符的库,比如箭号、复选框等,用于美化命令行输出。
log-symbols: 一个提供成功、错误、信息等不同日志级别符号的库。
ora: 一个用于在终端显示加载动画的库,常用于异步操作。
listr: 一个用于创建和管理任务队列的库,可以与 Inquirer.js 结合使用。
enquirer: Inquirer.js 的一个分支,提供了更多的问题类型和功能。
inquirer-autocomplete-prompt: 一个为 Inquirer.js 提供自动完成功能的插件。
inquirer-fuzzy-path: 一个允许用户通过模糊搜索选择文件或目录的插件。
inquirer-chalk-pipe: 一个让 Inquirer.js 提示输入支持 chalk-pipe 风格的字符串的插件。
inquirer-search-checkbox: 一个提供可搜索复选框的 Inquirer.js 插件。
inquirer-search-list: 一个提供搜索功能的列表选择器插件。
detect-port
用于检测某个端口是否可用。
安装
npm install detect-port
使用
import detect from "detect-port";
const port = 3000; // 你想要检测的端口号
detect(port).then((foundPort) => {
// 这里一定要判断返回的端口是否是验证的端口,因为如果检测的端口被占用了,则会返回一个推荐的端口号回来
if (port === foundPort) {
console.log('端口' + port + '没有被占用');
} else {
console.log('端口' + port + '已被占用,系统为你分配了端口' + foundPort);
}
}).catch(err => {
console.error(err);
});
案列
目录结构
配置文件
export const config = [
{
name: "应用test1",
port: 5173,
application: "test1",
},
{
name: "应用test2",
port: 5174,
application: "test2",
},
{
name: "应用test3",
port: 5175,
application: "test3",
},
{
name: "应用test4",
port: 5176,
application: "test4",
},
{
name: "应用test5",
port: 5177,
application: "test5",
},
{
name: "应用test6",
port: 5178,
application: "test6",
},
];
交互式命令行脚本
import { execa } from "execa";
import detect from "detect-port";
import inquirer from "inquirer";
import { config } from "./config.js";
// 子应用
const subApps = [];
/**
* 检查端口是否被占用
* @param {*} list
*/
const checkPortOccupiedList = config.map((item) => {
return new Promise((resolve, reject) => {
detect(item.port).then((port) => {
if (port === item.port) {
resolve({
...item,
disabled: false,
});
} else {
resolve({
...item,
disabled: true,
});
}
});
});
});
// 提问
const question = (apps) => {
inquirer
.prompt([
{
type: "checkbox",
name: "apps",
message: "请选择需要启动的子应用",
choices: apps.map((item) => {
return {
name: `${item.name}:${item.port}`,
value: item,
disabled: item.disabled ? "已启动" : false,
};
}),
},
])
.then((answer) => {
if (!answer.apps.length) {
console.log("未选择应用");
confirm(apps);
}
answer.apps.forEach((item) => {
const subApp = execa(
"pnpm",
[`dev:${item.application}`, `--port`, `${item.port}`],
{
stdio: "inherit",
}
);
subApp.on("error", (error) => {
console.log(`${item.name} error:`, error);
});
subApp.on("exit", (exit) => {
console.log(`${item.name} exit`);
});
subApps.push(subApp);
});
})
.catch((error) => {
console.log("error:", error);
});
};
// 确定
const confirm = (apps) => {
inquirer
.prompt([
{
type: "confirm",
message: "是否重新选择需要启动的子应用?",
name: "confirm",
},
])
.then((answer) => {
if (answer.confirm) {
question(apps);
} else {
console.log("退出应用对话框");
process.exit(0);
}
})
.catch((error) => {
console.log("error:", error);
});
};
// 主应用
const base = (name, command, port) => {
const baseServe = execa("pnpm", [command, "--port", port], {
detached: true,
stdout: "pipe",
stream: true,
});
baseServe.on("error", (error) => {
console.error(`error: ${error}`);
});
baseServe.on("close", (code) => {
console.log(`${name}进程退出,状态码:${code}`);
subApps.forEach((app) => {
app.kill();
});
process.exit(0);
});
};
// 启动服务
Promise.all(checkPortOccupiedList).then((ports) => {
const baseServe = ports.shift();
const name = baseServe.name;
const command = `dev:` + baseServe.application;
const port = "" + baseServe.port;
if (!baseServe.disabled) {
base(name, command, port);
}
question(ports);
});
package.json
{
"name": "script",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "node index.js",
"dev2": "node index2.js",
"dev:test1": "vite dev applications/Test1",
"dev:test2": "vite dev applications/Test2",
"dev:test3": "vite dev applications/Test3",
"dev:test4": "vite dev applications/Test4",
"dev:test5": "vite dev applications/Test5",
"dev:test6": "vite dev applications/Test6"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"detect-port": "^1.6.1",
"execa": "7.2.0",
"inquirer": "^9.2.23",
"vite": "^5.3.1"
}
}
vite.config.ts
import { defineConfig } from "vite";
export default defineConfig(() => {
return {
root: ".",
};
});
标签:脚本,console,name,answers,终端,inquirer,交互,port,log
From: https://www.cnblogs.com/letgofishing/p/18264383