понедельник, 19 сентября 2011 г.

Цветной ngrep (в продолжение предпоследнего поста)

ngrep - это что-то среднее между tcpdump и wireshark. Он может показаться не таким удобным как wireshark, зато превосходит по удобству использования tcpdump, поскольку основной особенностью ngrep является вывод на экран полезного содержимого пакетов (то, что обычно называют payload), а не всевозможных сетевых деталей. Кроме того, ngrep работает в терминале, а это, на мой взгляд, огромный плюс.

Итак, сначала картинка:


А теперь настройки .hlrc:
snippet ngrep   -191 '^\s*(?:POST|GET)\s+' '^[ITU]\s+' -211 '\[[A-Z]+\]' \
                -120 '^[A-Z]\S+: ' -130 '^#+' \
                -210 '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+' \
                -140 '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' -190 '^HTTP.*'
и .hl_functions:
function ngrep()
{
    sudo `env which ngrep` $@ | hl -sngrep
}
Чтобы этот пост не получился совсем уж скучным и банальным, ниже я покажу как предоставить права на запуск ngrep обычному пользователю (заметили sudo в функции ngrep?).

Итак, в обычной ситуации обычный пользователь не имеет права на запуск ngrep, поскольку может получить доступ к информации, для него не предназначенной. Но что если нам часто требуется получать доступ к содержимому сетевых пакетов (например, при тестировании какого-нибудь веб-приложения), или мы работаем на домашнем компьютере и доступ к информации о том, что происходит в нашей сетке не только не ослабляет безопасность системы, а наоборот повышает ее? В этом случае нужно добавить себя в список sudoers. Я не стану добавлять записи в файл /etc/sudoers, а создам новый файл (назовем его users) в директории /etc/sudoers.d/. Итак, от имени суперпользователя открываем файл /etc/sudoers.d/users и записываем в него следующие строки:
Cmnd_Alias  NETTASKS = /usr/sbin/ngrep, /usr/sbin/wireshark, /usr/sbin/tcpdump

user1      ALL = NOPASSWD: NETTASKS
Вместо user1 нужно поставить ваше настоящее регистрационное имя. После завершения редактирования файла необходимо изменить его атрибуты доступа:
chmod 0440 /etc/sudoers.d/users
Если этого не сделать, sudo откажется выполнять инструкции данного файла с соответствующей диагностикой. Ввиду данного изменения, при дальнейшем редактировании /etc/sudoers.d/users придется подтверждать внесение изменений при записи на диск (в vim это делается командой :w!), но лучше всего воспользоваться командой visudo -f /etc/sudoers.d/users, которая знает, как работать с файлами sudoers

Кроме права на запуск ngrep я наделил пользователя user1 правами на запуск wireshark и tcpdump. Теперь для запуска этих двух команд данный пользователь может ввести sudo wireshark и sudo tcpdump, а для запуска ngrep достаточно просто ввести ngrep, поскольку в интерактивной среде ngrep - это функция, записанная нами в файл .hl_functions, которая через sudo вызывает настоящий ngrep.

Заметим также, что ввод пароля пользователем при запуске этих команд не требуется, так как в файле /etc/sudoers.d/users перед задачами NETTASKS  для пользователя user1 указана опция NOPASSWD. Кроме того, слово ALL указывает на то, что пользователь user1 имеет право запускать команды с любого хоста. В целях безопасности, наверное имеет смысл заменить ALL на localhost, но в этом случае user1 не сможет запускать команды из NETTASKS удаленно.

Update. Не сразу заметил, что функция ngrep() в приведенном выше виде,  работает неверно для многих достаточно типичных случаев, в частности, когда паттерн для поиска пуст (например, в случае ngrep '' 'tcp port 80'). Проблема в том, что переменная $@ внутри ngrep() удаляет все кавычки, которые в случае пустого паттерна (или паттернов, содержащих пробелы) имеют значение. К счастью, если переменную $@ взять в двойные кавычки, то информация о пустых аргументах и аргументах, содержащих пробелы, не теряется, и ее можно восстановить. Я не знаю, существует ли в sh или bash штатный способ вернуть кавычки в этом случае на место, скорее всего нет, поэтому я написал отдельную функцию quote_args_if_needed() и поместил ее в начале файла .hl_functions.
function quote_args_if_needed()
{
    local result=''
    for i in "$@" ; do
        if [[ -z "$i" || "$i" =~ ' ' ]] ; then
            result=$result" '"$i"'"
            continue
        fi
        result=$result' '$i
    done
    echo $result
}
Функция проходит по переданным ей аргументам и добавляет их (через пробел) к изначально пустой переменной result. Если очередной аргумент пуст, либо содержит пробелы, то он обертывается одинарными кавычками. Мы будем передавать в quote_args_if_needed() значение "$@". Переменная $@, взятая в двойные кавычки представляет собой правильный массив переданных аргументов, в котором те аргументы, которые были заключены в кавычки, представляют единое целое (в отличие от бескавычечной версии, в которой все содержимое переданных аргументов разбивается по пробельным символам). Теперь нужно использовать новую функцию в ngrep():
function ngrep()
{
    eval sudo `env which ngrep` `quote_args_if_needed "$@"` | hl -sngrep
}
Как видите, мне пришлось обернуть все в eval. Это потому, что кавычки для аргументов, которые нам удалось вернуть с помощью quote_args_if_needed(), будут переданы без eval в `env which ngrep` дословно, и он будет ругаться на syntax error. Команда eval очистит нашу строку от кавычек, и при этом сохранит пустые аргументы и аргументы, содержащие пробелы.

суббота, 10 сентября 2011 г.

Переносы русских слов в texlive

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

Итак, после обновления texlive открываем файл /usr/share/texlive/texmf/tex/generic/config/language.dat (конкретный путь зависит от дистрибутива и репозитория) и добавляем в него строки
ruseng ruenhyph.tex
=russian
=english
сразу же за подобными определениями для english. После этого запускаем в терминале от имени root
fmtutil-sys --all
Теперь переносы должны работать.

Использование пользовательской подсветки команд в терминале (на примере make)

Сформулируем задачу следующим образом: мы хотим обеспечить собственные правила подсветки вывода произвольной команды в терминале. Для простоты будем считать, что наш терминал поддерживает 256 цветов (что уже давно справедливо и для vte, и для konsole, и для xterm) и мы используем bash. Задача абсолютно нетривиальная хотя бы потому, что нормального полноценного решения ее, насколько я знаю, пока не существует. Если бы вышеперечисленные терминалы поддерживали систему плагинов, то решить ее было бы, наверное, просто, но на данный момент это не так.

Стоит отметить, что существует целый ряд решений, которые могут помочь с расцвечиванием консоли для разных программ: это colordiff, GNU souce-highlight (который я часто использую для подсветки кода в этом блоге) и другие, а также использование различных цветовых опций общеизвестных команд, таких как ls, tree, grep и т.д., например в моем файле .bashrc определены алиасы
alias ls='ls -F --color=tty'
alias tree='tree -C'
Недостатки этих программ очевидны: собственно, они не решают поставленную нами задачу, так как предназначены для подсветки вывода заранее определенных программ по заранее определенным правилам.

Совсем недавно я прочитал про программу grc (generic colouriser). Точный ресурс сейчас точно не вспомню, но из свеженайденных - например здесь. Это уже что-то: в конфигах grc можно задать правила подсветки для вывода произвольной программы, основанные на регулярных выражениях. Я не стал устанавливать данную программу, поскольку имею собственное решение, очень похожее по смыслу и, надеюсь, не уступающее по возможностям. На нем и остановимся поподробнее.

Программа была написана на perl еще в 2007 или 2006 году. Она состоит из двух частей: модуля Term::Highlight, реализующего основную работу по поиску вхождений, соответствующих заданным регулярным выражениям, и вставке цветовых тегов на их границы, и скрипта hl, в котором определяются режимы работы программы и куда передаются аргументы, определяющие подсветку. В пакет программы входят справочные страницы man, поэтому разобраться в ней будет несложно. Скачать программу можно с sourceforge.net (здесь), или с cpan.org (здесь).

Изначально я планировал использовать Term::Highlight как фильтр для подсветки слов или регулярных выражений в длинном выводе программ, например так:
$ cat some_long_file | hl -100 some_word -101 another_word
Здесь осуществляется вывод на экран длинного файла some_long_file, при этом слова some_word и another_word будут подсвечены цветами, соответствующими значениям 100 и 101 в 256-цветной палитре терминала. Term::Highlight прекрасно справляется в ситуации, когда участки текста, соответствующие заданным словам или регулярным выражениям в hl оказываются вложены или пересекаются (причем многократно) друг с другом, например в случае
$ cat some_long_file | hl -100 some_word -101 'w\w+'
внутри подсвеченного цветом 100 слова some_word будет подсвечен цветом 101 участок, соответствующий выражению 'w\w+', т.е. слово word. Кроме этого, Term::Highlight поддерживает 8-цветные консоли, поиск в двоичных файлах, установку жирного текста, поиск без учета регистра и др. В дальнейшем я реализовал режим цветного grep и поддержку сниппетов.

Теперь подробнее о сниппетах. Очевидно, что для сложной подсветки, например синтаксиса какого-нибудь языка, нам понадобится передать в hl огромное число аргументов - это не удобно, а главное, система скорее всего не справится с такой длинной строкой и придется использовать xargs, а это очень неудобно. Поэтому в  версии 1.7 была добавлена возможность записывать именованные сниппеты в файле $HOME/.hlrc. Сниппеты содержат аргументы подсветки hl и их можно передавать по имени с помощью опции -s. Ниже мы напишем сниппет по имени make для подсветки вывода одноименной команды.

Буквально вчера я внес в Term::Highlight изменение, связанное с безопасным использованием hl в пайпах. Новая версия имеет номер 1.7.1. Отныне hl знает, куда направлен ее вывод, и если это не stdout, то цветовые теги вставляться не будут. Это поможет использовать hl для подсветки вывода произвольных программ и не беспокоиться в том случае, если вывод подсвечиваемой программы направлен через пайп на вход другой программы. Напомню, что точно так же работает ls --color=tty: если ее  вывод перенаправить в файл или на вход другой программы, или раздвоить с помощью tee , то цветовые теги исчезнут.

Итак, теперь нужно сделать так, чтобы при наборе make в командной строке терминала вместо реальной программы make подставлялось нечто, фильтрующее ее вывод с помощью hl. Первое, что приходит на ум - использовать алиас - не годится. Алиасы bash очень примитивны и мы не сможем с их помощью передать все возможные аргументы make, а затем через пайп установить фильтр hl. Поэтому будем использовать функцию с именем make, которая будет подменять настоящий make в интерактивной среде.

Я предлагаю сделать так: в файле $HOME/.bash_profile установить переменную HL_ALIASES, которая будет указывать на путь к файлу, содержащему нашу функцию. Пусть этот файл называется .hl_functions. Тогда в .bash_profile записываем строку
export HL_ALIASES=$HOME/.hl_functions
Затем в $HOME/.bashrc считываем содержимое этого файла:
[ -n "$HL_ALIASES" ] && [ -f "$HL_ALIASES" ] && . $HL_ALIASES
В файл .hl_functions поместим функцию make (в дальнейшем туда же можно поместить функции, определяющие подсветку вывода других программ):
function make()
{
    `env which make` $@ 2>&1 | hl -smake
}
Все очень просто: передаем stdout и stderr настоящего make (`env which make`) со всеми аргументами на вход команды hl, которая преобразует его на основании сниппета make (hl -smake). Такой подход безопасен в случае использования вывода подсвечиваемой программы для передачи на вход другой: hl не будет подставлять цветовые теги в этом случае. Более того, поскольку функция make() определена в .bashrc, то скриптам она будет неизвестна, поскольку объекты .bashrc определены для интерактивного использования. Соответственно можно совершенно не беспокоиться, если подсвечиваемая команда будет использоваться в каком-нибудь скрипте: в этом случае всегда будет вызываться настоящая программа. (Если вы действительно хотите использовать функции подсветки в скриптах, чего я делать крайне не рекомендую, то сорсить .hl_functions следует не в .bashrc, а в .bash_profile, а затем экспортировать определенные функции с помощью export -f).

Теперь приведем определение сниппета make, которое следует поместить в $HOME/.hlrc:
snippet make    -b -215 '^\s*gcc\b' '^\s*g\+\+(?=\s)' '^\s*libtool:\s*\w+:' \
                '^s*\/bin\/sh\s+[\w/.]+' -rb -108 '[\w/.-]+\.c\b(?!:)' \
                -119 '[\w/.-]+\.(cc|cpp|cxx|c\+\+)\b(?!:)' \
                -30 '\s*[\w/.*-]+\.o\b' -203 '(?<=\s-o)\s*[^-][\w/.-]*' \
                -42 '^\s*make\[\d+\]' -64 '(?:^|\s+)\-l[\w/+-]+' \
                -50 '^[\w/.-]+\.\w+:(?:\d+:\d+:)?' \
                -204 '^\s*(?:rm|mv|ln|cp)\s+(?:-\w+)?' \
                -196 '(?:О|о)шибка( \d+)?' -202 '(?:П|п)редупреждение( \d+)?' \
                -120 'Выход из.*$' 'Вход в.*$'
С помощью числовых тегов здесь определены цвета для следующих за ними регулярных выражений, на которых я останавливаться не буду - безусловно, для того чтобы эффективно использовать этот подход, вам самим необходимо знать, как составляются регулярные выражения perl. Тег (или опция) -b устанавливает, что следующие за ним слова и выражения нужно печатать жирным шрифтом, тег -rb отменяет жирное начертание. Подробнее о различных опциях hl можно прочитать на странице справки man hl. В приведенных выше правилах сниппета make задаются цвета подсветки команд gcc, g++, libtool и др., исходников C и C++, объектных файлов с суффиксом .o, предупреждений и ошибок компилятора (на русском языке), сообщений о входе и выходе make в директории, подключаемых с помощью опции -l библиотек, названий файлов, получаемых на выходе, которые задаются с помощью опции компилятора -o и других артефактов. Самое замечательное, что данные опции можно изменять и добавлять и они сразу становятся эффективным без ре-сорсинга файла .hl_functions.

А теперь главное - картинка. Это пример работы функции make при компиляции исходников sane-backends (картинка кликабельна):

воскресенье, 28 августа 2011 г.

Патч для ctags c лучшей поддержкой C++ и исправлениями

На днях залил новый патч для ctags, в котором было исправлено несколько багов, связанных c C++, в том числе один весьма серьезный - c неправильной обработкой символа '<'. Ну а самое главное - это добавление поддержки переменных, инициализируемых конструктором, например
int a( 90 );
SomeType b( "a_word" );
В текущей версии ctags такие переменные просто пропускаются, или иногда рассматриваются как прототипы функций (также см. описание бага здесь).

Патч можно взять здесь. Он может быть наложен как на последнюю стабильную версию ctags, так и на trunk. Вместе c патчем выложен тестовый файл test.cpp, на котором можно опробовать текущую и пропатченную версии ctags и сравнить их.

Если кто-то вдруг захочет опробовать патч и найдет регрессионные ошибки, то я попрошу по возможности прокомментировать их здесь (ну или на трекере ctags). Лично я проверял патч на довольно большом C++ проекте и серьезных регрессий не нашел. Напомню, что для включения в теги локальных переменных и прототипов функций, нужно в ctags добавить опцию --c++-kinds=+pl, кроме того, для получения максимальной информации о тегах можно добавить опцию c перечислением всех возможных полей: --fields=afmikKlnsStz.

пятница, 19 августа 2011 г.

Установка Firefox 6 и Thunderbird 6 в Fedora 14

Да, я так и не перешел на Fedora 15, так как не определился, чем заменить старый добрый Gnome 2. А пользоваться обновленными продуктами от Mozilla очень хочется. Так, недавно вышли очередные мажорные версии Firefox и Thunderbird, обе за номером 6. Естественно, в репозиториях Федоры их нет, зато они нашлись в репозитории Remi. Я не подключал данный репозиторий, поэтому просто скачал соответствующие пакеты (thunderbird, firefox и xulrunner6) и установил их. К сожалению, сразу же всплыла старая проблема с ужасными шрифтами без поддержки Byte Code Interpreter. Насколько я помню, исходный код библиотеки cairo был с изменениями включен в состав Firefox для сборки из-за какого-то странного бага с никому ненужными анимированными файлами gif. Благодаря этому, результирующий вид шрифтов стал зависеть не от того, какие системные библиотеки, отвечающие за рендеринг шрифтов, используются в вашей системе, а от того, какие библиотеки были у сборщика во время компиляции. Таким образом, товарищ Remi Collet при компиляции Firefox и Thunderbird использовал опцию, которая включает измененный вариант cairo, и кроме того, он располагал библиотеками для рендеринга шрифтов (fontconfig и libXft), которые поставлялись с Fedora 14 по умолчанию, что, в общем-то, логично и неудивительно.

Итак, проблема ясна. Будем лечить. Я не захотел собирать firefox и thunderbird с нуля, а решил воспользоваться SRPM пакетами из репозитория Remi. Найти их можно здесь. Очевидно, для того чтобы правильно пересобрать эти пакеты, нужно чтобы соответствующие правильные библиотеки были представлены в вашей  системе. Проще всего это сделать (если вы до сих пор этого не сделали и не используете репозиторий infinality), подключив репозиторий russianfedora-fixes и обновившись через yum update. После обновления будут установлены правильные fontconfig и libXft. Кроме того, для сборки нам понадобятся fontconfig-devel, libXft-devel и, разумеется, cairo-devel:
yum install fontconfig-devel libXft-devel cairo-devel
Перейдем к сборке rpm. Если вы еще не завели специального псевдопользователя для этой цели (а так рекомендует Федора), то сейчас самое время. От имени суперпользователя вводим
useradd mockbuild
Я назвал его mockbuild, хотя конкретное имя ничего не значит, можно выбрать на свой вкус. Задаем ему какой-нибудь пароль:
passwd mockbuild
Для того, чтобы пользователь-сборщик не появился в списке приглашения GDM, откроем файл /usr/share/gdm/gdm.schemas, найдем там строку <key>greeter/Exclude</key> и добавим в список пользователей ниже внутри тегов <default>...</default> нашего mockbuild.

Теперь нужно установить пакет для поддержки сборки rpm пакетов:
yum install rpmdevtools
Затем логинимся как mockbuild:
su - mockbuild
и создаем дерево директорий для сборки пакетов:
rpmdev-setuptree
Устанавливаем скачанные пакеты SRPM (от имени mockbuild, конечно) с помощью простой команды
rpm -ivh firefox-6.0-1.remi.src.rpm xulrunner6-6.0-1.remi.src.rpm thunderbird-6.0-1.remi.src.rpm
После этой команды исходники попадают в rpmbuild/SOURCES, а spec файлы - в директорию rpmbuild/SPECS. Переходим в директорию со spec файлами и правим. В файлах firefox6.spec и xulrunner6.spec после строки
echo "ac_add_options --enable-system-lcms" >> .mozconfig
добавляем строку
echo "ac_add_options --enable-system-cairo" >> .mozconfig
а в файле thunderbird.spec добавляем строку
ac_add_options --enable-system-cairo
где-нибудь между строками
cat <<EOF | tee -a .mozconfig
и
EOF
(например в самом конце, прямо перед строкой EOF).

Возможно, для сборки вам придется установить дополнительные пакеты. Так, мне пришлось установить autoconf213, lcms-devel, wireless-tools-devel и libvpx-devel. С помощью yum это сделать несложно.

Переходим к построению rpm, последовательно запуская команды
rpmbuild -ba xulrunner6.spec
rpmbuild -ba thunderbird.spec
В списке нет firefox, поскольку он зависит от построенного xulrunner6, поэтому, когда эти две команды отработают (а ждать нужно достаточно долго), переходим в директорию /home/mockbuild/rpmbuild/RPMS/your_arch, где your_arch - архитектура вашей системы (например, x86_64) и от имени суперпользователя обновляем свежесобранные пакеты:
rpm -Uvh thunderbird-6.0-1.fc14.x86_64.rpm  xulrunner6-6.0-1.fc14.x86_64.rpm xulrunner6-devel-6.0-1.fc14.x86_64.rpm
Если в вашей системе уже стоит xulrunner-devel, то его нужно будет предварительно удалить, так как он конфликтует с xulrunner6-devel. После установки новых пакетов возвращаемся в директорию SPECS и от имени mockbuild строим firefox:
rpmbuild -ba firefox6.spec
После завершения сборки снова возвращаемся в директорию RPMS и от имени суперпользователя обновляем firefox:
rpm -Uvh firefox-6.0-1.fc14.x86_64.rpm
После установки запускаем новые firefox и thunderbird и наслаждаемся правильным рендерингом шрифтов и проделанной работой :)

суббота, 13 августа 2011 г.

vim: практические шаги для комфортного использования 2

Продолжение темы советов по кастомизации vim, которые помогут повысить комфорт при использовании этого замечательного редактора. На этот раз ничего патчить не будем :)

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

Итак, маппинг по умолчанию: \m - подсветить слово, \n - снять подсветку, \r - ввести паттерн для подсветки. Этот маппинг ужасно тормозил (требовалось ждать около секунды, чтобы изменения вступили в силу), и я сначала думал, что это особенность реализации подсветки в Mark. Но проблема оказалась весьма тонкой. Выяснилось, что маппинг пересекается с маппингом плагина c.vim и еще одного установленного мною плагина. Так, в c.vim определено множество маппингов, начинающихся с \n и \r (\nc, \ns, \ni и т.д.). Таким образом, при вводе символов, соответствующих маппингу Mark, vim некоторое время ждал, а не введет пользователь еще один символ, который можно будет интерпретировать как маппинг из c.vim - это нормальное поведение vim.

Чтобы в следующий раз не наступать на грабли, можно посмотреть все маппинги, начинающиеся с определенного символа, скажем с \n (первый символ \ как таковой в маппинг не входит, так как является спецсимволом - так называемым лидером). Для этого нужно в командной строке vim ввести
:map \n
В новом маппинге я задействовал сочетание клавиш Ctrl-m:
nmap <C-m><C-m> <Plug>MarkSet
nmap <C-m>n     <Plug>MarkClear
nmap <C-m>r     <Plug>MarkRegex
vmap <C-m><C-m> <Plug>MarkSet
vmap <C-m>r     <Plug>MarkRegex
nmap <C-m>*     <Plug>MarkSearchCurrentNext
nmap <C-m>#     <Plug>MarkSearchCurrentPrev
nmap <C-m>/     <Plug>MarkSearchAnyNext
nmap <C-m>?     <Plug>MarkSearchAnyPrev
nmap <C-m>c     <Plug>MarkAllClear
nmap <C-m>t     <Plug>MarkToggle
nmap <C-m>s     :MarkSave<CR>
nmap <C-m>l     :MarkLoad<CR>
В принципе, тут все понятно: вместо \m нужно, удерживая клавишу Ctrl, два раза нажать m, вместо \n - нажать Ctrl-m, а затем n, вместо \r - Ctrl-m, а затем r. Кроме того, я добавил маппинг для переключения подсветки, которого нет по умолчанию: <Ctrl-m>t.

Update. По иронии судьбы Ctrl-m тоже плохой маппинг, так как это сочетание в vim полностью соответствует клавише Enter. Так что если оставить такой маппинг, то Enter в нормальном режиме будет так же тормозить, как раньше тормозил сам Mark. Клавиша Enter потенциально используется в vim достаточно часто, например в плагине taglist при переходе к метке под курсором. Поэтому в приведенных выше маппингах следует заменить все вхождения <C-m> на какое-нибудь другое, неиспользуемое сочетание клавиш, например на <C-k>.

2. Подсветка областей кода, не соответствующих принятому стандарту. Несмотря на грозное название, здесь имеется ввиду очень простая вещь. Допустим, вам требуется убедиться, что код, который вы в данный момент редактируете или только что открыли, не содержит знаков табуляции, все строки завершаются не пробельными символами и их длина не превышает 80 знаков. Для этого вам нужно иметь возможность подсвечивать паттерны регулярных выражений, соответствующих указанным требованиям. Сами паттерны не такие уж сложные, однако вы предпочитаете делать это простым нажатием пары клавиш и, кроме того, таким же простым нажатием убирать подсветку.

Здесь я покажу, как это сделать с помощью простого маппинга ,s (запятая - s), который будет включать или выключать соответствующую подсветку в зависимости от текущего состояния (т.е. реализует то, что в английском языке называется простым словом toggle).

Прежде всего объявим переменную g:RightBorder, которая будет содержать максимальную разрешенную длину строки (т.е. 80), и новую область подсветки FormatHints, с помощью которой мы будем подсвечивать код, не соответствующий стандарту:
let g:RightBorder = 80
highlight FormatHints term=standout ctermfg=250 ctermbg=229
            \ guifg=Red guibg=White
Определение FormatHints разделено на две строки как результат следования внутренней дисциплине и установленному правилу непревышения строкой 80 символов :) Здесь с помощью аргументов ctermfg и ctermbg определена подсветка для терминалов с поддержкой 256 цветов, которая, на мой взгляд, вполне подходит для темных цветовых схем (светло-серый текст на бледно-желтом фоне: в конце концов мы подсвечиваем "запрещенные" участки кода, поэтому светлое на светлом здесь вполне уместно). Поскольку я не использую GUI vim, значения для guibg и guifg выбраны произвольно.

Теперь определим функции для включения и выключения подсветки:
fun<SID>formathints()
    if !exists("w:m1") || w:m1 == 0
        let w:m1 = matchadd('FormatHints''\%>'.g:RightBorder.'v.\+'-1)
        let w:m2 = matchadd('FormatHints''[\t]'-1)
        let w:m3 = matchadd('FormatHints''[\t \r]\+$'-1)
    endif
endfun
fun<SID>formathints_hide()
    if exists("w:m1") && w:m1 > 0
        silentcall matchdelete(w:m1)
        silentcall matchdelete(w:m2)
        silentcall matchdelete(w:m3)
        let w:m1 = 0
        let w:m2 = 0
        let w:m3 = 0
    endif
endfun
С помощью переменных w:m1, w:m2 и w:m3 определяются паттерны, соответствующие областям кода, которые мы хотим подсвечивать. Приставка к переменной w: в vim означает, что будут созданы отдельные инстансы этой переменной в каждом отдельном окне vim, соответственно, наше переключение подсветки будет работать только в активном окне.

Теперь определим команды для включения и выключения подсветки:
command -bar ShowFormatHints call <SID>formathints()
command -bar HideFormatHints call <SID>formathints_hide()
Опция -bar нужна для использования команд в составе сложных выражений, содержащих элементы <Bar> в следующем итоговом маппинге:
nmap <silent> ,s :if !exists("w:m1") <Bar><Bar> w:m1 == 0 <Bar>
            \ ShowFormatHints <Bar> echo "Show format hints" <Bar> else <Bar>
            \ HideFormatHints <Bar> echo "Hide format hints" <Bar> endif<CR>
Напомним, что элементы <Bar> в маппингах расширяются в символ |, следовательно, без учета echo "Show format hints" и echo "Hide format hints", этому маппингу будет соответствовать команда
:if !exists("w:m1") || w:m1 == 0 | ShowFormatHints | else | HideFormatHints | endif
Если оставить этот маппинг без модификатора <silent> (и без дополнительных команд echo), то эта малоинформативная строка будет выводиться в командную строку vim каждый раз при нажатии ,s - добавление <silent> и команд echo делают использование этого маппинга намного более удобным за счет более информативных сообщений.

Итак, цель достигнута. Теперь при нажатии на клавиатуре комбинации ,s будет происходить переключение подсветки проблемных участков кода с выводом сопутствующей информации в командную строку vim.

3. Маппинг для переключения ограничительной колонки (colorcolumn). Цель почти такая же, как и в предыдущем пункте: создать маппинг для включения/выключения цветовой колонки, которая будет располагаться на 81-ом знаке и предупреждать пользователя о возможном нарушении стандарта кодирования, ограничивающего длину строк 80 знаками. Но реализовать эту задачу теперь проще:
nmap <silent> ,m :if &colorcolumn == 81 <Bar> set colorcolumn= <Bar>
            \ elseif !&colorcolumn <Bar> set colorcolumn=81 <Bar> endif<CR>
Новый маппинг соответствует комбинации клавиш ,m. На этот раз нет смысла выводить сообщения на дисплей, поскольку наличие или отсутствие ограничительной колонки заметно и так.

4. Маппинг для переключения режима paste. Режим paste бывает полезен, когда нужно вставить из буфера обмена многострочный участок кода без дополнительного форматирования. Маппинг соответствует комбинации клавиш ,p и выглядит следующим образом:
nmap <silent> ,p :set paste! <Bar> set paste?<CR>
Команда set paste! переключает (toggle) текущий режим paste, а команда set paste? выводит его значение на дисплей.

воскресенье, 26 июня 2011 г.

vim: практические шаги для комфортного использования

Ниже приводятся четыре практических совета, которые являются в большей степени методами устранения недостатков, присущих vim и его известным плагинам, нежели самостоятельными решениями. Поскольку в трех советах речь пойдет о модификации плагинов, будут представлены патчи, соответствующие текущим версиям этих плагинов. Патчи можно накладывать автоматически или вручную. В первом случае достаточно сохранить патч в файле, скажем c.vim.patch, затем перейти в директорию, в которой находится файл c.vim (в данном случае это $HOME/.vim/plugin/) и выполнить команду:
patch -p0 < /path/to/c.vim.patch
1. Поддержка директорий проектов в c.vim. Под поддержкой проекта будем понимать возможность использования различных шаблонов c.vim в зависимости от того, какому проекту принадлежит редактируемый файл. Как правило, файлы проекта расположены в отдельной директории, а сами проекты могут предъявлять разные требования к оформлению элементов, таких как заголовки файлов. В c.vim имеется поддержка отдельных проектов с помощью макроса STYLE (см. документацию c.vim), однако ее реализация имеет недостатки. В частности, этот подход подразумевает централизованное хранение всех шаблонов в директории ~/.vim/c-support/templates/. Нормальная поддержка проектов должна предполагать возможность хранения независимого набора шаблонов в директории проекта - это более гибкий и естественный подход.

Следующий патч (относительно c.vim версии 5.13) реализует возможность хранения шаблонов c.vim в директории проекта:
--- c.vim 2011-01-26 00:12:31.000000000 +0300
+++ c.vim.new 2011-06-26 13:07:16.000000000 +0400
@@ -327,6 +327,10 @@
 
 let s:C_SourceCodeExtensionsList = split( s:C_SourceCodeExtensions, '\s\+' )
 
+if exists( 'g:C_ProjectDirs' )
+ let s:C_ProjectList   = split( g:C_ProjectDirs, ',' )
+endif
+
 "------------------------------------------------------------------------------
 
 "------------------------------------------------------------------------------
@@ -2702,6 +2706,16 @@
     let s:C_FileVisited   = []
   let messsage     = ''
   "
+
+  for proj in s:C_ProjectList
+   if stridx(getcwd(), proj) != -1
+    if filereadable( proj."/.cvim.templates/Templates" )
+     call C_ReadTemplates( proj."/.cvim.templates/Templates" )
+     return
+    endif
+   endif
+  endfor
+
   if s:installation == 'system'
    "
    if filereadable( s:C_GlobalTemplateFile )
Здесь вводится новая глобальная переменная C_ProjectDirs, которая представляет собой список проектов, разделенных запятыми, для которых могут быть предложены отдельные наборы шаблонов. Например, в C_ProjectDirs добавим проект /path/to/proj1, далее в указанной директории создадим директорию .cvim.templates/, скопируем туда шаблоны из ~/.vim/c-support/templates/ и отредактируем по вкусу. Теперь при редактировании файлов в директории /path/to/proj1 и любой ее поддиректории будут использоваться новые шаблоны. При редактировании файла, не входящего ни в один из указанных в C_ProjectDirs проектов, будут использоваться шаблоны по умолчанию.

Переменную C_ProjectDirs можно определить в ~/.vimrc, например так:
let g:C_ProjectDirs = "/path/to/proj1,".$PROJ2WORKDIR
Здесь указаны 2 проекта: /path/to/proj1 и проект, определенный через переменную среды PROJ2WORKDIR, проекты разделены запятой.

2. Проблема взаимодействия fuzzyfinder и taglist. Taglist - самый популярный плагин vim. Он предоставляет отдельное окно для навигации по тегам исходного кода в открытых файлах. Это очень старый плагин и, к сожалению, давно не обновлялся, поэтому ему присущи недостатки, в частности, его сложно использовать при наличии нескольких табов (которые стали доступны в vim 7.0). Fuzzyfinder - очень интересный плагин, который легко использовать для навигации по файловой системе, тегам, закладкам и др. Особенностью данного плагина является то, что он не использует отдельное окно, а открывает временное окно поиска в левом верхнем углу терминала по запросу. К сожалению, при определенных настройках taglist, эти два плагина не дружат, что выражается в выводе нового буфера в окно taglist после закрытия окна поиска fuzzyfinder (см. здесь). Починить это поведение можно с помощью простого изменения в файле ~/.vim/autoload/l9/tempbuffer.vim (библиотека l9 поставляется вместе с fuzzyfinder, патч приведен относительно версии l9 1.1):
--- tempbuffer.vim 2010-09-28 23:55:28.000000000 +0400
+++ tempbuffer.vim.new 2011-06-26 13:13:54.000000000 +0400
@@ -22,7 +22,9 @@
   endif
   if bufnr('%') == s:dataMap[a:bufname].bufNr && winnr('#') != 0
     " if winnr('#') returns 0, "wincmd p" causes ringing the bell.
-    wincmd p
+    " my cmt: 'wincmd p' works badly with taglist - using 'wincmd j' instead
+    "wincmd p
+    wincmd j
   endif
 endfunction
 
@@ -43,7 +45,8 @@
 " a:listener:
 "   a:listener.onClose(written)
 function l9#tempbuffer#openScratch(bufname, filetype, lines, topleft, vertical, height, listener)
-  let openCmdPrefix = (a:topleft ? 'topleft ' : '')
+  " my cmt: open fuf buffer above current window ('leftabove' instead 'topleft')
+  let openCmdPrefix = (a:topleft ? 'leftabove ' : '')
         \           . (a:vertical ? 'vertical ' : '')
         \           . (a:height > 0 ? a:height : '')
   if !exists('s:dataMap[a:bufname]') || !bufexists(s:dataMap[a:bufname].bufNr)
Данный патч делает еще одно, на мой взгляд, полезное изменение в поведении fuzzyfinder: теперь окно поиска будет открываться не в левом верхнем угла терминала, а вверху окна, в котором осуществляется редактирование.

3. Поддержка ack в grep.vim. ack - это grep для пограммистов. Это очень удобная утилита для поиска текста в директории исходников, она знает о служебных директориях CVS, Subversion и др. и игнорирует их, поддерживает регулярные выражения perl и обладает другими полезными свойствами. В vim существует плагин, который поддерживает ack, однако он реализует простейшую обертку встроенного в vim grep и "мусорит" в stdout. Плагин grep.vim реализован лучше, однако не обладает поддержкой ack. Следующий патч реализует это поддержку в grep.vim (относительно версии grep.vim 1.9):
--- grep.vim 2011-06-26 13:19:02.000000000 +0400
+++ grep.vim.new 2011-06-26 13:22:22.000000000 +0400
@@ -317,6 +317,10 @@
     let Agrep_Path = 'agrep'
 endif
 
+if !exists("Ack_Path")
+    let Ack_Path = 'ack'
+endif
+
 " Location of the find utility
 if !exists("Grep_Find_Path")
     let Grep_Find_Path = 'find'
@@ -731,7 +735,7 @@
         let grep_opt = g:Grep_Default_Options
     endif
 
-    if a:grep_cmd != 'agrep'
+    if a:grep_cmd != 'agrep' && a:grep_cmd != 'ack'
         " Don't display messages about non-existent files
         " Agrep doesn't support the -s option
         let grep_opt = grep_opt . " -s"
@@ -749,6 +753,9 @@
     elseif a:grep_cmd == 'agrep'
         let grep_path = g:Agrep_Path
         let grep_expr_option = ''
+    elseif a:grep_cmd == 'ack'
+        let grep_path = g:Ack_Path
+        let grep_expr_option = ' -H --nogroup --nocolor'
     else
         return
     endif
@@ -763,7 +770,7 @@
                         \ g:Grep_Shell_Quote_Char
     endif
 
-    if filenames == ""
+    if filenames == "" && a:grep_cmd != 'ack'
         if v:version >= 700
             let filenames = input("Search in files: ", g:Grep_Default_Filelist,
                         \ "file")
@@ -777,8 +784,12 @@
 
     " Add /dev/null to the list of filenames, so that grep print the
     " filename and linenumber when grepping in a single file
-    let filenames = filenames . " " . g:Grep_Null_Device
-    let cmd = grep_path . " " . grep_opt . " -n "
+    if a:grep_cmd != 'ack'
+        let filenames = filenames . " " . g:Grep_Null_Device
+        let cmd = grep_path . " " . grep_opt . " -n "
+    else
+        let cmd = grep_path . " " . grep_opt
+    endif
     let cmd = cmd . grep_expr_option . " " . pattern
     let cmd = cmd . " " . filenames
 
@@ -810,6 +821,9 @@
 command! -nargs=* -complete=file Ragrep
             \ call s:RunGrepRecursive('Ragrep', 'agrep', 'set', <f-args>)
 
+command! -nargs=* -complete=file Ack
+            \ call s:RunGrep('Ack', 'ack', 'set', <f-args>)
+
 if v:version >= 700
 command! -nargs=* -complete=file GrepAdd
             \ call s:RunGrep('GrepAdd', 'grep', 'add', <f-args>)
Теперь, для того чтобы выполнить рекурсивный поиск вхождения sample в текущей директории из vim, достаточно ввести команду
:Ack sample
Тот же поиск, но во всех файлах с расширением .cc в текущей директории:
:Ack sample *.cc
4. Навигация между окнами с помощью комбинаций клавиш Ctrl-Arrow. Это очень полезный совет, который можно найти на многих ресурсах в сети. Суть идеи в том, чтобы при наличии нескольких открытых окон переключаться между ними простой комбинацией клавиш Ctrl-Up и Ctrl-Down. Такое поведение требует некоторой настройки, в частности, значение bufhidden должно быть равным delete, чтобы при навигации скрытые буферы (т.е. те файлы, которые ранее редактировались в открытых окнах, но были в дальнейшем скрыты новыми файлами) не открывались в отдельных окнах. Итак, настройки для данного поведения, которые следует поместить в ~/.vimrc выглядят так:
set switchbuf=usetab
autocmd BufEnter * setlocal bufhidden=delete
nmap <C-up>     :sbn<CR>
nmap <C-down>   :sbp<CR>
nmap <C-left>   :tabp<CR>
nmap <C-right>  :tabn<CR>
Последние два маппинга добавляют возможность навигации между табами по комбинациям клавиш Ctrl-Left и Ctrl-Right и не имеют отношения к рассматриваемой далее проблеме.

А проблема заключается в том, что если некоторый файл попал по той или иной причине в список bufhidden, то при его повторном открытии попасть в его окно с помощью комбинаций Ctrl-Up или Ctrl-Down становится невозможно. Это означает, что, скорее всего, vim не удаляет файл из списка bufhidden при его повторном открытии. Лечится это добавлением еще одной авто-команды в ~/.vimrc:
autocmd BufEnter * if empty(&buftype) | setlocal buflisted | endif
Update: Кажется редактор blogger превратил символы табуляции в патчах в пробелы :( Так что для автоматического наложения патчей теперь придется использовать опцию -l, например так:
patch -l -p0 < /path/to/c.vim.patch