概要:在这篇记录中,我们将探讨如何用node.js写个自定义复制脚本,主要作用就是,用命令行输入指定文件或文件夹路径,然后输入复制后重新命名的名字,即可在指定文件或文件夹的同级目录下复制一个相同的文件或文件夹,并且文件命名为用户传进去的名字。

单个文件或文件夹

先简单介绍一下,等下要用到的两个node.js模块:fs(文件系统内置模块),path(处理文件路径的内置模块)

  • path.resolve 路径解析函数,用于将路径解析为绝对路径:1.不带参数,返回的是当前的文件的绝对路径 2.带不是/开头的参数返回的是当前绝对路径拼接现在的参数 3.带./开头的参数同2 4.带/开头的参数则是绝对路径(其他:__dirname 总是指向被执行 js 文件的绝对路径)
  • path.join 用于连接路径 (其他:该方法会正确使用当前系统的路径分隔符,Unix系统是”/“,Windows系统是”\“。)
  • fs.existsSync 检测给定路径是否存在,同步 (其他:fs 模块中的方法均有异步和同步版本,异步的方法函数最后一个参数为回调函数,同步版的则没有回调函数但是会阻塞后续代码的执行)
  • process.argv Node.js 中的一个全局对象,包含了命令行参数的数组:1.第一个参数 process.argv[0] 是 Node.js 运行时的可执行文件路径 2.第二个参数是当前执行的脚本文件的路径 3.后面的参数是用户在命令行中输入的参数
  • fs.mkdirSync 创建目录,同步
  • fs.readdirSync 读取目录的内容,同步
  • fs.copyFileSync 复制文件,同步

ok,来开始编写第一版脚本吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const fs = require("fs");
const path = require("path");

function copyFileOrFolder(source, destination, newName) {
const sourcePath = path.resolve(source);
const destinationPath = path.resolve(destination, newName);

// 检查源路径是否存在
if (!fs.existsSync(sourcePath)) {
console.error("源文件或文件夹不存在。");
return;
}

// 检查目标路径是否已经存在
if (fs.existsSync(destinationPath)) {
console.error("目标文件或文件夹已经存在。");
return;
}

// 如果源是文件夹,则递归复制整个文件夹
const isDirectory = fs.lstatSync(sourcePath).isDirectory();
if (isDirectory) {
// 创建目录
fs.mkdirSync(destinationPath);
const files = fs.readdirSync(sourcePath);
// console.log(files); // 文件名,带后缀的
files.forEach((file) => {
const newSource = path.join(sourcePath, file);
copyFileOrFolder(newSource, destinationPath, file);
});
} else {
// 如果源是文件,则直接复制文件
fs.copyFileSync(sourcePath, destinationPath);
}

console.log(
`成功复制一个${
isDirectory === true ? "文件夹" : "文件"
}${source}-->${newName}`
);
}

// 从命令行参数获取输入
const [, , source, newName] = process.argv;
// console.log(process.argv);

// 指定目标文件夹为源文件的同级目录
const destination = path.dirname(source);

// 执行复制操作
copyFileOrFolder(source, destination, newName);

基本都有注释,就不一行一行解释了,说一下这一版的思路吧:首先我们得获取用户输入的文件(或文件夹)地址以及复制后的文件(或文件夹)命名,然后获取该地址的同级目录为目标文件夹,最后进行复制操作:1.检查源路径和目标路径是否存在 2.如果源是文件则直接复制,是文件夹则递归复制。

用法展示:

1
node copyPageToSpecifyPath.js xxx.js customName.js

要注意的是,如果是文件,记得带后缀哦!

配置路径

上面的脚本我们是针对单个文件或者文件夹进行的,但是在实际开发中,我们不能想复制一个就执行一下,也是浪费时间的,于是我们的2.0脚本要实现的就是,可以通过配置来批量复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 默认模板
const TEMPLATE = [
"../src/components/views/PIMSZdsz/AssetSystem",
"../src/services/views/PIMSZdsz/AssetSystem",
"../src/views/views/PIMSZdsz/AssetSystem",
];

// 通过模板进行复制,新命名,是否开启文本替换(正则匹配,包含命名以及里面的内容),文本替换目标
function copyToTemplate(newName, isTextReplace, replaceTarget) {
// 默认替换目标为模板最后一个文件夹名字
if (isTextReplace && !replaceTarget) {
replaceTarget = TEMPLATE[0]?.split("/")?.pop();
if (!replaceTarget) {
return console.error("未指定替换目标");
}
}
console.log("开始复制:", newName, isTextReplace, replaceTarget);
// 遍历模板数组,复制每个模板到目标路径
TEMPLATE.forEach((template) => {
const sourcePath = path.resolve(__dirname, template);
const destinationPath = path.resolve(__dirname, newName);

// 检查源路径是否存在
if (!fs.existsSync(sourcePath)) {
console.error(`模板文件或文件夹不存在:${template}`);
return;
}

// 检查目标路径是否已经存在
if (fs.existsSync(destinationPath)) {
console.error(`目标文件或文件夹已经存在:${destinationPath}`);
return;
}

// 如果源是文件夹,则递归复制整个文件夹
const isDirectory = fs.lstatSync(sourcePath).isDirectory();
if (isDirectory) {
fs.mkdirSync(destinationPath);
const files = fs.readdirSync(sourcePath);
files.forEach((file) => {
const newSource = path.join(sourcePath, file);
copyFileOrFolder(newSource, destinationPath, file);
});
} else {
// 如果源是文件,则直接复制文件
fs.copyFileSync(sourcePath, destinationPath);
}

console.log(
`成功复制一个${
isDirectory ? "文件夹" : "文件"
}${template} --> ${newName}`
);

// 如果需要进行文本替换
// if (isTextReplace) {
// // 读取目标文件的内容
// let fileContent = fs.readFileSync(destinationPath, "utf8");

// // 使用正则替换目标内容
// fileContent = fileContent.replace(replaceTarget, newName);

// // 将替换后的内容写入文件
// fs.writeFileSync(destinationPath, fileContent, "utf8");

// console.log(`成功替换文件内容:${replaceTarget} --> ${newName}`);
// }
});
}

// 这里就没有用命令行解析库,手搓吧
const parameterMap = {
isUseTemplate: null,
source: null,
newName: null,
isTextReplace: null,
replaceTarget: null,
};
// 先确认是否使用模板
parameterMap.isUseTemplate = Boolean(
process.argv
.slice(2)
.find(
(val) => val.toLowerCase() === "--t" || val.toLowerCase() === "--template"
)
);
// 再获取命令行参数,3种途径
process.argv.slice(2).forEach((val, index) => {
// 1.缩写参数 暂不实现
// if (val.toLowerCase() === "--r" || val.toLowerCase() === "--replace") {
// parameterMap.isTextReplace = true;
// }
if (val.includes("=")) {
// 2.key=value 类型参数
const [key, value] = val.split("=");
parameterMap[key] = value;
} else if (index < 3) {
// 3.按顺序获取
if (parameterMap.isUseTemplate) {
if (index === 0) {
parameterMap.newName = val;
} else if (index === 1 && !val.includes("--")) {
parameterMap.isTextReplace = val;
} else if (index === 2 && !val.includes("--")) {
parameterMap.replaceTarget = val;
}
} else {
if (index === 0) {
parameterMap.source = val;
} else if (index === 1 && !val.includes("--")) {
parameterMap.newName = val;
}
}
}
});

console.log(parameterMap);

if (parameterMap.isUseTemplate) {
copyToTemplate(
parameterMap.newName,
parameterMap.isTextReplace === null ? true : parameterMap.isTextReplace,
parameterMap.replaceTarget
);
} else {
// 指定目标文件夹为源文件的同级目录
const destination = path.dirname(parameterMap.source);
copyFileOrFolder(parameterMap.source, destination, parameterMap.newName);
}

可以先不用看关于解析命令行那的代码,我们先聚焦于copyToTemplate函数,我思路就是对模板里的路径进行forEach,挨个进行复制,是不是很简单。然后我信心满满的输入下面的命令

1
node ./scripts/copyPageToSpecifyPath.js Test --t

嘿,报错了,目标文件或文件夹已经存在,原来是我在获取目标路径const destinationPath = path.resolve(__dirname, newName);时,没有将路径搞对,导致文件直接复制到脚本旁边了。

最终脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
const fs = require("fs");
const path = require("path");

// 本脚本主要针对模板路径所在文件的同一层级为目的地址进行复制,使用方法如下:(加--t就是使用模板)
// 1.只是复制单个文件或文件夹:node ./scripts/copyPageToSpecifyPath.js ../src/components/views/PIMSZdsz/AssetSystem/index.js test.js
// 2.只是复制模板路径中的文件:node ./scripts/copyPageToSpecifyPath.js Test --t
// 3.复制并替换模板路径中文件的内容:node ./scripts/copyPageToSpecifyPath.js Test true --t
// 注:在使用模板的前提下第二个参数为true就是进行替换内容,需要的话可以在第3,第4参数配置替换目标和替换值,默认是模板中最后的文件名以及新文件名

// 默认模板
const TEMPLATE = [
"../src/components/views/PIMSZdsz/AssetSystem",
"../src/services/views/PIMSZdsz/AssetSystem",
"../src/views/PIMSZdsz/AssetSystem",
];

// 单个文件或者文件夹复制,源路径,目的文件夹,新命名
function copyFileOrFolder(
source,
destination,
newName,
isTextReplace = false,
replaceTarget,
replaceValue
) {
if (isTextReplace && !replaceTarget) {
replaceTarget = source.split("/")?.pop();
replaceValue = replaceValue ?? newName;
if (!replaceTarget) {
return console.error("未指定替换目标");
}
}

const sourcePath = path.resolve(__dirname, source);
const destinationPath = path.resolve(__dirname, destination, newName);

// 检查源路径是否存在
if (!fs.existsSync(sourcePath)) {
console.error("copyFileOrFolder:源文件或文件夹不存在。");
return;
}

// 检查目标路径是否已经存在
if (fs.existsSync(destinationPath)) {
console.error("copyFileOrFolder:目标文件或文件夹已经存在。");
return;
}

// 如果源是文件夹,则递归复制整个文件夹
const isDirectory = fs.lstatSync(sourcePath).isDirectory();
if (isDirectory) {
// 创建目录
fs.mkdirSync(destinationPath);
const files = fs.readdirSync(sourcePath);
// console.log(files); // 文件名,带后缀的
files.forEach((file) => {
const newSource = path.join(sourcePath, file);
let newFileName = file;
if (isTextReplace) {
newFileName = file.replaceAll(replaceTarget, replaceValue);
}
copyFileOrFolder(
newSource,
destinationPath,
newFileName,
isTextReplace,
replaceTarget,
replaceValue
);
});
} else {
// 如果源是文件,则直接复制文件
fs.copyFileSync(sourcePath, destinationPath);

// 如果需要进行文本替换
if (isTextReplace) {
// 读取目标文件的内容
let fileContent = fs.readFileSync(destinationPath, "utf8");
fileContent = fileContent.replaceAll(replaceTarget, replaceValue);
// 将替换后的内容写入文件
fs.writeFileSync(destinationPath, fileContent, "utf8");

console.log(
`copyFileOrFolder成功替换文件内容:${replaceTarget} --> ${replaceValue}`
);
}
}

console.log(
`copyFileOrFolder成功复制一个${
isDirectory === true ? "文件夹" : "文件"
}${source}-->${newName}`
);
}

// 通过模板进行复制,新命名,是否开启文本替换(正则匹配,包含命名以及里面的内容),文本替换目标
function copyToTemplate(newName, isTextReplace, replaceTarget, replaceValue) {
// 默认替换目标为模板最后一个文件夹名字
if (isTextReplace && !replaceTarget) {
replaceTarget = TEMPLATE[0]?.split("/")?.pop();
replaceValue = replaceValue ?? newName;
if (!replaceTarget) {
return console.error("未指定替换目标");
}
}
console.log(
"copyToTemplate开始复制:",
newName,
isTextReplace,
replaceTarget
);

// 遍历模板数组,复制每个模板到目标路径
let result = 0;
for (let i = 0; i < TEMPLATE.length; i++) {
const template = TEMPLATE[i];
const sourcePath = path.resolve(__dirname, template);
const destinationPath = path.resolve(
__dirname,
TEMPLATE[i]?.split("/")?.slice(0, -1).join("/"),
newName
);

// 检查源路径是否存在
if (!fs.existsSync(sourcePath)) {
console.error(`copyToTemplate:模板文件或文件夹不存在:${template}`);
return;
}

// 检查目标路径是否已经存在
if (fs.existsSync(destinationPath)) {
console.error(
`copyToTemplate:目标文件或文件夹已经存在:${destinationPath}`
);
return;
}

// 如果源是文件夹,则递归复制整个文件夹
const isDirectory = fs.lstatSync(sourcePath).isDirectory();
if (isDirectory) {
fs.mkdirSync(destinationPath);
const files = fs.readdirSync(sourcePath);
files.forEach((file) => {
const newSource = path.join(sourcePath, file);
let newFileName = file;
if (isTextReplace) {
newFileName = file.replaceAll(replaceTarget, replaceValue);
}
copyFileOrFolder(
newSource,
destinationPath,
newFileName,
isTextReplace,
replaceTarget,
replaceValue
);
});
} else {
// 如果源是文件,则直接复制文件
fs.copyFileSync(sourcePath, destinationPath);

// 如果需要进行文本替换
if (isTextReplace) {
// 读取目标文件的内容
let fileContent = fs.readFileSync(destinationPath, "utf8");
fileContent = fileContent.replaceAll(replaceTarget, replaceValue);
// 将替换后的内容写入文件
fs.writeFileSync(destinationPath, fileContent, "utf8");

console.log(
`copyToTemplate成功替换文件内容:${replaceTarget} --> ${replaceValue}`
);
}
}

console.log(
`copyToTemplate成功复制一个${
isDirectory ? "文件夹" : "文件"
}${template} --> ${newName}`
);
result++;
}

if (result === TEMPLATE.length) {
console.log("copyToTemplate:------------全部复制完成!------------");
} else {
console.log(
`copyToTemplate:---已复制${result}条路径所对应的文件或文件夹,剩余${
TEMPLATE.length - result
}条未复制-----`
);
}
}

// 这里就没有用命令行解析库,手搓吧
const parameterMap = {
isUseTemplate: null,
source: null,
newName: null,
isTextReplace: null,
replaceTarget: null,
replaceValue: null,
};
// 先确认是否使用模板
parameterMap.isUseTemplate = Boolean(
process.argv
.slice(2)
.find(
(val) => val.toLowerCase() === "--t" || val.toLowerCase() === "--template"
)
);
// 再获取命令行参数,3种途径
process.argv.slice(2).forEach((val, index) => {
// 1.缩写参数 暂不实现
// if (val.toLowerCase() === "--r" || val.toLowerCase() === "--replace") {
// parameterMap.isTextReplace = true;
// }
if (val.includes("=")) {
// 2.key=value 类型参数
const [key, value] = val.split("=");
parameterMap[key] = value;
} else if (index < 5) {
// 3.按顺序获取
if (parameterMap.isUseTemplate) {
if (index === 0) {
parameterMap.newName = val;
} else if (index === 1 && !val.includes("--")) {
parameterMap.isTextReplace = val;
} else if (index === 2 && !val.includes("--")) {
parameterMap.replaceTarget = val;
} else if (index === 3 && !val.includes("--")) {
parameterMap.replaceValue = val;
}
} else {
if (index === 0) {
parameterMap.source = val;
} else if (index === 1 && !val.includes("--")) {
parameterMap.newName = val;
} else if (index === 2 && !val.includes("--")) {
parameterMap.isTextReplace = val;
} else if (index === 3 && !val.includes("--")) {
parameterMap.replaceTarget = val;
} else if (index === 4 && !val.includes("--")) {
parameterMap.replaceValue = val;
}
}
}
});

console.log(parameterMap);

if (parameterMap.isUseTemplate) {
copyToTemplate(
parameterMap.newName,
parameterMap.isTextReplace === null ? true : parameterMap.isTextReplace,
parameterMap.replaceTarget,
parameterMap.replaceValue
);
} else {
// 指定目标文件夹为源文件的同级目录
const destination = path.dirname(parameterMap.source);
copyFileOrFolder(
parameterMap.source,
destination,
parameterMap.newName,
parameterMap.isTextReplace === null ? true : parameterMap.isTextReplace,
parameterMap.replaceTarget,
parameterMap.replaceValue
);
}

// TODO:参数有点多,后续优化成options配置
// TODO:当前是针对模板路径所在文件的同一层级进行复制的,后续增加指定复制路径的配置

这个脚本还有很多可以优化的地方,比如if分支过多,比如两个函数体内有部分逻辑重复,甚至可以合并成一个函数,但是因为最近工作比较忙,就先这样,后续再优化吧。

使用说明

现在来说明一下如何使用,首先你得有node.js,然后确定脚本所放位置,是直接在项目根路径下还是放哪,比如我所做的项目是将脚本统一放到scripts下面

然后选择想要的复制效果,接着用命令行传递参数并执行脚本文件:

1.只是复制单个文件或文件夹:

1
node ./scripts/copyPageToSpecifyPath.js ../src/components/views/PIMSZdsz/AssetSystem/index.js test.js

2.只是复制模板路径中的文件:

1
node ./scripts/copyPageToSpecifyPath.js Test --t

3.复制并替换模板路径中文件的内容:

1
node ./scripts/copyPageToSpecifyPath.js Test true --t

其实也可以用参数=值的形式,比如

1
node ./scripts/copyPageToSpecifyPath.js newName=Test isTextReplace=true --t

贴张效果图: