Node实现修改全局不规范文件名-解放双手

最近公司的项目的文件名要进行规范,首先是同事写了一个检查不规范文件名的脚本,然后我又写了一个全局搜索替换不规范文件名的脚本,接下来首先进行node的学习。

一、node模块学习

本章针对不熟悉node模块的同学,已经熟悉的可以跳过。

1.1 fs和path模块

/**
* fs 文件系统

* 分为同步和异步 推荐使用异步
* 异步函数的最后一个参数是一个回调函数,回调函数的参数第一个是报错信息,第二个是数据。
*
* 常用函数:
*
* readdir(path,callback) 异步 读取目录的内容
* readdirSync(path) 同步
*
* readFile(path,function(err,data)) 异步 读取文件的内容
* readFileSync(path) 同步
*
* writeFile(filename, data[, options], callback) 异步 写入文件内容 写入方式默认是w,覆盖原有的内容
* writeFileSync(filename, data[, options]) 同步
*
* stat(path,callback) 异步 path文件路径 如果直接写路径要写绝对路径,否则使用__dirname,
*                               回调函数有err,stats两个参数,stats是fs.Stats返回的对象
*                               返回的stats实例有方法如:
*                               isFile() 是否是文件
*                               isDirectory() 是否是目录
* statSync(path) 同步
*
* stat和readFile区别,stat用来初步解析目录中的文件和文件夹,而readFile用来读取具体文件的内容。
*

*  path模块

*  path.join(path1,path2) 把传入的路径进行连接

*  path.resolve([from],to) 把传入的to参数,结合前面的from参数,解析为一个绝对路径。

*  path.dirname(p) 路径中文件夹部分,即当前执行脚本所在的文件夹的路径

*  path.basename(p) 路径的最后一段,即当前脚本的文件名

*  path.extname(p)  文件的扩展名

*/

  • 首先公共的引入部分
//引入文件系统
const fs = require("fs");

//引入处理文件路径
const path = require('path');

//连接文件路径
var filepath = path.join(__dirname,'/test.txt');

//test.txt内容为content
  • readdir()示例
//遍历目录
//同步遍历
fs.readdir(__dirname,function(err,files){
 if(err){
  console.log(err)
 }else{
  files.forEach(function(file){
   console.log(file)
  })
 }
})

//异步遍历
var files = fs.readdirSync(__dirname);
console.log(“异步”+ files)

//执行结果
异步 [ 'test.js', 'test2.js', 'test.txt', 'test1' ]
test.js
test2.js
test.txt
test1
  • readFile()、readFileSync()示例
// 异步读取
fs.readFile(filepath,function(err,data){
 if(err){
  console.error(err);
 }else{
  console.log('异步读取',data.toString());
 }
})

// 同步读取
var data = fs.readFileSync(filepath);
console.log("同步读取: " + data.toString());
console.log("程序执行完毕。");

//执行结果:
同步读取: content
程序执行完毕。
异步读取:content
  • stat()示例
//解析目录中的文件或文件夹
fs.stat(filepath,function(err,stats){
 if(err){
  console.error(err)
 }else{
  console.log("读取的文件内容"+stats);//stats是一个对象
  console.log("是否是文件",stats.isFile()); //true
  console.log("是否是目录",stats.isDirectory()); //false

  //如果是文件就可以进一步读取文件的内容
  if(stats.isFile()){
   console.log('文件内容为',fs.readFileSync(filepath,'utf-8'));
  }
 }
})

//执行结果:
读取的文件内容[object Object]
是否是文件 true
是否是目录 false
文件内容为 content
  • writeFile()示例
//写入文件
fs.writeFile(filepath,'newcontent',function(err){
 if(err){
  console.error(err);
 }
 console.log('写入数据成功')

 fs.readFile(filepath,function(err,data){
  console.log("读取新数据",data.toString())
 })
})

//执行结果:
写入数据成功
读取新数据 newcontent
test.txt内容变为newcontent
  • 最后结合起来的用法

* 遍历文件步骤为:

* readdir遍历目录(传入的是一个目录),
* stat对每个文件或文件夹进行解析(传入的通常是一个通过path.join组合或者path.resolve解析成的目录和每个文件的路径),
* 判断如果是文件的话用readFile读取文件的内容,
* 通过正则匹配到内容进行替换,然后writeFile写入文件内容。

fs.readdir(__dirname,function(err,files){
 if(err){
  console.log(err)
 }else{
  //遍历每一个文件
  files.forEach(function(file){
   console.log(file)
   //构造文件的路径
   var filePath = path.join(__dirname,file);
   //解析文件
   fs.stat(filePath,function(err,stats){
    if(err){
     console.error(err)
    }else{
     //如果是文件就可以进一步读取文件的内容
     if(stats.isFile()){
      console.log('文件内容为',fs.readFileSync(filePath,'utf-8'));
      //写入新内容
      fs.writeFile(filePath,'newcontent')
     }else{
      //继续遍历
     }
    }
   })
  })
 }
})

//执行结果就是newcontent

 

1.2 minimist模块

process.argv 从node命令行中读取传入的参数

如执行命令 :node gitHooks/check-filename.js --scope=client/src/baseStore --module=all

打印process.argv结果如下:

是一个数组,第一个参数是node程序所在位置,第二个参数是js脚本所在位置,之后的参数就是输入的命令行参数了。

[ 'C:\\Program Files\\nodejs\\node.exe',
  'D:\\storm\\gitHooks\\check-filename.js',
  '--scope=client/src/baseStore',
  '--module=all' ]

要想使用这些参数,需要通过minimist 命令行参数解析引擎,对参数进行处理,

const minimist = require('minimist');
const argvs = minimist(process.argv.slice(2))

//打印结果如下,数组被转换成对象的形式
{ _: [], scope: 'client/src/baseStore', module: 'all' }
就可以通过argvs.scope/argv.module 获取到传入的参数的值

1.3 child_process模块

child_process子进程模块,用来创建一个子进程,并执行任务,例如shell命令,可以在js中直接调用shell命令去执行一些操作如git操作。

exec(command,[, options][, callback]) 异步  要执行的命令

execSync(command,[, options]) 同步

//获取到git add 提交到暂存区的文件名列表
let filesList = execSync("git diff --cached --name-only").toString('utf8').trim().split("\n");

//执行结果
[ 'client/src/baseStore/LoadBaseParams.js',
  'client/src/baseStore/actions.js' ]

1.4 export-excel模块

用来导出excel表格。用法如下:

var express = require('express');
var nodeExcel = require('excel-export');
var app = express();

app.get('/Excel', function(req, res){
  	var conf ={};
	conf.stylesXmlFile = "styles.xml";
    conf.name = "mysheet";
  	conf.cols = [{
		caption:'string',
        type:'string',
        beforeCellWrite:function(row, cellData){
			 return cellData.toUpperCase();
		},
        width:28.7109375
	},{
		caption:'date',
		type:'number',
	}];
  	conf.rows = [
 		['pi', 3.14],
 		["e",  2.7182],
        ["M&M<>'", 1.61803],
        ["null date", 1.414]  
  	];
  	var result = nodeExcel.execute(conf);
  	res.setHeader('Content-Type', 'application/vnd.openxmlformats');
  	res.setHeader("Content-Disposition", "attachment; filename=" + "Report.xlsx");
  	res.end(result, 'binary');
});

二、检查文件命名规范脚本

通过以上的几个模块的学习,就能读懂检查文件命名规范的脚本啦,如下所示:

运行命令示例  node  gitHooks/check-filename.js --scope=src --module=all 

--scope可以设置从项目根目录的哪里进行检测,module可以设置all对所有文件进行检测,以及commit只对本次提交的文件名进行检测。

commit的实现是通过git的钩子函数pre-commit来执行的,

首先安装  

npm i yorkie lint-staged@8.1.0 --save

然后在package.json中写法如下,在我们git commit 时,会执行到pre-commit,然后会关联到lint-staged,就会执行这条命令来进行检查,添加参数--noVerify可以跳过检查。

  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*": [
      "node gitHooks/check-filename.js --noVerify --scope=client --module=commit"
    ]
  }

 具体脚本如下:

const { execSync } = require("child_process");
const fs = require('fs')
const path = require('path')
const minimist = require("minimist")

const rootPath = path.join(__dirname, '..') //工程根目录路径
const rootDirName = rootPath.slice(rootPath.lastIndexOf('/') + 1) //工程文件夹名
const fileReg = /^[a-z]+[-0-9a-z]{0,}[^-]$/; //数字小写字母中华线组成字符串,且不以数字中华线开头,不以中华线结尾
const imgReg = /^[a-z]+[-@0-9a-z]{0,}[^-@]$/; //图片正则z

const imgExtArr = ['png','jpg','jpeg','gif']
let errorName = [];
//检查模式参数
let argv = 'commit';
//解析命令行参数
let argvParams = minimist(process.argv.slice(2))

if (argvParams['noVerify']) {
  console.log('-------no-verify 跳过检查------')
  process.exit(0);
  return
}

if (!argvParams['scope']) {
  console.log('-------no scope 请检查检测范围------')
  process.exit(-1);
  return
}

if (argvParams.module == 'all' || argvParams.module == 'commit') {
  argv = argvParams.module;
}
console.log('====检测模式参数====:' + argv)

if (argv == 'commit') {
  console.log('-------检测提交文件---------')
  let filesList = getFilenamesByCommand("git diff --cached --name-only").split("\n");
  filesList = filesList.filter(filePath=>{
    return filePath.indexOf(`${argvParams['scope']}`) != -1
  })
  console.log('待检测文件数量:' + filesList.length)
  errorName = getErrorFileName(filesList);
} else {
  console.log('---------检测全部文件--------')
  const rootDirs = path.resolve(path.join(__dirname, `../${argvParams['scope']}`))
  let filesList = getAllFiles(rootDirs);
  console.log('待检测文件数量:' + filesList.length)
  errorName = getErrorFileName(filesList);
}

//获取command命令对应的文件
function getFilenamesByCommand(command) {
  return execSync(command).toString('utf8').trim();
}
function getAllFiles(dirs) {
  let files = [];
  let fileList = fs.readdirSync(dirs);
  for (let file of fileList) {
    const currentPath = path.resolve(dirs, file)
    const stat = fs.statSync(currentPath)
    const isDirectory = stat.isDirectory();
    if (isDirectory) {
      files = files.concat(getAllFiles(currentPath))
    } else {
      files.push(currentPath)
    }
  }
  return files
}
//获取不符合规范的文件路径
function getErrorFileName(fileList) {
  let fileNames = [];
  for (let i = 0; i < fileList.length; i++) {
    const filePath = fileList[i];
    const arr = filePath.split("/");
    const filename = arr[arr.length - 1];
    const extname = path.extname(filePath).slice(1);
    let isMatch = false;
    if(imgExtArr.indexOf(extname) == -1){
      isMatch = fileReg.test(filename.split('.')[0]);
    }else{
      isMatch = imgReg.test(filename.split('.')[0]);
    }

    if (!isMatch) {
      //路径截取
      const index = filePath.indexOf(rootDirName);
      fileNames.push(filePath.slice(index < 0 ? 0 : index));
    }
  }
  return fileNames;
}

if (errorName.length) {
  console.log('不符合规范文件数量:' + errorName.length + '\n');
  console.log('文件规则:数字、小写字母、中华线 组成字符串,且不以数字中华线开头,不以中华线结尾' + '\n');
  console.log('图片规则:数字、小写字母、中华线、@符 组成字符串,且不以数字中华线开头,不以中华线、@符结尾' + '\n');
  console.log('不符合规范文件:' + '\n');
  console.log(errorName.join('\n'))
  process.exit(-1);
} else {
  console.log('is ok to commit')
  process.exit(0);
}

但是在使用过程中发现一个问题,就是在windows系统上对all所有文件进行检测的时候脚本检测的不正确,原因是在windows上通过 const currentPath = path.resolve(dirs, file)  获取到的路径再经过  files.push(currentPath)  处理就会变成 这种双斜杠的形式,'D:\\storm\\client\\src\\baseStore\\actions.js',所以在对文件路径进行分割出文件名时const arr = filePath.split("/");,就不能分割出正确的文件名,导致判断错误。对于commit情况,获取的路径是正确的,就可以判断正确。

使用os模块或者process.platform()函数就可以判断当前的操作系统,如果是windows就用\\来分割,就能正确检测了。

三、搜索替换不规范文件名脚本实现

大致思路是:

  1. 首先搜索出输入的搜索范围内的不符合规范的文件,生成一个文件名数组。
  2. 对文件名数组进行遍历,并根据另一个搜索范围,这个范围是搜索引入了此不规范文件的文件,如import、require。
  3. 根据正则匹配,对文件内容匹配以斜杠开头的不规范文件名,因为这样的一般都是引入的文件,如/searchInput。(目前没有发现以数字中划线开头,以中划线结尾的文件,而且不知道要改成什么,所以只针对驼峰命名形式的进行了修改。)
  4. 再次根据正则匹配,对此驼峰字符串转换成用中划线连接的字符串,如/search-input。
  5. 最后把此不规范的文件的文件名进行修改。

需要注意的是:

  1. 两个搜索范围最好设置成一样的,它会搜索出范围内所有的不规范的名字,然后匹配每一个去全局搜索替换,这样不容易有疏漏。
  2. 搜索范围不要指定到项目根目录下,因为像一些.git目录下的文件,fs是打不开的,会报错,最好指定到项目核心代码目录下。
  3. 根目录下的bulid、config等文件,在等全局替换完之后,要进行检查,可能会有引用scss文件的,需要手动矫正一下。
  4. 一些html中使用组件时,可能会有例一这样的写法,工具无法做到面面俱到,这些个别的情况目前需要手动矫正一下。
  5. 对于例二这样的写法,也是个别情况,也需要手动矫正。
  6. 图片经过fs的阅读会导致损坏,需要重新替换一下。
  7. 如例三,不规范的文件名和所在目录名重名的,会导致把目录也替换掉,需要手动矫正一下。
  8. 目前脚本还不太完善,只能对一般情况进行修改,降低人工劳动成本
//例一
<MultipleSelectDeptAndEe
>
  <div>
  </div>
</MultipleSelectDeptAndEe>
//例二
if (applyType == 5) {
   //外出申请/
 return t.re('OutboundApply');
} else if (applyType == 9) {
  //加班
 return t.re('OvertimeApply');
}

//例三
import MultipleSelectDeptAndEe from 'com/MultipleSelectDeptAndEe ';
//目录名和里面的MultipleSelectDeptAndEe.vue同名,导致误改。
import MultipleSelectDeptAndEe from 'com/multiple-select-dept-and-ee';

如何使用:

  1. 可在package.json中的script中添加npm快捷命令使用:

"rename": "node gitHooks/change-filename.js --searchScope=client --replaceScope=client" 

npm run rename   

     2.直接使用:node gitHooks/change-filename.js --searchScope=client --replaceScope=client 

     3.其中 searchScope 和replaceScope默认是client ,可省略、也可自定义

坑:一开始改文件名使用的是fs.rename() 在本地可以看到像Index、List,已经修改为了index、list,但当提交到git上后,查看文件名还是Index、List,

查了方法 说配置一下区分大小写就行,git config core.ignorecase false,然后git提交发现,保留了之前的大写Index,并新增了全部是我修改的index文件,这样肯定不行。

所以最后使用的是git rename ,这样git就能识别到文件是重命名了。

具体脚本如下:

const fs = require('fs')
const path = require('path')
const minimist = require("minimist")
const express = require('express');
const nodeExcel = require('excel-export');
const {execSync} = require('child_process');
const app = express();
 
const rootPath = path.join(__dirname, '..') //工程根目录路径
const rootDirName = rootPath.slice(rootPath.lastIndexOf('/') + 1) //工程文件夹名
const fileReg = /^[a-z]+[-0-9a-z]{0,}[^-]$/; //数字小写字母中华线组成字符串,且不以数字中华线开头,不以中华线结尾
//检测的文件类型
const checkExtArr = ['vue','js','ts','scss','css'];
//不符合规范的文件名路径
let errorName = [];
//不符合规范的文件名
const fileNameArr = [];
//替换次数
let index = 0;
//excel数据
var temp = [];
 
//解析命令行参数
let argvParams = minimist(process.argv.slice(2))
//不填写范围默认client下
let searchScope = 'client';
let replaceScope = 'client';
 //示例命令行写法:  node gitHooks/change-filename.js  --searchScope=client --replaceScope=client
if (argvParams['searchScope']) {
  searchScope = argvParams['searchScope'];
}
 
if (argvParams['replaceScope']) {
 replaceScope = argvParams['replaceScope']
}
  
 
//获取所有的文件
function getAllFiles(dirs) {
  let files = [];
  let fileList = fs.readdirSync(dirs);
  for (let file of fileList) {
    const currentPath = path.resolve(dirs, file)
    const stat = fs.statSync(currentPath)
    const isDirectory = stat.isDirectory();
    if (isDirectory) {
      files = files.concat(getAllFiles(currentPath))
    } else {
      files.push(currentPath)
    }
  }
  return files
}
//获取不符合规范的文件路径
function getErrorFileName(fileList) {
  let fileNames = [];
  for (let i = 0; i < fileList.length; i++) {
    //完整的路径
    const filePath = fileList[i];
    let arr = [];
    //因为windows获取的文件路径有问题,判断如果是windows并且是all的情况就用\\分割  其他windows情况未测
    if(process.platform == 'win32'){
      arr = filePath.split("\\");
    }else{
      arr = filePath.split("/");
    }
    //获取到文件名
    const filename = arr[arr.length - 1];
    const fileName = filename.split('.')[0];
 
    //文件扩展名
    const extname = path.extname(filePath).slice(1);
    let isMatch = false;
    //只对vue、js、ts、scss、css文件进行检测
    if(checkExtArr.indexOf(extname) != -1){
      isMatch = fileReg.test(fileName);
    }else{
      isMatch = true;
    }
     
    if (!isMatch) {
      //路径截取
      const index = filePath.indexOf(rootDirName);
      fileNames.push(filePath.slice(index < 0 ? 0 : index));
      //组成不规范文件名数组
      fileNameArr.push(fileName)
    }
  }
 
  return fileNames;
}
 
(async function(){
 
 async function searchAndReplace(scope,filename,errorName) {
 
  let file = await fs.readdirSync(scope);
    //遍历每一个文件
   for(let i = 0; i < file.length; i++){
     //构造文件的路径
     var filePath = path.join(scope,file[i]);
     let stats =  fs.statSync(filePath);
 
     //如果是文件就可以进一步读取文件的内容
     if(stats.isFile()){
 
      //读取文件内容
       var fileContent = fs.readFileSync(filePath,'utf-8');
       //设置匹配 /filename 形式的规则
       var reg = eval(`/\\/${filename}\\b/g`);
       //替换生成新内容
       var newContent = fileContent.replace(reg,function(word){
         index++;
         //把驼峰的替换成中划线连接的
         var result = word.replace(/[A-Z]/g,'-$&').toLowerCase().replace('/-','/');
 
         console.log('修改的文件为:',filePath,'其中不规范文件名为:',word,'重命名为:',result,"  ",filename,"的修改次数为:",index)
         //构造excel数据
         temp.push([errorName,filePath,word,result,index])
 
         return result;
      })
      //新内容写入文件
      fs.writeFileSync(filePath,newContent)
        
     }else{
       //如果是文件夹继续遍历
       await searchAndReplace(path.join(scope, file[i]),filename,errorName)
     }
    }
}
 
//生成excel函数,不需要可注掉。
 async function exportExcel(){
   app.get('/Excel', function(req, res){
    var conf ={};
    conf.name = "mysheet";
    conf.cols = [{
      caption:'不规则文件名路径',
      type:'string'
     },{
      caption:'目标修改的文件路径',
      type:'string'
     },{
      caption:'要修改的不规范字符串',
      type:'string'
     },{
      caption:'修改完的字符串',
      type:'string'
     },{
      caption:'不规则文件名修改的次数',
      type:'number'
     }];
 
     conf.rows = temp;
 
    var result = nodeExcel.execute(conf);
    res.setHeader('Content-Type', 'application/vnd.openxmlformats;charset=utf-8');
    res.setHeader("Content-Disposition", "attachment; filename=" + "Report.xlsx");
    res.end(result, 'binary');
    });
    
   app.listen(3000);
   console.log('Listening on port 3000');
}
 
 //1.搜索不规范文件
 const rootDirs = path.resolve(path.join(__dirname, `../${searchScope}`))
 let filesList = getAllFiles(rootDirs);
 //获取到不符合规范的文件名路径
 errorName = getErrorFileName(filesList);
 
 
 //2.进行文件内容和文件名修改
 for(let i = 0; i < fileNameArr.length; i++){
 
  //2.1搜索引入此不规范文件的文件,并对其内容进行修改
  const scope =  path.resolve(path.join(__dirname, `../${replaceScope}`))
  await searchAndReplace(scope,fileNameArr[i],errorName[i]);
  index = 0;
   
  //2.1对此不符合规范的文件的文件名进行修改
 
  //构建规范的文件名
  var newfileNameArr = fileNameArr[i].replace(/[A-Z]/g,'-$&').toLowerCase().replace(/^-/,'');
  //构建新的文件路径
  var reg = eval(`/${fileNameArr[i]}\\./`);
  var newFileName = errorName[i].replace(reg,newfileNameArr+'.')
  //进行文件名修改
  // fs.rename(errorName[i],newFileName,function(err){
  //   if(err){
  //     throw err;
  //   }
  // })
  execSync(`git mv ${errorName[i]} ${newFileName} `)
 }
  //最后导出excel表格
  exportExcel();
 
})()

导出统计结果:

  1. 工程: 检测文件数量:260 不符合规范数量:127
  2. 通过使用excel-export模块,就可以把结果生成一个excel,在浏览器访问 http://localhost:3000/Excel ,就能下载生成的表格。

 

本人能力有限,写的脚本不是很精炼、完美,欢迎提出宝贵意见和问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值