5 * Copyright (c) 2014 "Cowboy" Ben Alman
6 * Licensed under the MIT license.
7 * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
13 var util = require('util');
16 var hooker = require('hooker');
17 // Requiring this here modifies the String prototype!
18 var colors = require('colors');
19 // The upcoming lodash 2.5+ should remove the need for underscore.string.
20 var _ = require('lodash');
21 _.str = require('underscore.string');
22 _.mixin(_.str.exports());
25 var logUtils = require('grunt-legacy-log-utils');
27 function Log(options) {
28 // This property always refers to the "base" logger.
31 this.options = _.extend({}, {
32 // Show colors in output?
34 // Enable verbose-mode logging?
36 // Enable debug logging statement?
38 // Where should messages be output?
39 outStream: process.stdout,
40 // NOTE: the color, verbose, debug options will be ignored if the
41 // "grunt" option is specified! See the Log.prototype.option and
42 // the Log.prototype.error methods for more info.
44 // Where should output wrap? If null, use legacy Grunt defaults.
46 // Should logger start muted?
49 // True once anything has actually been logged.
50 this.hasLogged = false;
52 // Related verbose / notverbose loggers.
53 this.verbose = new VerboseLog(this, true);
54 this.notverbose = new VerboseLog(this, false);
55 this.verbose.or = this.notverbose;
56 this.notverbose.or = this.verbose;
58 // Apparently, people have using grunt.log in interesting ways. Just bind
59 // all methods so that "this" is irrelevant.
60 if (this.options.grunt) {
62 _.bindAll(this.verbose);
63 _.bindAll(this.notverbose);
68 // Am I doing it wrong? :P
69 function VerboseLog(parentLog, verbose) {
70 // Keep track of the original, base "Log" instance.
71 this.always = parentLog;
72 // This logger is either verbose (true) or notverbose (false).
73 this._isVerbose = verbose;
75 util.inherits(VerboseLog, Log);
77 VerboseLog.prototype._write = function() {
78 // Abort if not in correct verbose mode.
79 if (Boolean(this.option('verbose')) !== this._isVerbose) { return; }
81 return VerboseLog.super_.prototype._write.apply(this, arguments);
84 // Create read/write accessors that prefer the parent log's properties (in
85 // the case of verbose/notverbose) to the current log's properties.
86 function makeSmartAccessor(name, isOption) {
87 Object.defineProperty(Log.prototype, name, {
91 return isOption ? this.always._options[name] : this.always['_' + name];
93 set: function(value) {
95 this.always._options[name] = value;
97 this.always['_' + name] = value;
102 makeSmartAccessor('options');
103 makeSmartAccessor('hasLogged');
104 makeSmartAccessor('muted', true);
106 // Disable colors if --no-colors was passed.
107 Log.prototype.initColors = function() {
108 if (this.option('no-color')) {
109 // String color getters should just return the string.
110 colors.mode = 'none';
111 // Strip colors from strings passed to console.log.
112 hooker.hook(console, 'log', function() {
113 var args = _.toArray(arguments);
114 return hooker.filter(this, args.map(function(arg) {
115 return typeof arg === 'string' ? colors.stripColors(arg) : arg;
121 // Check for color, verbose, debug options through Grunt if specified,
122 // otherwise defer to options object properties.
123 Log.prototype.option = function(name) {
124 if (this.options.grunt && this.options.grunt.option) {
125 return this.options.grunt.option(name);
127 var no = name.match(/^no-(.+)$/);
128 return no ? !this.options[no[1]] : this.options[name];
131 // Parse certain markup in strings to be logged.
132 Log.prototype._markup = function(str) {
134 // Make _foo_ underline.
135 str = str.replace(/(\s|^)_(\S|\S[\s\S]+?\S)_(?=[\s,.!?]|$)/g, '$1' + '$2'.underline);
137 str = str.replace(/(\s|^)\*(\S|\S[\s\S]+?\S)\*(?=[\s,.!?]|$)/g, '$1' + '$2'.bold);
141 // Similar to util.format in the standard library, however it'll always
142 // convert the first argument to a string and treat it as the format string.
143 Log.prototype._format = function(args) {
144 args = _.toArray(args);
145 if (args.length > 0) {
146 args[0] = String(args[0]);
148 return util.format.apply(util, args);
151 Log.prototype._write = function(msg) {
153 if (this.muted) { return; }
154 // Actually write output.
155 this.hasLogged = true;
157 // Users should probably use the colors-provided methods, but if they
158 // don't, this should strip extraneous color codes.
159 if (this.option('no-color')) { msg = colors.stripColors(msg); }
160 // Actually write to stdout.
161 this.options.outStream.write(this._markup(msg));
164 Log.prototype._writeln = function(msg) {
165 // Write blank line if no msg is passed in.
166 this._write((msg || '') + '\n');
170 Log.prototype.write = function() {
171 this._write(this._format(arguments));
175 // Write a line of output.
176 Log.prototype.writeln = function() {
177 this._writeln(this._format(arguments));
181 Log.prototype.warn = function() {
182 var msg = this._format(arguments);
183 if (arguments.length > 0) {
184 this._writeln('>> '.red + _.trim(msg).replace(/\n/g, '\n>> '.red));
186 this._writeln('ERROR'.red);
190 Log.prototype.error = function() {
191 if (this.options.grunt && this.options.grunt.fail) {
192 this.options.grunt.fail.errorcount++;
194 this.warn.apply(this, arguments);
197 Log.prototype.ok = function() {
198 var msg = this._format(arguments);
199 if (arguments.length > 0) {
200 this._writeln('>> '.green + _.trim(msg).replace(/\n/g, '\n>> '.green));
202 this._writeln('OK'.green);
206 Log.prototype.errorlns = function() {
207 var msg = this._format(arguments);
208 this.error(this.wraptext(this.options.maxCols || 77, msg));
211 Log.prototype.oklns = function() {
212 var msg = this._format(arguments);
213 this.ok(this.wraptext(this.options.maxCols || 77, msg));
216 Log.prototype.success = function() {
217 var msg = this._format(arguments);
218 this._writeln(msg.green);
221 Log.prototype.fail = function() {
222 var msg = this._format(arguments);
223 this._writeln(msg.red);
226 Log.prototype.header = function() {
227 var msg = this._format(arguments);
228 // Skip line before header, but not if header is the very first line output.
229 if (this.hasLogged) { this._writeln(); }
230 this._writeln(msg.underline);
233 Log.prototype.subhead = function() {
234 var msg = this._format(arguments);
235 // Skip line before subhead, but not if subhead is the very first line output.
236 if (this.hasLogged) { this._writeln(); }
237 this._writeln(msg.bold);
241 Log.prototype.debug = function() {
242 var msg = this._format(arguments);
243 if (this.option('debug')) {
244 this._writeln('[D] ' + msg.magenta);
249 // Write a line of a table.
250 Log.prototype.writetableln = function(widths, texts) {
251 this._writeln(this.table(widths, texts));
255 // Wrap a long line of text.
256 Log.prototype.writelns = function() {
257 var msg = this._format(arguments);
258 this._writeln(this.wraptext(this.options.maxCols || 80, msg));
262 // Display flags in verbose mode.
263 Log.prototype.writeflags = function(obj, prefix) {
265 if (Array.isArray(obj)) {
266 wordlist = this.wordlist(obj);
267 } else if (typeof obj === 'object' && obj) {
268 wordlist = this.wordlist(Object.keys(obj).map(function(key) {
270 return key + (val === true ? '' : '=' + JSON.stringify(val));
273 this._writeln((prefix || 'Flags') + ': ' + (wordlist || '(none)'.cyan));
277 // Add static methods.
283 ].forEach(function(prop) {
284 Log.prototype[prop] = exports[prop] = logUtils[prop];