javascript
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { promisify } = require('util');
// 创建 Promise 版本的 readline.question
const question = promisify(readline.createInterface({
input: process.stdin,
output: process.stdout
}).question).bind(readline.createInterface({
input: process.stdin,
output: process.stdout
}));
// 日志记录器
class Logger {
constructor() {
this.logs = [];
this.operationId = Date.now();
}
addLog(message, type = 'info') {
const logEntry = {
timestamp: new Date().toISOString(),
type,
message
};
this.logs.push(logEntry);
console[type](message);
}
async saveLogs() {
const logFileName = `rename_log_${this.operationId}.json`;
try {
await fs.promises.writeFile(logFileName, JSON.stringify(this.logs, null, 2));
this.addLog(`操作日志已保存到: ${logFileName}`);
} catch (error) {
this.addLog(`无法保存日志文件: ${error}`, 'error');
}
}
}
/**
* 批量重命名文件
* @param {string} directory - 目标目录
* @param {Object} options - 配置选项
* @param {Logger} logger - 日志记录器
*/
async function batchRenameFiles(directory, options, logger) {
try {
// 验证目录是否存在
await fs.promises.access(directory, fs.constants.R_OK | fs.constants.W_OK);
const files = await fs.promises.readdir(directory);
let renameCount = 0;
let skipCount = 0;
let errorCount = 0;
// 处理扩展名过滤
const extensions = options.extensions
? options.extensions.split(',').map(ext => ext.trim().toLowerCase())
: null;
for (const file of files) {
try {
const filePath = path.join(directory, file);
const stats = await fs.promises.stat(filePath);
// 跳过目录
if (!stats.isFile()) {
skipCount++;
continue;
}
// 解析文件名
const parsed = path.parse(file);
// 检查扩展名过滤
if (extensions && !extensions.includes(parsed.ext.toLowerCase().replace('.', ''))) {
skipCount++;
continue;
}
// 执行替换操作
let newName = parsed.name;
if (options.replaceFrom) {
const regex = options.useRegex
? new RegExp(options.replaceFrom, 'g')
: options.replaceFrom;
newName = newName.replace(regex, options.replaceTo || '');
}
// 添加前缀和后缀
newName = `${options.prefix || ''}${newName}${options.suffix || ''}${parsed.ext}`;
// 如果名称没变则跳过
if (file === newName) {
skipCount++;
continue;
}
// 构建新路径并检查是否已存在
const newPath = path.join(directory, newName);
try {
await fs.promises.access(newPath);
logger.addLog(`文件已存在,跳过: ${newPath}`, 'warn');
skipCount++;
continue;
} catch (e) {
// 文件不存在,可以继续
}
// 执行重命名
await fs.promises.rename(filePath, newPath);
renameCount++;
logger.addLog(`重命名成功: ${file} → ${newName}`);
} catch (error) {
errorCount++;
logger.addLog(`处理文件 ${file} 时出错: ${error}`, 'error');
}
}
logger.addLog(`\n操作完成:
- 成功重命名: ${renameCount} 个文件
- 跳过: ${skipCount} 个文件
- 错误: ${errorCount} 个文件`);
} catch (error) {
logger.addLog(`无法访问目录 ${directory}: ${error}`, 'error');
}
}
/**
* 交互式模式
*/
async function interactiveMode() {
const logger = new Logger();
logger.addLog('=== 文件批量重命名工具 ===');
try {
const directory = await question('请输入目录路径: ');
const prefix = await question('要添加的前缀(直接回车跳过): ');
const suffix = await question('要添加的后缀(直接回车跳过): ');
const replaceFrom = await question('要替换的文本(直接回车跳过): ');
const replaceTo = await question('替换为的文本(直接回车跳过): ');
const useRegex = (await question('使用正则表达式? (y/n, 默认n): ')).toLowerCase() === 'y';
const extensions = await question('要处理的文件扩展名(多个用逗号分隔,直接回车处理所有): ');
logger.addLog('\n即将执行以下操作:');
logger.addLog(`目录: ${directory}`);
logger.addLog(`前缀: "${prefix}"`);
logger.addLog(`后缀: "${suffix}"`);
logger.addLog(`替换: "${replaceFrom}" → "${replaceTo}"`);
logger.addLog(`使用正则: ${useRegex}`);
logger.addLog(`文件类型: ${extensions || '所有'}`);
const confirm = await question('\n确认执行? (y/n): ');
if (confirm.toLowerCase() === 'y') {
await batchRenameFiles(directory, {
prefix,
suffix,
replaceFrom,
replaceTo,
useRegex,
extensions: extensions || undefined
}, logger);
} else {
logger.addLog('操作已取消');
}
} catch (error) {
logger.addLog(`交互过程中出错: ${error}`, 'error');
} finally {
await logger.saveLogs();
process.exit(0);
}
}
/**
* 命令行参数模式
*/
async function cliMode() {
const logger = new Logger();
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
logger.addLog(`
文件批量重命名工具
用法:
node rename.js [目录路径] [选项]
选项:
-p, --prefix [前缀] 添加文件名前缀
-s, --suffix [后缀] 添加文件名后缀
-f, --from [文本] 要替换的文本
-t, --to [文本] 替换为的文本
-e, --ext [扩展名] 要处理的文件扩展名(逗号分隔)
-r, --regex 使用正则表达式替换
-i, --interactive 进入交互模式
-h, --help 显示帮助信息
示例:
node rename.js ./photos -p "vacation_" -s "_2023" -f "DSC" -t "Photo" -e "jpg,png"
`);
process.exit(0);
}
if (args.includes('-i') || args.includes('--interactive')) {
return interactiveMode();
}
try {
// 解析命令行参数
const directory = args[0];
const options = {
prefix: getArgValue(args, ['-p', '--prefix']),
suffix: getArgValue(args, ['-s', '--suffix']),
replaceFrom: getArgValue(args, ['-f', '--from']),
replaceTo: getArgValue(args, ['-t', '--to']),
extensions: getArgValue(args, ['-e', '--ext']),
useRegex: args.includes('-r') || args.includes('--regex')
};
logger.addLog('开始批量重命名...');
logger.addLog(`目录: ${directory}`);
logger.addLog(`选项: ${JSON.stringify(options, null, 2)}`);
await batchRenameFiles(directory, options, logger);
} catch (error) {
logger.addLog(`命令行模式出错: ${error}`, 'error');
} finally {
await logger.saveLogs();
process.exit(0);
}
}
// 辅助函数:从参数中获取值
function getArgValue(args, keys) {
for (const key of keys) {
const index = args.indexOf(key);
if (index !== -1 && index + 1 < args.length) {
return args[index + 1];
}
}
return '';
}
// 启动程序
if (require.main === module) {
cliMode();
}
// 导出函数以便作为模块使用
module.exports = {
batchRenameFiles,
Logger
};
1. 安装
将上述代码保存为 rename.js
文件,然后添加可执行权限:
bash
复制
chmod +x rename.js
2. 交互式模式运行
bash
复制
./rename.js -i
# 或
node rename.js --interactive
3. 命令行模式运行
bash
复制
# 基本用法
./rename.js /path/to/directory -p "prefix_" -s "_suffix" -f "old" -t "new" -e "jpg,png"
# 使用正则表达式替换
./rename.js /path/to/directory -f "\\d+" -t "NUM" -r
# 查看帮助
./rename.js -h
4. 作为模块使用
javascript
复制
const { batchRenameFiles, Logger } = require('./rename');
const logger = new Logger();
batchRenameFiles('./my-files', {
prefix: 'archive_',
replaceFrom: 'draft',
replaceTo: 'final',
extensions: 'docx,txt'
}, logger);
-
Node.js 环境:
-
需要已安装 Node.js(建议版本 12+)
-
检查是否已安装:
bash
复制
node -v
-
如果未安装,请从 Node.js 官网 下载安装
-
-
脚本文件:
- 将前面提供的完整代码保存为
rename.js
- 将前面提供的完整代码保存为
使用方法
1. 直接运行(作为脚本)
bash
复制
# 添加执行权限(Linux/Mac)
chmod +x rename.js
# 运行
./rename.js -i # 交互模式
./rename.js /path/to/folder -p "new_" # 命令行模式
2. 通过 Node 运行
bash
复制
node rename.js -i # 交互模式
node rename.js /path/to/folder -s "_backup" # 命令行模式
3. 作为模块使用
javascript
复制
// 在你的项目中
const { batchRenameFiles } = require('./rename.js');
batchRenameFiles('/path/to/files', {
prefix: '2023_',
replaceFrom: 'old',
replaceTo: 'new'
});