Захотелось мне тут раскрасить вывод консольных скриптов. Поиск показал, что все придумано до нас, работало еще в DOS'е и называется "Escape-последовательности".

Для подобной раскраски есть интересная библиотечка kevinlebrun/colors.php, но мне она не подошла - хотя бы потому, что она не позволяет без заметных костылей раскрашивать части выводимых сообщений.

И я решил написать сам.

Началось все с функции вывода статуса. Вызывалась она в скриптах, выводящих отчет в браузер и выглядела так:

echo_status_cli("In my basket I have <font color='red'>five tomatoes</font>.
Also I have <font color='yellow'>10 apples</font> 
and <font color='green'>another green apple</font>");

И тут я решил вывод раскрасить. Как?

Очевидно, нужно выделить из строки соответствующие теги, взять у них атрибут color, взять содержимое тега и содержимое раскрасить Escape-последовательностями.

С помощью https://regex101.com/ составил нужную регулярку:

#\<font[\s]+color=[\\\'\"]([\D]+)[\\\'\"]\>(.*)\<\/font\>#U

Намучавшись с функцией preg_replace() решил клеить строчку сам.

function echo_status_cli($message = "", $breakline = TRUE)
{
    static $fgcolors = array(...);
    $pattern = '#\<font[\s]+color=[\\\'\"]([\D]+)[\\\'\"]\>(.*)\<\/font\>#U';
    preg_match_all($pattern, $message, $matches);

    $colors = $matches[1];
    $messages = $matches[0];

    $msgs = array_map( function($i) use ($fgcolors, $colors, $messages) {
        $c_index = isset( $fgcolors[ $colors[$i] ]) ? $colors[$i] : 'white';
        $c = $fgcolors[ $c_index ];
        $msg = strip_tags( $messages[ $i ]);
        return "\033[{$c}m{$msg}\033[0m";
    }, array_keys($messages));

    $message = (count($msgs) > 1) ? implode(' ', $msgs) : $msgs[0];
    if ($breakline === TRUE) $message .= PHP_EOL;
    echo $message;
}
//(значение массива $fgcolors опущено для сокращения кода)//

Что мы тут делаем?
Во-первых разбиваем полученную строку по регулярному выражению. В $colors и $messages попадает содержимое соответствующих карманов (в $colors - цвета, в $messages - вся строка с тегом).
Во-вторых, к набору строк $messages мы применяем array_map() с callback-функцией.

Немного хитрой магии:

... use ($fgcolors, $colors, $messages)

Это совершенно не описанная в документации (по крайней мере в документации к array_map() ) штуковина. Эта конструкция передает внутрь замыкания (анонимной функции) переменные, перечисленные в скобках. В данном случае - массив ESCAPE-значений цветов, значения цветов из карманов регулярки и массив строк.

Зачем же мы используем третьим параметром array_map() именно array_keys($messages)?

Если мы передадим анонимной функции просто массив $messages - она проитерирует значение каждого элемента массива, а получить доступ к ключам не получится. В интернете предлагают использовать array_filter() , но с PHP 5.4.? в замыкание нельзя передавать значение по ссылке (Fatal error: Call-time pass-by-reference has been removed). В общем, все плохо :(

Если же мы передаем array_keys($message) - array_map() передает в анонимную функцию индекс (0...n) , по которому мы извлекаем как цвет, так и саму строчку.

Казалось бы, задача решена? Нет. Мы прекрасно раскрашиваем строки, потом их склеиваем функцией implode()... и видим, что всё, что было между тегами

</font>. Also I have <font 

рассосалось.

Дальше я еще раз сломал моск на функции preg_replace() :-(

А потом я наткнулся на Skillz: Регулярные выражения для чайников с очень подробным рассказом о регулярках... и в сааааааааааааааааааамом конце коротенькое упоминание о preg_replace_callback

И я решил попробовать снова (заодно использовал именование карманов). Код оказался элегантным и простым:

    
    $pattern = '#(?<Full>\<font[\s]+color=[\\\'\"](?<Color>[\D]+)[\\\'\"]\>(?<Content>.*)\<\/font\>)#U';
    $message = preg_replace_callback($pattern, function($matches) use ($fgcolors){
        $color = $matches['Color'];
        $color = isset( $fgcolors[ $color ]) ? $fgcolors[ $color ] : $fgcolors[ 'white' ];
        $message = $matches['Content'];
        return "\033[{$color}m{$message}\033[0m";
    }, $message);
    $message = strip_tags( $message);
    ....

Мы анализируем строку $messages (3 аргумент) при помощи паттерна $pattern (1 аргумент). И совпадения передаем в callback-функцию, которая возвращает нам новое значение (которым preg_replace_callback() и заменяет найденное.


Итак, результирующий код: Gist: KarelWintersky/echo_status_cli.php

function echo_status_cli($message = "", $breakline = TRUE)
{
    static $fgcolors = array(
        'black' => '0;30',
        'dark gray' => '1;30',
        'blue' => '0;34',
        'light blue' => '1;34',
        'green' => '0;32',
        'light green' => '1;32',
        'cyan' => '0;36',
        'light cyan' => '1;36',
        'red' => '0;31',
        'light red' => '1;31',
        'purple' => '0;35',
        'light purple' => '1;35',
        'brown' => '0;33',
        'yellow' => '1;33',
        'light gray' => '0;37',
        'white' => '1;37');

    $pattern = '#(?<Full>\<font[\s]+color=[\\\'\"](?<Color>[\D]+)[\\\'\"]\>(?<Content>.*)\<\/font\>)#U';
    $message = strip_tags(preg_replace_callback($pattern, function($matches) use ($fgcolors){
        $color = isset( $fgcolors[ $matches['Color'] ]) ? $fgcolors[ $matches['Color'] ] : $fgcolors[ 'white' ];
        return "\033[{$color}m{$matches['Content']}\033[0m";
    }, $message) );
    if ($breakline === TRUE) $message .= PHP_EOL;
    echo $message;
}

**Опыт:** - Передача в замыкание нескольких значений извне для использования внутри - preg_replace_callback()

P.S. Позже этот код "уехал" в недра микрофреймворка μArris и вызывается CLIConsole::say()