воскресенье, 29 апреля 2012 г.

vim: автоматическое переключение раскладки клавиатуры при входе и выходе из режима ввода

Если нужно писать в vim что-то на русском языке, то одной из самых раздражающих неприятностей является необходимость постоянного ручного переключения раскладки клавиатуры на латинскую при выходе из режима ввода и обратно на русскую - при входе в режим ввода (даже одна эта фраза уже раздражает). Оказывается, можно очень просто автоматизировать этот процесс, во всяком случае в среде под управлением X server. Волшебным средством автоматизации будет использование внешней программы, которая умеет возвращать значение текущей раскладки и устанавливать новую. При поиске такого волшебного средства я опробовал xte из пакета программ xautomation и xdotool. Эти программы можно установить прямо из репозитория моего дистрибутива (что, конечно же, плюс), но они не умеют выполнять первую задачу, поэтому на роль волшебного средства не годятся. Поискав еще немного я нашел то, что надо: программу xkb-switch. Я не буду описывать ее опции, они достаточно просты, а сразу приведу код, который нужно добавить в .vimrc, чтобы автоматическое переключение на латиницу при выходе из режима ввода заработало:
fun! <SID>xkb_switch(mode)
    let cur_layout = substitute(system('xkb-switch'), '\n', '', 'g')
    if a:mode == 0
        if cur_layout != 'us'
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout') && b:xkb_layout != cur_layout
            call system('xkb-switch -s '.b:xkb_layout)
        endif
    endif
endfun

if executable('xkb-switch')
    autocmd InsertEnter * call <SID>xkb_switch(1)
    autocmd InsertLeave * call <SID>xkb_switch(0)
endif
Все интересное происходит в функции xkb_switch(mode), которая вызывается с разными аргументами mode при выходе из режима ввода (значение mode=0) и входе в режим ввода (значение mode=1). В первом случае мы вызываем системную команду xkb-switch -s us, которая принудительно переводит раскладку клавиатуры в латиницу, запоминая какая раскладка была на выходе из режима ввода в буферной переменной b:xkb_layout. Во втором случае (при входе в режим ввода) проверяется, существует ли переменная b:xkb_layout, и если это так, то вызывается xkb-switch c опцией -s, в которую передается значение этой переменной. Это позволяет восстановить раскладку клавиатуры, установленную в данном буфере перед выходом из режима ввода. Я еще раз хочу подчеркнуть: в данном буфере! То есть раскладка устанавливается для каждого буфера такой, которой она была при выходе из режима ввода именно в нем, а это очень удобно.

Update. Небольшое дополнение для тех, кто пользуется режимом выделения текста (Select mode). Это очень удобный режим, когда текст сначала выделяется, а затем поверх него пишется новый текст (аналог классического режима выделения текста в офисных редакторах). Перейти в него из нормального режима vim можно, набрав gh (нормальный режим выделения), gH (строчный режим выделения) или g<C-h> (блочный режим выделения). Кроме того, можно переключаться между визуальным режимом и режимом выделения текста с помощью <C-g>, а также настроить vim таким образом, чтобы выделение текста мышью переводило его в режим выделения текста (вместо визуального режима по умолчанию).

Поскольку я иногда применяю режим выделения текста, то быстро обнаружил, что при его использовании, автоматическое переключение раскладки ведет себя неподобающе, а именно: если ожидается переключение на кириллицу, то первый введенный символ остается латинским, а следующие, как и ожидалось, печатаются кириллицей. Это легко объясняется тем, как работает режим выделения текста: вначале vim проверяет, является ли вводимый символ печатным, затем, если это так, этот символ вводится в начале выделенного текста, а уже после этого vim переходит в режим ввода. Очевидно, что первый введенный символ останется латинским, так как событие InsertEnter в момент его ввода еще не наступило.

Для того, чтобы исправить это недоразумение, я добавил новую функцию xkb_mappings_load(), которая переопределяет вышеперечисленные команды gh, gH, g<C-h> и <C-g>, с помощью маппингов, которые вызывают функцию xkb_switch() с нужным аргументом mode. Сразу хочу отметить, что проблема останется для тех, кто использует мышь для перехода в режим выделения текста (вы можете добавить аналогичный маппинг для этого случая сами), а также в случае выхода из режима выделения с помощью клавиши <Esc> (дело в том, что xkb_switch() может перевести раскладку в кириллицу, выход же из режима выделения текста с помощью <Esc> минует режим ввода, а следовательно мы можем оказаться в нормальном режиме с кириллической раскладкой).
fun! <SID>xkb_mappings_load()
    for hcmd in ['gh', 'gH', 'g^H']
        exe "nnoremap <buffer> <silent> ".hcmd.
                    \ " :call <SID>xkb_switch(1)<CR>".hcmd
    endfor
    xnoremap <buffer> <silent> <C-g> :<C-u>call <SID>xkb_switch(1)<CR>gv<C-g>
    snoremap <buffer> <silent> <C-g> <C-g>:<C-u>call <SID>xkb_switch(0)<CR>gv
    let b:xkb_mappings_loaded = 1
endfun

fun! <SID>xkb_switch(mode)
    let cur_layout = substitute(system('xkb-switch'), '\n', '', 'g')
    if a:mode == 0
        if cur_layout != 'us'
            if !exists('b:xkb_mappings_loaded')
                call <SID>xkb_mappings_load()
            endif
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout') && b:xkb_layout != cur_layout
            call system('xkb-switch -s '.b:xkb_layout)
        endif
    endif
endfun
Обратите внимание: ^H в первой строке функции xkb_mappings_load() не является записью двух символов ^ и H! Это один символ, который соответствует <Bs> в таблице ASCII. Для того, чтобы ввести его в vim, следует в режиме ввода набрать <C-v>008.

Функция xkb_mappings_load() вызывается из xkb_switch() только в случае необходимости, то есть когда при выходе из режима ввода раскладка клавиатуры не является латинской (точнее us), и только для текущего буфера. Почему я просто не замапил эти команды в глобальной области, например рядом с автокомандами InsertEnter и InsertLeave? Все просто: маппинги <C-g> в визуальном режиме с использованием вызова команд в нормальном режиме не даются даром: заметили использование команды gv внутри маппингов, которая говорит: выдели-ка потерянное выделение заново? Потеря и восстановление области выделения приводят к перерисовке текста на экране. Поскольку редактировать русские тексты приходится очень нечасто, то глобальное определение маппингов для <C-g> ухудшило бы юзабилити vim без необходимости.

Update 2. По каким-то причинам вызов функции system() из autocmd InsertEnter может иногда приводить к появлению мусора в строке ввода если vim стартовал в режиме ввода (например был запущен командой vim -c start или, при наличии плагинов c.vim или perl-support.vim, пользователь открыл для редактирования новый C/C++ или Perl файл и соответствующий плагин вставил хедер с переводом vim в режим ввода). Ниже приводится вариант функции xkb_switch(), в котором system() не вызывается при первом переводе буфера в режим ввода, а также добавлен комментарий с упоминанием этого неприятного феномена.
fun! <SID>xkb_switch(mode)
    if a:mode == 0
        " by some reason calling system() function may produce garbage on
        " display if vim started in Insert mode (e.g. when c-support,
        " perl-support etc. apply file header for a new file), so definition
        " of cur_layout was moved inside if- and else- blocks from top level
        let cur_layout = substitute(system('xkb-switch'), '\n', '', 'g')
        if cur_layout != 'us'
            if !exists('b:xkb_mappings_loaded')
                call <SID>xkb_mappings_load()
            endif
            call system('xkb-switch -s us')
        endif
        let b:xkb_layout = cur_layout
    elseif a:mode == 1
        if exists('b:xkb_layout')
            let cur_layout = substitute(system('xkb-switch'), '\n', '', 'g')
            if b:xkb_layout != cur_layout
                call system('xkb-switch -s '.b:xkb_layout)
            endif
        endif
    endif
endfun
Update 3. См. важное обновление здесь.

воскресенье, 25 марта 2012 г.

nginx: переменные с дефисом (тире) в названии, часть II

О проблеме переменных с дефисом в названии я рассказывал в предыдущем сообщении. Здесь мы напишем наш собственный модуль для решения этой проблемы. Учитывая, что задача имеет весьма общий характер, мы назовем наш модуль myutil в предположении, что в дальнейшем мы сможем расширять этот модуль для решения других подобных задач. Ну а пока myutil будет решать только одну задачу, предоставляя директиву
            myutil_var_alias $dst $src;
Директива myutil_var_alias ожидает ровно два аргумента: переменную-источник ($src), и переменную-назначение ($dst), и решает очень простую задачу: создает новую переменную $dst и копирует в нее значение существующей переменной $src. Фактически наша директива решает более общую задачу, чем та, которую мы определили сначала, поскольку никаких ограничений на имя переменной-источника мы не накладываем. В частности, она решает также и нашу проблему, если имя переменной-источника содержит символ '-'.

Определим некоторые ограничения для myutil_var_alias. Пусть областью ее применения являются только блок location и блок if внутри location. Это ограничение упростит реализацию модуля, но оно не является принципиальным.

А теперь приступим к реализации модуля. Общую информацию о построении модулей nginx я приводил здесь, поэтому не буду говорить о назначении файла config и переменных типов ngx_command_t, ngx_http_module_t и ngx_module_t в исходном файле модуля.

Это содержимое файла config:
ngx_addon_name=ngx_http_myutil_module
HTTP_MODULES="$HTTP_MODULES $ngx_addon_name"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_myutil_module.c"
Это начало исходного файла ngx_http_myutil_module.c:
#include <ngx_core.h>
#include <ngx_http.h>

typedef struct
{
    ngx_array_t  varalias_data;
}  ngx_http_myutil_loc_conf_t;

typedef struct
{
    ngx_int_t    self;
    ngx_int_t    index;
}  ngx_http_myutil_var_elem_t;

static void *  ngx_http_myutil_create_loc_conf( ngx_conf_t *  cf );
static char *  ngx_http_myutil_merge_loc_conf( ngx_conf_t *  cf, void *  parent,
                                               void *  child );
static char *  ngx_http_myutil_var_alias( ngx_conf_t *  cf,
                                          ngx_command_t *  cmd, void *  conf );

static ngx_command_t  ngx_http_myutil_commands[] =
{
    {
        ngx_string( "myutil_var_alias" ),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_CONF_TAKE2,
        ngx_http_myutil_var_alias,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    },

    ngx_null_command
};

static ngx_http_module_t  ngx_http_myutil_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_myutil_create_loc_conf,       /* create location configuration */
    ngx_http_myutil_merge_loc_conf         /* merge location configuration */
};

ngx_module_t  ngx_http_myutil_module = {
    NGX_MODULE_V1,
    &ngx_http_myutil_module_ctx,           /* module context */
    ngx_http_myutil_commands,              /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};
Наш модуль будет хранить конфигурационные данные уровня location (тип ngx_http_myutil_loc_conf_t), в то время как данные уровня http и server отсутствуют - это потому, что мы упростили себе задачу, ограничив применение директивы myutil_var_alias только двумя блоками location и if-location. Конфигурационные данные уровня location содержат массив varalias_data, элементами которого будут являться объекты типа ngx_http_myutil_var_elem_t. Этот тип предназначен для хранения двух индексов переменных - переменной $dst (поле self) и переменной $src (поле index). Массив нам нужен для того, чтобы была возможность использовать несколько директив myutil_var_alias внутри одного location.

В переменной ngx_http_myutil_module_ctx мы определили только два ненулевых хендлера конфигурации: ngx_http_myutil_create_loc_conf() и ngx_http_myutil_merge_loc_conf(). Первая функция отвечает за создание конфигурации уровня location, а также инициализирует массив varalias_data. Вторая функция определяет стратегию использования родительских данных конфигурации уровня location при переходе к дочерней конфигурации (слияние данных конфигураций происходит в контексте запроса при положительном значении условия в блоке if). Наша стратегия будет заключаться в копировании родительских данных в дочернюю конфигурацию.
static void *  ngx_http_myutil_create_loc_conf( ngx_conf_t *  cf )
{
    ngx_http_myutil_loc_conf_t *  lcf = NULL;

    lcf = ngx_pcalloc( cf->pool, sizeof( ngx_http_myutil_loc_conf_t ) );

    if ( ! lcf )
        return NULL;

    if ( ngx_array_init( &lcf->varalias_data, cf->pool, 1,
                         sizeof( ngx_http_myutil_var_elem_t ) ) != NGX_OK )
        return NULL;

    return lcf;
}


static char *  ngx_http_myutil_merge_loc_conf( ngx_conf_t *  cf, void *  parent,
                                               void *  child )
{
    ngx_http_myutil_loc_conf_t *  prev = parent;
    ngx_http_myutil_loc_conf_t *  conf = child;

    ngx_uint_t                    i;

    for ( i = 0; i < prev->varalias_data.nelts; ++i )
    {
        ngx_http_myutil_var_elem_t *  elem = NULL;

        elem = ngx_array_push( &conf->varalias_data );

        if ( ! elem )
            return NGX_CONF_ERROR;

        *elem = ( ( ngx_http_myutil_var_elem_t * )
                                        prev->varalias_data.elts )[ i ];
    }

    return NGX_CONF_OK;
}
Так выглядит функция ngx_http_myutil_var_alias():
static char *  ngx_http_myutil_var_alias( ngx_conf_t *  cf,
                                          ngx_command_t *  cmd, void *  conf )
{
    ngx_str_t *                   value     = cf->args->elts;
    ngx_http_variable_t *         v         = NULL;
    ngx_http_myutil_loc_conf_t *  lcf       = conf;
    ngx_int_t                     index     = NGX_ERROR;
    ngx_int_t                     v_idx     = NGX_ERROR;
    ngx_http_myutil_var_elem_t *  res       = NULL;
    ngx_uint_t *                  v_idx_ptr = NULL;

    ngx_uint_t                    i;

    for ( i = 1; i < 3; ++i )
    {
        if ( value[ i ].data[ 0 ] != '$' )
        {
            ngx_conf_log_error( NGX_LOG_EMERG, cf, 0,
                                "%V: invalid variable name '%V'",
                                &cmd->name, &value[ i ] );
            return NGX_CONF_ERROR;
        }

        value[ i ].len--;
        value[ i ].data++;
    }

    index = ngx_http_get_variable_index( cf, &value[ 2 ] );

    if ( index == NGX_ERROR )
        return NGX_CONF_ERROR;

    v = ngx_http_add_variable( cf, &value[ 1 ], NGX_HTTP_VAR_CHANGEABLE );

    if ( v == NULL )
        return NGX_CONF_ERROR;

    v_idx = ngx_http_get_variable_index( cf, &value[ 1 ] );

    if ( v_idx == NGX_ERROR )
        return NGX_CONF_ERROR;

    res = ngx_array_push( &lcf->varalias_data );

    if ( ! res )
        return NGX_CONF_ERROR;

    res->self  = v_idx;
    res->index = index;

    v_idx_ptr = ngx_pnalloc( cf->pool, sizeof( ngx_uint_t ) );

    if ( ! v_idx_ptr )
        return NGX_CONF_ERROR;

    *v_idx_ptr     = v_idx;
    v->data        = ( uintptr_t )v_idx_ptr;
    v->get_handler = ngx_http_myutil_get_var_alias_value;

    return NGX_CONF_OK;
}
Переменная value - это список аргументов директивы myutil_var_alias, начиная с индекса 1, value[ 0 ] - это имя самой директивы. Соответственно, в цикле for мы просто проверяем, что оба аргумента директивы начинаются с символа $, если это не так - то бракуем конфигурацию с сообщением об ошибке. Далее получаем индекс переменной-источника, регистрируем переменную-назначение (v), и получаем ее индекс, создаем объект res типа ngx_http_myutil_var_elem_t, присваиваем его полям полученные индексы и заталкиваем его в массив varalias_data. В самом конце определяем два важных атрибута переменной v: data, в который помещаем ее индекс (это понадобится для поиска нужного элемента в массиве varalias_data при вызове get-хендлера этой переменной), а также собственно get_handler переменной v - указатель на функцию ngx_http_myutil_get_var_alias_value(). Get-хендлер будет вызываться всякий раз, когда nginx понадобится получить значение этой переменной, собственно, эта функция предназначена для присваивания значения переменной. Вот ее код:
static ngx_int_t  ngx_http_myutil_get_var_alias_value( ngx_http_request_t *  r,
                            ngx_http_variable_value_t *  v, uintptr_t  data )
{
    ngx_http_myutil_loc_conf_t *  lcf     = NULL;
    ngx_http_variable_value_t *   var     = NULL;
    ngx_array_t *                 vaarray = NULL;
    ngx_http_myutil_var_elem_t *  vavar   = NULL;
    ngx_int_t *                   index   = NULL;

    ngx_uint_t                    i;

    if ( ! data )
        return NGX_ERROR;

    lcf = ngx_http_get_module_loc_conf( r, ngx_http_myutil_module );

    vaarray = &lcf->varalias_data;
    vavar   = vaarray->elts;
    index   = ( ngx_int_t * )data;

    for ( i = 0; i < vaarray->nelts ; ++i )
    {
        ngx_http_variable_value_t *  found = NULL;

        if ( *index != vavar[ i ].self )
            continue;

        found = ngx_http_get_indexed_variable( r, vavar[ i ].index );

        if ( found && found->len > 0 )
        {
            var = found;
            break;
        }
    }

    if ( ! var )
        return NGX_ERROR;

    v->len          = var->len;
    v->data         = var->data;
    v->valid        = 1;
    v->no_cacheable = 0;
    v->not_found    = 0;

    return NGX_OK;
}
Тут вообще все просто. В цикле for ищем элемент массива varalias_data, значение поля self которого совпадает со значением, переданным в третьем аргументе функции data, затем ищем переменную, индекс которой соответствует значению поля index найденного элемента. Полям данных переменной v, переданной в функцию в качестве второго аргумента, присваиваем длину данных найденной переменной (var->len) и собственно указатель на данные (var->data), а также дежурные значения для полей valid, no_cacheable и not_found.

Вот собственно и все. Собираем модуль и проверяем его работу на следующей конфигурации:
events {
    worker_connections  1024;
}

http {
    map $arg_some-var $map_arg_some_var {
        ~(?<match>.*)   $match;
    }

    server {
        listen          8010;
        server_name     router;

        location /test_var_alias.html {
            set $some_var $arg_some-var;
            myutil_var_alias $va_some_var $arg_some-var;
            echo "value: '$arg_some-var'";
            echo "set: '$some_var', map: '$map_arg_some_var'";
            echo "alias: '$va_some_var'";
        }
    }
}
Проверяем:
$ curl 'http://localhost:8010/test_var_alias.html?some-var=a'
value: '-var'
set: '-var', map: '-var'
alias: 'a'
Работает! А прямой доступ через echo, а также set и map по-прежнему возвращают ерунду.

Надо сказать еще об одном важном моменте. Переменные конфигурации nginx имеют глобальную область видимости. Это значит, что если мы объявим переменную $va_some_var в каком-нибудь ином локейшне, или на более высоком уровне, или даже в другом виртуальном сервере в рамках одной и той же конфигурации, то это будет все равно одна и та же переменная. То, что ее значение привязано к конкретному контексту (в нашем случае - локейшну) - это полностью заслуга ее get_handler, который привязывает данные переменной к данным конкретного локейшна (в нашем случае эти данные находятся внутри конфигурации уровня location в массиве varalias_data). Что из этого следует? То, что данные, жестко связанные с переменной, не должны изменяться в процессе парсинга конфигурации. Мы используем два таких жестко связанных с переменной типа данных - это контекст, передаваемый в качестве третьего аргумента в get_handler переменной (мы передаем туда ее индекс), и собственно сам get_handler - указатель на конкретную функцию. Понятно, что индекс переменной не изменится в процессе чтения конфигурации ни разу - за этим проследит сам nginx. А вот get_handler измениться может! Представьте, что у вас есть директива (из нашего же модуля или какого-нибудь другого), которая тоже объявляет переменную и назначает ей другой хендлер, и вы применили ее с использованием того же имени переменной где-нибудь ниже по тексту конфигурации (вы то уверены, что эти две переменные, несмотря на то, что имеют одно и то же имя, никак не связаны). В этом случае произойдет неожиданная неприятность - хендлер переменной, объявленной первой директивой перезапишется! И вы будете получать в качестве ее значения совсем не тот результат! Выход из такой ситуации простой - не используйте одинаковые имена переменных в директивах, которые привязывают к этим переменным разные хендлеры. Самое логичное - это выбрать какой-нибудь специальный префикс для переменных, связанных с определенным типом директив. В нашем случае для переменных, объявленных директивой myutil_var_alias я использовал префикс va_, подразумевая расшифровку Var Alias. Если следовать этому простому правилу, можно избежать подобных неприятностей.

суббота, 24 марта 2012 г.

nginx: переменные с дефисом (тире) в названии, часть I

Недавно столкнулся с занимательной проблемой в nginx: как получить значение переменной с символом '-' в названии в файле конфигурации. Такими переменными могут легко оказаться какие-нибудь аргументы строки запроса (например, $arg_some-var) или имя куки (например, $cookie_some-cookie). Скажем, мы ожидаем, что в аргументе some-var находится имя апстрима, на который нужно переправить запрос (немного надуманно, но почему бы нет), и наша простейшая тестовая конфигурация выглядит так:
events {
    worker_connections  1024;
}

http {
    upstream ubackend {
        server localhost:8020;
    }

    server {
        listen          8010;
        server_name     router;

        location /test_dash_variable.html {
            proxy_pass http://$arg_some-var;
            echo_after_body "Hi! Value is '$arg_some-var'";
        }
    }

    server {
        listen          8020;
        server_name     backend;

        location /test_dash_variable.html {
            echo "In backend!";
        }
    }
}
Видите, даже подсветке кода не нравится имя $arg_some-var. Что же произойдет, если мы направим запрос с аргументом some-var, равным ubackend? Мы ожидаем попасть в бэкенд, а вот что произойдет на самом деле:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=ubackend'
<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.0.8</center>
</body>
</html>
Hi! Value is '-var'
Получили 502 и какое-то неожиданное значение переменной $arg_some-var, равное -var. Загадка имеет простое объяснение: выражение справа от директивы proxy_pass обрабатывается внутренним парсером выражений nginx, для которого символ '-' является своего рода лексемой. Этот внутренний парсер разбирает выражение $arg_some-var как значение-переменной-$arg_some -- символ '-' -- строка 'var'. Переменная $arg_some не определена, соответственно значением данного выражения будет -var. Эта проблема поднималась раньше и был предложен простой патч (см. здесь), в котором символу '-' было разрешено быть частью имен переменных. Патч не был принят из опасения сломать обратную совместимость (я понял так).

Как можно обойти эту проблему? Правильный путь - воспользоваться директивой, которая не передает аргумент с переменной парсеру выражений, а берет имя переменной как есть. Директива if из модуля rewrite - отличный кандидат. Она рассматривает первый аргумент после открывающей круглой скобки как имя переменной. Мы проверим, как это работает, добавив директиву if в location /test_dash_variable.html сервера router:
        location /test_dash_variable.html {
            proxy_pass http://$arg_some-var;
            echo_after_body "Hi! Value is '$arg_some-var'";
            if ($arg_some-var = 'a') {
                echo "Bye! Value is '$arg_some-var'";
                break;
            }
        }
Теперь, если мы передадим значение a в аргументе some-var, мы должны будем попасть в обработчик внутри if, если if действительно работает так, как мы ожидаем:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=a'
Bye! Value is '-var'
Работает! Хотя echo по-прежнему настаивает, что значение $arg_some-var равно -var. Ничего удивительного, echo использует парсер выражений для получения значения своего аргумента. Ладно, if c одной стороны хорошо, но с другой стороны if - это зло! К тому же if не может отобразить бесконечное множество разных вариантов обработки переменных в конфигурации nginx. Нужно искать другие решения. Первое что приходит на ум - воспользоваться директивой set из того же модуля rewrite. После простого эксперимента выясняется, что set тоже пользуется парсером выражений (да и следовало ли ожидать другого?) и нам не подходит:
        location /test_dash_variable.html {
            set $some_var $arg_some-var;
            echo "Hi! Variable 'some_var' is '$some_var'";
        }
Проверяем:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=a'
Hi! Variable 'some_var' is '-var'
Все то же самое. Можно еще воспользоваться директивой map, казалось бы это то, что нам нужно: берет одну переменную и мапит ее на другую. Имя второй переменной мы выбираем сами, а содержание маппинга мы, с помощью регулярных выражений, настроим таким образом, чтобы мапились любые значения первой переменной в значения второй переменной один к одному. Вот соответствующая конфигурация:
events {
    worker_connections  1024;
}

http {
    map $arg_some-var $map_arg_some_var {
        ~(?<match>.*)   $match;
    }

    server {
        listen          8010;
        server_name     router;

        location /test_dash_variable.html {
            set $some_var $arg_some-var;
            echo "Hi! Variable 'map_arg_some_var' is '$map_arg_some_var'";
        }
    }
}
Проверяем:
$ curl 'http://localhost:8010/test_dash_variable.html?some-var=a'
Hi! Variable 'map_arg_some_var' is '-var'
Увы, ничего не получается. А все потому, что директива map мапит не переменную-в-переменную, а выражение-в-переменную. В роли выражения выступает наша переменная $arg_some-var, а что из этого следует, мы уже прекрасно знаем.

Итак, ни один использованный подход не смог решить нашу проблему. Поэтому для ее решения мы напишем наш собственный модуль, заодно потренируемся. В следующей части ...

воскресенье, 15 января 2012 г.

vim: плагин Powerline

Вчера установил новый плагин Powerline. Этот плагин позволяет сделать статусную строку vim весьма привлекательной, с возможностью легко различать текущий режим. Ниже я привожу два снимка окна терминала с двумя открытыми файлами и буфером tagbar справа (на официальном сайте Powerline тоже есть скриншоты, см. здесь). Активный буфер расположен внизу, соответственно Powerline подсвечивает его статусную строку ярче и информативнее. На первом снимке буфер находится в нормальном режиме, на втором - в режиме ввода.


Как видно на снимках, Powerline поддерживает tagbar, но не совсем из коробки, но об этом позже. Сначала о том, как настроить Powerline, чтобы он показывал статусную строку в максимально привлекательном виде.

Просто установить Powerline для этого недостаточно. Стандартные консольные шрифты не содержат этих привлекательных символов - угловых стрелок, символа FT и др., которые видны на приведенных снимках. Эти символы определены в файле PowerlineSymbols.sfd в директории fontpatcher/ плагина. В той же директории находится файл README.rst, в котором приведена инструкция для настройки шрифта консоли. Я попробую пересказать ее своими словами и добавить некоторые дополнительные сведения, которые я почерпнул при установке нового шрифта. Если вы боитесь или не хотите устанавливать новый шрифт, то можно использовать и стандартную настройку Powerline по умолчанию (она называется compatible), в этом случае вместо привлекательных стрелочек вы увидите простые консольные символы '|'. Есть еще один вариант настройки, который не предусматривает изменения стандартного шрифта, но делает статусную строку все же немного привлекательнее: это использование стандартных символов Unicode. Для этого поместите в файл .vimrc настройку
let g:Powerline_symbols = 'unicode'
Вернемся к установке нового шрифта. Хотя новый шрифт - это громко сказано. Фактически следует просто взять тот шрифт, который используется в вашей консоли и добавить в него новые символы из PowerlineSymbols.sfd с помощью программы fontpatcher из директории fontpatcher/. fontpatcher использует FontForge, поэтому следует предварительно установить этот пакет в вашу систему. Лично я использую шрифт DejaVu Sans Mono, который в моей системе (Fedora 16) находится в директории /usr/share/fonts/dejavu/. Чтобы добавить новые символы в этот шрифт, перейдем в директорию fontpatcher/ и выполним команду:
./fontpatcher --symbol-font PowerlineSymbols.sfd /usr/share/fonts/dejavu/DejaVuSansMono.ttf
Теперь в этой директории должен появиться новый файл DejaVuSansMono-Powerline.otf, который следует поместить в директорию $HOME/.fonts/. После этого от имени суперпользователя запускаем команду
fc-cache -vf
Теперь следует установить данный шрифт в качестве шрифта терминала. Заходим в настройки терминала и находим диалог выбора шрифта. fontpatcher добавляет в имя шрифта строку for Powerline, соответственно в моем случае шрифт будет называться DejaVu Sans Mono for Powerline. В терминале XFCE новый шрифт будет доступен сразу, в konsole - не знаю, ну а в случае gnome-terminal из Gnome 3 придется выполнить массу неочевидных и логически необоснованных действий (как это заведено в Gnome 3). Вы не найдете нового шрифта в диалоге выбора gnome-terminal. Я не знаю, может быть есть лучший способ, но я просто решил сделать новый шрифт системным Мonospace шрифтом в Gnome 3. И даже это сделать не так-то просто: для начала нужно установить (по крайней мере в Fedora 16) пакет gnome-tweak-tool, затем собственно запустить gnome-tweak-tool и в диалоге выбора Monospace шрифта выбрать DejaVu Sans Mono for Powerline. После этого действия выбор данного шрифта останется недоступным в gnome-terminal (ну чего еще-то ему не хватает? :) ), зато теперь можно выбрать Системный моноширинный шрифт и им, наконец-то, окажется наш пропатченный DejaVu Sans Mono.

Огромный плюс в установке нового шрифта - это то, что рендерится он качественнее, чем оригинал, вот это сюрприз!

Для правильного отображения новых символов в .vimrc нужно добавить строку
let g:Powerline_symbols = 'fancy'
Однако, если вы успели поэкспериментировать с другими значениями g:Powerline_symbols, то вам придется удалить кеш-файл, который находится в /tmp/Poweline.cache (либо, если у вас установлена переменная среды $TEMP, в $TEMP/Powerline.cache).

Еще одна тонкость - возможно вы заметите, что при переходе из режима ввода в нормальный режим путем нажатия клавиши Esc, обновление статусной строки будет задерживаться примерно на секунду. Чтобы избежать этого, поместите в .vimrc строку
set timeout timeoutlen=1000 ttimeoutlen=50
(в данном случае важно установить относительно небольшое значение ttimeoutlen, например 50 миллисекунд, значение timeoutlen для данной проблемы не играет роли, и, если вы не знаете, что это такое, то вообще не указывайте здесь timeoutlen).

И еще одна тонкость. По умолчанию, если в vim открыто единственное окно, то статусная строка вообще не показывается. Чтобы статусная строка присутстствовала всегда, добавьте в .vimrc строку
set laststatus=2
Теперь вернемся к tagbar. Powerline поддерживает статусную строку tagbar - это видно на приведенных скриншотах. Однако работает это криво. Во-первых, статусная строка tagbar не изменяется при первом открытии окна tagbar; во-вторых, при переходе на тег из окна tagbar в окно целевого буфера, статусные строки также не перерисовываются, несмотря на то, что активное окно изменилось. Проблема в том, что в самый ответственный момент, когда происходят события открытия окна tagbar и перехода на новую метку, в коде tagbar (в функциях файла autoload/tagbar.vim) устанавливаются значения опции eventignore в all. Соответственно все автокоманды, определенные в Powerline (а также и в других плагинах, и любые пользовательские) игнорируются. Мы не станем убирать установку eventignore в all, вдруг это сделано неспроста и что-нибудь сломается. Мы просто введем понятие хуков для трех событий: отрисовки статусной строки при запуске нового окна tagbar, покидания окна tagbar при переходе на метку, и перехода в целевое окно из окна tagbar при переходе на метку. Соответственно, названия хуков будут g:tagbar_statusline_hook, g:tagbar_leave_hook, g:tagbar_dstwin_enter_hook. Хуки следует разместить, соответственно, в функциях s:InitWindow() и s:JumpToTag() в файле autoload/tagbar.vim. Вот простейший патч (относительно версии tagbar 2.3.0):
--- tagbar.vim  2011-12-24 09:14:54.000000000 +0400
+++ tagbar.vim.new  2012-01-15 18:07:07.691191871 +0400
@@ -1478,17 +1478,21 @@
     " Reset fold settings in case a plugin set them globally to something
     " expensive. Apparently 'foldexpr' gets executed even if 'foldenable' is
     " off, and then for every appended line (like with :put).
     setlocal foldmethod&
     setlocal foldexpr&
 
-    " Earlier versions have a bug in local, evaluated statuslines
-    if v:version > 701 || (v:version == 701 && has('patch097'))
-        setlocal statusline=%!TagbarGenerateStatusline()
+    if exists('g:tagbar_statusline_hook')
+        execute g:tagbar_statusline_hook
     else
-        setlocal statusline=Tagbar
+        " Earlier versions have a bug in local, evaluated statuslines
+        if v:version > 701 || (v:version == 701 && has('patch097'))
+            setlocal statusline=%!TagbarGenerateStatusline()
+        else
+            setlocal statusline=Tagbar
+        endif
     endif
 
     " Script-local variable needed since compare functions can't
     " take extra arguments
     let s:compare_typeinfo = {}
 
@@ -2427,13 +2431,19 @@
     let eventignore_save = &eventignore
     set eventignore=all
 
     " This elaborate construct will try to switch to the correct
     " buffer/window; if the buffer isn't currently shown in a window it will
     " open it in the first window with a non-special buffer in it
+    if exists('g:tagbar_leave_hook')
+        execute g:tagbar_leave_hook
+    endif
     wincmd p
+    if exists('g:tagbar_dstwin_enter_hook')
+        execute g:tagbar_dstwin_enter_hook
+    endif
     let filebufnr = bufnr(taginfo.fileinfo.fpath)
     if bufnr('%') != filebufnr
         let filewinnr = bufwinnr(filebufnr)
         if filewinnr != -1
             execute filewinnr . 'wincmd w'
         else
Теперь определим хуки в .vimrc:
let g:tagbar_statusline_hook = 'call Pl#UpdateStatusline(0)'
let g:tagbar_leave_hook = 'call Pl#UpdateStatusline(0)'
let g:tagbar_dstwin_enter_hook = 'call Pl#UpdateStatusline(1)'
После этого tagbar будет правильно работать с Powerline.