命令行工具揭秘

  1. 你想知道命令行工具是怎么运行的吗?
  2. 当你在终端输入一句命令 vue create my-project,按下回车键的时候,发生了什么?
  3. npx 是什么?
  4. 如何实现一个命令行工具

当我们运行 npx @vue/cli create my-project 创建一个项目时,就是通过 npx 执行了@vue/cli包中的可执行文件。"可执行文件"就是我们通常说的脚本,许多 npm 包的可执行文件是用 JavaScript 编写的,例如: xx.tsxx.mtsxx.js 等等,使用 Node.js 环境来运行。在 package.json 文件中,npm 包的开发者可以通过 bin 字段来指定可执行文件的路径,命令作为key,可执行文件路径作为value。例如 @vue/cli 可执行文件就是bin/vue.js, 不论是执行 vue create my-project 还是执行 npx @vue/cli create my-project ,运行的都是bin/vue.js 文件的内容,具体的 create 命令是 bin/vue.js 文件中的 create program

get it!

现在我们知道了,vue create my-project 命令的运行逻辑,之所以可以创建 my-project 项目,是因为我们全局安装了vue-cli,通过 package.json 中bin字段的匹配, vue 命令对应的可执行文件为 bin/vue.js,所以vue create 执行的就是bin/vue.js中的 create 程序。npx @vue/cli create my-project 也是一样,只是找可执行文件的方式不同,npx @vue/cli 就会找 vue-cli 中的可执行文件,也就是 package.json中bin字段的配置的bin/vue.js,create 同样是执行 bin/vue.js 文件中的 create 程序。

Commander

上面我们提到可执行文件bin/vue.js中的 create 程序,接下来我们看看 create 程序是什么。

Commander 完整的 node.js 命令行解决方案。如果我们需要写一个命令行工具,肯定会用到它,他可以帮助我们定义命令,以及命令的参数。在这里我们需要简单了解几个它的方法:

command()

通过该方法可以定义命令,比如 my-command.js:

ini 复制代码
const program = new Command();
program.command('create [projectName]')

这样我们就定义了一个 create 命令,可以通过执行 node my-command.js create (node my-command.js create myProject) 来执行。[projectName] 表示给 create 命令定义了一个可选的参数,如果需要一个必选的参数,需要用 <projectName> 来定义,例如:

arduino 复制代码
const program = new Command();
program.command('create <projectName>')
option()

该方法用来定义选项,那选项是什么呢?如下,这些 -p、-d、-i 这些就是选项,可以简单地把选项理解为额外的参数。

option() 同时可以附加选项的简介。每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或多个单词),使用逗号、空格或|分隔。例如:

arduino 复制代码
const program = new Command();
program.option('-d, --debug', 'output extra debugging')

选项有全局选项和命令选项之分,全局选项是定义在 program 上的选项,可以在任何命令中使用。这里定义的 -d 就是一个全局选项,是直接绑定在program上的,我们可以可以通过执行: node my-command.js -d 来执行。

命令选项是定义在具体命令上,只能在该命令中使用,例如:

arduino 复制代码
const program = new Command();
program
  .command('create [projectName]')
  .option('-d, --debug', 'output extra debugging')

这里定义的 -d 就是一个命令选项,是绑定在command上的,我们可以可以通过执行: node my-command.js create -d 来执行。


注意⚠️: 当我们定义了 command 后,commander 会进入"命令模式"。在命令模式下,所有输入都会被解析为命令或命令的选项。

arduino 复制代码
const program = new Command();
program
  .option('-d, --debug', 'output extra debugging')
  .command('create [projectName]')

此时,全局选项不可单独使用:node my-command.js -d 🙅;只能这样使用:node my-command.js -d create(node my-command.js create -d)

action()

该方法用来定义处理函数,命令处理函数的参数,为该命令声明的所有参数,除此之外还会附加两个额外参数:一个是解析出的选项,另一个则是该命令对象自身。例如:

javascript 复制代码
program
.option('-d, --debug', 'output extra debugging')
.command('create [projectName]')
.option('-s, --small', 'small pizza size')
.option('-p, --pizza-type <type>', 'flavour of pizza')
.action((argument, options, cmd) => {
    console.log(argument, '====',options, '===', cmd);
});

如果我们执行: node my-command.js create -p cheese 这个命令

可以看到,第一个参数 argument 为 undefined, options 为 { pizzaType: cheese }, cmd 为该命令对象自身。我们先不管 argument 参数,options 为 { pizzaType: cheese } 是因为我们的命令传入了 -p cheese 选项,没错,Commander 在暴露参数时会将多个单词的长选项名转为驼峰命名法(camel-case)作为 key。因为我们定义了 option('-p, --pizza-type ', 'flavour of pizza') ,所以这里抛出的参数是 { pizzaType: cheese }。

do it !

ok,现在我们知道了命令行工具是如何运行的,以及如何定义的,那现在我们来写一个工具:查找所有 ts 文件中的var,将其替换为let

前置条件

文件: index.ts 、index.html

typescript 复制代码
class User {
    private nameVar: string;
    private age_var: number;

    constructor(name: string, age: number) {
        this.nameVar = name;
        this.age_var = age;
    }

    getUserInfo(): string {
        return `用户名: ${this.nameVar}, 年龄: ${this.age_var}`;
    }
}

// 创建用户实例的函数
function createUser(): void {
    var userName: string = "张三";  
    var userAge: number = 25;  
    
    const user = new User(userName, userAge);
    console.log(user.getUserInfo());
}
必备知识

我们这个工具的作用是查找所有ts文件中的var,将其替换为let,那我们就要读取文件,我们简单了解一下与操作文件相关的2个模块:fspath

fs 模块和 path 模块 是 Node.js 的两个内置模块。

fs 模块(File System)提供了一系列用于与文件系统进行交互的 API,允许你对文件和目录进行读取、写入、修改、删除等操作。我们这里用到 3 个API:

  1. fs.readdirSync(dir) 读取指定目录下的所有文件和子目录的名称,并将这些名称作为一个字符串数组返回
  2. fs.readFileSync(path, 'utf8') 用于读取指定文件的全部文件内容
  3. fs.writeFileSync(path, result, 'utf8') 将数据写入指定文件

path 模块提供了一些实用工具,用于处理和转换文件路径。我们这里用到path模块的 1 个API:

  1. path.join(dir, fileName) 用于将所有给定的 path 片段连接在一起,例如,如果 dir/users/adminfileexample.txt,那么 path.join(dir, file) 将返回 /users/admin/example.txt。它可以确保你得到的路径是正确的,无论你是在 Windows(使用反斜杠 `` 作为路径分隔符)还是在 Unix/Linux(使用正斜杠 / 作为路径分隔符)上运行代码。path.join 方法会自动处理这些差异,并返回一个适合当前操作系统的路径。
实现
  1. 找到 my-vue-project 目录下的所有 js 文件
  2. 读取 js 文件内容
  3. 将 js 文件内容,使用正则匹配替换将var替换为let
  4. 将替换后的内容重新写入文件内
ini 复制代码
const fs = require('fs');
const path = require('path');
const commander = require('commander');
const program = new commander.Command();

program
.command('replace-var')
.option('-p, --path [path]', 'file path')
.action(( options, cmd) => {
    const dir = options.path || '.';
    // 获取所有文件,过滤出ts文件
    const files = fs.readdirSync(dir).filter((filePath) => {
        return filePath.endsWith('.ts');
    });
    files.forEach(file => {
        // 拼接完整的文件路径
        const fullPath = path.join(dir, file);
        // 读取文件内容
        const jsCode = fs.readFileSync(fullPath, 'utf8');
        // 替换
        const result = jsCode.replaceAll(' var ', ' let ');
        if (result) {
            // 将替换后的内容写入文件内
            fs.writeFileSync(fullPath, result, 'utf8');
        }
    });
});

program.parse(process.argv);

finish~


相关推荐
大土豆的bug记录4 小时前
鸿蒙进行视频上传,使用 request.uploadFile方法
开发语言·前端·华为·arkts·鸿蒙·arkui
数据潜水员4 小时前
跨域,前端
node.js
maybe02094 小时前
前端表格数据导出Excel文件方法,列自适应宽度、增加合计、自定义文件名称
前端·javascript·excel·js·大前端
HBR666_4 小时前
菜单(路由)权限&按钮权限&路由进度条
前端·vue
A-Kamen5 小时前
深入理解 HTML5 Web Workers:提升网页性能的关键技术解析
前端·html·html5
锋小张6 小时前
a-date-picker 格式化日期格式 YYYY-MM-DD HH:mm:ss
前端·javascript·vue.js
鱼樱前端7 小时前
前端模块化开发标准全面解析--ESM获得绝杀
前端·javascript
yanlele7 小时前
前端面试第 75 期 - 前端质量问题专题(11 道题)
前端·javascript·面试
前端菜鸟日常7 小时前
EJS缓存解决多页面相同闪动问题
前端框架·node.js
前端小白۞8 小时前
el-date-picker时间范围 编辑回显后不能修改问题
前端·vue.js·elementui