diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..d8c96bcc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +printWidth: 100 +tabWidth: 4 +useTabs: true +bracketSpacing: false +trailingComma: es5 +singleQuote: true +arrowParens: "avoid" + diff --git a/bin/ES6/cli.js b/bin/ES6/cli.js index d7f874ea..603100b5 100644 --- a/bin/ES6/cli.js +++ b/bin/ES6/cli.js @@ -28,11 +28,13 @@ const yargs = require('yargs') '> rexreplace pattern replacement [fileGlob|option]+') .example(`> rexreplace 'Foo' 'xxx' myfile.md`, `'foobar' in myfile.md will become 'xxxbar'`) .example('') - .example(`> rr Foo xxx myfile.md`, `The alias 'rr' can be used instead of 'rexreplace'`) + .example(`> rr xxx Foo myfile.md`, `The alias 'rr' can be used instead of 'rexreplace'`) .example('') .example(`> rexreplace '(f?(o))o(.*)' '$3$1€2' myfile.md`, `'foobar' in myfile.md will become 'barfoo'`) .example('') .example(`> rexreplace '^#' '##' *.md`, `All markdown files in this dir got all headlines moved one level deeper`) + .example('') + .example(`> rexreplace 'a' 'b' 'myfile.md' 'src/**/*.*' `, `Provide multiple files or glob if needed`) .version('v', 'Print rexreplace version (can be given as only argument)', rexreplace.version) .alias('v', 'version') .boolean('V') @@ -155,6 +157,12 @@ The following values defaults to \`❌\` if haystack does not originate from a f All variables, except from module, date objects, \`nl\` and \`_\`, has a corresponding variable name followed by \`_\` where the content has an extra space at the end (for easy concatenation). `) + .string('x') + .describe('x', 'Exclude files with a path that matches this regular expression. Will follow same regex flags and setup as the main search. Can be used multiple times.') + .alias('x', 'exclude-re') + .string('X') + .describe('X', 'Exclude files found with this glob. Can be used multiple times.') + .alias('X', 'exclude-glob') /* .boolean('N') .alias('N', 'void-newline') @@ -183,7 +191,7 @@ All variables, except from module, date objects, \`nl\` and \`_\`, has a corresp /* // Ideas .boolean('n') - .describe('n', "Do replacement on file names instead of file content (rename the files)") + .describe('n', "Do replacement on file path/names instead of file content (rename/move the files)") .alias('n', 'name') // https://github.com/eugeneware/replacestream @@ -241,10 +249,12 @@ function unescapeString(str = '') { }); let pipeInUse = false; let pipeData = ''; - config.globs = yargs.argv._; config.pipedData = null; config.showHelp = yargs.showHelp; config.pattern = pattern; + config.includeGlob = yargs.argv._; + config.excludeGlob = [...yargs.argv.excludeGlob].filter(Boolean); + config.excludeRe = [...yargs.argv.excludeRe].filter(Boolean); if (config.replacementJs) { config.replacement = replacement; } diff --git a/bin/ES6/engine.js b/bin/ES6/engine.js index 6afe3358..6b7c691f 100644 --- a/bin/ES6/engine.js +++ b/bin/ES6/engine.js @@ -3,20 +3,30 @@ const path = require('path'); const globs = require('globs'); const now = new Date(); import { outputConfig, step, debug, chat, info, error, die } from './output'; +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 = { engine: 'V8' }) { outputConfig(config); step('Displaying steps for:'); step(config); - config.pattern = getFinalPattern(config) || ''; - config.replacement = getFinalReplacement(config) || ''; + config.pattern = getFinalPattern(config.pattern, config) || ''; + config.replacement = getFinalReplacement(config.replacement, config) || ''; config.replacementOri = config.replacement; - config.regex = getFinalRegex(config) || ''; + config.regex = getFinalRegex(config.pattern, config) || ''; step(config); if (handlePipedData(config)) { return doReplacement('Piped data', config, config.pipedData); } - config.files = globs2paths(config.globs); + config.files = getFilePaths(config); if (!config.files.length) { return error(config.files.length + ' files found'); } @@ -29,230 +39,229 @@ export function engine(config = { engine: 'V8' }) { .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); - }); - } +} +function openFile(file, config) { + if (config.voidAsync) { + chat('Open sync: ' + file); + var data = fs.readFileSync(file, config.encoding); + return doReplacement(file, config, data); } - // postfix argument names to limit the probabillity of user inputted javascript accidently using same values - function doReplacement(_file_rr, _config_rr, _data_rr) { - debug('Work on content from: ' + _file_rr); - // Variables to be accessible from js. - if (_config_rr.replacementJs) { - _config_rr.replacement = dynamicReplacement(_file_rr, _config_rr, _data_rr); - } - // Main regexp of the whole thing - const result = _data_rr.replace(_config_rr.regex, _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(/:/g, '_').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); + else { + chat('Open async: ' + file); + fs.readFile(file, config.encoding, function (err, data) { + if (err) { + return error(err); } - return info(_file_rr); - } - // Let me know when fs gets promise'fied - fs.rename(oriFile, backupFile, (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, _config_rr, _data_rr) { + debug('Work on content from: ' + _file_rr); + // Variables to be accessible from js. + if (_config_rr.replacementJs) { + _config_rr.replacement = dynamicReplacement(_file_rr, _config_rr, _data_rr); + } + // Main regexp of the whole thing + const result = _data_rr.replace(_config_rr.regex, _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); } - 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); - } - }); + info(_file_rr); }); } - function handlePipedData(config) { - step('Check Piped Data'); - if (config.globs.length) { - if (!config.replacementJs) { - chat('Piped data never used.'); + //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); } - return false; } - if (null !== config.pipedData && !config.pipedDataUsed) { - config.dataIsPiped = true; - config.output = true; - return true; + catch (e) { + return error(e); } - return false; - } - function getFinalPattern(conf) { - step('Get final pattern'); - let pattern = replacePlaceholders(conf.pattern, conf); - /*if (config.patternFile) { - pattern = fs.readFileSync(pattern, 'utf8'); - pattern = new Function('return '+pattern)(); - }*/ - step(pattern); - return pattern; + return info(_file_rr); } - function getFinalReplacement(conf) { - step('Get final replacement'); - /*if(config.replacementFile){ - return oneLinerFromFile(fs.readFileSync(replacement,'utf8')); - }*/ - conf.replacement = replacePlaceholders(conf.replacement, conf); - if (conf.replacementPipe) { - step('Piping replacement'); - conf.pipedDataUsed = true; - if (null === conf.pipedData) { - return die('No data piped into replacement'); - } - conf.replacement = conf.pipedData; + // Let me know when fs gets promise'fied + fs.rename(oriFile, backupFile, (err) => { + if (err) { + return error(err); } - if (conf.outputMatch) { - step('Output match'); - if (parseInt(process.versions.node) < 6) { - return die('outputMatch is only supported in node 6+'); + fs.writeFile(oriFile, result, _config_rr.encoding, (err) => { + if (err) { + return error(err); } - 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 && - /\$\d/.test(conf.replacement) && - parseInt(process.versions.node) < 6) { - return die('Captured groups for javascript replacement is only supported in node 6+'); + 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.'); } - step(conf.replacement); - return conf.replacement; + return false; } - /*function oneLinerFromFile(str){ - let lines = str.split("\n"); - if(lines.length===1){ - return str; - } - return lines.map(function (line) { - return line.trim(); - }).join(' '); + if (null !== config.pipedData && !config.pipedDataUsed) { + config.dataIsPiped = true; + config.output = true; + return true; + } + return false; +} +function getFinalPattern(pattern, conf) { + 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)(); }*/ - function getFinalRegex(config) { - step('Get final regex with engine: ' + config.engine); - let pattern = config.pattern; - if (config.literal) { - pattern = pattern.replace(/[-\[\]{}()*+?.,\/\\^$|#\s]/g, '\\$&'); - } - 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); - } - catch (e) { - if (config.debug) - throw new Error(e); - die(e.message); - } - break; - default: - die(`Engine ${config.engine} not supported`); + step(pattern); + return pattern; +} +function getFinalReplacement(replacement, conf) { + 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'); } - step(regex); - return regex; + replacement = conf.pipedData; } - 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 (conf.outputMatch) { + step('Output match'); + if (parseInt(process.versions.node) < 6) { + return die('outputMatch is only supported in node 6+'); } - if (config.unicode) { - flags += 'u'; - } - step(flags); - return flags; + 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); + } + 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) { @@ -280,7 +289,7 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { '};' + 'require = r;' + 'return eval(__code_rr);'); - const needsByteOrSize = /bytes|size/.test(_config_rr.replacement); + 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)); @@ -288,11 +297,11 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { const pathInfo = path.parse(_file); _dirpath = pathInfo.dir; _dirpath_rel = path.relative(_cwd, _dirpath); - _dirname = _file.match(/[\\\/]+([^\\\/]+)[\\\/]+[^\\\/]+$/)[1]; + _dirname = (_file.match(re.folderName) || ' _')[1]; _filename = pathInfo.base; _name = pathInfo.name; _ext = pathInfo.ext; - if (betterToReadfromFile || /[mc]time/.test(_config_rr.replacement)) { + if (betterToReadfromFile || re.mctime.test(_config_rr.replacement)) { const fileStats = fs.statSync(_file); _bytes = fileStats.size; _size = readableSize(_bytes); @@ -312,7 +321,7 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { 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); } - // Captures groups present, so need to run once per match + // 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; @@ -326,10 +335,6 @@ function dynamicReplacement(_file_rr, _config_rr, _data_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)}`; } -const re = { - euro: /€/g, - section: /§/g, -}; function replacePlaceholders(str = '', conf) { if (!conf.voidEuro) { str = str.replace(re.euro, '$'); @@ -339,21 +344,19 @@ function replacePlaceholders(str = '', conf) { } return str; } -function globs2paths(_globs = []) { - const globsToInclude = []; - const globsToExclude = []; - _globs.filter(Boolean).forEach((glob) => { - if ('!' === glob[0] || '^' === glob[0]) { - globsToExclude.push(glob.slice(1)); - } - else { - globsToInclude.push(glob); - } - }); - let filesToInclude = globs.sync(globsToInclude); - if (globsToExclude.length) { - const filesToExclude = globs.sync(globsToExclude); - return filesToInclude.filter((el) => !filesToExclude.includes(el)); +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; } diff --git a/bin/rexreplace.cli.js b/bin/rexreplace.cli.js index 394ccea7..944fc55e 100755 --- a/bin/rexreplace.cli.js +++ b/bin/rexreplace.cli.js @@ -72,6 +72,16 @@ var path = require('path'); var globs = require('globs'); var now = new Date(); + var re = { + euro: /€/g, + section: /§/g, + mctime: /[mc]time/, + colon: /:/g, + capturedGroupRef: /\$\d/, + regexSpecialChars: /[-\[\]{}()*+?.,\/\\^$|#\s]/g, + byteOrSize: /bytes|size/, + folderName: /[\\\/]+([^\\\/]+)[\\\/]+[^\\\/]+$/, + }; var version = '7.1.2'; function engine(config) { if ( config === void 0 ) config = { engine: 'V8' }; @@ -79,15 +89,15 @@ outputConfig(config); step('Displaying steps for:'); step(config); - config.pattern = getFinalPattern(config) || ''; - config.replacement = getFinalReplacement(config) || ''; + config.pattern = getFinalPattern(config.pattern, config) || ''; + config.replacement = getFinalReplacement(config.replacement, config) || ''; config.replacementOri = config.replacement; - config.regex = getFinalRegex(config) || ''; + config.regex = getFinalRegex(config.pattern, config) || ''; step(config); if (handlePipedData(config)) { return doReplacement('Piped data', config, config.pipedData); } - config.files = globs2paths(config.globs); + config.files = getFilePaths(config); if (!config.files.length) { return error(config.files.length + ' files found'); } @@ -100,232 +110,231 @@ .filter(function (filepath) { return (fs.existsSync(filepath) ? true : error('File not found:', filepath)); }) // Do the replacement .forEach(function (filepath) { return 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); - }); - } + } + function openFile(file, config) { + if (config.voidAsync) { + chat('Open sync: ' + file); + var data = fs.readFileSync(file, config.encoding); + return doReplacement(file, config, data); } - // postfix argument names to limit the probabillity of user inputted javascript accidently using same values - function doReplacement(_file_rr, _config_rr, _data_rr) { - debug('Work on content from: ' + _file_rr); - // Variables to be accessible from js. - if (_config_rr.replacementJs) { - _config_rr.replacement = dynamicReplacement(_file_rr, _config_rr, _data_rr); - } - // Main regexp of the whole thing - var result = _data_rr.replace(_config_rr.regex, _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 - var oriFile = path.normalize(path.join(process.cwd(), _file_rr)); - var salt = new Date().toISOString().replace(/:/g, '_').replace('Z', ''); - var 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); + else { + chat('Open async: ' + file); + fs.readFile(file, config.encoding, function (err, data) { + if (err) { + return error(err); } - return info(_file_rr); - } - // Let me know when fs gets promise'fied - fs.rename(oriFile, backupFile, function (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, _config_rr, _data_rr) { + debug('Work on content from: ' + _file_rr); + // Variables to be accessible from js. + if (_config_rr.replacementJs) { + _config_rr.replacement = dynamicReplacement(_file_rr, _config_rr, _data_rr); + } + // Main regexp of the whole thing + var result = _data_rr.replace(_config_rr.regex, _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); } - fs.writeFile(oriFile, result, _config_rr.encoding, function (err) { - if (err) { - return error(err); - } - if (!_config_rr.keepBackup) { - fs.unlink(backupFile, function (err) { - if (err) { - return error(err); - } - info(_file_rr); - }); - } - else { - info(_file_rr); - } - }); + info(_file_rr); }); } - function handlePipedData(config) { - step('Check Piped Data'); - if (config.globs.length) { - if (!config.replacementJs) { - chat('Piped data never used.'); + //Make sure data is always on disk + var oriFile = path.normalize(path.join(process.cwd(), _file_rr)); + var salt = new Date().toISOString().replace(re.colon, '_').replace('Z', ''); + var 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); } - return false; } - if (null !== config.pipedData && !config.pipedDataUsed) { - config.dataIsPiped = true; - config.output = true; - return true; + catch (e) { + return error(e); } - return false; + return info(_file_rr); } - function getFinalPattern(conf) { - step('Get final pattern'); - var pattern = replacePlaceholders(conf.pattern, conf); - /*if (config.patternFile) { - pattern = fs.readFileSync(pattern, 'utf8'); - pattern = new Function('return '+pattern)(); - }*/ - step(pattern); - return pattern; - } - function getFinalReplacement(conf) { - step('Get final replacement'); - /*if(config.replacementFile){ - return oneLinerFromFile(fs.readFileSync(replacement,'utf8')); - }*/ - conf.replacement = replacePlaceholders(conf.replacement, conf); - if (conf.replacementPipe) { - step('Piping replacement'); - conf.pipedDataUsed = true; - if (null === conf.pipedData) { - return die('No data piped into replacement'); - } - conf.replacement = conf.pipedData; + // Let me know when fs gets promise'fied + fs.rename(oriFile, backupFile, function (err) { + if (err) { + return error(err); } - if (conf.outputMatch) { - step('Output match'); - if (parseInt(process.versions.node) < 6) { - return die('outputMatch is only supported in node 6+'); + fs.writeFile(oriFile, result, _config_rr.encoding, function (err) { + if (err) { + return error(err); } - return function () { - var arguments$1 = arguments; - - 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$1[i]); - } - process.stdout.write('\n'); - return ''; - }; - } - // If captured groups then run dynamicly - //console.log(process); - if (conf.replacementJs && - /\$\d/.test(conf.replacement) && - parseInt(process.versions.node) < 6) { - return die('Captured groups for javascript replacement is only supported in node 6+'); + if (!_config_rr.keepBackup) { + fs.unlink(backupFile, function (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.'); } - step(conf.replacement); - return conf.replacement; + return false; } - /*function oneLinerFromFile(str){ - let lines = str.split("\n"); - if(lines.length===1){ - return str; - } - return lines.map(function (line) { - return line.trim(); - }).join(' '); + if (null !== config.pipedData && !config.pipedDataUsed) { + config.dataIsPiped = true; + config.output = true; + return true; + } + return false; + } + function getFinalPattern(pattern, conf) { + 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)(); }*/ - function getFinalRegex(config) { - step('Get final regex with engine: ' + config.engine); - var pattern = config.pattern; - if (config.literal) { - pattern = pattern.replace(/[-\[\]{}()*+?.,\/\\^$|#\s]/g, '\\$&'); - } - var regex; - var 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 { - var RE2 = require('re2'); - regex = new RE2(pattern, flags); - } - catch (e$1) { - if (config.debug) - { throw new Error(e$1); } - die(e$1.message); - } - break; - default: - die(("Engine " + (config.engine) + " not supported")); - } - step(regex); - return regex; - } - function getFlags(config) { - step('Get flags'); - var flags = ''; - if (!config.voidGlobal) { - flags += 'g'; - } - if (!config.voidIgnoreCase) { - flags += 'i'; - } - if (!config.voidMultiline) { - flags += 'm'; - } - if (config.dotAll) { - flags += 's'; + step(pattern); + return pattern; + } + function getFinalReplacement(replacement, conf) { + 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'); } - if (config.unicode) { - flags += 'u'; + replacement = conf.pipedData; + } + if (conf.outputMatch) { + step('Output match'); + if (parseInt(process.versions.node) < 6) { + return die('outputMatch is only supported in node 6+'); } - step(flags); - return flags; + return function () { + var arguments$1 = arguments; + + 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$1[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); + var regex; + var 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 { + var RE2 = require('re2'); + regex = new RE2(pattern, flags); + } + catch (e$1) { + if (config.debug) + { throw new Error(e$1); } + die(e$1.message); + } + break; + default: + die(("Engine " + (config.engine) + " not supported")); } + step(regex); + return regex; + } + function getFlags(config) { + step('Get flags'); + var 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) { @@ -353,7 +362,7 @@ '};' + 'require = r;' + 'return eval(__code_rr);'); - var needsByteOrSize = /bytes|size/.test(_config_rr.replacement); + var needsByteOrSize = re.byteOrSize.test(_config_rr.replacement); var 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)); @@ -361,11 +370,11 @@ var pathInfo = path.parse(_file); _dirpath = pathInfo.dir; _dirpath_rel = path.relative(_cwd, _dirpath); - _dirname = _file.match(/[\\\/]+([^\\\/]+)[\\\/]+[^\\\/]+$/)[1]; + _dirname = (_file.match(re.folderName) || ' _')[1]; _filename = pathInfo.base; _name = pathInfo.name; _ext = pathInfo.ext; - if (betterToReadfromFile || /[mc]time/.test(_config_rr.replacement)) { + if (betterToReadfromFile || re.mctime.test(_config_rr.replacement)) { var fileStats = fs.statSync(_file); _bytes = fileStats.size; _size = readableSize(_bytes); @@ -385,7 +394,7 @@ 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); } - // Captures groups present, so need to run once per match + // Capture groups used, so need to run once per match return function () { var arguments$1 = arguments; @@ -403,10 +412,6 @@ 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))); } - var re = { - euro: /€/g, - section: /§/g, - }; function replacePlaceholders(str, conf) { if ( str === void 0 ) str = ''; @@ -418,23 +423,21 @@ } return str; } - function globs2paths(_globs) { - if ( _globs === void 0 ) _globs = []; - - var globsToInclude = []; - var globsToExclude = []; - _globs.filter(Boolean).forEach(function (glob) { - if ('!' === glob[0] || '^' === glob[0]) { - globsToExclude.push(glob.slice(1)); - } - else { - globsToInclude.push(glob); - } - }); - var filesToInclude = globs.sync(globsToInclude); - if (globsToExclude.length) { - var filesToExclude = globs.sync(globsToExclude); - return filesToInclude.filter(function (el) { return !filesToExclude.includes(el); }); + function getFilePaths(conf) { + var includeGlob = conf.includeGlob; + var excludeGlob = conf.excludeGlob; + var excludeRe = conf.excludeRe; + var filesToInclude = globs.sync(includeGlob); + if (excludeRe.length) { + excludeRe + .map(function (el) { return getFinalPattern(el, conf); }) + .forEach(function (re) { + filesToInclude = filesToInclude.filter(function (el) { return !el.match(re); }); + }); + } + if (excludeGlob.length) { + var filesToExclude = globs.sync(excludeGlob); + filesToInclude = filesToInclude.filter(function (el) { return !filesToExclude.includes(el); }); } return filesToInclude; } @@ -467,11 +470,13 @@ '> rexreplace pattern replacement [fileGlob|option]+') .example("> rexreplace 'Foo' 'xxx' myfile.md", "'foobar' in myfile.md will become 'xxxbar'") .example('') - .example("> rr Foo xxx myfile.md", "The alias 'rr' can be used instead of 'rexreplace'") + .example("> rr xxx Foo myfile.md", "The alias 'rr' can be used instead of 'rexreplace'") .example('') .example("> rexreplace '(f?(o))o(.*)' '$3$1€2' myfile.md", "'foobar' in myfile.md will become 'barfoo'") .example('') .example("> rexreplace '^#' '##' *.md", "All markdown files in this dir got all headlines moved one level deeper") + .example('') + .example("> rexreplace 'a' 'b' 'myfile.md' 'src/**/*.*' ", "Provide multiple files or glob if needed") .version('v', 'Print rexreplace version (can be given as only argument)', version) .alias('v', 'version') .boolean('V') @@ -556,6 +561,12 @@ .boolean('j') .alias('j', 'replacement-js') .describe('j', "Treat replacement as javascript source code. \nThe statement from the last expression will become the replacement string. \nPurposefully implemented the most insecure way possible to remove _any_ incentive to consider running code from an untrusted part. \nThe full match will be available as a javascript variable named $0 while each captured group will be available as $1, $2, $3, ... and so on. \nAt some point, the $ char _will_ give you a headache when used from the command line, so use €0, €1, €2, €3... instead. \nIf the javascript source code references to the full match or a captured group the code will run once per match. Otherwise, it will run once per file. \n\nThe code has access to the following variables: \n`r` as an alias for `require` with both expanded to understand a relative path even if it is not starting with `./`, \n`fs` from node, \n`path` from node, \n`globs` from npm, \n`pipe`: the data piped into the command (null if no piped data), \n`find`: pattern searched for (the needle), \n`text`: full text being searched i.e. file content or piped data (the haystack), \n`bytes`: total size of the haystack in bytes, \n`size`: human-friendly representation of the total size of the haystack, \n`time`: String representing the local time when the command was invoked,\n`time_obj`: date object representing `time`,\n`now`: alias for `time`,\n`cwd`: current process working dir, \n`nl`: a new-line char,\n`_`: a single space char (for easy string concatenation).\n\nThe following values defaults to `❌` if haystack does not originate from a file:\n`file`: contains the full path of the active file being searched (including full filename), \n`file_rel`: contains `file` relative to current process working dir, \n`dirpath`: contains the full path without filename of the active file being searched, \n`dirpath_rel`: contains `dirpath` relative to current process working dir, \n`filename`: is the full filename of the active file being searched without path, \n`name`: filename of the active file being searched with no extension, \n`ext`: extension of the filename including leading dot, \n`mtime`: ISO inspired representation of the last local modification time of the current file, \n`ctime`: ISO representation of the local creation time of the current file. \n`mtime_obj`: date object representing `mtime`, \n`ctime_obj`: date object representing `ctime`. \n\nAll variables, except from module, date objects, `nl` and `_`, has a corresponding variable name followed by `_` where the content has an extra space at the end (for easy concatenation). \n") + .string('x') + .describe('x', 'Exclude files with a path that matches this regular expression. Will follow same regex flags and setup as the main search. Can be used multiple times.') + .alias('x', 'exclude-re') + .string('X') + .describe('X', 'Exclude files found with this glob. Can be used multiple times.') + .alias('X', 'exclude-glob') /* .boolean('N') .alias('N', 'void-newline') @@ -584,7 +595,7 @@ /* // Ideas .boolean('n') - .describe('n', "Do replacement on file names instead of file content (rename the files)") + .describe('n', "Do replacement on file path/names instead of file content (rename/move the files)") .alias('n', 'name') // https://github.com/eugeneware/replacestream @@ -645,10 +656,12 @@ }); var pipeInUse = false; var pipeData = ''; - config.globs = yargs.argv._; config.pipedData = null; config.showHelp = yargs.showHelp; config.pattern = pattern; + config.includeGlob = yargs.argv._; + config.excludeGlob = [].concat( yargs.argv.excludeGlob ).filter(Boolean); + config.excludeRe = [].concat( yargs.argv.excludeRe ).filter(Boolean); if (config.replacementJs) { config.replacement = replacement; } diff --git a/bin/rexreplace.cli.min.js b/bin/rexreplace.cli.min.js index 7dd3df55..944fc55e 100755 --- a/bin/rexreplace.cli.min.js +++ b/bin/rexreplace.cli.min.js @@ -1,23 +1,704 @@ #!/usr/bin/env node -(function(){function z(a){p.debug&&console.error(w.gray(JSON.stringify(a,null,4)))}function g(a){p.verbose&&z(a)}function R(a,c){void 0===a&&(a=1);void 0===c&&(c="");c&&console.error(+c);process.exit(a)}function S(a){function c(b,d){if(d.voidAsync){A("Open sync: "+b);var e=m.readFileSync(b,d.encoding);return f(b,d,e)}A("Open async: "+b);m.readFile(b,d.encoding,function(h,k){return h?q(h):f(b,d,k)})}function f(b,d,e){z("Work on content from: "+b);d.replacementJs&&(d.replacement=pa(b,d,e));var h=e.replace(d.regex, -d.replacement);if(!d.outputMatch){if(d.output)return z("Output result from: "+b),process.stdout.write(h);if(h===e)A("Nothing changed in: "+b);else{e="";z("Write new content to: "+b);if(d.voidBackup)return m.writeFile(b,h,d.encoding,function(l){if(l)return q(l);B(b)});var k=r.normalize(r.join(process.cwd(),b));e=(new Date).toISOString().replace(/:/g,"_").replace("Z","");var n=k+"."+e+".backup";if(d.voidAsync){try{m.renameSync(k,n),m.writeFileSync(k,h,d.encoding),d.keepBackup||m.unlinkSync(n)}catch(l){return q(l)}return B(b)}m.rename(k, -n,function(l){if(l)return q(l);m.writeFile(k,h,d.encoding,function(v){if(v)return q(v);d.keepBackup?B(b):m.unlink(n,function(t){if(t)return q(t);B(b)})})})}}}void 0===a&&(a={engine:"V8"});p=a;g("Displaying steps for:");g(a);a.pattern=function(b){g("Get final pattern");b=T(b.pattern,b);g(b);return b}(a)||"";a.replacement=function(b){g("Get final replacement");b.replacement=T(b.replacement,b);if(b.replacementPipe){g("Piping replacement");b.pipedDataUsed=!0;if(null===b.pipedData)return x("No data piped into replacement"); -b.replacement=b.pipedData}if(b.outputMatch)return g("Output match"),6>parseInt(process.versions.node)?x("outputMatch is only supported in node 6+"):function(){var d=arguments;g(arguments);if(3===arguments.length)return g("Printing full match"),process.stdout.write(arguments[0]+"\n"),"";for(var e=1;eparseInt(process.versions.node))return x("Captured groups for javascript replacement is only supported in node 6+"); -g(b.replacement);return b.replacement}(a)||"";a.replacementOri=a.replacement;a.regex=function(b){g("Get final regex with engine: "+b.engine);var d=b.pattern;b.literal&&(d=d.replace(/[-\[\]{}()*+?.,\/\\^$|#\s]/g,"\\$&"));g("Get flags");var e="";b.voidGlobal||(e+="g");b.voidIgnoreCase||(e+="i");b.voidMultiline||(e+="m");b.dotAll&&(e+="s");b.unicode&&(e+="u");g(e);switch(b.engine){case "V8":try{var h=new RegExp(d,e)}catch(k){if(b.debug)throw Error(k);x(k.message)}break;case "RE2":try{h=new (require("re2"))(d, -e)}catch(k){if(b.debug)throw Error(k);x(k.message)}break;default:x("Engine "+b.engine+" not supported")}g(h);return h}(a)||"";g(a);if(function(b){g("Check Piped Data");return b.globs.length?(b.replacementJs||A("Piped data never used."),!1):null===b.pipedData||b.pipedDataUsed?!1:(b.dataIsPiped=!0,b.output=!0)}(a))return f("Piped data",a,a.pipedData);a.files=qa(a.globs);if(!a.files.length)return q(a.files.length+" files found");A(a.files.length+" files found");g(a);a.files.filter(function(b){return m.existsSync(b)? -!0:q("File not found:",b)}).forEach(function(b){return c(b,a)})}function U(a){if(1===a)return"1 Byte";var c=Math.floor(Math.log(a)/Math.log(1024));return(a/Math.pow(1024,c)).toFixed(c?1:0)+" "+["Bytes","KB","MB","GB","TB"][c]}function pa(a,c,f){var b=ra,d=P(b),e=c.pipedData,h=c.pattern,k=c.replacementOri,n=process.cwd(),l="\u274c",v="\u274c",t="\u274c",E="\u274c",F="\u274c",G="\u274c",H="\u274c",I="\u274c",J="\u274c",K="\u274c",L=new Date(0),M=new Date(0),u=-1,C="\u274c",V=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);'), -W=/bytes|size/.test(c.replacement),D=W&&5E7process.argv.length)/-v|--?version$/i.test(process.argv[process.argv.length-1])?(console.log("7.1.2"),process.exitCode=0,process.exit()):Q=/-h|--?help$/i.test(process.argv[process.argv.length- -1])?1:2;else{var na=process.argv.splice(2,2);var wa=na[0];var oa=na[1]}var y=require("yargs").strict().usage("RexReplace 7.1.2: Regexp search and replace for files using lookahead and backreference to matching groups in the replacement. Defaults to global multiline case-insensitive search.\n\n> rexreplace pattern replacement [fileGlob|option]+").example("> rexreplace 'Foo' 'xxx' myfile.md","'foobar' in myfile.md will become 'xxxbar'").example("").example("> rr Foo xxx myfile.md","The alias 'rr' can be used instead of 'rexreplace'").example("").example("> rexreplace '(f?(o))o(.*)' '$3$1\u20ac2' myfile.md", -"'foobar' in myfile.md will become 'barfoo'").example("").example("> rexreplace '^#' '##' *.md","All markdown files in this dir got all headlines moved one level deeper").version("v","Print rexreplace version (can be given as only argument)","7.1.2").alias("v","version").boolean("V").describe("V","More chatty output").alias("V","verbose").boolean("L").describe("L","Literal string search (no regex used when searching)").alias("L","literal").boolean("I").describe("I","Void case insensitive search pattern.").alias("I", -"void-ignore-case").boolean("G").describe("G","Void global search (stop looking after the first match).").alias("G","void-global").boolean("s").describe("s","Have `.` also match newline.").alias("s","dot-all").boolean("M").describe("M","Void multiline search pattern. Makes ^ and $ match start/end of whole content rather than each line.").alias("M","void-multiline").boolean("u").describe("u","Treat pattern as a sequence of unicode code points.").alias("u","unicode").default("e","utf8").alias("e","encoding").describe("e", -"Encoding of files/piped data.").alias("E","engine").describe("E","What regex engine to use:").choices("E",["V8"]).default("E","V8").boolean("q").describe("q","Only display errors (no other info)").alias("q","quiet").boolean("Q").describe("Q","Never display errors or info").alias("Q","quiet-total").boolean("H").describe("H","Halt on first error").alias("H","halt").default("H",!1).boolean("d").describe("d","Print debug info").alias("d","debug").boolean("\u20ac").describe("\u20ac","Void having '\u20ac' as alias for '$' in pattern and replacement parameters").alias("\u20ac", -"void-euro").boolean("\u00a7").describe("\u00a7","Void having '\u00a7' as alias for '\\' in pattern and replacement parameters").alias("\u00a7","void-section").boolean("o").describe("o","Output the final result instead of saving to file. Will also output content even if no replacement has taken place.").alias("o","output").boolean("A").alias("A","void-async").describe("A","Handle files in a synchronous flow. Good to limit memory usage when handling large files. ").boolean("B").describe("B","Avoid temporary backing up file. Works async (independent of -A flag) and will speed up things but at one point data lives only in memory, and you will lose the content if the process is abrupted.").alias("B", -"void-backup").boolean("b").describe("b","Keep a backup file of the original content.").alias("b","keep-backup").boolean("m").describe("m","Output each match on a new line. Will not replace any content but you still need to provide a dummy value (like `_`) as replacement parameter. If search pattern does not contain matching groups the full match will be outputted. If search pattern does contain matching groups only matching groups will be outputted (same line with no delimiter). ").alias("m","output-match").boolean("T").alias("T", -"trim-pipe").describe("T","Trim piped data before processing. If piped data only consists of chars that can be trimmed (new line, space, tabs...) it will become an empty string. ").boolean("R").alias("R","replacement-pipe").describe("R","Replacement will be piped in. You still need to provide a dummy value (like `_`) as replacement parameter.").boolean("j").alias("j","replacement-js").describe("j","Treat replacement as javascript source code. \nThe statement from the last expression will become the replacement string. \nPurposefully implemented the most insecure way possible to remove _any_ incentive to consider running code from an untrusted part. \nThe full match will be available as a javascript variable named $0 while each captured group will be available as $1, $2, $3, ... and so on. \nAt some point, the $ char _will_ give you a headache when used from the command line, so use \u20ac0, \u20ac1, \u20ac2, \u20ac3... instead. \nIf the javascript source code references to the full match or a captured group the code will run once per match. Otherwise, it will run once per file. \n\nThe code has access to the following variables: \n`r` as an alias for `require` with both expanded to understand a relative path even if it is not starting with `./`, \n`fs` from node, \n`path` from node, \n`globs` from npm, \n`pipe`: the data piped into the command (null if no piped data), \n`find`: pattern searched for (the needle), \n`text`: full text being searched i.e. file content or piped data (the haystack), \n`bytes`: total size of the haystack in bytes, \n`size`: human-friendly representation of the total size of the haystack, \n`time`: String representing the local time when the command was invoked,\n`time_obj`: date object representing `time`,\n`now`: alias for `time`,\n`cwd`: current process working dir, \n`nl`: a new-line char,\n`_`: a single space char (for easy string concatenation).\n\nThe following values defaults to `\u274c` if haystack does not originate from a file:\n`file`: contains the full path of the active file being searched (including full filename), \n`file_rel`: contains `file` relative to current process working dir, \n`dirpath`: contains the full path without filename of the active file being searched, \n`dirpath_rel`: contains `dirpath` relative to current process working dir, \n`filename`: is the full filename of the active file being searched without path, \n`name`: filename of the active file being searched with no extension, \n`ext`: extension of the filename including leading dot, \n`mtime`: ISO inspired representation of the last local modification time of the current file, \n`ctime`: ISO representation of the local creation time of the current file. \n`mtime_obj`: date object representing `mtime`, \n`ctime_obj`: date object representing `ctime`. \n\nAll variables, except from module, date objects, `nl` and `_`, has a corresponding variable name followed by `_` where the content has an extra space at the end (for easy concatenation). \n").help("h").describe("h", -"Display help.").alias("h","help").epilog("Inspiration: .oO(What should 'sed' have been by now?)");(function(){if(0b.indexOf("-")&&(a[b]=y.argv[b])});var c=!1,f="";a.globs=y.argv._;a.pipedData=null;a.showHelp=y.showHelp;a.pattern=wa;a.replacement=a.replacementJs?oa:va(oa);if(process.stdin.isTTY){if(a.replacementPipe)return ma();S(a)}else process.stdin.setEncoding(a.encoding),process.stdin.on("readable",function(){var b= -process.stdin.read();if(null!==b)for(c=!0,f+=b;b=process.stdin.read();)f+=b}),process.stdin.on("end",function(){c&&(y.argv.trimPipe&&(f=f.trim()),a.pipedData=f);S(a)})})()})(); +(function () { + 'use strict'; + + var font = {}; + font.red = font.green = font.gray = function (str) { return str; }; + // check for node version supporting chalk - if so overwrite `font` + //const font = import('chalk'); + var config = null; + var outputConfig = function (_config) { + config = _config; + }; + var info = function (msg, data) { + if ( data === void 0 ) data = ''; + + if (config.quiet || config.quietTotal) { + return; + } + console.error(font.gray(msg), data); + }; + var chat = function (msg, data) { + if ( data === void 0 ) data = ''; + + if (config.verbose) { + info(msg, data); + } + else { + debug(msg + ' ' + data); + } + }; + var die = function (msg, data, displayHelp) { + if ( msg === void 0 ) msg = ''; + if ( data === void 0 ) data = ''; + if ( displayHelp === void 0 ) displayHelp = false; + + if (displayHelp && !config.quietTotal) { + config.showHelp(); + } + msg && error(' ❌ ' + msg, data); + kill(); + }; + var error = function (msg, data) { + if ( data === void 0 ) data = ''; + + if (!config.quiet && !config.quietTotal) { + console.error(font.red(msg), data); + } + if (config.halt) { + kill(msg); + } + return false; + }; + function debug(data) { + if (config.debug) { + console.error(font.gray(JSON.stringify(data, null, 4))); + } + } + function step(data) { + if (config.verbose) { + debug(data); + } + } + function kill(error, msg) { + if ( error === void 0 ) error = 1; + if ( msg === void 0 ) msg = ''; + + msg && console.error(+msg); + process.exit(error); + } + + var fs = require('fs'); + var path = require('path'); + var globs = require('globs'); + var now = new Date(); + var re = { + euro: /€/g, + section: /§/g, + mctime: /[mc]time/, + colon: /:/g, + capturedGroupRef: /\$\d/, + regexSpecialChars: /[-\[\]{}()*+?.,\/\\^$|#\s]/g, + byteOrSize: /bytes|size/, + folderName: /[\\\/]+([^\\\/]+)[\\\/]+[^\\\/]+$/, + }; + var version = '7.1.2'; + function engine(config) { + if ( config === void 0 ) config = { 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'); + step(config); + config.files + // Correct filepath + //.map(filepath=>path.normalize(process.cwd()+'/'+filepath)) + // Find out if any filepaths are invalid + .filter(function (filepath) { return (fs.existsSync(filepath) ? true : error('File not found:', filepath)); }) + // Do the replacement + .forEach(function (filepath) { return 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, _config_rr, _data_rr) { + debug('Work on content from: ' + _file_rr); + // Variables to be accessible from js. + if (_config_rr.replacementJs) { + _config_rr.replacement = dynamicReplacement(_file_rr, _config_rr, _data_rr); + } + // Main regexp of the whole thing + var result = _data_rr.replace(_config_rr.regex, _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 + var oriFile = path.normalize(path.join(process.cwd(), _file_rr)); + var salt = new Date().toISOString().replace(re.colon, '_').replace('Z', ''); + var 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, function (err) { + if (err) { + return error(err); + } + fs.writeFile(oriFile, result, _config_rr.encoding, function (err) { + if (err) { + return error(err); + } + if (!_config_rr.keepBackup) { + fs.unlink(backupFile, function (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) { + 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) { + 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 () { + var arguments$1 = arguments; + + 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$1[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); + var regex; + var 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 { + var RE2 = require('re2'); + regex = new RE2(pattern, flags); + } + catch (e$1) { + if (config.debug) + { throw new Error(e$1); } + die(e$1.message); + } + break; + default: + die(("Engine " + (config.engine) + " not supported")); + } + step(regex); + return regex; + } + function getFlags(config) { + step('Get flags'); + var 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'; + } + var 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) { + var _time_obj = now; + var _time = localTimeString(_time_obj); + var _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 + var _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);'); + var needsByteOrSize = re.byteOrSize.test(_config_rr.replacement); + var 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); + var 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)) { + var 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 () { + var arguments$1 = arguments; + + step(arguments); + var __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$1[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) { + if ( dateObj === void 0 ) 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) { + if ( str === void 0 ) str = ''; + + if (!conf.voidEuro) { + str = str.replace(re.euro, '$'); + } + if (!conf.voidSection) { + str = str.replace(re.section, '\\'); + } + return str; + } + function getFilePaths(conf) { + var includeGlob = conf.includeGlob; + var excludeGlob = conf.excludeGlob; + var excludeRe = conf.excludeRe; + var filesToInclude = globs.sync(includeGlob); + if (excludeRe.length) { + excludeRe + .map(function (el) { return getFinalPattern(el, conf); }) + .forEach(function (re) { + filesToInclude = filesToInclude.filter(function (el) { return !el.match(re); }); + }); + } + if (excludeGlob.length) { + var filesToExclude = globs.sync(excludeGlob); + filesToInclude = filesToInclude.filter(function (el) { return !filesToExclude.includes(el); }); + } + return filesToInclude; + } + + var assign; + var pattern, replacement; + // To avoid problems with patterns or replacements starting with '-' the two first arguments can not contain flags and are removed before yargs does it magic - but we still need to handle -version and -help + var needHelp = 0; + if (process.argv.length < 4) { + if (/-v|--?version$/i.test(process.argv[process.argv.length - 1])) { + console.log(version); + process.exitCode = 0; + process.exit(); + } + else if (/-h|--?help$/i.test(process.argv[process.argv.length - 1])) { + needHelp = 1; + } + else { + needHelp = 2; + } + } + else { + (assign = process.argv.splice(2, 2), pattern = assign[0], replacement = assign[1]); + } + var yargs = require('yargs') + .strict() + .usage('RexReplace ' + + version + + ': Regexp search and replace for files using lookahead and backreference to matching groups in the replacement. Defaults to global multiline case-insensitive search.\n\n' + + '> rexreplace pattern replacement [fileGlob|option]+') + .example("> rexreplace 'Foo' 'xxx' myfile.md", "'foobar' in myfile.md will become 'xxxbar'") + .example('') + .example("> rr xxx Foo myfile.md", "The alias 'rr' can be used instead of 'rexreplace'") + .example('') + .example("> rexreplace '(f?(o))o(.*)' '$3$1€2' myfile.md", "'foobar' in myfile.md will become 'barfoo'") + .example('') + .example("> rexreplace '^#' '##' *.md", "All markdown files in this dir got all headlines moved one level deeper") + .example('') + .example("> rexreplace 'a' 'b' 'myfile.md' 'src/**/*.*' ", "Provide multiple files or glob if needed") + .version('v', 'Print rexreplace version (can be given as only argument)', version) + .alias('v', 'version') + .boolean('V') + .describe('V', 'More chatty output') + .alias('V', 'verbose') + //.conflicts('V', 'q') + //.conflicts('V', 'Q') + .boolean('L') + .describe('L', 'Literal string search (no regex used when searching)') + .alias('L', 'literal') + .boolean('I') + .describe('I', 'Void case insensitive search pattern.') + .alias('I', 'void-ignore-case') + .boolean('G') + .describe('G', 'Void global search (stop looking after the first match).') + .alias('G', 'void-global') + .boolean('s') + .describe('s', 'Have `.` also match newline.') + .alias('s', 'dot-all') + .boolean('M') + .describe('M', 'Void multiline search pattern. Makes ^ and $ match start/end of whole content rather than each line.') + .alias('M', 'void-multiline') + .boolean('u') + .describe('u', 'Treat pattern as a sequence of unicode code points.') + .alias('u', 'unicode') + .default('e', 'utf8') + .alias('e', 'encoding') + .describe('e', 'Encoding of files/piped data.') + .alias('E', 'engine') + .describe('E', 'What regex engine to use:') + .choices('E', ['V8' ]) + .default('E', 'V8') + .boolean('q') + .describe('q', 'Only display errors (no other info)') + .alias('q', 'quiet') + .boolean('Q') + .describe('Q', 'Never display errors or info') + .alias('Q', 'quiet-total') + .boolean('H') + .describe('H', 'Halt on first error') + .alias('H', 'halt') + .default('H', false) + .boolean('d') + .describe('d', 'Print debug info') + .alias('d', 'debug') + .boolean('€') + .describe('€', "Void having '€' as alias for '$' in pattern and replacement parameters") + .alias('€', 'void-euro') + .boolean('§') + .describe('§', "Void having '§' as alias for '\\' in pattern and replacement parameters") + .alias('§', 'void-section') + .boolean('o') + .describe('o', 'Output the final result instead of saving to file. Will also output content even if no replacement has taken place.') + .alias('o', 'output') + //.conflicts('o','O') + .boolean('A') + .alias('A', 'void-async') + .describe('A', "Handle files in a synchronous flow. Good to limit memory usage when handling large files. " + + '') + .boolean('B') + .describe('B', 'Avoid temporary backing up file. Works async (independent of -A flag) and will speed up things but at one point data lives only in memory, and you will lose the content if the process is abrupted.') + .alias('B', 'void-backup') + .boolean('b') + .describe('b', 'Keep a backup file of the original content.') + .alias('b', 'keep-backup') + .boolean('m') + .describe('m', "Output each match on a new line. " + + "Will not replace any content but you still need to provide a dummy value (like `_`) as replacement parameter. " + + "If search pattern does not contain matching groups the full match will be outputted. " + + "If search pattern does contain matching groups only matching groups will be outputted (same line with no delimiter). " + + "") + .alias('m', 'output-match') + .boolean('T') + .alias('T', 'trim-pipe') + .describe('T', "Trim piped data before processing. " + + "If piped data only consists of chars that can be trimmed (new line, space, tabs...) it will become an empty string. " + + '') + .boolean('R') + .alias('R', 'replacement-pipe') + .describe('R', "Replacement will be piped in. You still need to provide a dummy value (like `_`) as replacement parameter." + + '') + .boolean('j') + .alias('j', 'replacement-js') + .describe('j', "Treat replacement as javascript source code. \nThe statement from the last expression will become the replacement string. \nPurposefully implemented the most insecure way possible to remove _any_ incentive to consider running code from an untrusted part. \nThe full match will be available as a javascript variable named $0 while each captured group will be available as $1, $2, $3, ... and so on. \nAt some point, the $ char _will_ give you a headache when used from the command line, so use €0, €1, €2, €3... instead. \nIf the javascript source code references to the full match or a captured group the code will run once per match. Otherwise, it will run once per file. \n\nThe code has access to the following variables: \n`r` as an alias for `require` with both expanded to understand a relative path even if it is not starting with `./`, \n`fs` from node, \n`path` from node, \n`globs` from npm, \n`pipe`: the data piped into the command (null if no piped data), \n`find`: pattern searched for (the needle), \n`text`: full text being searched i.e. file content or piped data (the haystack), \n`bytes`: total size of the haystack in bytes, \n`size`: human-friendly representation of the total size of the haystack, \n`time`: String representing the local time when the command was invoked,\n`time_obj`: date object representing `time`,\n`now`: alias for `time`,\n`cwd`: current process working dir, \n`nl`: a new-line char,\n`_`: a single space char (for easy string concatenation).\n\nThe following values defaults to `❌` if haystack does not originate from a file:\n`file`: contains the full path of the active file being searched (including full filename), \n`file_rel`: contains `file` relative to current process working dir, \n`dirpath`: contains the full path without filename of the active file being searched, \n`dirpath_rel`: contains `dirpath` relative to current process working dir, \n`filename`: is the full filename of the active file being searched without path, \n`name`: filename of the active file being searched with no extension, \n`ext`: extension of the filename including leading dot, \n`mtime`: ISO inspired representation of the last local modification time of the current file, \n`ctime`: ISO representation of the local creation time of the current file. \n`mtime_obj`: date object representing `mtime`, \n`ctime_obj`: date object representing `ctime`. \n\nAll variables, except from module, date objects, `nl` and `_`, has a corresponding variable name followed by `_` where the content has an extra space at the end (for easy concatenation). \n") + .string('x') + .describe('x', 'Exclude files with a path that matches this regular expression. Will follow same regex flags and setup as the main search. Can be used multiple times.') + .alias('x', 'exclude-re') + .string('X') + .describe('X', 'Exclude files found with this glob. Can be used multiple times.') + .alias('X', 'exclude-glob') + /* + .boolean('N') + .alias('N', 'void-newline') + .describe('N', + `Avoid having newline when outputting data (or when piping). `+ + `Normally . `+ + '' + ) + + + -E (Expect there to be no match and return exit 1 if found) + -e (Expect there to be batch and return exit 1 if not found) + */ + /* .boolean('P') + .describe('P', "Pattern is a filename from where the pattern will be generated. If more than one line is found in the file the pattern will be defined by each line trimmed and having newlines removed followed by other all rules (like -€).)") + .alias('P', 'pattern-file') + + .boolean('R') + .alias('R', 'replacement-file') + .describe('R', + `Replacement is a filename from where the replacement will be generated. ` + + `If more than one line is found in the file the final replacement will be defined by each line trimmed and having newlines removed followed by all other rules (like -€).` + ) + + */ + /* // Ideas + + .boolean('n') + .describe('n', "Do replacement on file path/names instead of file content (rename/move the files)") + .alias('n', 'name') + + // https://github.com/eugeneware/replacestream + .integer('M') + .describe('M', "Maximum length of match. Set this value only if any single file of your ") + .alias('M', 'max-match-len') + .default('M', false) + + + + .boolean('G') + .describe('G', "filename/globas are filename(s) for files containing one filename/globs on each line to be search/replaced") + .alias('G', 'globs-file') + + .boolean('g') + .describe('g', "filename/globs will be piped in. If any filename/globs are given in command the piped data will be prepened") + .alias('g', 'glob-pipe') + + + .boolean('j') + .describe('j', "Pattern is javascript source that will return a string giving the pattern to use") + .alias('j', 'pattern-js') + + + .boolean('glob-js') + .describe('glob-js', "filename/globs are javascript source that will return a string with newline seperating each glob to work on") + + + */ + .help('h') + .describe('h', 'Display help.') + .alias('h', 'help') + .epilog("Inspiration: .oO(What should 'sed' have been by now?)"); + function backOut(exitcode) { + if ( exitcode === void 0 ) exitcode = 1; + + yargs.showHelp(); + //io(help); + process.exitCode = exitcode; + process.exit(); + } + function unescapeString(str) { + if ( str === void 0 ) str = ''; + + return new Function(("return '" + (str.replace(/'/g, "\\'")) + "'"))(); + } + (function () { + if (0 < needHelp) { + return backOut(needHelp - 1); + } + // All options into one big config object for the rexreplace core + var config = {}; + // Use only camelCase full lenght version of settings so we make sure the core can be documented propperly + Object.keys(yargs.argv).forEach(function (key) { + if (1 < key.length && key.indexOf('-') < 0) { + config[key] = yargs.argv[key]; + } + }); + var pipeInUse = false; + var pipeData = ''; + config.pipedData = null; + config.showHelp = yargs.showHelp; + config.pattern = pattern; + config.includeGlob = yargs.argv._; + config.excludeGlob = [].concat( yargs.argv.excludeGlob ).filter(Boolean); + config.excludeRe = [].concat( yargs.argv.excludeRe ).filter(Boolean); + if (config.replacementJs) { + config.replacement = replacement; + } + else { + config.replacement = unescapeString(replacement); + } + /*if(Boolean(process.stdout.isTTY)){ + config.output = true; + }*/ + if (Boolean(process.stdin.isTTY)) { + if (config.replacementPipe) { + return backOut(); + } + engine(config); + } + else { + process.stdin.setEncoding(config.encoding); + process.stdin.on('readable', function () { + var chunk = process.stdin.read(); + if (null !== chunk) { + pipeInUse = true; + pipeData += chunk; + while ((chunk = process.stdin.read())) { + pipeData += chunk; + } + } + }); + process.stdin.on('end', function () { + if (pipeInUse) { + if (yargs.argv.trimPipe) { + pipeData = pipeData.trim(); + } + config.pipedData = pipeData; + } + engine(config); + }); + } + })(); + +})(); diff --git a/package.json b/package.json index 0ed83f03..c738b3f8 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,6 @@ "version": "7.1.2", "description": "Smoothly search & replace in files from CLI.", "author": "Mathias Rangel Wulff", - "funding": { - "paymail": "mwulff@moneybutton.com" - }, "license": "MIT", "main": "src/engine.js", "repository": { @@ -70,8 +67,8 @@ "yarn": "1.22.19" }, "resolutions": { - "ansi-regex": "^5.0.1", - "tough-cookie": "^4.1.3" + "ansi-regex": "5.0.1", + "tough-cookie": "4.1.3" }, "directories": { "test": "test" diff --git a/src/cli.ts b/src/cli.ts index e81f1f8a..ea1b046c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -34,8 +34,9 @@ const yargs = require('yargs') .example(`> rexreplace 'Foo' 'xxx' myfile.md`, `'foobar' in myfile.md will become 'xxxbar'`) .example('') - .example(`> rr Foo xxx myfile.md`, `The alias 'rr' can be used instead of 'rexreplace'`) + .example(`> rr xxx Foo myfile.md`, `The alias 'rr' can be used instead of 'rexreplace'`) .example('') + .example( `> rexreplace '(f?(o))o(.*)' '$3$1€2' myfile.md`, `'foobar' in myfile.md will become 'barfoo'` @@ -45,7 +46,11 @@ const yargs = require('yargs') `> rexreplace '^#' '##' *.md`, `All markdown files in this dir got all headlines moved one level deeper` ) - + .example('') + .example( + `> rexreplace 'a' 'b' 'myfile.md' 'src/**/*.*' `, + `Provide multiple files or glob if needed` + ) .version('v', 'Print rexreplace version (can be given as only argument)', rexreplace.version) .alias('v', 'version') @@ -215,6 +220,17 @@ The following values defaults to \`❌\` if haystack does not originate from a f All variables, except from module, date objects, \`nl\` and \`_\`, has a corresponding variable name followed by \`_\` where the content has an extra space at the end (for easy concatenation). ` ) + .string('x') + .describe( + 'x', + 'Exclude files with a path that matches this regular expression. Will follow same regex flags and setup as the main search. Can be used multiple times.' + ) + .alias('x', 'exclude-re') + + .string('X') + .describe('X', 'Exclude files found with this glob. Can be used multiple times.') + .alias('X', 'exclude-glob') + /* .boolean('N') .alias('N', 'void-newline') @@ -245,7 +261,7 @@ All variables, except from module, date objects, \`nl\` and \`_\`, has a corresp /* // Ideas .boolean('n') - .describe('n', "Do replacement on file names instead of file content (rename the files)") + .describe('n', "Do replacement on file path/names instead of file content (rename/move the files)") .alias('n', 'name') // https://github.com/eugeneware/replacestream @@ -311,10 +327,13 @@ function unescapeString(str = '') { let pipeInUse = false; let pipeData = ''; - config.globs = yargs.argv._; + config.pipedData = null; config.showHelp = yargs.showHelp; config.pattern = pattern; + config.includeGlob = yargs.argv._; + config.excludeGlob = [...yargs.argv.excludeGlob].filter(Boolean); + config.excludeRe = [...yargs.argv.excludeRe].filter(Boolean); if (config.replacementJs) { config.replacement = replacement; } else { diff --git a/src/engine.ts b/src/engine.ts index f932e6c4..7f497d1b 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -8,6 +8,17 @@ const now = new Date(); import {outputConfig, step, debug, chat, info, error, die} from './output'; +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'}) { @@ -16,13 +27,13 @@ export function engine(config: any = {engine: 'V8'}) { step('Displaying steps for:'); step(config); - config.pattern = getFinalPattern(config) || ''; + config.pattern = getFinalPattern(config.pattern, config) || ''; - config.replacement = getFinalReplacement(config) || ''; + config.replacement = getFinalReplacement(config.replacement, config) || ''; config.replacementOri = config.replacement; - config.regex = getFinalRegex(config) || ''; + config.regex = getFinalRegex(config.pattern, config) || ''; step(config); @@ -30,7 +41,7 @@ export function engine(config: any = {engine: 'V8'}) { return doReplacement('Piped data', config, config.pipedData); } - config.files = globs2paths(config.globs); + config.files = getFilePaths(config); if (!config.files.length) { return error(config.files.length + ' files found'); @@ -48,278 +59,276 @@ export function engine(config: any = {engine: 'V8'}) { // 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); + } - 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); +// 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); - // Variables to be accessible from js. - if (_config_rr.replacementJs) { - _config_rr.replacement = dynamicReplacement(_file_rr, _config_rr, _data_rr); - } + // Variables to be accessible from js. + if (_config_rr.replacementJs) { + _config_rr.replacement = dynamicReplacement(_file_rr, _config_rr, _data_rr); + } - // Main regexp of the whole thing - const result = _data_rr.replace(_config_rr.regex, _config_rr.replacement); + // Main regexp of the whole thing + const result = _data_rr.replace(_config_rr.regex, _config_rr.replacement); - // The output of matched strings is done from the replacement, so no need to continue - if (_config_rr.outputMatch) { - return; - } + // 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); - } + 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; - } + // 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 = ''; + // Release the memory while storing files + _data_rr = ''; - debug('Write new content to: ' + _file_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(/:/g, '_').replace('Z', ''); - const backupFile = oriFile + '.' + salt + '.backup'; + // 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); + }); + } - 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); + //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); } - return info(_file_rr); + } 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); } - // Let me know when fs gets promise'fied - fs.rename(oriFile, backupFile, (err) => { + fs.writeFile(oriFile, result, _config_rr.encoding, (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 { + if (!_config_rr.keepBackup) { + fs.unlink(backupFile, (err) => { + if (err) { + return error(err); + } info(_file_rr); - } - }); - }); - } - - function handlePipedData(config) { - step('Check Piped Data'); - - if (config.globs.length) { - if (!config.replacementJs) { - chat('Piped data never used.'); + }); + } else { + info(_file_rr); } + }); + }); +} - return false; - } +function handlePipedData(config) { + step('Check Piped Data'); - if (null !== config.pipedData && !config.pipedDataUsed) { - config.dataIsPiped = true; - config.output = true; - return true; + if (config.includeGlob.length) { + if (!config.replacementJs && config.pipedData) { + chat('Piped data never used.'); } return false; } - function getFinalPattern(conf: any) { - step('Get final pattern'); - let pattern = replacePlaceholders(conf.pattern, conf); - - /*if (config.patternFile) { - pattern = fs.readFileSync(pattern, 'utf8'); - pattern = new Function('return '+pattern)(); - }*/ - - step(pattern); - return pattern; + if (null !== config.pipedData && !config.pipedDataUsed) { + config.dataIsPiped = true; + config.output = true; + return true; } - function getFinalReplacement(conf: any) { - step('Get final replacement'); - /*if(config.replacementFile){ - return oneLinerFromFile(fs.readFileSync(replacement,'utf8')); - }*/ + return false; +} - conf.replacement = replacePlaceholders(conf.replacement, conf); +function getFinalPattern(pattern, conf: any) { + step('Get final pattern'); + pattern = replacePlaceholders(pattern, conf); - if (conf.replacementPipe) { - step('Piping replacement'); - conf.pipedDataUsed = true; - if (null === conf.pipedData) { - return die('No data piped into replacement'); - } - conf.replacement = conf.pipedData; - } + if (conf.literal) { + pattern = pattern.replace(re.regexSpecialChars, '\\$&'); + } - if (conf.outputMatch) { - step('Output match'); + /*if (config.patternFile) { + pattern = fs.readFileSync(pattern, 'utf8'); + pattern = new Function('return '+pattern)(); + }*/ - if (parseInt(process.versions.node) < 6) { - return die('outputMatch is only supported in node 6+'); - } + step(pattern); + return pattern; +} - return function () { - step(arguments); +function getFinalReplacement(replacement, conf: any) { + step('Get final replacement'); + /*if(config.replacementFile){ + return oneLinerFromFile(fs.readFileSync(replacement,'utf8')); + }*/ - if (arguments.length === 3) { - step('Printing full match'); - process.stdout.write(arguments[0] + '\n'); - return ''; - } + replacement = replacePlaceholders(replacement, conf); - for (var i = 1; i < arguments.length - 2; i++) { - process.stdout.write(arguments[i]); - } - process.stdout.write('\n'); - return ''; - }; + 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 captured groups then run dynamicly - //console.log(process); - if ( - conf.replacementJs && - /\$\d/.test(conf.replacement) && - parseInt(process.versions.node) < 6 - ) { - return die('Captured groups for javascript replacement is only supported in node 6+'); + if (parseInt(process.versions.node) < 6) { + return die('outputMatch is only supported in node 6+'); } - step(conf.replacement); + return function () { + step(arguments); + + if (arguments.length === 3) { + step('Printing full match'); + process.stdout.write(arguments[0] + '\n'); + return ''; + } - return conf.replacement; + for (var i = 1; i < arguments.length - 2; i++) { + process.stdout.write(arguments[i]); + } + process.stdout.write('\n'); + return ''; + }; } - /*function oneLinerFromFile(str){ - let lines = str.split("\n"); - if(lines.length===1){ - return str; - } - return lines.map(function (line) { - return line.trim(); - }).join(' '); - }*/ + // 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+'); + } - function getFinalRegex(config) { - step('Get final regex with engine: ' + config.engine); + step(replacement); - let pattern = config.pattern; + return replacement; +} - if (config.literal) { - pattern = pattern.replace(/[-\[\]{}()*+?.,\/\\^$|#\s]/g, '\\$&'); - } +/*function oneLinerFromFile(str){ + let lines = str.split("\n"); + if(lines.length===1){ + return str; + } + return lines.map(function (line) { + return line.trim(); + }).join(' '); +}*/ - 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); - } catch (e) { - if (config.debug) throw new Error(e); - die(e.message); - } - break; - default: - die(`Engine ${config.engine} not supported`); - } +function getFinalRegex(pattern, config) { + step('Get final regex with engine: ' + config.engine); + + let regex; - step(regex); + let flags = getFlags(config); - return regex; + 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); + } catch (e) { + if (config.debug) throw new Error(e); + die(e.message); + } + break; + default: + die(`Engine ${config.engine} not supported`); } - function getFlags(config) { - step('Get flags'); + step(regex); - let flags = ''; + return regex; +} - if (!config.voidGlobal) { - flags += 'g'; - } +function getFlags(config) { + step('Get flags'); - if (!config.voidIgnoreCase) { - flags += 'i'; - } + let flags = ''; - if (!config.voidMultiline) { - flags += 'm'; - } + if (!config.voidGlobal) { + flags += 'g'; + } - if (config.dotAll) { - flags += 's'; - } + if (!config.voidIgnoreCase) { + flags += 'i'; + } - if (config.unicode) { - flags += 'u'; - } + if (!config.voidMultiline) { + flags += 'm'; + } - step(flags); + if (config.dotAll) { + flags += 's'; + } - return flags; + if (config.unicode) { + flags += 'u'; } + + step(flags); + + return flags; } function readableSize(size) { @@ -429,7 +438,7 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { 'return eval(__code_rr);' ); - const needsByteOrSize = /bytes|size/.test(_config_rr.replacement); + 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) { @@ -438,12 +447,12 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { const pathInfo = path.parse(_file); _dirpath = pathInfo.dir; _dirpath_rel = path.relative(_cwd, _dirpath); - _dirname = _file.match(/[\\\/]+([^\\\/]+)[\\\/]+[^\\\/]+$/)[1]; + _dirname = (_file.match(re.folderName) || ' _')[1]; _filename = pathInfo.base; _name = pathInfo.name; _ext = pathInfo.ext; - if (betterToReadfromFile || /[mc]time/.test(_config_rr.replacement)) { + if (betterToReadfromFile || re.mctime.test(_config_rr.replacement)) { const fileStats = fs.statSync(_file); _bytes = fileStats.size; _size = readableSize(_bytes); @@ -513,7 +522,7 @@ function dynamicReplacement(_file_rr, _config_rr, _data_rr) { code_rr ); } - // Captures groups present, so need to run once per match + // Capture groups used, so need to run once per match return function () { step(arguments); @@ -613,11 +622,6 @@ function localTimeString(dateObj = new Date()) { ).slice(-2)}.${('00' + dateObj.getMilliseconds()).slice(-3)}`; } -const re = { - euro: /€/g, - section: /§/g, -}; - function replacePlaceholders(str = '', conf: any) { if (!conf.voidEuro) { str = str.replace(re.euro, '$'); @@ -630,23 +634,22 @@ function replacePlaceholders(str = '', conf: any) { return str; } -function globs2paths(_globs: string[] = []) { - const globsToInclude: string[] = []; - const globsToExclude: string[] = []; +function getFilePaths(conf) { + let {includeGlob, excludeGlob, excludeRe} = conf; - _globs.filter(Boolean).forEach((glob) => { - if ('!' === glob[0] || '^' === glob[0]) { - globsToExclude.push(glob.slice(1)); - } else { - globsToInclude.push(glob); - } - }); + let filesToInclude = globs.sync(includeGlob); - let filesToInclude = globs.sync(globsToInclude); + if (excludeRe.length) { + excludeRe + .map((el) => getFinalPattern(el, conf)) + .forEach((re) => { + filesToInclude = filesToInclude.filter((el) => !el.match(re)); + }); + } - if (globsToExclude.length) { - const filesToExclude = globs.sync(globsToExclude); - return filesToInclude.filter((el) => !filesToExclude.includes(el)); + if (excludeGlob.length) { + const filesToExclude = globs.sync(excludeGlob); + filesToInclude = filesToInclude.filter((el) => !filesToExclude.includes(el)); } return filesToInclude; diff --git a/test/cli/run.sh b/test/cli/run.sh index bfbc28fd..9ae216f3 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -22,6 +22,7 @@ source $DIR/aserta.sh reset() { echo 'Resetting testdata' echo 'foobar' > my.file + echo 'abc123' > your.file } @@ -217,6 +218,43 @@ assert "rexreplace '*+*' 'b' my.file -o --literal" 'foobar' +# -x +reset + rexreplace 'b' '*+*' my.file +assert "cat my.file" 'foo*+*ar' +assert "cat your.file" 'abc123' +reset + rexreplace 'b' '*+*' '*.file' +assert "cat my.file" 'foo*+*ar' +assert "cat your.file" 'a*+*c123' +reset + rexreplace 'b' '*+*' '*.file' -x y +assert "cat my.file" 'foobar' +assert "cat your.file" 'abc123' +reset + rexreplace 'b' '*+*' '*.file' -x ^y +assert "cat my.file" 'foo*+*ar' +assert "cat your.file" 'abc123' + + +# -X +reset + rexreplace 'b' '*+*' '*.file' -X '*.file' +assert "cat my.file" 'foobar' +assert "cat your.file" 'abc123' +reset + rexreplace 'b' '*+*' '*.file' -X 'y*' +assert "cat my.file" 'foo*+*ar' +assert "cat your.file" 'abc123' + + + + + + + + + # # -P # reset @@ -268,6 +306,7 @@ assert "rexreplace '*+*' 'b' my.file -o --literal" 'foobar' # reset rm my.file +rm your.file assert_end "rexreplace"