rexreplace
Version:
Search & replace across files with a CLI tool that makes you trust what you are doing.
665 lines (557 loc) • 13.6 kB
text/typescript
const fs = require('fs');
const path = require('path');
const globs = require('fast-glob');
const now = new Date();
import {outputConfig, step, debug, chat, info, error, die} from './output.ts';
const re = {
euro: /€/g,
section: /§/g,
mctime: /[mc]time/,
colon: /:/g,
capturedGroupRef: /\$\d/,
regexSpecialChars: /[-\[\]{}()*+?.,\/\\^$|#\s]/g,
byteOrSize: /bytes|size/,
folderName: /[\\\/]+([^\\\/]+)[\\\/]+[^\\\/]+$/,
};
export const version = 'PACKAGE_VERSION';
export function engine(config: any = {engine: 'V8'}) {
outputConfig(config);
step('Displaying steps for:');
step(config);
config.pattern = getFinalPattern(config.pattern, config) || '';
config.replacement = getFinalReplacement(config.replacement, config) || '';
config.replacementOri = config.replacement;
config.regex = getFinalRegex(config.pattern, config) || '';
step(config);
if (handlePipedData(config)) {
return doReplacement('Piped data', config, config.pipedData);
}
config.files = getFilePaths(config);
if (!config.files.length) {
return error(config.files.length + ' files found');
}
chat(config.files.length + ' files found');
// Ensure predictable sequence of output
if (config.files.length > 1) {
config.voidAsync = config.voidAsync || config.outputMatch || config.output;
}
step(config);
config.files
// Correct filepath
//.map(filepath=>path.normalize(process.cwd()+'/'+filepath))
// Find out if any filepaths are invalid
.filter((filepath) => (fs.existsSync(filepath) ? true : error('File not found:', filepath)))
// Do the replacement
.forEach((filepath) => openFile(filepath, config));
}
function openFile(file, config) {
if (config.voidAsync) {
chat('Open sync: ' + file);
var data = fs.readFileSync(file, config.encoding);
return doReplacement(file, config, data);
} else {
chat('Open async: ' + file);
fs.readFile(file, config.encoding, function (err, data) {
if (err) {
return error(err);
}
return doReplacement(file, config, data);
});
}
}
// postfix argument names to limit the probabillity of user inputted javascript accidently using same values
function doReplacement(_file_rr: string, _config_rr: any, _data_rr: string) {
debug('Work on content from: ' + _file_rr);
// Main regexp of the whole thing
const result = _data_rr.replace(
_config_rr.regex,
_config_rr.replacementJs
? dynamicReplacement(_file_rr, _config_rr, _data_rr)
: _config_rr.replacement
);
// The output of matched strings is done from the replacement, so no need to continue
if (_config_rr.outputMatch) {
return;
}
if (_config_rr.output) {
debug('Output result from: ' + _file_rr);
return process.stdout.write(result);
}
// Nothing replaced = no need for writing file again
if (result === _data_rr) {
chat('Nothing changed in: ' + _file_rr);
return;
}
// Release the memory while storing files
_data_rr = '';
debug('Write new content to: ' + _file_rr);
// Write directly to the same file (if the process is killed all new and old data is lost)
if (_config_rr.voidBackup) {
return fs.writeFile(_file_rr, result, _config_rr.encoding, function (err) {
if (err) {
return error(err);
}
info(_file_rr);
});
}
//Make sure data is always on disk
const oriFile = path.normalize(path.join(process.cwd(), _file_rr));
const salt = new Date().toISOString().replace(re.colon, '_').replace('Z', '');
const backupFile = oriFile + '.' + salt + '.backup';
if (_config_rr.voidAsync) {
try {
fs.renameSync(oriFile, backupFile);
fs.writeFileSync(oriFile, result, _config_rr.encoding);
if (!_config_rr.keepBackup) {
fs.unlinkSync(backupFile);
}
} catch (e) {
return error(e);
}
return info(_file_rr);
}
// Let me know when fs gets promise'fied
fs.rename(oriFile, backupFile, (err) => {
if (err) {
return error(err);
}
fs.writeFile(oriFile, result, _config_rr.encoding, (err) => {
if (err) {
return error(err);
}
if (!_config_rr.keepBackup) {
fs.unlink(backupFile, (err) => {
if (err) {
return error(err);
}
info(_file_rr);
});
} else {
info(_file_rr);
}
});
});
}
function handlePipedData(config) {
step('Check Piped Data');
if (config.includeGlob.length) {
if (!config.replacementJs && config.pipedData) {
chat('Piped data never used.');
}
return false;
}
if (null !== config.pipedData && !config.pipedDataUsed) {
config.dataIsPiped = true;
config.output = true;
return true;
}
return false;
}
function getFinalPattern(pattern, conf: any) {
step('Get final pattern');
pattern = replacePlaceholders(pattern, conf);
if (conf.literal) {
pattern = pattern.replace(re.regexSpecialChars, '\\$&');
}
/*if (config.patternFile) {
pattern = fs.readFileSync(pattern, 'utf8');
pattern = new Function('return '+pattern)();
}*/
step(pattern);
return pattern;
}
function getFinalReplacement(replacement, conf: any) {
step('Get final replacement');
/*if(config.replacementFile){
return oneLinerFromFile(fs.readFileSync(replacement,'utf8'));
}*/
replacement = replacePlaceholders(replacement, conf);
if (conf.replacementPipe) {
step('Piping replacement');
conf.pipedDataUsed = true;
if (null === conf.pipedData) {
return die('No data piped into replacement');
}
replacement = conf.pipedData;
}
if (conf.outputMatch) {
step('Output match');
if (parseInt(process.versions.node) < 6) {
return die('outputMatch is only supported in node 6+');
}
return function () {
step(arguments);
if (arguments.length === 3) {
step('Printing full match');
process.stdout.write(arguments[0] + '\n');
return '';
}
for (var i = 1; i < arguments.length - 2; i++) {
process.stdout.write(arguments[i]);
}
process.stdout.write('\n');
return '';
};
}
// If captured groups then run dynamicly
//console.log(process);
if (
conf.replacementJs &&
re.capturedGroupRef.test(conf.replacement) &&
parseInt(process.versions.node) < 6
) {
return die('Captured groups for javascript replacement is only supported in node 6+');
}
step(replacement);
return replacement;
}
/*function oneLinerFromFile(str){
let lines = str.split("\n");
if(lines.length===1){
return str;
}
return lines.map(function (line) {
return line.trim();
}).join(' ');
}*/
function getFinalRegex(pattern, config) {
step('Get final regex with engine: ' + config.engine);
let regex;
let flags = getFlags(config);
switch (config.engine) {
case 'V8':
try {
regex = new RegExp(pattern, flags);
} catch (e) {
if (config.debug) throw new Error(e);
die(e.message);
}
break;
case 'RE2':
try {
//const RE2 = require('re2');
//regex = new RE2(pattern, flags);
regex = new RegExp(pattern, flags);
} catch (e) {
if (config.debug) throw new Error(e);
die(e.message);
}
break;
default:
die(`Engine ${config.engine} not supported`);
}
step(regex);
return regex;
}
function getFlags(config) {
step('Get flags');
let flags = '';
if (!config.voidGlobal) {
flags += 'g';
}
if (!config.voidIgnoreCase) {
flags += 'i';
}
if (!config.voidMultiline) {
flags += 'm';
}
if (config.dotAll) {
flags += 's';
}
if (config.unicode) {
flags += 'u';
}
step(flags);
return flags;
}
function readableSize(size) {
if (1 === size) {
return '1 Byte';
}
const i = Math.floor(Math.log(size) / Math.log(1024));
return (
(size / Math.pow(1024, i)).toFixed(!!i ? 1 : 0) + ' ' + ['Bytes', 'KB', 'MB', 'GB', 'TB'][i]
);
}
function dynamicReplacement(_file_rr, _config_rr, _data_rr) {
const _time_obj = now;
const _time = localTimeString(_time_obj);
const _pipe = _config_rr.pipedData,
_text = _data_rr,
_find = _config_rr.pattern,
code_rr = _config_rr.replacementOri,
_cwd = process.cwd(),
_now = _time,
_ = ' ',
_nl = '\n';
// prettier-ignore
let _file = '❌',
_file_rel = '❌',
_dirpath = '❌',
_dirpath_rel = '❌',
_dirname = '❌',
_filename = '❌',
_name = '❌',
_ext = '❌',
_mtime = '❌',
_ctime = '❌',
_mtime_obj = new Date(0),
_ctime_obj = new Date(0),
_bytes = -1,
_size = '❌',
dynamicContent = new Function(
'require',
'fs',
'globs',
'path',
'pipe',
'pipe_',
'find',
'find_',
'text',
'text_',
'file',
'file_',
'file_rel',
'file_rel_',
'dirpath',
'dirpath_',
'dirpath_rel',
'dirpath_rel_',
'dirname',
'dirname_',
'filename',
'filename_',
'name',
'name_',
'ext',
'ext_',
'cwd',
'cwd_',
'now',
'now_',
'time_obj',
'time',
'time_',
'mtime_obj',
'mtime',
'mtime_',
'ctime_obj',
'ctime',
'ctime_',
'bytes',
'bytes_',
'size',
'size_',
'nl',
'_',
'__code_rr',
'var path = require("path");' +
'var __require_ = require;' +
'var r = function(file){' +
'var result = null;' +
'try{' +
'result = __require_(file);' +
'} catch (e){' +
'var dir = /^[\\\/]/.test(file) ? "" : cwd;' +
'result = __require_(path.resolve(dir, file));' +
'};' +
'return result;' +
'};' +
'require = r;' +
'return eval(__code_rr);'
);
const needsByteOrSize = re.byteOrSize.test(_config_rr.replacement);
const betterToReadfromFile = needsByteOrSize && 50000000 < _text.length; // around 50 Mb will lead to reading filezise from file instead of copying into buffer
if (!_config_rr.dataIsPiped) {
_file = path.normalize(path.join(_cwd, _file_rr));
_file_rel = path.relative(_cwd, _file);
const pathInfo = path.parse(_file);
_dirpath = pathInfo.dir;
_dirpath_rel = path.relative(_cwd, _dirpath);
_dirname = (_file.match(re.folderName) || ' _')[1];
_filename = pathInfo.base;
_name = pathInfo.name;
_ext = pathInfo.ext;
if (betterToReadfromFile || re.mctime.test(_config_rr.replacement)) {
const fileStats = fs.statSync(_file);
_bytes = fileStats.size;
_size = readableSize(_bytes);
_mtime_obj = fileStats.mtime;
_ctime_obj = fileStats.ctime;
_mtime = localTimeString(_mtime_obj);
_ctime = localTimeString(_ctime_obj);
//console.log('filesize: ', fileStats.size);
//console.log('dataSize: ', _bytes);
}
}
if (needsByteOrSize && -1 === _bytes) {
_bytes = Buffer.from(_text).length;
_size = readableSize(_bytes);
}
// Run only once if no captured groups (replacement cant change)
if (!/\$\d/.test(_config_rr.replacement)) {
return dynamicContent(
require,
fs,
globs,
path,
_pipe,
_pipe + _,
_find,
_find + _,
_text,
_text + _,
_file,
_file + _,
_file_rel,
_file_rel + _,
_dirpath,
_dirpath + _,
_dirpath_rel,
_dirpath_rel + _,
_dirname,
_dirname + _,
_filename,
_filename + _,
_name,
_name + _,
_ext,
_ext + _,
_cwd,
_cwd + _,
_now,
_now + _,
_time_obj,
_time,
_time + _,
_mtime_obj,
_mtime,
_mtime + _,
_ctime_obj,
_ctime,
_ctime + _,
_bytes,
_bytes + _,
_size,
_size + _,
_nl,
_,
code_rr
);
}
// Capture groups used, so need to run once per match
return function () {
step(arguments);
const __pipe = _pipe,
__text = _text,
__find = _find,
__file = _file,
__file_rel = _file_rel,
__dirpath = _dirpath,
__dirpath_rel = _dirpath_rel,
__dirname = _dirname,
__filename = _filename,
__name = _name,
__ext = _ext,
__cwd = _cwd,
__now = _now,
__time_obj = _time_obj,
__time = _time,
__mtime_obj = _mtime_obj,
__mtime = _mtime,
__ctime_obj = _ctime_obj,
__ctime = _ctime,
__bytes = _bytes,
__size = _size,
__nl = _nl,
__ = _,
__code_rr = code_rr;
var capturedGroups = '';
for (var i = 0; i < arguments.length - 2; i++) {
capturedGroups += 'var $' + i + '=' + JSON.stringify(arguments[i]) + '; ';
}
return dynamicContent(
require,
fs,
globs,
path,
__pipe,
__pipe + __,
__find,
__find + __,
__text,
__text + __,
__file,
__file + __,
__file_rel,
__file_rel + __,
__dirpath,
__dirpath + __,
__dirpath_rel,
__dirpath_rel + __,
__dirname,
__dirname + __,
__filename,
__filename + __,
__name,
__name + __,
__ext,
__ext + __,
__cwd,
__cwd + __,
__now,
__now + _,
__time_obj,
__time,
__time + _,
__mtime_obj,
__mtime,
__mtime + _,
__ctime_obj,
__ctime,
__ctime + _,
__bytes,
__bytes + __,
__size,
__size + __,
__nl,
__,
capturedGroups + __code_rr
);
};
}
function localTimeString(dateObj = new Date()) {
return `${dateObj.getFullYear()}-${('0' + (dateObj.getMonth() + 1)).slice(-2)}-${(
'0' + dateObj.getDate()
).slice(
-2
)} ${('0' + dateObj.getHours()).slice(-2)}:${('0' + dateObj.getMinutes()).slice(-2)}:${(
'0' + dateObj.getSeconds()
).slice(-2)}.${('00' + dateObj.getMilliseconds()).slice(-3)}`;
}
function replacePlaceholders(str = '', conf: any) {
if (!conf.voidEuro) {
str = str.replace(re.euro, '$');
}
if (!conf.voidSection) {
str = str.replace(re.section, '\\');
}
return str;
}
function getFilePaths(conf) {
let {includeGlob, excludeGlob, excludeRe} = conf;
let filesToInclude = globs.sync(includeGlob);
if (excludeRe.length) {
excludeRe
.map((el) => getFinalPattern(el, conf))
.forEach((re) => {
filesToInclude = filesToInclude.filter((el) => !el.match(re));
});
}
if (excludeGlob.length) {
const filesToExclude = globs.sync(excludeGlob);
filesToInclude = filesToInclude.filter((el) => !filesToExclude.includes(el));
}
return filesToInclude.sort();
}