首页 > 代码库 > 利用正则实现彩色控制台输出

利用正则实现彩色控制台输出

最近忙了一阵less的二次开发的工作,期间遇到了不少需要向控制台输出彩色文字的需求。翻了下以前同事的代码,发现要么自己拼转义字符串,要么使用一些不太好用的第三方库,总之都不是很合自己的口味。按照自己的口味,一个好的第三方库应该满足如下需求: 要支持丰富的颜色设置,同时设置颜色又不能太累赘,而且要支持console.log的通配符表示方法以减少拼字符串的工作。cli-color和colors的语法类似,都是采用方法来设定字符串颜色:

// colors
console.log("this is an error".error);

// cli-color 
console.log(clc.red(‘red‘) + ‘ plain ‘ + clc.blue(‘blue‘));

如果字符串中的颜色比较多,而且字符串中还包含动态数据,那么就需要大量的拼字符串的工作,丑陋且容易出错,因此这种坑爹的方案直接忽略。

既然这些现成的库不好用,干脆就自己写一个。作为一个前端平时自己接触的最多的是html,受html语法的启发,打算采用标签的形式来设置字符颜色,而不是采用方法的形式。比如要输出红绿两种颜色的文本,可以采用如下方式:Foo.log(‘<red>red color <green>green color</green></red>‘)。这种方式有两个优点: 第一,便于表现丰富的颜色,尤其是颜色嵌套的情况,如果采用cli-color那种方式来表现多个颜色的嵌套,估计拼字符串会让你想吐;第二,省去了记忆方法名和拼字符串。

那么这种设计是否容易实现呢? 在回答这个问题之前我们先简单说一下实现彩色输出的原理。向控制台输出彩色文字主要利用了ansi 中的转义字符(ANSI转义字符表)。众多的转义字符中有一部分是设置控制台的渲染方式的,其中输出控制采用如下语法来声明:\x1b[nmx1b的值是27,在ASC码表中表示转义字符,后面的[nm是模式设置,[m是常量,n为变量。通过设置N的值可以实现不同的输出设置,下面为常用的N值

  • 0 开始以暗色模式显示文本, 文字颜色为用户设置的控制台默认颜色,一般为白色
  • 1 开始以亮色模式显示文本, 文字颜色为用户设置的控制台默认颜色,一般为白色
  • 30 ~ 27 分别以black、red、green、 yellow、blue、pink、cyan、white颜色来显示文本

需要特别注意的一点:过这些输出设置不仅对本次输出起效,而且将一直起效,直至遇到新的设置或控制台退出! 所以在使用的时候一定要记得重置颜色设置,免得影响后面的控制台输出。我们通过下面的demo来检验下这些转义字符的功能。注意划红线的语句部分,虽然这条语句中没有对输出进行任何设置,但因为上一条命令中设置了控制台颜色,所以这次输出依然采用的上次的设置。
控制台输出

叙述了这么多,终于可以回答上面的那个问题了:那么这种设计是否容易实现呢?答案是:很简单。我们仅仅需要用这则处理3类字符串就可以了: 转义字符、颜色开始tag,颜色结束tag。处理策略也很简单:

  • 遇到由斜线开始的转义字符, 直接返回斜线后面的字符
  • 遇到开始Tag, 查看是否为支持的颜色,若不支持,不做处理原样返回;若支持,返回tag对应到 颜色转义字符,并将该颜色转义字符压栈。
  • 遇到结束Tag, 查看是否是支持的颜色,若不支持,不做处理原样返回;若支持,弹栈并返回栈顶颜色对应的转义字符,若栈为空,则设置为系统默认颜色。
  • 其他情况一律不做处理,原样返回(这一步主要是预防自己没有预料的一些匹配出现,这个例子中应该用不到,为了保险起见还是留着吧)。
  • 为了防止用户标签没有闭合而影响其他控制台输出,在最后预防行的设置颜色为默认颜色。

这部分的逻辑已经封并发布到了npm的rich-console模块,下面为具体的实现代码和demo运行结果截图。顺便说一下ANSI中支持的转义内容还很多比如设置背景色、设置加粗、下划线等,但这些支持的并不好,未能动物所下很多都不支持,再加上这些功能更用的比较少,因此这些功能被有意忽略了。
demo效果

/**
 * 获得带颜色转义字符的控制台输出模板.
 * @param  {String}tmpl        包含标签的模板字符串
 * @param  {boolean}isBright   是否高亮,default false
 * @return {String}
 * @public
 */
function getRichTmpl(tmpl, isBright){
    if(typeof tmpl == ‘object‘){ return tmpl; }

    var fontStyle = isBright == true ? ‘\u001b[1m‘ : ‘‘;
    var ESCAPES  = {
        black  : (fontStyle + ‘\u001b[30m‘),
        red    : (fontStyle + ‘\u001b[31m‘),
        green  : (fontStyle + ‘\u001b[32m‘),
        yellow : (fontStyle + ‘\u001b[33m‘),
        orange : (fontStyle + ‘\u001b[33m‘),
        blue   : (fontStyle + ‘\u001b[34m‘),
        pink   : (fontStyle + ‘\u001b[35m‘),
        cyan   : (fontStyle + ‘\u001b[36m‘),
        white  : (fontStyle + ‘\u001b[37m‘),
        noColor: ‘\u001b[0m‘
    }    

    var NO_COLOR = ESCAPES.noColor;
    var styleStack = [];
    var reg = new RegExp((
         ‘(\\\\.)‘     // 由\表示的转义字符
       + ‘|<(\\w+)>‘   // 样式开始标签
       + ‘|</(\\w+)>‘  // 样式结束标签
    ), ‘g‘);

    var handleTag = function(str){
        return str.replace(reg, function(m, $1, $2, $3){
            // 若是转义字符之间返回\后面的字符
            if ($1) { return $1.slice(1); }

            // 若为不支持的颜色直接忽略,否则返回样式开始字符并将样式压栈
            if ($2) { 
                var style = ESCAPES[$2];
                if(style){
                    styleStack.push(style);
                    return style;
                }else{
                    return m;
                }
            }              

            // 若为不支持的颜色直接忽略,否则从样式栈中弹出当前样式并返回
            // 栈顶样式,若栈为空返回系统默认样式
            if ($3) {
                if(ESCAPES[$3]){
                    styleStack.pop();
                    var len = styleStack.length;
                    var topStyle = len > 0 ? styleStack[len - 1] : null;
                    return (topStyle ? topStyle : NO_COLOR);
                }else{
                    return m;
                }
            }

            // others 
            return m;
        }) + NO_COLOR; // 最末尾的两个重置用来防止用户标签不闭合进而污染整个控制台输出
    };

    return handleTag(tmpl);
}

/**
 * 向控制台输出彩色文字.
 * @param {String}cont
 * @example
 *   showColorText(
 *      ‘<red>%s <green>%s</green>! </red>‘, 
 *      ‘hello‘, 
 *      ‘wold‘
 *   );
 * @public
 */
function output(cont){
    // 若用户输入的是一个object则调用系统的console输出object结构
    if(typeof cont == ‘object‘){
        console.log(cont);
        return;
    }

    var moreArgs = [].slice.call(arguments, 1);
    moreArgs.unshift(getRichTmpl(cont));
    console.log.apply(console, moreArgs);
}

/**
 * 以红色文字向控制台输出错误信息.
 * @param  {String|Object}cont
 * @param  {Object...}
 * @public
 */
function outputError(cont){
    if(typeof cont == ‘object‘){
        console.log(cont);
    }else{
        var moreArgs = [].slice.call(arguments, 1);
        moreArgs.unshift(‘<red>‘ + cont + ‘</red>‘);
        output.apply(null, moreArgs);
    }
}

module.exports = {
    getRichTmpl: getRichTmpl,
    error: outputError,
    log: output
}