grunt-search
Version:
Grunt plugin that searches a list of files and logs all findings in various formats.
399 lines (331 loc) • 11.5 kB
JavaScript
/*
* grunt-search
* https://github.com/benkeen/grunt-search
*
* Copyright (c) 2013 Ben Keen
* Licensed under the MIT license.
*/
"use strict";
module.exports = function(grunt) {
grunt.registerMultiTask("search", "Grunt plugin that searches a list of files for particular search strings and logs all findings in various formats.", function() {
// merge task-specific and/or target-specific options with these defaults
var options = this.options({
searchString: null,
logFile: null,
logFormat: 'json', // json/xml/text/junit/custom/console
customLogFormatCallback: null,
failOnMatch: false,
scopeMatchToFile: false,
JUnitTestsuiteName: 'jshint',
JUnitFailureMessage: 'Substring matched',
outputExaminedFiles: false,
onComplete: null,
onMatch: null,
logCondition: null,
isJUnitFailure: null
});
// validate the options
if (!_validateOptions(options)) {
return;
}
// if the searchString isn't a regular expression, convert it to one
if (!Array.isArray(options.searchString)) {
options.searchString = [options.searchString];
}
// now convert all strings in the array to regexps
var cleanSearchStrings = [];
options.searchString.forEach(function (item) {
if (!(item instanceof RegExp)) {
cleanSearchStrings.push({ regexp: new RegExp(item, "g"), original: item });
} else {
cleanSearchStrings.push({ regexp: item, original: item });
}
});
// now iterate over all specified file groups
this.files.forEach(function(f) {
// filter out invalid files and folders
var filePaths = [];
f.src.filter(function(filepath) {
if (grunt.file.isDir(filepath)) {
return;
}
// *** this was in the gruntplugin example, but it doesn't seem to even GET here if the file specified
// doesn't exist... ***
if (!grunt.file.exists(filepath)) {
grunt.log.warn('Source file "' + filepath + '" not found.');
} else {
filePaths.push(filepath);
}
});
// now search the files for the search string(s). This is pretty poor from a memory perspective: it loads
// the entire file into memory and runs the reg exp on it
var matches = {};
var numMatches = 0;
for (var i=0; i<filePaths.length; i++) {
var file = filePaths[i];
var src = grunt.file.read(file);
var lines = src.split("\n");
var matchLines = [];
var matchStrings = [];
var foundMatch = false;
// yikes.
for (var j=1; j<=lines.length; j++) {
for (var k=0; k<cleanSearchStrings.length; k++) {
var currSearchString = cleanSearchStrings[k];
var lineMatches = lines[j-1].match(currSearchString.regexp);
if (!lineMatches) {
continue;
}
foundMatch = true;
matchLines.push(j);
for (var m=0; m<lineMatches.length; m++) {
matchStrings.push(lineMatches[m]);
var lineMatch = {
file: file,
line: j,
match: lineMatches[m],
searchString: currSearchString.original
};
if (!options.scopeMatchToFile && (options.logCondition === null || options.logCondition(lineMatch) === true)) {
if (!matches.hasOwnProperty(file)) {
matches[file] = [];
}
matches[file].push({ line: j, match: lineMatches[m], searchString: currSearchString.original });
numMatches++;
if (options.onMatch !== null) {
options.onMatch(lineMatch);
}
}
}
}
}
var fileMatch = {
file: file,
line: matchLines,
match: matchStrings
};
if (foundMatch && options.scopeMatchToFile && (options.logCondition === null || options.logCondition(fileMatch) === true)) {
if (!matches.hasOwnProperty(file)) {
matches[file] = [];
}
matches[file].push({ line: matchLines, match: matchStrings });
numMatches++;
if (options.onMatch !== null) {
options.onMatch(fileMatch);
}
}
}
// write the log file - even if there are no results. It'll just contain a "numResults: 0" which is useful
// in of itself
_generateLogFile(options, filePaths, matches, numMatches);
if (numMatches > 0 && options.failOnMatch) {
grunt.fail.fatal("Matches of " + options.searchString.toString() + " found");
}
if (options.onComplete !== null) {
options.onComplete({ numMatches: numMatches, matches: matches });
}
grunt.log.writeln("Num matches: " + numMatches);
});
});
var _validateOptions = function(options) {
var optionErrors = [];
if (options.searchString === null) {
optionErrors.push("Missing options.searchString value.");
}
if (options.logFormat !== "console" && options.logFormat !== "custom" && options.logFile === null) {
optionErrors.push("Missing options.logFile value.");
}
if (optionErrors.length) {
for (var i=0; i<optionErrors.length; i++) {
grunt.log.error("Error: ", optionErrors[i]);
}
}
return optionErrors.length === 0;
};
var _generateLogFile = function(options, filePaths, results, numResults) {
var content = '';
if (options.logFormat === "json") {
content = _getJSONLogFormat(options, filePaths, results, numResults);
} else if (options.logFormat === "xml") {
content = _getXMLLogFormat(options, filePaths, results, numResults);
} else if (options.logFormat === "junit") {
content = _getJUnitLogFormat(options, filePaths, results, numResults);
} else if (options.logFormat === "text" || options.logFormat === "console") {
content = _getTextLogFormat(options, filePaths, results, numResults);
} else if (options.logFormat === "custom") {
if (_isFunction(options.customLogFormatCallback)) {
options.customLogFormatCallback({
filePaths: filePaths,
results: results,
numResults: numResults
});
return;
} else {
grunt.log.error("Error: custom logFormat option selected, but invalid/missing customLogFormatCallback option defined.");
return;
}
}
if (options.logFormat !== "console") {
grunt.file.write(options.logFile, content);
} else {
grunt.log.writeln(content);
}
};
/**
* This generates a JSON formatted file of the match results. Boy I miss templating. :-)
* @param options
* @param results
* @param numResults
* @returns {string}
* @private
*/
var _getJSONLogFormat = function(options, filePaths, results, numResults) {
var content = "{\n\t\"numResults\": " + numResults + ",\n" +
"\t\"creationDate\": \"" + _getISODateString() + "\",\n" +
"\t\"results\": {\n";
var group = [];
for (var file in results) {
var groupStr = "\t\t\"" + file + "\": [\n";
var matchGroup = [];
for (var i=0; i<results[file].length; i++) {
matchGroup.push("\t\t\t{\n" +
"\t\t\t\t\"line\": " + results[file][i].line + ",\n" +
"\t\t\t\t\"match\": " + "\"" + _cleanStr(results[file][i].match) + "\"" +
"\n\t\t\t}");
}
groupStr += matchGroup.join(",\n") + "\n";
groupStr += "\t\t]"
group.push(groupStr);
}
content += group.join(",\n");
content += "\n\t}"
if (options.outputExaminedFiles) {
content += ",\n\t\"examinedFiles\": [\n";
var files = [];
for (var i=0; i<filePaths.length; i++) {
files.push("\t\t\"" + _cleanStr(filePaths[i]) + "\"");
}
content += files.join(",\n");
content += "\n\t]";
}
content += "\n}";
return content;
};
var _getXMLLogFormat = function(options, filePaths, results, numResults) {
var content = "<?xml version=\"1.0\"?>\n" +
"<search>\n" +
"\t<numResults>" + numResults + "</numResults>\n" +
"\t<creationDate>" + _getISODateString() + "</creationDate>\n" +
"\t<results>";
var matchGroup = "";
for (var file in results) {
for (var i=0; i<results[file].length; i++) {
matchGroup += "\n\t\t<result>\n" +
"\t\t\t<file>" + file + "</file>\n" +
"\t\t\t<line>" + results[file][i].line + "</line>\n" +
"\t\t\t<match>" + results[file][i].match + "</match>\n" +
"\t\t\t<searchStr>" + results[file][i].searchString + "</searchStr>\n" +
"\t\t</result>";
}
}
content += matchGroup + "\n" + "\t</results>\n";
if (options.outputExaminedFiles) {
content += "\t<examinedFiles>\n";
for (var i=0; i<filePaths.length; i++) {
content += "\t\t<file>" + filePaths[i] + "</file>\n";
}
content += "\t</examinedFiles>\n";
}
content += "</search>";
return content;
};
var _getTextLogFormat = function(options, filePaths, results, numResults) {
var content = "Num results: " + numResults + "\n" +
"Creation date: " + _getISODateString() + "\n" +
"Results:\n";
for (var file in results) {
for (var i=0; i<results[file].length; i++) {
content += "\tFile: " + file + "\n" +
"\tLine: " + results[file][i].line + "\n" +
"\tMatch: " + results[file][i].match + "\n" +
"\tSearch: " + results[file][i].searchString + "\n\n";
}
}
if (options.outputExaminedFiles) {
content += "Examined files:\n";
for (var i=0; i<filePaths.length; i++) {
content += "\t" + filePaths[i] + "\n";
}
}
return content;
};
var _getJUnitLogFormat = (function() {
function failTestCaseXML(name, matches, message) {
return [
"\t<testcase name=\"" + name + "\">",
"\t\t<failure message=\"" + message + "\">",
"\t\t<![CDATA[",
matches.map(function(failure, num, message) {
return (num + 1) + ". line " + failure.line + ": " + failure.match;
}).join("\n"),
"\t\t]]>",
"\t\t</failure>",
"\t</testcase>"
].join("\n");
}
function successTestCaseXML(name, matches) {
return [
"\t<testcase name=\"" + name + "\">",
"\t</testcase>"
].join("\n");
}
return function(options, filePaths, results, numResults) {
var parts = Object.keys(results).reduce(function(parts, file) {
parts.numTests += 1;
var isFailure = true;
if (options.isJUnitFailure !== null) {
isFailure = options.isJUnitFailure(file, results[file]);
}
if (isFailure) {
parts.numFailures += results[file].length;
parts.testCases.push(failTestCaseXML(file, results[file], options.JUnitFailureMessage));
} else {
parts.testCases.push(successTestCaseXML(file, results[file], options.JUnitFailureMessage));
}
return parts;
}, {
numTests : 0,
numFailures : 0,
testCases : []
});
var content = [
"<?xml version=\"1.0\"?>",
"<testsuite name=\"" + options.JUnitTestsuiteName + "\" tests=\"" + parts.numTests + "\" failures=\"" + parts.numFailures + "\" errors=\"0\">",
parts.testCases.join("\n"),
"</testsuite>"
].join("\n");
grunt.verbose.writeln(options.searchString.toString().yellow + ' > ' + options.logFile);
grunt.verbose.writeln(content);
return content;
};
}());
// helpers ----------------
var _cleanStr = function(str) {
return str.replace(/"/g, "\\\"");
}
var _getISODateString = function() {
var d = new Date();
function pad(n) {
return n < 10 ? '0' + n : n;
}
return d.getUTCFullYear() + '-'+
pad(d.getUTCMonth()+1) + '-' +
pad(d.getUTCDate()) + ' ' +
pad(d.getUTCHours()) + ':' +
pad(d.getUTCMinutes()) + ':' +
pad(d.getUTCSeconds())
};
var _isFunction = function(obj) {
return !!(obj && obj.constructor && obj.call && obj.apply);
};
};