前言
cac 是一个命令行参数输入的辅助工具,这篇文章就借用这个工具,来做一个文件复制工具
文件复制工具有三种需求
- 通过--s ,--source 设置源文件,--d --destination 设置目标文件
- 没有参数显示说明哪个文件,可以用先后顺序来表示源文件和目标文件
- 也可以只用一个参数表示文件,另一个自然是其他文件
实现这个功能之前,先来了解 cac 这个工具,如何处理命令行的参数
先准备一个空文件夹,然后安装 cac
mkdir cac
cd cac
npm init -y
npm i cac
创建一个文件 index.js,填入下面这个代码
/**@type {import('cac')} */
const cac = require("cac");
/**@type {import('cac').CAC} */
const cli = cac();
cli.option("--type <type>", "choose a type", {
default: "node",
});
const parsed = cli.parse();
console.log(JSON.stringify(parsed, null, " "));
执行:
node index.js --type test
控制台输出内容:
好了,现在我们一个完成了非常简单的命令行参数解析的功能,在命令行中设置 type 为'test',然后在打印出来的 options 对象中,就可以看到 type 的值确实为 test
下面我们来看看 cac 有哪些 api
option
command 外的 option 配置
option 作用是设置变量,来接收命令行中的参数值。optino 有两种,一种跟在 command 之后的,一种不跟在 command 之后的。先看不跟在 command 之后,就像上面的 type
对于不跟在 command 之后的 option 变量,cac 对其要求相当得宽松。
基本用法:命令行中,--变量名
,后面紧跟变量值
,匹配好的变量,就会被放在 options 对象里面。 如果没有紧跟变量名的变量值,那就会被放在 args 中
cli.option("--test <test>", "test option");
node index.js --test test1 test2 test3
test1
,test2
放到了 args 中
功能简单,有几个需要注意的地方:
注意的几个地方
- 命令行中输入的变量可以不与 option 中定义的变量名称匹配,可以为任意名称,甚至任意数量
node index.js --test test1 --test2 test2 --test3 test3
option 中只定义了 test 变量,但是命令行中输入了多个变量,也全都被 cac 捕获到放在 options 对象里面
- 相同变量可以出现多次,表示该变量有多个值
node index.js --test test1 --test test2 --test3 test3
命令行中有两个 test,值分别是test1
,test2
,所以在输出的 options 中,可以看到 test 的值变成了数组,数组的内容是test1
,test2
- 如果变量后面没有设值,那么在 options 中,该变量就会被设置成 true,即布尔类型
node index.js --test test1 --test2 test2 --test3
test3 的值变成了 true
- 如果删掉变量名之后的括号,该变量就变成了 boolean 值
cli.option("--test", "test option");
node index.js --test test1 --test test2 --test3 test3
因为 option 中 test 后面的括号删掉了,所以 test 被识别为 boolean 类型。那紧跟着 --test
的值就自然放在了 args 中了
command 内的 option 配置
command 内的 option 就要严格很多了。
先看 command 基本用法。command 表示需要执行的命令,像 npm 中的 npm init
,npm install
中的 init
和install
cli.command("rm <file>", "input remove file")
.option("--d", "is directory")
.action((file, options) => {
console.log('file: ',file);
console.log('options: ',options);
});
上面代码定义一个 rm
的命令,表示要移除某个文件。其中的,表示用户需要在 rm 命令之后输入文件名,文件名用 file 变量接收
然后 action 函数中的回调函数的第一个参数就是 file 变量了。第二个参数 options 就是上文中普通的 options 对象
command 也会有自己的 option,option 的用法和上文的用法一致。下面来看看实际效果
node index.js rm test.txt --d false
出分成两块来看,第一块是 action 中回调函数的输出。第二块就是原本的 parsed 对象的输出。着重看第一块。
可以看到 file 变量成功被匹配了,作为了 action 回调函数的第一个参数。 并且-d 变量的匹配结果放在了 options 对象中
在第二块可以看到 args 中有 test.txt,这是因为 test.txt 没有 options 中的变量接收
看似很简单,但还是有很多需要注意的地方
注意的几个地方
- command 中定义的
命令变量
,使用尖括号后表示必填,即必须提供命令变量的值,并且可以在 option 回调函数中单独获取。如果没有使用尖括号,表示选填,而且不能在 option 回调函数中单独获取
还是用上面的 command 配置做测试
node index.js rm
没有提供 rm 命令的值,就会报错。将改成 [file]
cli.command("rm [file]", "input remove file")
.option("--d", "is directory")
.action((file, options) => {
console.log('file: ',file);
console.log('options: ',options);
});
现在没有报错了,但出现有意思的事情。action 中回调函数的第一个参数编程了 options,第二个参数变成了 undefined。由此可以猜到回调函数的入参设置逻辑了
这逻辑大家可以想想为什么
注意⚠️, 如果使用了选填模式,那么就不能用 action 函数单独接收 file 的值了,只能去 args 中拿
node index.js rm test.txt
命令行中就算输入了 test.txt
,option 回调函数中还是只有一个参数
- 不能使用 option 中没有定义的变量名
node index.js rm test.txt --d --f
使用了没有定义的 f 变量,就报错了
这里和 command 外的 option 配置很不一样,command 外的 option 可以接收任意的变量名,无论有没有定义过。
- options 的变量名如果使用了尖括号,就必须提供变量名的值,否则就报错。如果使用方括号或者没有括号,就是可选的
cli.command("rm [file]", "input remove file")
.option("--d <d>", "is directory")
.action((file, options) => {
console.log('file: ',file);
console.log('options: ',options);
});
node index.js rm test.txt --d
node index.js rm test.txt --d true
注意这里是不提供 d
的值,而不是不设置 d
。如果直接不设置 d
,则不会报错。
这里和 command 外的 option 配置也不一样,对于 command 外的 option,无论变量名是否用了尖括号,还是方括号,都不用提供变量的值。如果不提供变量的值,会将变量默认置为 true
- 变量名可以使用简写,类似
--h
,--help
,或者npm i
,npm install
表示同一个意思
cli.command("rm [file]", "input remove file")
.option("--d, --is-directory <d>", "is directory")
.action((file, options) => {
console.log('file: ',file);
console.log('options: ',options);
});
node index.js rm test.txt --d true
可以看到 options 对象里面多了两个 key,一个是 d
,还有一个是 isDirectory
cac 会将短横线命令法转成小驼峰
用命令行实现文件 copy
好了 cac 的 API 讲解得差不多了,可以上手实现文章开头提出的需求了
- 通过--s ,--source 设置源文件,--d --destination 设置目标文件
- 如果没有参数显示说明哪个文件,可以用先后顺序来表示源文件和目标文件
- 也可以只用一个参数表示其中一种文件,另一个文件名不需要参数
实现 copy 功能
// copy.js
const fs = require("node:fs");
const path = require("node:path");
const copy = async (source, address) => {
const currentDir = process.cwd();
const addressPath = path.join(currentDir, address);
const sourcePath = path.join(currentDir, source);
try {
await fs.promises.copyFile(sourcePath, addressPath, fs.constants.COPYFILE_EXCL);
} catch (error) {
console.error(error);
}
};
exports.copy = copy;
这里准备了一段代码,实现了 copy 函数,函数接收源文件,和目标文件名。然后使用fs.promises.copyFile
实现文件 copy
最后使用 exports
将 copy 导出
代码准备好了。讲讲我使用 cac 工具和 copy 代码结合的思路。
- 首先使用 cac 读取命令行中的源文件名,和目标文件名
- 将文件名传入 copy 函数中,实现文件的拷贝
修改 index.js 代码
const cac = require("cac");
const { copy } = require("./copy");
/**@typedef {import('cac').CAC} CAC*/
/**@type {CAC} */
const cli = cac();
cli.option("--r, --resource [resource]", "set resource file")
.option(
"--d, --destination [destination]",
"set destination file"
);
const parsed = cli.parse();
/**@typedef {typeof cli.parse} parse*/
/**
* @param {ReturnType<parse>} parsed
* @returns
*/
const getFiles = (parsed) => {
const options = parsed.options;
let resource = options.r,
destination = options.d;
if (!resource && !destination) {
return parsed.args.slice(0, 2);
}
if (!resource) {
return [parsed.args[0], destination];
}
if (!destination) {
return [resource, parsed.args[0]];
}
return [resource, destination];
};
copy(...getFiles(parsed));
先使用 option 配置读取命令行输入的文件名,配置中resource
和destination
都使用了 command 外的 option,表示可填可不填。
重点的参数变量
处理逻辑,是放在了函数getFiles 中。
首先判断两个文件名都没有使用变量名来接收,即!resource && !destination
为真。然后读取 args 中的前两项。可能用户会输出超过两个文件名,但我们只读取前两项。
如果其中一个没有使用变量名来接收,那就分开来判断。
代码很简单吧😁。下面来测试测试效果
测试代码
先准备一个 test.txt 文件
然后使用命令,将这个 test.txt 复制,复制后的文件名是 test2.txt
node index.js test.txt test2.txt
copy 成功。这里没有使用任何变量来接受文件名。
node index.js -r test.txt test3.txt
node index.js test.txt -d test4.txt
node index.js -r test.txt -d test5.txt
有个小设计:如果复制后的文件名已经存在的话,就会报错
完美
总结
这篇文章分享了 cac 的 API 使用,并且做了实用场景小 demo--实现文件的 copy