跨平台CMD极简命令实现

158 阅读7分钟

1. 需求

最近想简化自己的常用命令,之前用windows的时候还好,只要配置alias就好了,现在工作只用一台 MP,配置alias比较麻烦,找了一圈都没找到很好的方案,干脆自己动手。下面是常用命令、对应极简命令举例:

  • npm run dev -- i d
  • ipconfig -- i ip
  • yarn add -- i ya
  • yarn add axios -- i a axios
  • ...

2. 实现

前端实现CLI比较简单,下面是实现步骤:

1. 注册命令 i

  1. 初始化
npm init -y
  1. 修改 package.json
{
"name": "simple-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"bin": {
"i": "./alias/index.js"
},
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
  1. 新增 alias/index.js
#! /usr/bin/env node

console.log('极简命令')
  1. 验证输出
% node alias/index.js
极简命令
  1. 注册本地命令
npm link
  1. 验证
% i
极简命令

2. i d 的输出

上面完成本地命令的基本注册,下面实现简化命令 i d

  1. 安装所需工具库
cnpm install url fs-extra shelljs commander chalk address
  • url:处理路径
  • fs-extra:处理文件
  • shelljs:执行最终命令
  • commander:命令监听
  • chalk:美化控制台
  • address:地址查询跨平台
  1. 命令监听
  • 修改./alias/index.js代码
#! /usr/bin/env node

import { program } from "commander";

program
// 命令:i d
.command("d")
// 描述信息
.description("极简命令")
.action(() => {
console.log("极简命令");
});

// 必须,让命令生效
program.parse(process.argv);
  • 验证
% i --help
Usage: simple [options] [command]

Options:
-h, --help display help for command

Commands:
d 极简命令
help [command] display help for command
% i d
极简命令

这样就实现了自定义命令i的监听。 

  1. 执行对应命令 

node执行命令需要用到shelljs,修改代码

#! /usr/bin/env node

import { program } from "commander";
import shell from "shelljs";

const exec = shell.exec;

program
// 命令:i d
.command("d")
// 描述信息
.description("极简命令")
.action(() => {
console.log("极简命令");
exec("npm run dev");
});

// 必须,让命令生效
program.parse(process.argv);

验证

% i d
极简命令
npm ERR! Missing script: "dev"
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/chenhaoyin/.npm/_logs/2023-03-28T10_01_06_350Z-debug-0.log

提示内容是指package.jsonscripts没有dev,所以要修改下scripts

"scripts": {
    "dev": "echo 自定义命令"
},

然后验证可以看到控制台输出了新增的echo内容,也就实现了i d的监听及内容输出。

 % i d
极简命令

> @1.0.0 dev
> echo 自定义命令

自定义命令

        实现这个极简命令的目的,是前端启动项目的时候想简化npm run dev命令,因为npm link是全局注册的,所以可以切换到真实的前端项目然后执行i d验证,接着看控制台输出

% simple d

> h5@0.0.0 dev
> vite

WARN postcss-px-to-viewport: postcss.plugin was deprecated. Migration guide:
https://evilmartians.com/chronicles/postcss-8-plugin-migration

Port 5173 is in use, trying another one...

VITE v3.2.5 ready in 1050 ms

➜ Local: http://localhost:5174/

可以看到vite项目已经正常运行,这样就实现了极简命令列表的第一个npm run dev的极简模式。

3. 批量实现

上面只是实现了最简单的一个常用命令,如果想做批量处理,可以批量注册,然后如果想更进一步比如想通过配置实现,就用到开始下载的urlfs-extra模块。下面是实现步骤:

  1. 路径处理

因为process.cwd()"type": "module"中获取到的是当前命令的执行地址,这样在别的项目执行简化命令的时候会因地址问题而报错,所以不使用cwd方案。

下面使用url结合import.meta.url获取当前目录全路径。 看实现代码:

import { fileURLToPath } from "url";
// 当前文件全路径
const filename = fileURLToPath(import.meta.url);
// 当前目录全路径
const dirname = filename.replace("index.js", "");

console.log(__dirname);
  1. 接下来引入json文件

引入 json 文件有两种方式

  • import data from './data.json' assert { type: 'json' }
  • fs模块解析

    第一种是实验性功能,选择第二种。

const files = fs.readdirSync(__dirname);
console.log("目录下所有 json 文件", files);

        使用fs解析还有另外一个好处就是可以直接解析目录,然后解析目录下的所有json文件,这样如果想批量新增不同命令,只要新增一个不同的json文件即可,要注意的是key的区分。

在alias下新增alias-npm.json,内容是:

{
"desc": "npm 相关命令",
"n": "npm",
"nv": "npm -v",
"nd": "npm run dev",
"ni": "npm install"
}

然后验证,看控制台

% node alias/index.js
目录下所有 json 文件 [ 'alias-npm.json', 'index.js' ]
Usage: index [options] [command]

        可以看到控制台输出了当前目录下的所有文件,这样就实现了配置文件的读取,方便进行下面的批量处理。

  1. 解析

获取到文件之后就是解析文件内容,然后为了方便批量处理,简单封装一下处理函数

// 获取简化命令列表
function getAlias() {
  let alias = {};
  const files = fs.readdirSync(**dirname);
  files.forEach((src) => {
  // 读取所有的 alias[-][xxx].json 文件
  if (src.startsWith("alias") && src.lastIndexOf(".json") !== -1) {
  // 读取 json 内容
  const content = fs.readFileSync(**dirname + src, {
    flag: "r",
    type: "json",
    encoding: "utf-8",
  });
  //
  alias = {
  ...alias,
  ...JSON.parse(content),
  };
  }
  });
  delete alias.desc;
  // console.log(alias);
  return Object.entries(alias); //.filter((item) => item[0] !== "desc");
}

        接着执行getAlias()函数,就能批量获取alias下的所有json文件,并解析出对应的commandexec所需要的入参。

  1. 批量注册

依赖上面的函数,并把注册命令的步骤简单封装,代码如下

// 设置简化命令
function setCommand(cmd = []) {
    const [key, value] = cmd;
    // console.log(key, value);
    program
    // 命令注册
    .command(key) 
    // 描述信息
    .description("极简命令") 
    .action((pacakage = "", ...argvs) => {
        exec(value);
    }); 
}

然后根据获取到的alias 命令集,遍历执行封装好的setCommand注册命令

// alias 命令集
const aliasEntries = getAlias();
// 遍历执行
for (const cmd of aliasEntries) {
    // 注册命令
    setCommand(cmd);
}

这样就完成了json文件配置的批量注册。

下面是分别输入i ni nv来验证alias-npm.json配置命令是否成功

% i n
npm <command>

Usage:

npm install install all the dependencies in your project
% i nv
9.5.1

根据输出内容,可以看到已经实现了想要的功能。

到此,也就实现了批量新增、批量注册自定义命令,然后根据需要只要新增对应JSON文件,就可以实现批量命令简化的目的。

4. 其他

有一些常用功能,可以单独配置。

比如因为经常要手机调试H5而频繁获取ip地址,在windows可以通过ipconfig快速获取,在mac要用比如ipconfig getifaddr en6等命令比较麻烦。

而结合跨平台js库addressjs可以快速实现这个功能。新增下面代码:

import address from "address";
// 获取 IP
const IP = address.ip();
// ip 地址输出
program 
    .command("ip")
    .description("获取 IP 地址")
    .action(() => {    
        console.log("ip 地址 " + IP);
    });

然后在控制台输入i ip验证

% i ip
ip 地址 11.11.11.111

可以看到控制台输出了本地的IP地址,这样比输入一串ipconfig getifaddr en6轻松的多。

3. 源码

前面讲了实现的主要流程,下面是实现源码,而且在上面的基础上新增了部分参数监听,可以实现如i a axios而实际执行的是yarn add axios的功能。

#! /usr/bin/env node

import { fileURLToPath } from "url";
import fs from "fs-extra";
import shell from "shelljs";
import { program } from "commander";
import chalk from "chalk";
import address from "address";

// 当前文件全路径
const **filename = fileURLToPath(import.meta.url);
// 当前目录全路径
const **dirname = __filename.replace("index.js", "");

const pkg = fs.readJsonSync(
__filename.replace("/alias/index.js", "") + "/package.json"
);

const exec = shell.exec;

// 获取 IP
const IP = address.ip();

// 获取简化命令列表
function getAlias() {
let alias = {};
const files = fs.readdirSync(**dirname);
files.forEach((src) => {
// 读取所有的 alias[-][xxx].json 文件
if (src.startsWith("alias") && src.lastIndexOf(".json") !== -1) {
// 读取 json 内容
const content = fs.readFileSync(**dirname + src, {
flag: "r",
type: "json",
encoding: "utf-8",
});
//
alias = {
...alias,
...JSON.parse(content),
};
}
});
delete alias.desc;
// console.log(alias);
return Object.entries(alias);
}

function init() {
// console.log(alias.pp);
program.option("-d,--debug", "whether to enable debug mode?", false);

program.on("option:debug", () => {
console.log("开启了 debug 模式");
});
program.on("command:*", (obj) => {
console.error(chalk.redBright(`未知命令:${obj[0]}`));
});

// 监听 --help 指令
program.on("--help", function () {
// 前后两个空行调整格式,更舒适
console.log(
"\n",
`输入 ${chalk.cyan("ai <command> --help")} 查看命令详情.`,
"\n"
);
});

// alias 命令集
const aliasEntries = getAlias();
// 遍历执行
for (const cmd of aliasEntries) {
// 注册命令
setCommand(cmd);
}
}

// 设置简化命令
function setCommand(cmd = []) {
const [key, value] = cmd;
// console.log(key, value);
program
// 命令:ai create
.command(key)
// 别名 ai c 相当于 ai create
// .alias("p")
// 描述信息
.description("极简命令")
// <>表示是必输字符,[]表示可选输入字符
.argument("[pacakageName]", "安装包名称")
.argument("[others]", "其他参数")
.argument("[unkown]", "未知参数")
.option("-v, --version", "版本")
.option("-V, --version", "版本")
.option("--host, --host <type>", "host")
.option("--port, --port <type>", "port")
.option("-D, --save-dev", "devDependencies 本地安装")
.option("-S, --save", "deDependencies 非安装")
.option("-c, --check", "check option")
.option("-C, --no-check", "no check option")
.option("-p, --peer", "peerdependencies")
.option("-f, --force", "force")
// .option("-o, --output <output>", "output options", "./temp")
.description("example program for argument")
.action((pacakage = "", ...argvs) => {
// console.log(pacakage, argvs[0], argvs[1], argvs[2]);
// option 参数
const options = argvs[argvs.length - 2];
// 参数解析
const { version, save, saveDev, host, port, force } = options || {};
// deDependencies 还是 devDependencies
const dependencies = save ? "--save" : saveDev ? "--save-dev" : "";
const isforce = force ? "--force" : "";
const isversion = version ? "-v" : "";
const ishost = host ? `--host=${host}` : "";
const isport = port ? `--port=${port}` : "";

      // 获取其他参数
      let desc = "";
      for (const item of argvs) {
        if (typeof item === "string") {
          desc = desc.trim() + ` ${item} `;
        }
      }

      // 实际命令拼接
      const fullCMD =
        `${value} ${pacakage} ${desc} ${dependencies} ${isforce} ${isversion} ${ishost} ${isport}`
          .split(" ")
          .filter((str) => str)
          .join(" ");

      console.log(chalk.gray("running:") + chalk.cyan(fullCMD));
      exec(fullCMD);
    });

}

// ip 地址输出
program
// 命令:ai create
.command("ip")
.description("获取 IP 地址")
.action(() => {
console.log(chalk.gray("ip 地址 ") + chalk.cyan(IP));
});

init();

// 生成版本
program.version(pkg.version);

// 必须,让命令生效
program.parse(process.argv);

源码地址