likes
comments
collection
share

用commander写一个简单的Node命令行工具

作者站长头像
站长
· 阅读数 21

命令行工具是什么?

命令行工具是一个可执行脚本或者可执行文件。

平时开发中用到的 create-react-app @vue/cli vitejs 等都是命令行工具,这些命令行工具一般都安装到全局的/usr/local/bin里面(注:类Unix环境)。打开/usr/local/bin应该会看到平常安装过的nodejs全局命令行工具。

命令行文件与普通文件的区别

命令行文件需要在第一行指定一下"脚本文件的解释器" #!/usr/bin/env node 这句话是指定用nodejs作为解释器,执行当前文件下面的内容。

命令行是如何发布到全局中的

  1. 在package.json中的 bin中声明需要编写的命令行名称,比如下面的示例声明了一个 command-demo命令
{
  "name": "command-demo",
  "version": "0.0.1",
  "description": "command-demo",
  "bin": {
  // 这里声明命令行名称
            "command-demo": "./bin/index.js"
  }
 ...
}
  • 如果是本地开发或者测试可以先执行npm link发布到本地全局,npm link执行的位置是package.json同级的目录。 然后执行 npm link <package name> 安装到其它目录。安装到其它目录的时候是需要在其它目录package.json同级目录执行。注意 package name是package.json中name对应的值。
  • 如果想发布到npm库中可以先执行npm publish 具体可以参考 npm publish; 发布成功之后,可以通过npm 或者yarn直接安装到全局中就可以直接使用这个命令行功能了
  • bin中声明的命令并不是只可以安装到全局中,也可以安装到某一个项目中,安装之后会在node_moudles/.bin/这个目录中生成对应的命令。安装到某个项目中的的命令可以 ./node_modules/.bin/<command> 这样执行
  1. 命令行对应的文件,比如上面例子中 ./bin/index.js,并不是一定要放到 ./bin 目录中,但是是推荐放到./bin目录中。

实践一下: 创建一个最简单的命令行

命令行功能描述:执行command-demo的时候输出Hello World

#!/usr/bin/env node
console.log(`Hello World`);

按照上面介绍的方法

实现一个创建基础模版的脚手架

分析涉及的主要功能:

创建基础模版有两种方式:

  1. 通过远程下载。利用download-git-repo这个工具可以实现拉取远程的文件,然后下载到指定位置。这样的好处是远程文件修改之后,脚手架可以直接拉取最新的修改内容。坏处就是这个git库权限需要所有人访问。否则拉取失败。
  2. 本地拷备的方式。大概意思是脚手架包含需要的模版文件。这样安装脚手架的时候模版文件也会安装到本地。这样创建的时候只要拿到对应的文件直接拷备到指定的位置就可以了。

参数解析及help介绍:

脚手架肯定是少不了参数的比如vue-cli-service serve vue-cli-service build这样脚手架可以根据不同的参数执行不同的方法。这里借助commander的能力。

介绍一下 commander

commander 可以帮助我们编写命令行。主要功能是将参数解析为选项(option)和命令参数(command),并且当命令行使用错误的时候提示错误,以及帮助系统。

比如 一个命令选项 --template {name} 这个选项必须在--template 后面跟一个name,如果不加的话就需要提示错误。

option用法

option主要用法是支持命令行传参数,一种参数是不需要对应的值,一种参数是必须要传值。

比如:我们想实现一个功能,比如命令行中包括 --info,-i 的时候去打印一些信息

#!/usr/bin/env node

const { program } = require("commander");
program
    // 不需要对应的值
    .option("-i, --info")
    .option("-t, --tpl <string>");
// 解析,这一步很不重要,不能去掉
program.parse();
const options = program.opts();
// 输出解析之后的结果
console.log(options, " options");

上面的脚本可以这样执行:

  1. 只包含 info参数 node ./command.js -i 或者 node ./command.js --info
  2. 只包含 tpl 参数 node ./command.js -t {模板名} 或者 node ./command.js -tpl {模板名},注意上面的{模板名}不能省略
  3. 既包含 info 也包含 tpl参数。 node ./command.js -i -t {模板名} 或者node ./command.js -t {模板名} -i 上面的-i或者 -t 可以替换成 --info 或者 --tpl。需要注意的:-t 后面的 {模板名}不能省略且只能放到 -t或者--tpl紧后面。

options配置的参数,可以通过program.opts() 返回的对象来获取内容。

command、description、arguments

command用来自义一个命令,然后arguments是定义参数的并且不能省略。description是用来描述当前命令。

const { program } = require("commander");
program
  .command("split")
  .description("Split a string into substrings and display as an array")
  .argument("<string>", "string to split")
  .option("--first", "display just the first substring")
  .option("-s, --separator <char>", "separator character", ",")
  .action((str, options) => {
    const limit = options.first ? 1 : undefined;
    console.log(str, " str");
    console.log(options, " options");
    console.log(str.split(options.separator, limit));
  });

program.parse();

以上定义了一个split命令,参数是string;可选参数是 --first、--separator;具体操作在action里面。 注意:每个command对应一个action,写多个也只有最后一个生效。

action

action是对应command的操作。action的参数是一个函数。这个函数包含三个参数,第一个对应的是argument,第二个对应option,它是一个对象,对象的属性就是option的key,第三个就是program对象,包含对象的所有信息。

有了以上的知识储备,在开发一个脚手架工具就很容易了

#!/usr/bin/env node

const fs = require("fs");
const path = require("path");
const { program } = require("commander");
function copyDirectoryContents(source, destination) {
  // 读取源目录中的文件和子目录
  const files = fs.readdirSync(source);

  // 遍历源目录中的文件和子目录
  files.forEach((file) => {
    const sourcePath = path.join(source, file);
    const destinationPath = path.join(destination, file);

    // 判断当前项是文件还是目录
    const isDirectory = fs.statSync(sourcePath).isDirectory();

    if (isDirectory) {
      // 如果是目录,则递归地拷贝目录中的内容
      fs.mkdirSync(destinationPath);
      copyDirectoryContents(sourcePath, destinationPath);
    } else {
      // 如果是文件,则直接拷贝文件
      fs.copyFileSync(sourcePath, destinationPath);
    }
  });
}
program
  .command("create")
  .description("创建一个项目")
  .argument("<string>", "项目名称")
  .option("-t, --tpl <char>", "选择模版技术栈,默认vue", "vue")
  .action((str, options, rest) => {
    console.log(str, " str");
    console.log(options, " options");
    // 获取要创建项目的目录地址
    const directoryPath = `${process.cwd()}/${str}`;
    //目录不存在的时候创建目录
    if (!fs.existsSync(directoryPath)) {
      try {
        console.log("开始创建项目目录");
        fs.mkdirSync(directoryPath, { recursive: true });
        console.log("项目目录创建成功");
        //把内容拷到创建的目录中
        // vue项目
        if (options.tpl === "vue") {
          copyDirectoryContents(`${__dirname}/../vue`, directoryPath);
          // react项目
        } else {
          copyDirectoryContents(`${__dirname}/../react`, directoryPath);
        }
      } catch (err) {
        console.error("无法创建目录", err);
      }
    } else {
      throw new Error(`项目${str}已经存在`);
    }
  });
program.program.parse();

以上就实现了一个最简单的脚手架工具。目录结构如下:

 用commander写一个简单的Node命令行工具

当然一个完善的脚手架还有很多功能,比如判断当前是否有比较新的版本,检查输入内容等。