среда, 6 июля 2016 г.

Conky: мои настройки отображения погоды (yahooapis и jq) и музыки (cmus)

Оригинальные настройки conky я взял отсюда. В этот пакет, кроме собственно конфигураций окон, входят шрифты для отображения простого текста и погодных символов. К сожалению, в мае этого года окно weather_date перестало показывать погоду. Как выяснилось, это окно использует yahooapis, а Yahoo изменил свой API как раз в это время. В общем, пришлось переписать это окно для новых yahooapis, а заодно улучшить производительность за счет уменьшения вызовов процессов-фильтров, познакомиться с замечательным парсером JSON jq и перевести настройки в новый формат с помощью скрипта convert.lua, который поставляется вместе с обновленным conky. На следующей картинке показано, как выглядит окно weather_date на моих рабочем и домашнем компьютерах.
Запускать conky с окном weather_date нужно так:
conky -c ~/.grayscale/conkyrc/weather_date
А это сам скрипт weather_date.
conky.config = {
--###############
--###############PERFORMANCE_SETTINGS
--###############
    update_interval = 5,
    total_run_times = 0,
    net_avg_samples = 2,
    imlib_cache_size = 0,
    double_buffer = true,
    no_buffers = true,

--###############
--###############TEXT_SETTINGS
--###############
    use_xft = true,
    font = 'GE Inspira:bold:pixelsize=12',
    xftalpha = 0.1,
    override_utf8_locale = true,
    text_buffer_size = 512,

--###############
--###############WINDOW_SPECIFICATIONS
--###############
    background = true,
    own_window = true,
    own_window_transparent = true,
    own_window_type = 'normal',
    own_window_class = 'conky-semi',
    own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
    own_window_argb_visual = true,
    own_window_argb_value = 0,
    draw_outline = false,
--# Window border
    draw_borders = false,
    pad_percents = 0,
    border_inner_margin = 4,
    top_name_width = 10,
    use_spacer = 'right',
--#Size and position
    alignment = 'top_left',
    gap_x = 1600,
    gap_y = 58,
    minimum_width = 0, minimum_height = 0,
    maximum_width = 240,

--###############
--###############GRAPHICS_SETTINGS
--###############
    draw_shades = false,
    default_shade_color = '#292421',
    short_units = true,
--#Default Colors
    default_color = '#efefef',
    default_shade_color = '#1d1d1d',
--#Color Title
    color1 = '#bcbcbc',
    color2 = '#00d787',
    color3 = '#00d787',
};

conky.text = [[
#################
#################DATE & TIME
#################
${voffset 10}${font GE Inspira:pixelsize=50}${color1}${time %H:%M}\
${voffset -20}${offset 5}${font GE Inspira:pixelsize=25}${color2}${time %d}\
${voffset -15}${font GE Inspira:pixelsize=20}${color1}${time  %b}${time %Y}\
${voffset 35}${offset -105}${font GE Inspira:pixelsize=22}${color1}${time %A}\
${color}${font}
${voffset 5}${color3}${hr 2}${color}\

################
################DOWNLOADING WEATHER INFO AND SAVING IT AS ~/.cache/weather.json
################
${execi 1800 nm-online -t 60 && curl -s \
    -G 'http://query.yahooapis.com/v1/public/yql?format=json' \
    --data-urlencode 'q=select * from weather.forecast where woeid in \
    (select woeid from geo.places(1) where text="Saint-Petersburg, Russia") \
    and u="c"' -o ~/.cache/weather.json}
################MAIN WEATHER IMAGE
${voffset -10}${offset 20}${font conkyweather:size=140}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.condition.code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${color}${font}
################WEATHER CONDITIONS
${alignc 10}${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.condition.text' \
    ~/.cache/weather.json}${color}${font}
##############EXTRACTING CURRENT/HIGH TEMP IN DEGREE CELSIUS
${offset 30}${font GE Inspira:pixelsize=50}${color3}\
${execi 1800 jq -r '.query.results.channel.item.condition.temp' \
    ~/.cache/weather.json}°C/${font GE Inspira:pixelsize=30}\
${color3}${execi 1800 jq -r '.query.results.channel.item.forecast[0].high' \
    ~/.cache/weather.json}°C\
${color}${font}\

#################
#################EXTRACTING LOCATION
#################
${voffset 16}${offset 16}${font GE Inspira:bold:pixelsize=20}${color1}\
${execi 1800 jq -j '.query.results.channel.location | .city + ", ", .country' \
    ~/.cache/weather.json}${color}${font}
${color3}${hr 2}${color}\

#################
#################EXTRACTING WEATHER INFO
#################
##PRESSURE     HUMIDITY
${voffset 5}${font GE Inspira:bold:pixelsize=12}${color2}\
Pressure : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.pressure' \
    ~/.cache/weather.json}mb\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Humidity : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.humidity' \
    ~/.cache/weather.json}%\
${color}${font}
##SUNRISE     SUNSET
${font GE Inspira:bold:pixelsize=12}${color2}\
Sunrise : ${color1}\
${execi 1800 jq -r '.query.results.channel.astronomy.sunrise' \
    ~/.cache/weather.json}\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Sunset : ${color1}\
${execi 1800 jq -r '.query.results.channel.astronomy.sunset' \
    ~/.cache/weather.json}${color}${font}
##WIND     VISIBILITY
${font GE Inspira:bold:pixelsize=12}${color2}\
Wind : ${color1}${execi 1800 jq -r '.query.results.channel.wind.speed' \
    ~/.cache/weather.json}km/hr\
${alignr -16}${font GE Inspira:bold:pixelsize=12}${color2}\
Visibility : ${color1}\
${execi 1800 jq -r '.query.results.channel.atmosphere.visibility' \
    ~/.cache/weather.json}km${color}${font}
${color3}${hr 2}${color}\

#################
#################WEATHER FORECAST IMAGES FOR NEXT 2 DAYS
#################
${voffset 10}${font conkyweather:size=70}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.forecast[1].code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${tab 72}${font conkyweather:size=70}${color1}\
${execi 1800 grep "^$(jq -r '.query.results.channel.item.forecast[2].code' \
    ~/.cache/weather.json) =" ~/.grayscale/data/compare | cut -d " " -f3}\
${color}${font}\

#################
#################EXTRACTING LOW/HIGH TEMP IN DEGREE CELSIUS FOR NEXT 2 DAYS
#################
${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].day' \
    ~/.cache/weather.json} : ${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].low' \
    ~/.cache/weather.json}°/${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[1].high' \
    ~/.cache/weather.json}°\
${alignr -16}${font GE Inspira:bold:pixelsize=15}${color2}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].day' \
    ~/.cache/weather.json} : ${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].low' \
    ~/.cache/weather.json}°/${font GE Inspira:bold:pixelsize=15}${color1}\
${execi 1800 jq -r '.query.results.channel.item.forecast[2].high' \
    ~/.cache/weather.json}°${color}${font}
${voffset 5}${color3}${hr 2}${color}\

#################
#################CALENDER DISPLAY
#################
${voffset 10}${font nimbus mono L:bold:size=12}${color1}\
${execpi 1800 DJS=`date +%_d`; cal | \
    sed -e s/"\(^\|[^0-9]\)$DJS"'\b'/'\1${color2}'"$DJS"'$color'/ -e s/^/'  '/}
]];
Внутри conky.config находятся настройки геометрии окна, объявляются различные цвета и т.п. Собственно содержимое окна описано внутри conky.text. В параграфе, озаглавленном как DOWNLOADING WEATHER …, каждые полчаса (1800 секунд) с помощью execi и curl, после предварительной проверки доступности сети командой nm-online, выполняется запрос на query.yahooapis.com в формате YQL (Yahoo! Query Language). Ответ от Yahoo сохраняется в файле ~/.cache/weather.json. Обратите внимание на то, что данная команда execi ничего не выводит на экран, то есть фактически в данном случае conky работает как cron! Я выбрал формат вывода JSON потому, что он легко и непринужденно парсится прямо из командной строки командой jq, которая позволяет задавать довольно сложные фильтры для поиска данных, предоставляя своеобразный язык запросов, который очень подробно, с примерами, описан в man jq. Все оставшиеся параграфы внутри conky.text вплоть до вывода календаря вызывают jq для поиска значений определенных погодных категорий внутри файла ~/.cache/weather.json. Например, внутри параграфа EXTRACTING LOCATION выполняется запрос jq -j '.query.results.channel.location | .city + ", ", .country' ~/.cache/weather.json, который выводит значения city и country, находящиеся в пути /query/results/channel/location структуры JSON и выводит их в одной строке (опция -j). Поскольку jq работает из командной строки, имеется возможность выводить погодные условия прямо на терминал! Например, погоду на завтра можно вывести командой
jq '.query.results.channel.item.forecast[1]' ~/.cache/weather.json
{
  "code": "12",
  "date": "07 Jul 2016",
  "day": "Thu",
  "high": "17",
  "low": "12",
  "text": "Rain"
}
При этом вывод в терминале будет синтаксически подсвечен! А теперь о выводе информации из cmus. Cmus — это аудиоплеер для терминала. В оригинальном пакете была представлена поддержка плеера clementine, но я им не пользуюсь. Информация из clementine выводилась в окне net_hdd с помощью execpi 2 ~/.grayscale/data/clementine, скрипт clementine в директории ~/.grayscale/data/ был написан на perl. Сначала я написал аналогичный скрипт cmus, тоже на perl, но в новом conky большое количество проблем заставило переписать его на lua и загружать в conky командой lua_load. Вот так выглядит информация из cmus, когда он запущен.
Для того, чтобы это заработало, в net_hdd, предварительно обработанном скриптом convert.lua, внутрь conky.config нужно добавить строку
        lua_load = '~/.grayscale/data/cmus.lua',
Внутрь conky.text, вместо настроек для clementine, нужно добавить строки
################
################CMUS_DISPLAY
################
${voffset 5}${offset 22}\
${font GE Inspira:bold:pixelsize=15}${color2}CMUS${voffset 2}\
${offset 5}${color3}${hr 2}${color}${font}
${if_running cmus}${lua_parse cmus}${else}${voffset 46}${endif}
Обратите внимание, что в случае, когда cmus не запущен, в окно net_hdd выводится пустой вертикальный сдвиг высотой 46 пикселей: это сделано для того, чтобы окно не мерцало при запуске и закрытии cmus. Величина сдвига была определена экспериментально: она должна точно соответствовать высоте совокупной информации из cmus, когда он запущен, выводимой conky. Кроме того, стоит отметить, что lua_parse, в отличие от execpi, не имеет собственной настройки интервала обновления данных, а следовательно скорость обновления данных определяется системной настройкой окна update_interval внутри conky.config. Скрипт cmus.lua в директории ~/.grayscale/data/ выглядит так.
function conky_echo(a)
    return a
end

function conky_cmus()
    local artist   = 'N/A';
    local title    = 'N/A';
    local album    = 'N/A';
    local progress = 0;
    local pos      = 0;
    local length   = 0;
    local status   = '';
    
    local color1   = 'bcbcbc';
    local color2   = 'ffa300';
    local color3   = 'ffff5f';
    
    f = assert( io.popen( 'cmus-remote -Q' ) ) or os.exit( 1 )
     
    for line in f:lines() do
        local v = string.match( line, '^status%s*(.*)' )
        if v ~= nil and v ~= '' and v ~= 'playing' then
            status = ' [' .. v .. ']'; goto next
        end
        v = string.match( line, '^duration%s*(.*)' )
        if v ~= nil and v ~= '' then length = tonumber( v ); goto next end
        v = string.match( line, '^position%s*(.*)' )
        if v ~= nil and v ~= '' then pos = tonumber( v ); goto next end
        v = string.match( line, '^tag album%s*(.*)' )
        if v ~= nil and v ~= '' then album = v; goto next end
        v = string.match( line, '^tag artist%s*(.*)' )
        if v ~= nil and v ~= '' then artist = v; goto next end
        v = string.match( line, '^tag title%s*(.*)' )
        if v ~= nil and v ~= '' then title = v; goto next end
        ::next::
    end
      
    f:close()
    
    if pos > 0 and length > 0 then
        progress = math.floor( 100 * pos / length )
    end
    
    return '${voffset 5}${offset 6}${font StyleBats:size=10}${color ' ..
            color1 .. '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Title: ${color ' .. color3 .. '}${alignr}' .. title ..
            '${color ' .. color2 .. '}' .. status ..
            '\n${offset 6}${font StyleBats:size=10}${color ' .. color1 ..
            '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Artist: ${color ' .. color3 .. '}${alignr}' .. artist ..
            '\n${offset 6}${font StyleBats:size=10}${color ' .. color1 ..
            '}k${voffset -2}${offset 3}${color ' .. color2 ..
            '}${font}Album: ${color ' .. color3 .. '}${alignr}' .. album ..
            '${color ' .. color1 .. '}${font}' ..
            '\n${voffset 1}${offset 8}${lua_bar echo ' .. progress .. '}'
end
Прошу меня простить, если что-то здесь сделано неизящно или неэффективно — это моя первая программа на lua :)

пятница, 1 июля 2016 г.

Проприетарные видеодрайверы Nvidia в репозиториях Fedora от negativo17

Очередной апгрейд Fedora (до версии 24) в очередной раз навернул графику на моем стареньком десктопе с Nvidia GeForce 8. На этот раз проблема оказалась в отсутствии нужных драйверов на RPM Fusion (их там нет до сих пор). Моей старой видеокарте нужен специальный драйвер серии 340xx. Поиск этой серии на Russian Fedora тоже не увенчался успехом. Перспектива собирать драйвер самому мне не улыбалась: нет никакого удовольствия делать это каждый раз при очередном обновлении ядра. В общем, лень — двигатель прогресса — привела меня на замечательный ресурс negativo17.org, в котором на странице repositories находится список репозиториев Fedora с разными несвободными приложениями типа Skype и Rar, в том числе и с проприетарными драйверами Nvidia. Инструкции по установке драйверов и прочие подробности изложены на этой странице. Поскольку мне был нужен драйвер серии 340xx, для установки репозитория я использовал команду
dnf config-manager --add-repo=http://negativo17.org/repos/fedora-nvidia-340.repo
Остальное всё по инструкции (почти).
dnf remove \*nvidia\*
dnf install nvidia-driver akmod-nvidia
akmods --force
И перезагрузка. Важно отметить, что автор репозиториев является активным мейнтейнером Fedora (см. здесь), поэтому их использование не должно создавать риск нарушения безопасности.

воскресенье, 31 января 2016 г.

vim-xkbswitch: управление keymap в нормальном режиме и в строках поиска

На днях добавил указанную фичу в vim-xkbswitch. Суть проста. При выходе из режима ввода (Insert mode) в нормальный режим (Normal mode) хотелось бы, чтобы после ввода команд r и f, а также в режиме поиска в командной строке (команды / и ?), системная раскладка переключалась на ту, что была в режиме ввода. К сожалению, реализовать такое с нуля технически весьма затруднительно. К счастью, можно воспользоваться встроенной в vim поддержкой keymap, и более того, управлять ею с помощью настройки iminsert и imsearch при выходе в нормальный режим. В общем, если есть желание управлять keymap из vim-xkbswitch, то в файл .vimrc следует добавить такие строки.
let g:XkbSwitchAssistNKeymap = 1    " for commands r and f
let g:XkbSwitchAssistSKeymap = 1    " for search lines
set keymap=russian-jcukenwin
set iminsert=0
set imsearch=0
Значение keymap в данном случае зависит от дополнительной раскладки, с которой вы работаете. Если по какой-то причине имя системной раскладки (возвращается бэкенд-библиотекой, например xkb-switch) не совпадает с именем keymap (которое обычно присваивается переменной b:keymap_name в файле keymap), то можно настроить отображение из первого во второе, например
let g:XkbSwitchKeymapNames = {'ru' : 'ru_keymap'}
Здесь ru — имя системной раскладки, а ru_keymap — предполагаемое значение b:keymap_name. Если вы работаете с несколькими раскладками одновременно (например, стандартной для нормального режима американской, русской и украинской), то возможно при выходе из режима ввода вы захотите, чтобы keymap указывал на ту из трех раскладок, которая была использована последней. Для этого вам понадобится динамическая настройка keymap, которую можно задействовать с помощью следующих настроек в .vimrc (вместо тех, что были приведены выше).
let g:XkbSwitchAssistNKeymap = 1    " for commands r and f
let g:XkbSwitchAssistSKeymap = 1    " for search lines
let g:XkbSwitchDynamicKeymap = 1
let g:XkbSwitchKeymapNames = {'ru' : 'russian-jcukenwin',
            \ 'uk' : 'ukrainian-jcuken'}
Обратите внимание, здесь не требуется настройка keymap по умолчанию (хотя и не возбраняется), а в качестве значений в словаре g:XkbSwitchKeymapNames используются имена keymap, а не значения b:keymap_name. Если в нормальном режиме вы захотите переключиться на стандартную раскладку нормального режима, то просто временно зайдите в режим ввода и в нем переключитесь на соответствующую раскладку (это можно также сделать командой :setlocal iminsert=0). В строке поиска то же самое можно сделать сочетанием клавиш Ctrl-^. Все это хорошо, но есть одна серьезная проблема. Системный индикатор раскладки будет по-прежнему показывать стандартную в нормальном режиме раскладку, ведь vim-xkbswitch здесь не действует, а keymap не влияет на системную раскладку. На помощь приходит статусная строка, которую можно легко настроить для отображения значения keymap, когда iminsert равен 1, в плагине Powerline. Индикатор keymap будет отображать активную keymap раскладку для команд r и f и строк поиска, в случае, если iminsert равен 1, в то время как индикатор системной раскладки будет отображать стандартную для нормального режима раскладку. В режиме ввода индикатор keymap не действует, поскольку vim-xkbswitch отключает keymap установкой iminsert в 0. Такое решение, кстати, позволяет пользователю видеть, какая раскладка станет активной при переходе из нормального режима в режим ввода, а это здо́рово, на мой взгляд! Я покажу, как настроить индикатор keymap в старом Powerline, в новейших репликах типа vim-airline действия должны быть похожи. Итак, в файле autoload/Powerline/Segments.vim в массиве, передаваемом в функцию Pl#Segment#Init() перед строкой, содержащей слово fileformat, вставляем строки
    \ Pl#Segment#Create('keymap_name'     ,
        \'%{&iminsert && exists("b:keymap_name") ? b:keymap_name : ""}',
        \ Pl#Segment#Modes('!N')),
, в файле autoload/Powerline/Themes/default.vim (или в другом файле темы Powerline, в зависимости от того, какую тему вы используете) в список, передаваемый в функцию Pl#Theme#Buffer(), перед строкой, содержащей слово fileformat, вставляем строку
        \ , 'keymap_name'
, и, наконец, в файле autoload/Powerline/Colorschemes/default.vim (или в другом файле цветовой схемы Powerline, если вы используете нестандартную схему), в произвольном месте списка, передаваемого в функцию Pl#Colorscheme#Init(), вставляем строки
    \ Pl#Hi#Segments(['keymap_name'], {
        \ 'n': ['brightestorange', 'gray2'],
        \ 'i': ['brightestorange', 'darkestblue'],
        \ }),
    \
После этого выполняем команду
:PowerlineClearCache
, перезагружаем vim и наслаждаемся индикацией keymap. Напоминаю, что стандартная в нормальном режиме раскладка отображаться не будет. Вот так может выглядеть статусная строка Powerline с индикатором keymap (обратите внимание на оранжевые буквы ru).

воскресенье, 27 декабря 2015 г.

nginx module to enable haskell binding to nginx configuration files

Do you like haskell and nginx? I love them both and this inspired me to write an nginx module for inlining haskell source code straight into nginx configuration files. The module was published on github as nginx-haskell-module and shipped with an nginx configuration example to show its basic usage. Let’s look at it.
user                    nobody;
worker_processes        2;

events {
    worker_connections  1024;
}

http {
    default_type        application/octet-stream;
    sendfile            on;

    haskell ghc_extra_flags '-hide-package regex-pcre';

    haskell compile /tmp/ngx_haskell.hs '

import qualified Data.Char as C
import           Text.Regex.PCRE
import           Safe

toUpper = map C.toUpper
NGX_EXPORT_S_S (toUpper)

takeN x y = take (readDef 0 y) x
NGX_EXPORT_S_SS (takeN)

NGX_EXPORT_S_S (reverse)

matches :: String -> String -> Bool
matches a b = not $ null (getAllTextMatches $ a =~ b :: [String])
NGX_EXPORT_B_SS (matches)

    ';

    server {
        listen       8010;
        server_name  main;
        error_log    /tmp/nginx-test-haskell-error.log;
        access_log   /tmp/nginx-test-haskell-access.log;

        location / {
            haskell_run toUpper $hs_a $arg_a;
            echo "toUpper ($arg_a) = $hs_a";
            if ($arg_b) {
                haskell_run takeN $hs_a $arg_a $arg_b;
                echo "takeN ($arg_a, $arg_b) = $hs_a";
                break;
            }
            if ($arg_c) {
                haskell_run reverse $hs_a $arg_c;
                echo "reverse ($arg_c) = $hs_a";
                break;
            }
            if ($arg_d) {
                haskell_run matches $hs_a $arg_d $arg_a;
                echo "matches ($arg_d, $arg_a) = $hs_a";
                break;
            }
        }
    }
}
Haskell source code is placed inside the second argument of the directive haskell compile. In this example it contains some imports, three definitions of functions and four special export directives to introduce the functions on the nginx configuration level. There are four types of export directives: NGX_EXPORT_S_S, NGX_EXPORT_S_SS, NGX_EXPORT_B_S and NGX_EXPORT_B_SS for functions of types String -> String, String -> String -> String, String -> Bool and String -> String -> Bool respectively. The code gets written into the path specified in the first argument of the directive haskell compile (it must be an absolute path) and then compiled to a shared library at the very start of nginx. Sometimes ghc may require extra options besides defaults. And here is the case. As soon as import of Text.Regex.PCRE can be ambiguous (because two haskell packages regex-pcre and regex-pcre-builtin provide it), ghc must know which package to use. There is a special ghc flag -hide-package for hiding unwanted packages and it was used in this example by the directive haskell ghc_extra_flags. There is another nginx haskell directive haskell load which is similar to the haskell compile except it does not require the second argument (i.e. the haskell source code). The directive tries to load compiled shared library that corresponds to the path specified in its first argument (/tmp/ngx_haskell.so in this example). If the code argument is present but there is not compiled shared library, the latter will be first compiled and then loaded. If the haskell code fails to compile then nginx won’t start (or won’t reload workers if the code had been wrongly changed in the configuration file and nginx has been sent the SIGHUP signal). Any errors will be logged. To run the compiled haskell code in the nginx context there is another nginx directive haskell_run. It is allowed in server, location and location-if configuration clauses and may accept three or four arguments depending on the arity of the exported function to run which is specified in the first argument of the directive. The second argument introduces an nginx variable where return value of the haskell function will be saved. For example directive
                haskell_run takeN $hs_a $arg_a $arg_b;
introduces a new nginx variable $hs_a which will be calculated on demand as result of running an exported haskell function takeN with arguments $arg_a and $arg_b. Let’s do some curl tests. First of all nginx must be built and run. Besides the haskell module the build requires nginx echo module. It must be specified in options --add-module of nginx configure script.
./configure --add-module=<path-to-nginx-echo-module> --add-module=<path-to-nginx-haskell-module>
Placeholders <path-to-nginx-... are to be replaced with real paths to the modules. After running make we start the nginx daemon.
./objs/nginx -c /home/lyokha/devel/nginx-haskell-module/nginx.conf
[1 of 1] Compiling NgxHaskellUserRuntime ( /tmp/ngx_haskell.hs, /tmp/ngx_haskell.o )
Linking /tmp/ngx_haskell.so ...
Nginx option -c specifies location of the configuration file. The shared library /tmp/ngx_haskell.so was built upon start which was printed on the terminal. And now we are going to ask nginx server to do some haskell calculations!
curl 'http://localhost:8010/?a=hello_world'
toUpper (hello_world) = HELLO_WORLD
curl 'http://localhost:8010/?a=hello_world&b=4'
takeN (hello_world, 4) = hell
curl 'http://localhost:8010/?a=hello_world&b=oops'
takeN (hello_world, oops) = 
curl 'http://localhost:8010/?c=intelligence'
reverse (intelligence) = ecnegilletni
curl 'http://localhost:8010/?d=intelligence&a=^i'
matches (intelligence, ^i) = 1
curl 'http://localhost:8010/?d=intelligence&a=^I'
matches (intelligence, ^I) = 0
Hmm, I did not escape caret inside the URL in the matches examples: seems that curl allowed it, anyway the calculations were correct. Let’s do some changes in the haskell code inside the configuration file, say takeN will be
takeN x y = ("Incredible! " ++) $ take (readDef 0 y) x
, reload nginx configuration
pkill -HUP nginx
and do curl again.
curl 'http://localhost:8010/?a=hello_world&b=4'
takeN (hello_world, 4) = Incredible! hell
Nice, kind of runtime haskell reload! Now I want to explain some details of implementation and concerns about efficiency and exceptions. Haskell code gets compiled while reading nginx configuration file. It means that ghc must be accessible during nginx runtime (except when using haskell load with already compiled haskell library). Only one haskell compile (or haskell load) directive is allowed through the whole configuration. Directive haskell compile (or haskell load) must be placed before directives haskell_run and after directive haskell ghc_extra_flags (if any). Compiled haskell library gets loaded by dlopen() call when starting a worker process (i.e. haskell runtime is loaded for each nginx worker process separately). The haskell code itself is wrapped inside a special haskell FFI code (it can be found in /tmp/ngx_haskell.hs in case of the above example). The FFI tries its best to minimize unnecessary allocations when reading arguments that were passed from nginx, however for S_S and S_SS function types it does allocate a new string for return value which gets promptly copied to an nginx memory pool and freed. It was possible to avoid allocations for return values, but in this case ghc should have known about internal nginx string implementation, so I decided to sacrifice efficiency to avoid runtime dependency on nginx source code (again, this dependency would not have been necessary if this special FFI interface had been compiled in a separate module, but… probably I’ll do that in future). Another concern about efficiency is using in the exported haskell handlers haskell Strings which are simple lists of characters (which is assumed to be inefficient). On the other hand they are lazy (and this often means efficient). Ok, this is a traditional haskell trade-off matter… What about haskell exceptions? C code is unable to handle them and this is a bad news. However writing pure haskell code makes a strong guarantee about high level of exception safety (at least all IO exceptions must go). In the above example I used function readDef from module Safe instead of its partial counterpart read which increased safety level as well.

понедельник, 7 декабря 2015 г.

Шрифты infinality-ultimate для Fedora 23

Добрый человек собрал репозиторий с пропатченными пакетами для infinality. Спасибо ему большое! Некоторое время я вообще думал, что infinality мертв, оказывается нет: здесь находится активный репозиторий разработки, но без поддержки Fedora. Лично я установил только основные патчи, без шрифтов — так, как написано в секции Installation — на мой вкус этого вполне достаточно. До этого у меня стояли версии freetype и fontconfig от Russian Fedora и, в общем-то, я не жаловался на рендеринг, однако теперь я вижу разницу: шрифты стали четче, что-ли. Короче, чувствую, что стало лучше, но доказать не могу :) И сравнительные картинки приводить не стану: не сохранил оригинальный вид, да и в интернете такие сравнения найти несложно.

вторник, 1 декабря 2015 г.

Не такой уж простой модуль nginx для создания комбинированных апстримов

Когда-то давно я написал статью о простом модуле nginx для создания комбинированных апстримов. В ней шла речь о реализации простой директивы add_upstream на уровне блока upstream в конфигурации nginx, которая позволяет добавлять серверы из других апстримов: очень удобно, когда вам требуется собрать апстримы, скомбинированные из нескольких других апстримов, без копирования объявлений составляющих их серверов. На данный момент я больше не могу назвать этот модуль простым, поскольку кроме расширенной функциональности, в его реализации появились разнообразные механизмы nginx, такие как фильтрация заголовков и тела ответов, доступ к переменным и подзапросы (subrequests). Теперь модуль называется модулем комбинированных апстримов, он выложен на гитхабе и снабжен подробной документацией на английском языке. В этой статье я хочу перечислить все возможности данного модуля с примерами их использования.
  • Директива add_upstream в блоке upstream. Это то, с чего все начиналось.
    • Конфигурация
      events {
          worker_connections  1024;
      }
      
      http {
          upstream u1 {
              server localhost:8020;
          }
          upstream u2 {
              server localhost:8030;
          }
          upstream u3 {
              server localhost:8040;
          }
          upstream ucombined {
              add_upstream u1;
              add_upstream u2;
              add_upstream u3 backup;
          }
      
          server {
              listen       127.0.0.1:8010;
              server_name  main;
      
              location / {
                  proxy_pass http://ucombined;
              }
          }
          server {
              listen       127.0.0.1:8020;
              server_name  server1;
              location / {
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8030;
              server_name  server2;
              location / {
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8040;
              server_name  server3;
              location / {
                  echo "Passed to $server_name";
              }
          }
      }
      
    • Тест
      for i in `seq 10` ; do curl 'http://localhost:8010/' ; done
      Passed to server1
      Passed to server1
      Passed to server2
      Passed to server2
      Passed to server1
      Passed to server1
      Passed to server2
      Passed to server2
      Passed to server1
      Passed to server1
      
      Правильно. Если вам интересно, почему каждый сервер опрашивается по два раза подряд, то ответ таков. Мой системный файл /etc/hosts содержит следующие две строки.
      127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
      ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
      
      Это значит, что loopback интерфейс имеет два адреса — для IPv4 и IPv6 (по крайней мере в /etc/hosts, который nginx читает на старте). Для каждого адреса nginx создает отдельный элемент-сервер в списке round robin peers. Достаточно закомментировать вторую строку в /etc/hosts и перезапустить nginx, чтобы получить настоящий round robin цикл в этом тесте.
  • Директива combine_server_singlets в блоке upstream. Эта штука позволяет плодить апстримы в невероятных количествах :) Представьте, что у вас есть такой апстрим
        upstream u1 {
            server localhost:8020;
            server localhost:8030;
            server localhost:8040;
        }
    
    и вы хотите создать три следующих производных апстрима-синглета (не надо спрашивать зачем, у меня была такая задача и я точно знаю, что она имеет смысл).
        upstream u11 {
            server localhost:8020;
            server localhost:8030 backup;
            server localhost:8040 backup;
        }
        upstream u12 {
            server localhost:8020 backup;
            server localhost:8030;
            server localhost:8040 backup;
        }
        upstream u13 {
            server localhost:8020 backup;
            server localhost:8030 backup;
            server localhost:8040;
        }
    
    Не нужно их создавать вручную! Достаточно поместить новую директиву внутрь порождающего апстрима
        upstream u1 {
            server localhost:8020;
            server localhost:8030;
            server localhost:8040;
            combine_server_singlets;
        }
    
    и апстримы-синглеты будут созданы автоматически. Для тонкой настройки имен порожденных апстримов директива предоставляет два опциональных параметра: суффикс и разрядное выравнивание порядкового номера апстрима.
    • Конфигурация
      events {
          worker_connections  1024;
      }
      
      http {
          upstream u1 {
              server localhost:8020;
              server localhost:8030;
              server localhost:8040;
              combine_server_singlets;
              combine_server_singlets _tmp_ 2;
          }
      
          server {
              listen       127.0.0.1:8010;
              server_name  main;
      
              location /1 {
                  proxy_pass http://u11;
              }
              location /2 {
                  proxy_pass http://u1_tmp_02;
              }
              location /3 {
                  proxy_pass http://u1$cookie_rt;
              }
          }
          server {
              listen       127.0.0.1:8020;
              server_name  server1;
              location / {
                  add_header Set-Cookie "rt=1";
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8030;
              server_name  server2;
              location / {
                  add_header Set-Cookie "rt=2";
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       127.0.0.1:8040;
              server_name  server3;
              location / {
                  add_header Set-Cookie "rt=3";
                  echo "Passed to $server_name";
              }
          }
      }
      
    • Тест
      curl 'http://localhost:8010/1'
      Passed to server1
      curl 'http://localhost:8010/2'
      Passed to server2
      curl 'http://localhost:8010/3'
      Passed to server1
      curl -D- -b 'rt=2' 'http://localhost:8010/3'
      HTTP/1.1 200 OK
      Server: nginx/1.8.0
      Date: Tue, 01 Dec 2015 10:59:00 GMT
      Content-Type: text/plain
      Transfer-Encoding: chunked
      Connection: keep-alive
      Set-Cookie: rt=2
      
      Passed to server2
      curl -D- -b 'rt=3' 'http://localhost:8010/3'
      HTTP/1.1 200 OK
      Server: nginx/1.8.0
      Date: Tue, 01 Dec 2015 10:59:10 GMT
      Content-Type: text/plain
      Transfer-Encoding: chunked
      Connection: keep-alive
      Set-Cookie: rt=3
      
      Passed to server3
      
      Обмен кукой rt дает подсказку, где синглетные апстримы могут быть полезны.
  • Апстрэнды (upstrands). Это такие комбинированные апстримы, внутри которых составляющие их апстримы не теряют свою целостность и идентичность. Слово upstrand образовано из двух составляющих: upstream и strand и означает пучок или жилу апстримов. Я касался деталей реализации апстрэндов в этой статье на английском языке. В двух словах, апстрэнд представляет собой высокоуровневую структуру, которая может опрашивать составляющие ее апстримы по кругу (round robin) до тех пор, пока не найдет апстрим, удовлетворяющий заданному условию — код ответа апстрима (HTTP response) не должен входить в список, заданный директивой next_upstream_statuses. Технически апстрэнды являются блоками — такими же как и апстримы. Они точно так же задаются внутри секции http конфигурации nginx, но вместо серверов составляющими их компонентами являются обычные апстримы. Апстримы добавляются в апстрэнд с помощью директивы upstream. Если имя апстрима начинается с символа тильда, то оно рассматривается как регулярное выражение. Отдельные апстримы внутри апстрэнда могут быть помечены как бэкапные, также имеется возможность блэклистить апстримы на определенное время с помощью параметра blacklist_interval. Опрос нескольких апстримов внутри апстрэнда реализован с помощью механизма подзапросов (subrequests). Этот механизм запускается в результате доступа к встроенной переменной upstrand_NAME, где NAME соответствует имени существующего апстрэнда. Я предполагаю, что в основном апстрэнды будут применяться в директиве proxy_pass модуля nginx proxy, однако здесь нет искусственных ограничений: доступ к механизму запуска подзапросов через переменную позволяет использовать апстрэнды в любом пользовательском модуле. На случай, если имя апстрэнда заранее неизвестно (например, оно приходит в куке), предусмотрена директива dynamic_upstrand, которая записывает имя следующего апстрима предполагаемого апстрэнда в свой первый аргумент-переменную на основании оставшегося списка аргументов (имя апстрэнда будет соответствовать первому не пустому аргументу из этого списка). Директива доступна на уровнях конфигурации server, location и location-if. Апстрэнды предоставляют несколько статусных переменных, среди них upstrand_addr, upstrand_status, upstrand_cache_status, upstrand_connect_time, upstrand_header_time, upstrand_response_time, upstrand_response_length — все они соответствуют аналогичным переменным из модуля upstream, только хранят значения всех посещенных апстримов в рамках данного HTTP запроса — и upstrand_path, в которой записан хронологический порядок (путь) посещения апстримов в рамках данного запроса. Статусные переменные полезны для анализа работы апстрэндов в access логе. А теперь пример конфигурации и curl-тест.
    • Конфигурация
      events {
          worker_connections  1024;
      }
      
      http {
          upstream u01 {
              server localhost:8020;
          }
          upstream u02 {
              server localhost:8030;
          }
          upstream b01 {
              server localhost:8040;
          }
          upstream b02 {
              server localhost:8050;
          }
      
          upstrand us1 {
              upstream ~^u0 blacklist_interval=10s;
              upstream b01 backup;
              next_upstream_statuses 5xx;
          }
          upstrand us2 {
              upstream b02;
              next_upstream_statuses 5xx;
          }
      
          log_format  fmt '$remote_addr [$time_local]\n'
                          '>>> [path]          $upstrand_path\n'
                          '>>> [addr]          $upstrand_addr\n'
                          '>>> [response time] $upstrand_response_time\n'
                          '>>> [status]        $upstrand_status';
      
          server {
              listen       127.0.0.1:8010;
              server_name  main;
              error_log    /tmp/nginx-test-upstrand-error.log;
              access_log   /tmp/nginx-test-upstrand-access.log fmt;
      
              dynamic_upstrand $dus1 $arg_a us2;
      
              location /us {
                  proxy_pass http://$upstrand_us1;
              }
              location /dus {
                  dynamic_upstrand $dus2 $arg_b;
                  if ($arg_b) {
                      proxy_pass http://$dus2;
                      break;
                  }
                  proxy_pass http://$dus1;
              }
          }
          server {
              listen       8020;
              server_name  server1;
      
              location / {
                  echo "Passed to $server_name";
                  #return 503;
              }
          }
          server {
              listen       8030;
              server_name  server2;
      
              location / {
                  echo "Passed to $server_name";
                  #return 503;
              }
          }
          server {
              listen       8040;
              server_name  server3;
      
              location / {
                  echo "Passed to $server_name";
              }
          }
          server {
              listen       8050;
              server_name  server4;
      
              location / {
                  echo "Passed to $server_name";
              }
          }
      }
      
    • Тест
      for i in `seq 6` ; do curl 'http://localhost:8010/us' ; done
      Passed to server1
      Passed to server2
      Passed to server1
      Passed to server2
      Passed to server1
      Passed to server2
      
      В логах nginx мы увидим
      tail -f /tmp/nginx-test-upstrand-*
      ==> /tmp/nginx-test-upstrand-access.log <==
      
      ==> /tmp/nginx-test-upstrand-error.log <==
      
      ==> /tmp/nginx-test-upstrand-access.log <==
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u01
      >>> [addr]          (u01) 127.0.0.1:8020
      >>> [response time] (u01) 0.000
      >>> [status]        (u01) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u02
      >>> [addr]          (u02) 127.0.0.1:8030
      >>> [response time] (u02) 0.000
      >>> [status]        (u02) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u01
      >>> [addr]          (u01) 127.0.0.1:8020
      >>> [response time] (u01) 0.000
      >>> [status]        (u01) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u02
      >>> [addr]          (u02) 127.0.0.1:8030
      >>> [response time] (u02) 0.001
      >>> [status]        (u02) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u01
      >>> [addr]          (u01) 127.0.0.1:8020
      >>> [response time] (u01) 0.000
      >>> [status]        (u01) 200
      127.0.0.1 [01/Dec/2015:16:52:03 +0300]
      >>> [path]          u02
      >>> [addr]          (u02) 127.0.0.1:8030
      >>> [response time] (u02) 0.001
      >>> [status]        (u02) 200
      
      А теперь давайте закомментируем директивы echo и раскомментируем директивы return 503 в локейшнах двух первых бэкендов (server1 и server2), перезапустим nginx и протестируем снова.
      for i in `seq 6` ; do curl 'http://localhost:8010/us' ; done
      Passed to server3
      Passed to server3
      Passed to server3
      Passed to server3
      Passed to server3
      Passed to server3
      
      Логи nginx.
      ==> /tmp/nginx-test-upstrand-access.log <==
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          u01 -> u02 -> b01
      >>> [addr]          (u01) 127.0.0.1:8020 (u02) 127.0.0.1:8030 (b01) 127.0.0.1:8040
      >>> [response time] (u01) 0.001 (u02) 0.000 (b01) 0.000
      >>> [status]        (u01) 503 (u02) 503 (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      127.0.0.1 [01/Dec/2015:16:58:06 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      
      Ждем десять секунд — заблэклисченные апстримы должны разблэклиститься, и повторяем снова.
      for i in `seq 2` ; do curl 'http://localhost:8010/us' ; done
      Passed to server3
      Passed to server3
      
      Логи nginx.
      127.0.0.1 [01/Dec/2015:17:01:44 +0300]
      >>> [path]          u01 -> u02 -> b01
      >>> [addr]          (u01) 127.0.0.1:8020 (u02) 127.0.0.1:8030 (b01) 127.0.0.1:8040
      >>> [response time] (u01) 0.000 (u02) 0.000 (b01) 0.001
      >>> [status]        (u01) 503 (u02) 503 (b01) 200
      127.0.0.1 [01/Dec/2015:17:01:44 +0300]
      >>> [path]          b01
      >>> [addr]          (b01) 127.0.0.1:8040
      >>> [response time] (b01) 0.000
      >>> [status]        (b01) 200
      
      А теперь протестируем работу динамических апстрэндов (предварительно вернув оригинальные настройки локейшнов двух первых бэкендов).
      curl 'http://localhost:8010/dus?a=us1'
      Passed to server1
      curl 'http://localhost:8010/dus?a=us2'
      Passed to server4
      curl 'http://localhost:8010/dus?a=foo&b=us1'
      Passed to server2
      curl 'http://localhost:8010/dus'
      Passed to server4
      curl 'http://localhost:8010/dus?b=foo'
      <html>
      <head><title>500 Internal Server Error</title></head>
      <body bgcolor="white">
      <center><h1>500 Internal Server Error</h1></center>
      <hr><center>nginx/1.8.0</center>
      </body>
      </html>
      
      В первом запросе мы через аргумент a попали на один из апстримов апстрэнда us1 — им оказался апстрим u01, который содержит единственный сервер server1. Во втором запросе, тоже через аргумент a, мы попали на апстрэнд us2 — апстрим b02 — сервер server4. В третьем запросе мы задействовали новый динамический апстрим dus2 через аргумент b, который отправил нас на второй апстрим (round robin же) u02 апстрэнда us1 и сервер server2. В четвертом запросе мы не предоставили аргументов и сработал последний не пустой элемент динамического апстрэнда dus1us2 с его единственным апстримом b02 и единственным сервером server4. В последнем запросе я показал, что может произойти, если динамический апстрэнд вернет пустое значение. В данном случае значение dus2 оказалось пустым и директива proxy_pass, попытавшись выполнить проксирование на неверно сформированный адрес http://, вернула ошибку 500.
    Апстрэнды могут быть полезны, во-первых, для создания двумерных round robin циклов, когда вы знаете, что если некоторый сервер из определенного апстрима вернул неудовлетворительный ответ, то нет необходимости обращаться к другим серверам этого апстрима, а следует незамедлительно переходить к следующему апстриму — простой upstream round robin механизм не способен эмулировать такое поведение, поскольку серверы внутри апстрима не могут образовывать кластеры, и, во-вторых, для переноса части логики протокола уровня приложения с клиента на роутер. Например, если в логике приложения код ответа 204, присланный из некоторого апстрима, обозначает отсутствие данных и клиенту следует тут же проверить наличие данных в другом апстриме, то можно ограничить общение клиента с бэкендом всего одним запросом, перенеся опрос всех направлений-апстримов на плечи роутера, в котором все эти апстримы будут помещены в один апстрэнд. Такой подход полезен еще тем, что инкапсулирует знание топологии бэкендов внутри роутера, ведь клиентам это знание больше не нужно.

среда, 11 ноября 2015 г.

cgrep vs ack

Cgrep и ack — это две grep-подобные программы, приспособленные для поиска внутри исходных текстов на разных языках программирования. Ack написана на perl и существует довольно давно, в то время как cgrep — относительно новая программа, написанная на haskell и обладающая большим количеством экспериментальных возможностей. Я не буду подробно сравнивать всевозможные аспекты использования этих программ, но отмечу их наиболее яркие положительные качества и недостатки и в конце приведу мои собственные настройки для декорирования их вывода на терминал. Итак, начнем с положительных качеств ack.
  • Возможность настроить рекурсивный поиск только внутри исходников указанного языка программирования (или нескольких языков). Соответствие файла определенному языку программирования определяется несколькими способами: расширением имени файла, точным именем файла (например, Makefile для make) и соответствием имени файла или первой строки в файле заданному шаблону регулярного выражения. Настройки соответствий языков по умолчанию можно вывести с помощью опции ack --dump: список языков находится внутри опций --type-add. Собственно включение в поиск определенного языка задается синтетической опцией --язык, где язык соответствует одному из значений в выведенном списке, стоящих сразу после символа =. Например, чтобы включить поиск внутри исходников C++, нужно указать опцию --cpp.
  • Возможность гибкой настройки вывода контекста вокруг строк с найденными совпадениями: для этого имеются целых три опции -A, -B и -C.
  • Гибкая настройка формата вывода, включая цветовую подсветку найденных совпадений и возможность перенаправления во внешнюю программу или скрипт с опцией --pager.
  • Задание настроек по умолчанию в файле .ackrc в домашней директории и поддиректориях.
  • Поиск файлов по имени или регулярному выражению (опция -g). Это просто киллер-фича! Теперь от громоздкой и негибкой команды find . -name '*pattern*' можно отказаться. Или почти отказаться… Есть два тонких момента, даже три. Во-первых, имеются опции по умолчанию --ignore-file, которые можно вывести с помощью опции --dump: в них указано, какие файлы будут игнорироваться при поиске. В частности, туда должны входить файлы jpg, png и тому подобные. Чтобы отменить все настройки по умолчанию, в том числе настройки игнорирования файлов, нужно просто добавить опцию --ignore-ack-defaults. Но… Вряд ли после этого ack начнет находить jpg и png файлы! Потому что они бинарные! И это было во-вторых. Чтобы преодолеть это ограничение, нужно поступить так, как сказано здесь, то есть добавить еще две опции --type-set='all:match:.*' -k (см. также определение алиаса ackf в конце этой статьи). Ну а в-третьих, пустые директории, а также всякого рода потоки и другие не-файлы, искусственно привязанные к файловой системе (сокеты, FIFO и т.п.), ack будет игнорировать в любом случае.
Прежде чем перейти к плюсам cgrep, я отмечу ее серьезный недостаток по сравнению с ack. Это невозможность вывода контекста вокруг строк с найденными совпадениями. А теперь плюсы.
  • Скорость и возможность параллельного исполнения. Это все-таки haskell!
  • Поддержка разных типов поиска. Кроме дословного поиска и поиска совпадения с регулярным выражением (включая POSIX и PCRE), сюда входят префиксный и суффиксный поиски, а также поиск с учетом расстояния Левенштейна. Последний тип поиска позволяет находить похожие слова или слова с возможными ошибками.
  • Поддержка семантического поиска позволяет искать совпадения в коде, комментариях или строковых литералах отдельно (опции -c, -m и -l), ограничивать поиск специфическими типами идентификаторов, такими как ключевые слова, литералы, директивы препроцессора и операторы (наилучшим образом поддерживаются языки C и C++). Опция -S позволяет настроить семантический поиск с помощью специального семантического языка (простейшие примеры можно увидеть, запустив cgrep --help).
  • Как и в ack, можно настроить поиск только внутри исходников определенного языка (или языков). Список поддерживаемых языков и соответствующие им расширения имен файлов можно вывести с помощью опции --lang-maps. Чтобы включить в поиск определенный язык, нужно указать его в опции --lang, например, --lang=Cpp.
  • Как и в ack, можно гибко настроить формат вывода с помощью опции --format. Вывод подсветки задается, но сами цвета не настраиваются (в функции cgr, которую я приведу ниже, изменение цветов подсветки достигается с помощью обработки в sed).
  • Некоторые настройки по умолчанию можно записать в файле .cgreprc в домашней директории.
Нужно отметить, что поиск на основе регулярных выражений в cgrep отличается от того, как это реализовано в ack или в обычном grep. Если последние ищут совпадения в каждой отдельной строке ввода, то cgrep применяет шаблон ко всему входному тексту. Это может приводить к тонким семантическим отличиям для некоторых шаблонов. Например, класс символов \s включает переносы строк, а значит найденное совпадение после применения шаблона с \s может быть многострочным. Ситуация осложняется тем, что cgrep выведет только первую строку совпадения (а это уже баг, на мой взгляд), которая может не содержать значимой информации, тем самым дезориентируя пользователя. Поэтому я советую вместо \s использовать класс горизонтальных пробелов \h (правда, он доступен только в PCRE) — это будет гарантировать отсутствие переносов строк в найденном совпадении. Ниже я привожу картинку с выводом ack и cgrep на моем терминале, а также настройки и скрипты, которые позволяют добиться этого результата. Файл .ackrc.
--color-match=rgb551
--color-filename=rgb342
--color-lineno=rgb233

--noheading
--pager=sed -e 's/\(.\+\)\([:-]\)\(\x1b\[38;5;109m[[:digit:]]\+\x1b\[0m\)\2/\1 ⎜ \3 ⎜ /'
            -e 's/\x1b/@eCHr@/g' | column -t -s-o⎜ |
        sed -e 's/@eCHr@/\x1b/g'
            -e 's/\(\s\+\)\(\x1b\[38;5;109m[[:digit:]]\+\x1b\[0m\)\(\s\+\)/\3\2\1/' |
        hl -u -255 -b '^--$' -rb -216 '\x{239C}'
Обратите внимание, я разбил последнюю строку с опцией --pager на пять отдельных строк для удобочитаемости. На самом деле это должна быть одна строка и разбивать ее на части нельзя! А это функция-обертка cgr (ее можно поместить в .bashrc).
function cgr
{
    `env which cgrep` --color --format='#f ⎜ #n ⎜ #l' -r "$@" |
            sed -e 's/\x1b\[1m\x1b\[94m/@eCHrF@/' \
                -e 's/\x1b\[1m/@eCHrB@/g' \
                -e 's/\x1b\[m/@eCHrE@/g' |
            column -t -s-o|
            sed -e 's/@eCHrF@/\x1b\[38;5;150m/' \
                -e 's/@eCHrB@/\x1b\[38;5;227m/g' \
                -e 's/@eCHrE@/\x1b\[m/g' \
                -e 's/\(\s\+\)\([[:digit:]]\+\)\(\s\+\)/\3\2\1/' |
            hl -u -73 '\h+\d+\h+(?=\x{239C})' -216 '\x{239C}'
}
(Здесь переносы допустимы). Программа hl доступна отсюда — она нужна для дополнительной подсветки колонки с номерами строк, я писал о ней здесь и здесь. Ну и алиас для поиска файлов с помощью ack.
alias ackf='ack --ignore-ack-defaults --type-set=all:match:. -k --color --nopager -g'
Update. Начиная с версии util-linux 2.28 программа column научилась игнорировать управляющие последовательности ANSI, поэтому ужасные вре́менные замены текста типа @eCHr@ в опции --pager из .ackrc и в функции cgr больше не нужны. Соответственно, теперь они будут выглядеть так (содержимое опции --pager должно по-прежнему находиться в одной строке).
--pager=sed 's/\(.\+\)\([:-]\)\(\x1b\[38;5;109m[[:digit:]]\+\x1b\[0m\)\2/\1 ⎜ \3 ⎜ /' |
        column -t -s-o⎜ |
        sed 's/\(\s\+\)\(\x1b\[38;5;109m[[:digit:]]\+\x1b\[0m\)\(\s\+\)/\3\2\1/' |
        hl -u -255 -b '^--$' -rb -216 '\x{239C}'

function cgr
{
    `env which cgrep` --color --format='#f ⎜ #n ⎜ #l' --no-column -r "$@" |
            column -t -s-o|
            sed -e 's/\x1b\[1;94m/\x1b\[38;5;150m/' \
                -e 's/\x1b\[1m/\x1b\[38;5;227m/g' \
                -e 's/\(\s\+\)\([[:digit:]]\+\)\(\s\+\)/\3\2\1/' |
            hl -u -73 '\h+\d+\h+(?=\x{239C})' -216 '\x{239C}'
}
Также обратите внимание на некоторые изменения в функции cgr, связанные с обновленным cgrep, в частности на новую опцию --no-column.