命令行工具揭秘

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

当我们运行 npx @vue/cli create my-project 创建一个项目时,就是通过 npx 执行了@vue/cli包中的可执行文件。"可执行文件"就是我们通常说的脚本,许多 npm 包的可执行文件是用 JavaScript 编写的,例如: xx.ts、xx.mts、xx.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个模块:fs 和 path。

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/admin,file 是 example.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~


相关推荐
jktantan6 小时前
Vue keep-alive 切标签后滚动位置回顶?因为 onDeactivated 里读到的 scrollTop 是 0
前端·javascript·vue.js
只睡四小时7 小时前
MCP 弃用 SSE?Node 分帧漏 81.7% 事件
node.js·webassembly·实时通信·mcp·streamable http
2601_962885727 小时前
quotes 字段全解:涨跌幅、振幅、换手率的口径与单位
java·前端·spring
excel7 小时前
encodeURI 和 encodeURIComponent 有什么区别?
前端
前端精髓7 小时前
调用openrouter的免费API模型
java·前端·javascript
赛博守夜人8 小时前
AI安全攻防与治理之十四:AI 安全测试不能脱离传统安全,Web、API、移动端与模型攻击面的组合拳
前端·人工智能·安全
开开心心_Every8 小时前
快速隐藏程序工具自定义快捷键一键切换
java·开发语言·前端·spring·智能手机·word·intellij-idea
networknomad8 小时前
OpenReplay:自己搭的会话回放工具,给前端排错用的
前端·其他
缓慢更新9 小时前
AI 辅助生成单测:从接口定义到用例的流水线
java·服务器·前端·网络·数据库·人工智能
陈随易10 小时前
AI 运维服务器,安全又可靠的方法
前端·后端·程序员