Как стать автором
Обновить

Mein Konfig: экскурсия по dotfiles (часть 2)

Время на прочтение71 мин
Количество просмотров6.3K
Статья-продолжение первой части, в которой не уместилось всё что хотел описать. Напомню, что в ней я начал описывать своё рабочее окружение и dotfiles.




grep



Чаще всего less-ом приходится смотреть вывод результатов поиска GNU grep:

----- ~/.zshrc -----
GREP_ARGS=(
    --colour=always
    --with-filename
    --line-number
    --dereference-recursive
    --binary-files=without-match
    --exclude-dir=.git
    --exclude-dir=.tags
)
g() { grep $GREP_ARGS $@ | less }
GS() { grep $GREP_ARGS $@ | sort --numeric-sort | less }
alias -g G="| grep --colour=always"


Все опции говорят сами за себя. Отображать название файлов, показывать номера строк, включать цвета и перенаправлять всё в пейджер. Самый часто используемый pipe-aware алиас G: f bar G foo W будет, грубо говоря, аналогом find. -name "*bar*" | grep foo | wc -l. GS бывает используется для некоторых задач где важен порядок следования численно отсортированных результатов. А написан он полностью заглавными буквами, так как gs это Ghostscript, а набирая gS часто ошибаюсь в моменте нажатия вовремя Shift-а для заглавной «S».

Почему GNU grep, ведь у меня же есть grep из коробки в самой FreeBSD? Как бы не уважал GNU проект за его следование идеям свободного ПО, как бы ненавидел GNU проект за умение создавать монструозный по размерам, пухлый и переусложнённый софт, да ещё и регулярно с GNU-специфичными расширениями и форматами, но GNU grep на порядки быстрее *BSD версий. Когда-то его производительность была не такой и использовался популярный ack, написанный на Perl. По точно такой же причине использую GNU sed по умолчанию — он на порядок быстрее может отрабатывать.

.zshenv



В ~/.zshenv файле находятся многочисленные настройки переменных окружения. Большую его часть уже показал. Но пока ещё остаются белые пятна:

----- ~/.zshenv -----
path=(~/bin ~/local/bin /usr/local/bin /usr/local/sbin /usr/bin /usr/sbin /bin /sbin)
manpath=(
    ~/local/share/man
    /usr/local/lib/perl5/5.26/perl/man
    /usr/local/lib/perl5/site_perl/man
    /usr/share/openssl/man
    /usr/local/share/man
    /usr/local/man
    /usr/share/man
)
export -TU INFOPATH infopath
infopath=(~/local/share/info /usr/local/share/info /usr/local/info)

manpath=(~/src/suckless/tabbed $manpath)
path=(~/src/suckless/tabbed $path)

path=(~/work/goredo $path)

export -TU PKG_CONFIG_PATH pkg_config_path
pkg_config_path=(~/local/lib/pkgconfig ~/local/libdata/pkgconfig)

# C related {{{
export CFLAGS="-I$HOME/local/include -I/usr/local/include"
export LDFLAGS="-L$HOME/local/lib -L/usr/local/lib"
export -TU LD_LIBRARY_PATH ld_library_path
ld_library_path=(~/local/lib)
# }}}

# Go related {{{
export GOPATH=$HOME/work/gopath
path=($GOPATH/bin ~/work/go/bin $path)
# }}}


Всё это настройка путей до исполняемых файлов, man, info, pkg-config, и прочего. zsh умеет делать «связанные» переменные массивов со строковыми переменными с сериализованные представлениями этих массивов. Ведь PATH=foo:bar это, по сути, массив из foo и bar. Некоторые переменные априори встроены и известны (path, manpath), а на некоторые явно создаю binding через export -TU. Работа с массивами более удобна и error prone.

----- ~/.zshenv -----
export XDG_CACHE_HOME=/tmp/stargrave-xdg
export XDG_CONFIG_HOME=/tmp/stargrave-xdg
export XDG_DATA_HOME=/tmp/stargrave-xdg
export XDG_RUNTIME_DIR=/tmp/stargrave-xdg


Так уж получилось, но практически всё что делает freedesktop.org, мне противет всем Unix, DJB-way и suckless мировоззрениям. Хочу, чтобы всё что касается одной программы, находилось сосредоточенным в одном месте. В Unix мире плюс-минус так и было принято. XDG же делает совершенно противоположно. Благо, везёт, что почти все программы что по умолчанию уважают XDG переменные окружения — мне не интересны в плане хранения их состояний. Поэтому в tmpfs создаю директории для них, где оседают кэши и default конфиги.

Оставшаяся часть переменных окружения:

----- ~/.zshev -----
export SHELL=/bin/zsh
export EDITOR=vim
export TZ=Europe/Moscow
export IFCONFIG_FORMAT=inet:cidr,inet6:cidr
export SSH_AUTH_SOCK=$HOME/.ssh/agent
export MAILDIR=$HOME/mail
export DBUS_SESSION_BUS_ADDRESS=disabled:
export MYSQL_HISTFILE=/tmp/.mysql_history
export SHARNESS_TEST_SRCDIR=$HOME/local/stow/sharness/share/sharness


  • IFCONFIG_FORMAT заставляет выводить адреса ifconfig команды в CIDR формате, привычном и удобном для меня.
  • DBUS_SESSION_BUS_ADDRESS — запрет автоматического включения DBus демона. Очередное противящее мне freedesktop.org творение.


autoenv



----- ~/.zshrc -----
export AUTOENV_AUTH_FILE=~/.zautoenv-auth
. ~/work/zsh-autoenv/autoenv.zsh


Когда перехожу в директорию рабочего проекта, то хочу подготовить с ним работу. Для Python проектов это может означать включение virtualenv. Для проектов на Си это добавление/переопределение PATH, LD_LIBRARY_PATH. Когда-то создавал PROJ/.init файл, на который делал source (.-команда). Бывает что в него добавлял и cd в рабочую директорию проекта. То есть, сначала мне надо сделать source, а потом ещё возможно и cd в нужную поддиректорию. С «отменой» произведённых изменений переменных не парился — просто запуская очередной shell с чистыми переменными.

Нашёл готовые решения для этой задачи: direnv (который мне не нравится из-за громоздкости, сложности и негибкости) и zsh-autoenv. Но в исходном коде последнего много лишнего. Поэтому почти полностью переписал этот плагин, в основном с оптимизациями производительности и убиранием огромной части кода.

Теперь, если перехожу, например, в свой PyDERASN проект, даже в одну из его поддиректорий (cd ~pyd/tests), то в нём автоматически запустится:

----- ~pyd/.autoenv.zsh -----
venv
workon pyderasn3


----- ~/.zshrc -----
venv() { . /usr/local/bin/virtualenvwrapper.sh }


Активирующий virtualenv. Если покину иерархию ~pyd директорий, то автоматически запустится:

----- ~pyd/.autoenv_leave.zsh -----
deactivate


Но очень часто нужно изменять переменные окружения, которые бы неплохо потом восстанавливать до оригинального значения:

----- ~arbeit/.autoenv.zsh -----
autostash LD_LIBRARY_PATH MANPATH TESSERACT=ALTERED_STATE
ld_library_path=(
    ~/work/arbeit.ru/deps/libressl/lib
    ~/work/arbeit.ru/local/lib
    $ld_library_path
)
manpath=(~/work/arbeit.ru/deps/libressl/share/man $manpath)


autostash является единственной командой оставшейся в моей версии zsh-autoenv, запоминающей оригинальное значение переменных. Главное удобство autostash в том, что в .autoenv_leave.zsh не нужно вообще ничего писать для восстановления значений! Поэтому в данном arbeit проекте нет никакого .autoenv_leave.zsh. В отличии от direnv, всё эти файлы являются обычными zsh скриптами, а не очередным языком со своими командами. Можно и цвета поменять и приглашение командной строки и всё что придёт в голову.

А для пущей безопасности, zsh-autoenv требует явного одобрения запуска этих скриптов, сохраняя контрольную сумму в перманентном файле. Подсунуть .autoenv.zsh в git коммит для компрометации компьютера не выйдет.

.zprofile



Замечу, что для удобной работы в zsh не обязательно иметь кучу конфигурационных файлов на сотни строк. Конфиг для root-а состоит всего-лишь из:

----- /root/.zshrc -----
bindkey -v
bindkey "^[[1~" beginning-of-line
bindkey "^[[4~" end-of-line
export PS1="%2~%# "
export PAGER=less
export IFCONFIG_FORMAT=inet:cidr,inet6:cidr
export LESSHISTFILE=-
export XDG_CACHE_HOME=/tmp/root-xdg
export XDG_CONFIG_HOME=/tmp/root-xdg
export XDG_DATA_HOME=/tmp/root-xdg
export XDG_RUNTIME_DIR=/tmp/root-xdg
mkdir -p /tmp/root-xdg
umask 077


А чтобы научиться пользоваться множеством фич из серии !$:h:s/m/f/ (подстановка последнего аргумента предыдущей команды, у которого отрезан хвост (как правило, имя файла до директории) и в нём замена «m» на «f» — отнюдь не надуманный пример) — нужно читать документацию zsh. Я это делаю просто время от времени, со случайной позиции, находя что-то новое и интересное, пытаясь его использовать. Как правило, прочитать и узнать то можно о дюжине новых трюков, но в голове останется только пара — поэтому процесс регулярно надо повторять. Оно стоит того!

Последнее что покажу про zsh это процесс запуска и выхода:

----- ~/.zprofile -----
umask 077
[[ -r $LESSKEY ]] || lesskey -o $LESSKEY
[[ -e $XDG_DATA_HOME ]] || mkdir -p $XDG_DATA_HOME /tmp/stargrave-lldb /tmp/stargrave-flags
[[ -n "$XAUTHORITY" ]] || ~/bin/rem
~/bin/cleanup.sh


----- ~/.zlogout -----
~/bin/cleanup.sh


----- ~/bin/cleanup.sh -----
#!/bin/sh
rm -fr \
    ~/.*_history \
    ~/.cache ~/.config ~/.local* ~/.thumbnails \
    ~/.java \
    ~/zathura.core


Изначально zsh читает .zshenv, затем во время логина .zprofile, затем .zshrc, а при выходе .zlogout. Всё что касается интерактивной сессии должно находится в .zshrc. Каждый раз при логине делается очистка домашней директории от возможно появляющегося мусора.

----- ~/bin/rem -----
remind -g ~/rem/index.rem


----- ~/rem/index.rem -----
BANNER %
INCLUDE /home/stargrave/rem/monthly.rem
INCLUDE /home/stargrave/rem/payments.rem
INCLUDE /home/stargrave/rem/birthdays.rem
INCLUDE /home/stargrave/rem/domains.rem
INCLUDE /home/stargrave/rem/peryear.rem
INCLUDE /home/stargrave/rem/onetime.rem
FSET sortbanner(x) iif(x == today(), "***** Heute *****", "===== %b =====")


Для напоминаний о датах/событиях использую remind утилиту. Прежде пользовался родным calendar, но в нём нельзя настроить период за который необходимо заранее предупреждать о датах. Какие-то требуют предупреждения за месяц, а какие-то за 1-2 дня. Последняя строка конфигурации заставляет показывать через сколько дней произойдёт то или иное событие.

Почему некоторые команды у меня сделаны в виде алиасов, а некоторые в виде вынесенных в отдельный файл скриптов? Исключительно вопрос частоты их использования. Команды работы с git и ssh — очень частые, поэтому потребовали бы ощутимого overhead-а на ОС и ФС считывая и интерпретируя код из ~/bin/XXX скрипта. А загромождать namespace и память zsh редко используемыми алиасами тоже не гоже.

t



Календарь используется только для отдалённых дат, для ежемесячных вещей, типа «оплатить квартиру». А в качестве блокнота у меня t скрипт. Когда-то это была Python программа tnote. Но пришлось переписать, ибо запуск Python — достаточно медленная операция. Переписал на POSIX shell, а недавно на zsh, дабы почти полностью избавиться от вызовов внешних команд.

Это одна из самых часто используемых команд ежечасно! Она может сохранить заметку, показать их краткий список, удалить и отредактировать. Как только у меня появляется в голове хоть что-то, что нужно бы куда то записать, то сразу ввожу в любом свободном терминале, например, t a купить хлеба. И регулярно просматриваю и проверяю что у меня в заметках. Вот буквально сейчас у меня:

% t
[0] принтер (1)
[1] отсканировать фотографии (1)
[2] скопировать фотографии с фотоаппарата (1)
[3] ведьма блэр (1)
[4] nncp ck wait (8)
[5] forth book (1)


Когда посмотрю «один из самых страшных фильмов» (как гласит пресса), то сделаю t d 3. t e 4 отредактирует заметку под номером 4. А t 4 её полностью покажет (в данном случае она на восемь строчек). Кроме того, добавив N=XXX переменную окружения можно вести несколько namespace-ов, в которых веду эфемерные заметки по разным рабочим проектам, типа текущих IP-адресов и административных задач не требующих занесения в трэкер.

Когда у меня был Palm PDA, то с ним не расставался, записывая абсолютно всё что надо бы было не забыть или проверить. Без него у меня бы был блокнот с ручкой. t утилита именно для подобных задач. Если что-то не записал ручкой или клавиатурой, то могу забыть через полчаса — а так хотя бы помню факт записи. Это не долговременные заметки типа:

----- ~/notes/go-build-no-optimization -----
go build -gcflags="all=-N -l"
go build -ldflags=-s


Когда-то прилично использовал vimwiki плагин, дающий Wiki систему внутри Vim и даже рендерящую её в HTML файлы, на основе чего делал многие свои сайты. Но потом пришёл к выводу, что всё это переусложнение и мне достаточно простых текстовых файлов в ~/notes, календаря и «горячих» короткоживущих t заметок. А для сайтов лучше что-то другое использовать.

Музыка



Одна из утилит заслуживающая особое внимание — GNU parallel. Это скрипт на Perl, в котором использую наверное только 5% из всех возможностей, но простота его применения влюбляет. Множество CPU-hungry программ не распараллеливаются: например, мультимедиа конвертеры. А музыку обработать хочется побыстрее. Делаем parallel flac -d ::: **.flac чтобы, заняв все процессоры, запустить кучу flac декодеров. parallel opusdec {} {.}.wav ::: **.opus чтобы сделать из них Opus. Можно аудиокниги пределать в MP3 чтобы слушать на плеере:

parallel 'opusdec {} {.}.wav && lame --abr 128 {.}.wav && rm {.}.wav' ::: **.opus


Мультимедиа файлы частенько надо массово переименовать. Например, скачал с YouTube-а разные клипы от RetroAhoy и все они имеют общий ненужный (мне) префикс. Используем встроенный в zsh плагин zmv:

zmv 'RetroAhoy - (*)' '$1'


Все .jpeg переименовать в .jpg: zmv -W '*.jpeg' '*.jpg'

zmv '(*).MetalBand-AlbumName-(*)-(*).wv' '$1-$3-$2.wv'


Во всей иерархии директорий, во всех именах заменить пробелы на подчёркивания:

cur=`pwd` ; for i (**(/)) { cd "$i" ; zmv '*' '$f:gs/ /_' ; cd $cur }


Все эти примеры взял из своего ~/secure/.history, не надуманные.

Как у меня устроена музыкальная коллекция? Примитивно: каждый альбом в своей директории, каждый трэк в отдельном файле. Исполнитель-Год-Название альбома. Все пробелы в именах заменены на подчёркивания. Если это откуда то извне пришедшие MP3, то в них удаляю все IDv2 тэги: id3v2 -D *.

Я требую lossless вариант музыки! Очень даже запросто откажусь слушать её lossy версию, ибо с моими неплохими наушниками, усилителем и ЦАПом могу отличать 320Kbps -q 9 MP3 файл от lossless. И я получаю дикое удовольствие от хорошего звука. К сожалению, некоторые мною любимые жанры (лютый порно/копро грайндкор с харш нойзом) бывает невозможно нигде достать, даже нелегально скачивая, кроме как в этом MP3 (ё моё, XXI век, а всё до сих пор люди используют этот формат, ведь есть и свободные кодеки, в разы лучше сжимающие!).

Бывает, приходится доставать запись с пластинки, даже современных альбомов, так как на них бывает отличающееся сведение с более широким динамическим диапазоном. Это ценнее чем полнота частот и присутствующие потрескивания.

Кириллические имена могут быть неудобны из-за всяких архаичных ОС. Для транслитерации их имён использую один из своих старейших скриптов:

----- ~/bin/torn -----
#!/usr/bin/env perl
[...]
binmode STDOUT, ":utf8";
my $VERSION = "0.10";
my $src; my $dst; my $src_filename;

opendir DIR, "." or die "Can not open directory\n";
foreach (sort readdir DIR) {
    # Skip directory itself
    next if /^\.{1,2}$/;
    next if -d and not $ENV{FORCE_DIR};

    $src_filename = $_;
    $src = decode "utf-8", $src_filename;
    $dst = $src;

    # Basic corrections for music files
    $dst =~ s/ /_/g;
    $dst =~ s/_-_/-/g;
    $dst =~ s/_\(/\(/g;
    $dst =~ s/-\(/-/g;
    $dst =~ s/\)_/-/g;
    $dst =~ s/\,_/\,/g;
    $dst =~ s/\[//g;
    $dst =~ s/\]//g;
    $dst =~ s/\(_/\(/g;
    $dst =~ s/_\)/\)/g;
    $dst =~ s/\&/and/g;

    $dst =~ y/абвгдеёзийклмнопрстуфхцьъыэ/abvgdeezijklmnoprstufhcjjye/;
    $dst =~ y/АБВГДЕЁЗИЙКЛМНОПРСТУФХЦЬЪЫЭ/ABVGDEEZIJKLMNOPRSTUFHCJJYE/;
    $dst =~ s/ж/zh/g;
    $dst =~ s/ч/ch/g;
    $dst =~ s/ш/sh/g;
    $dst =~ s/щ/sch/g;
    $dst =~ s/я/ja/g;
    $dst =~ s/ю/ju/g;
    $dst =~ s/Ж/Zh/g;
    $dst =~ s/Ч/Ch/g;
    $dst =~ s/Ш/Sh/g;
    $dst =~ s/Щ/Sch/g;
    $dst =~ s/Я/Ja/g;
    $dst =~ s/Ю/Ju/g;

    # Lowercase file extensions
    if ($dst =~ /^(.*)\.([^\.]+)$/) {
        $dst = $1 . "." . lc $2;
    };

    # Change looking of track numbers
    if ($dst =~ /^(\d+)[-.]_*(.+)$/) {
        $dst = "$1.$2";
    };

    next if ($src_filename eq $dst);
    print "$src -> $dst\n";
    die "\"$dst\" exists" if -e $dst;
    rename $src_filename, $dst;
};


Для lossless сжатия использую WavPack. Судя по всему (я тут диванный эксперт), он лучше спроектирован и проще чем FLAC. Плюс на несколько процентов получше жмёт. Плюс не имеет ограничений на формат входных данных, типа поддержки 32-bit float, которые было мне встречались. На объёме в несколько сотен гигабайт музыки он мне дал несколько десятков гигабайт свободного места (преобразование из FLAC) — тоже приятно.

На работе, где не может идти речь про открытые наушники, а значит и хорошего качества звучания, вполне годится и lossy. Использую Opus кодек. Прежде был Vorbis, но Opus на десятки процентов лучше может сжать при схожем на слух качестве.

Когда ко мне попадают архивы с одним .flac и .cue файлами, то декодирую, cuebreakpoints *.cue | shnsplit *.wav командой разбиваю получившийся .wav на трэки, далее древнейшим скриптом (который бы надо переписать, но раз работает — не тронь) генерирую команды переименования на основе названий песен из CUE файла:

----- ~/bin/cueparser.sh -----
#!/bin/sh
c=1
iconv -f latin1 -t utf-8 "$1" |
sed -n 's/^  *TITLE .\(.*\)".*$/\1/p' | while read trackname; do
    v=`printf "%02d\n" $c`
    echo mv split-track$v.wav $v.\"$trackname\".wav
    c=$(( $c + 1 ))
done


После чего, сжимаю parallel wavpack -hh ::: **.wav командой. Иногда применяю нормализацию по пиковому уровню для всего альбома: normalize --peak *.wav. cdparanoia используется для считывания купленных аудио дисков.

mpv



В качестве проигрывателя, с самого момента рождения и до последних лет использовал mplayer+mencoder. Но mpv имеет лучшую поддержку аппаратного декодирования, существенно быстрее делает перемотку, плюс умеет gapless playback, подходя в качестве музыкального проигрывателя.

----- ~/.mpv/config -----
vo=gpu
profile=gpu-hq
hwdec=vaapi
hwdec-codecs=h264,hevc,vp8,mpeg2video,vc1
scale=ewa_lanczossharp
cscale=ewa_lanczossharp
cache=yes
cache-secs=60
display-tags=*
ytdl=no
gapless-audio=yes
autosync=30
oss-mixer-device=/dev/mixer
oss-mixer-channel=vol

[AO]
audio-device=oss//dev/dsp3

[AM]
audio-device=oss//dev/dsp2
oss-mixer-device=/dev/mixer2
oss-mixer-channel=pcm

[VN]
af=lavfi=loudnorm

[NV]
video=no


----- ~/.mpv/input.conf -----
PGUP seek 600
PGDWN seek -600
Shift+PGUP add chapter 1
Shift+PGDWN add chapter -1
ENTER playlist-next force
BS set speed 1.0
VOLUME_UP run "mixer" "-f" "${oss-mixer-device}" "${oss-mixer-channel}" "+2"
VOLUME_DOWN run "mixer" "-f" "${oss-mixer-device}" "${oss-mixer-channel}" "-2"


Настройки клавиш ввода связаны с моей 20-летней привычкой использования MPlayer. Проще перебить клавиши, чем перепривыкать. В начале статье упоминал про ~/.Xmodmap файл, в котором как раз были binding-и мультимедиа клавиш моей клавиатуры.

В основном конфиге mpv настройки аппаратного декодирования, буферизации, gapless проигрывания и разные профили связанные с выходными источниками звука и применением нормализации громкости. Почему эти профили имеют такие короткие названия?

----- ~/bin/mp -----
#!/usr/bin/env perl
exec "mpv", map { s/^([A-Z][A-Z])$/--profile=$1/ ; $_ } @ARGV;


Потому что запускаю mp скрипт, преобразующий двухбуквенные слова-аргументы в --profile=XX. mp AM VN ... сделает вывод звука на HDMI монитора с нормализацией звука.

Для кодирования/перекодирования в mencoder уже стало не хватать возможностей. Всегда сторонился FFmpeg проекта, видя его непонятные аргументы, считая что любое действие с ним это сплошная боль. Но познакомившись поближе, всё же потеплел. Со всеми задачами справляется, хотя и не без помощи самого большого ~/notes/cmd/ffmpeg. mpv меня напрягал ещё тем после mplayer, что в нём нельзя было делать такие вещи как dump звукового потока (если ничего не путаю) — теперь много подобных задач переехало на ffmpeg.

Применений у ffmpeg у меня тьма. Одно из самых частых это «перекомпоновка» контейнера, убирая из него как можно больше метаинформации, оставляя только определённые аудио/видео дорожки:

ffmpeg -i IN -map 0:v:0 -map 0:a:0 -sn -map_metadata -1 -codec copy -y OUT


Контейнер AVI оставляю в покое. А вот никаких MP4 не позволю себе иметь, переконвертируя в Matroska. Хочу поддерживать форматы и кодеки созданные людьми для людей, а не корпорациями для DRM-а. Частенько Matroska/WebM контейнеры надо создавать из отдельных аудио и видео файлов (отдельно сжал Opus-ом аудио, отдельно закодировал в VP8/VP9 vpxenc-ом видео) — для этого использую MKVToolNix инструментарий. Например очистка Matroska контейнера от метаинформации, которая или должна быть заполнена идеально (нереальный вариант) или пускай её не будет вовсе, делается так:

----- ~/bin/mkvclean.sh -----
#!/bin/sh
mkvpropedit \
    --edit info \
    --delete title \
    --delete date \
    --set muxing-application=- \
    --set writing-application=- \
    --chapters "" \
    --tags all: $@


Как смотрю 4K видео? Которое в сжатом видео гарантированно не сможет у меня быть декодировано в real-time? Перекодирую как можно быстрее с уменьшением разрешения в MPEG2, имеющим (относительно) малые расходы на CPU и экономно тратит дисковое пространство:

----- ~/bin/4k2mpeg2.sh -----
#!/bin/sh
ffmpeg -i "$1" -vf scale=w=iw/4:h=ih/4 -c:v mpeg2video -b:v 10000k -y "$1".mpg


Картинки



Для работы с изображениями использую NetPBM набор Unix-friendly утилит. Для сканированных книжек DjVu формат (DjVuLibre программа) незаменим. cjpeg -optimize -progressive для получения JPEG. Для оптимизации уже существующих JPEG: jpegtran -optimize -progressive -perfect -copy none. pngcrush -rem alla -rem allb для попыток сжатия PNG. Для снимков экрана lossless WebP (cwebp -pass 10 -alpha_filter best -m 6 -mt -q 100 -lossless) формат может дать на 50-60% лучшее сжатие!

JPEG полученные из сторонних источников конечно же стараюсь не пересжимать, ибо ещё бОльшая потеря будет, но удаляю EXIF информацию из них: exiftool -all= -overwrite_original.

А недавно стал поклонником JPEG2000 формата, особенно с учётом иссякания патентов на него. Все отсканированные фотографии храню в OpenJPEG lossless сжатом виде: opj_compress -t 1024,1024 -i $i -o $i:r.jp2. Во-первых, отсканированные изображения с большим DPI даже не уместятся в какой-нибудь WebP, так как в нём ограничение размеров картинки. Во-вторых, .jp2 файл на треть меньше места занимает чем PNG. Да и в целом wavelet сжатие мне импонирует. Но приходится ограничивать размер tile, ибо будет уж слишком большое потребление оперативной памяти. Из-за его ресурсоёмкости на огромных по размерам отсканированных изображениях, рядом делаю .webp preview небольшого разрешения.

Просматриваю изображения sxiv программой: минималистична, быстра, поддерживает rotate, zoom, и показ множества картинок в виде таблицы из preview. Удовлетворяет всем, кроме поддержки JPEG2000 формата (но это претензия к Imlib2).

Для чтения PDF, лучше чем Zathura ничего не видел! Удобнейшие vi-клавиши, возможность показа презентаций, копирования и поиска текста. Стоят плагины для просмотра EPUB и DjVu форматов. Крайне важно для меня использовать MuPDF backend для PDF файлов, так как он может работать, без преувеличений, на порядок быстрее Poppler! Речь про то, что в Poppler можно секунд десять ждать когда отобразится жирная иллюстрация, тогда как в MuPDF она появится без ощутимых задержек. Плюс MuPDF умеет конвертировать FictionBook2 в формат пригодный для чтения внутри Zathura.

----- ~/.zathurarc -----
set sandbox strict
set dbus-service false


Zathura не идеальна — она тянет по зависимости GTK3 и DBus. Но… пока это меньшее из зол с которым готов смириться, ибо уж очень удобный просмотрщик.

Из коробки в ней нет никаких tab-ов. А иногда приходится читать параллельно с десяток документов. Стэковое представление окон в dwm будет не удобно. Zathura поддерживает XEmbed расширение X11, позволяющее одним программам («tab manager») включать в себя дочерние embeddable окна. Unix-way! Использую tabbed suckless менеджер для этой задачи. На данный момент, кроме Zathura, больше GUI программ для tab-ов у меня нет, хотя изначально туда встраивал и Lynx (когда не догадался что tmux достаточно).

----- ~/bin/zat -----
#!/bin/sh
export PATH=$HOME/local/bin:$PATH
export XDG_DATA_DIRS=$HOME/local
export LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH
winid=`start-tabbed.sh zathura -c`
bin.zathura -e $winid "$1"
rm -f zathura.core # ага, zathura любит делать coredump


----- ~/bin/start-tabbed.sh -----
#!/bin/sh
tabname=$1
shift
w=/tmp/tabbed-$tabname
[ -r "$w" ] && {
    read winid < $w
    wmclass=`xprop -id $winid WM_CLASS 2>/dev/null`
    echo "$wmclass" | grep -q tabbed-$tabname && {
        echo $winid
        exit
    }
}
[ -z "$NOTABSTART" ] || exit 1
tabbed -n tabbed-$tabname -d $@ > $w
exec $0 $tabname $@


~/bin/start-tabbed.sh выглядит сложным для простого запуска Zathura, но когда-то он использовался и для других. Его задача либо добавить окно в уже запущенный менеджер, либо запустить его с нуля.

zsh кстати поддерживает функционал по запуску программ на основе расширений файлов. Можно «исполнить» .pdf и автоматически откроется Zathura. Но что-то у меня не появилось привычки открывать программы таким образом. А всё что не начинает активно мною используется — удаляется, дабы не мозолить глаза и не усложнять конфиги на пустом месте.

Блокировку экрана делаю вызовом mylock скрипта из dmenu:

----- ~/bin/mylock -----
#!/bin/sh
xset dpms force off
gpgconf --reload gpg-agent
SSH_AUTH_SOCK=$HOME/.ssh/agent ssh-add -D
slock


Он «отключает» электропитание монитора и очищает ключницы GnuPG и SSH агента, блокируя экран suckless slock утилитой.

Архивы



Для форматов архивов у меня есть чёткое предпочтение: POSIX.1-2001 pax формат, который обратно совместим с ustar. Чисто технически это ustar, внутри которого есть особые директории с keyvalue атрибутами, хранящими длинные имена файлов, точные временные штампы и другую метаинформацию. К тому же, этот формат является стандартом, а не каким-нибудь GNU-специфичным расширением.

Я понимаю что есть задачи где потоковая природа tar будет мешать. В принципе, ничего не имею против ZIP64 для этого, у которого есть индекс файлов. Но в нём не сохранить Unix-специфичные атрибуты файла.

Но вот беда: tar API не сказать что Unix-way friendly, сравнивая с cpio, в котором работа с путями и поиск файлов это задача сторонней программы (find). POSIX стандарт описывает как сам pax формат, так и pax архиватор, имеющий cpio-like интерфейс. cpio имеет существенные ограничения формата, не позволяя многое заархивировать (длинные имена файлов, размеры файлов), а pax выглядит как лучшее из двух миров (API и формат). Но почему то исторически так сложилось, что pax нету в большинстве дистрибутивах. А даже если и сама команда имеется, то не факт что она поддерживает pax format.

Благо, всё же tar можно использовать в cpio-like режиме, читая файл со списком файлов из stdin: find… | tar cI — .... Более того, по умолчанию libarchive, используемый в FreeBSD для архиваторов из коробки, делает ustar формат архива, если никакие метаданные не будут потеряны, и только в случае потерь он будет делать pax.

tar используется мной и для записи/чтения данных на LTO5 стример. В курсе, что LTO5 уже поддерживает LTFS, но даже не пытался пробовать использовать ленты в этом режиме. Мне действительно хватает обычного потокового чтения заранее сжатых (и зашифрованных) архивов с диска.

Для сжатия я ярый поклонник Zstandard алгоритма. Работая существенно быстрее чем zlib/gzip, он ещё и лучше сжимает. Это если самый банальный простой вызов zstd команды через pipe. Он может из коробки распараллеливаться, занимая все CPU. А указывая высокие степени сжатия (--ultra -22), можно лишь на считанные проценты сжать похуже чем xz, который использовал прежде.

Но основная причина моей любви к Zstandard — умопомрачительная скорость декомпрессии. До zstd, даже не подозревал как часто упирался, при распаковке какой-нибудь программы, в CPU. Фактически CPU всегда был бутылочным горлышком, тогда как с zstd упираюсь в диски. Кроме того, с параметрами по умолчанию не упираюсь в CPU даже при созданий резервных копий: zfs send -R… | zstd | gpg -e -r offline -z 0 --force-aead, а только в скорость своей SATA SSD. FreeBSD уже уйму лет имеет zstd из коробки, поддерживая это сжатие даже в демоне ротации логов.

По сути, остаётся либо gzip ради совместимости, либо zstd для всего остального: и для быстрого сжатия и для архивного хранения с сильной степенью сжатия.

Feeds



Кроме почтовых рассылок, личной почтовой переписки, иногда синхронного общения по IRC, получаю более 400+ новостных рассылок в RSS и Atom форматах. Много лет использовал rss2email программу, которая отправляла новости в виде почтовых сообщений, попадающих в отдельный почтовый ящик. Но всё же вернулся на Newsboat.

----- ~/.newsboat/config -----
history-limit 0
text-width 80
bind-key ^Y up
bind-key ^E down
notify-beep yes
cleanup-on-quit yes

max-items 100

auto-reload yes
reload-time 120
reload-threads 10
suppress-first-reload yes

feed-sort-order lastupdated
browser "~/bin/www %u &"


  • Binding-и колеса прокрутки.
  • Beep оповещения когда появляются новости.
  • Проверка всех feed-ов каждые два часа в десять HTTP потоков.
  • Хранение только сотни последних новостей. Когда надолго уезжаю (оставаясь без компьютера), то эту опцию комментирую, ибо за неделю на некоторых ресурсах гораздо больше новостей приходит.
  • Открывается www броузер: в зависимости от флагового WG файла это либо Lynx, либо Xombrero (об этом писал выше).


Знание о всех своих подписках храню в recfile формате, также выложенном на домашней страницы. Пример записей:

----- ~hp/links.rec -----
Title: ImperialViolet
Note: Adam Langley's blog
URL: https://www.imperialviolet.org/
Category: Crypto
Category: IT
Category: Personal
Feed: https://www.imperialviolet.org/iv-rss.xml

Title: Few thoughts of cryptographic engineering
Note: Matthew Green's blog
URL: https://blog.cryptographyengineering.com/
Category: Crypto
Feed: https://blog.cryptographyengineering.com/feed/


Тут хранятся и ссылки на сами «домашние» страницы, так и ссылки на feed-ы, которые могут жить на совершенно разных доменах. Я стал поклонником recfile формата (и recutils утилит для работы с ним) из-за простоты как для компьютера, так и удобства для человека. Плюс возможностью задавать простую схему валидации записей. Можно тривиально сделать выборку всех feed URL-ов для вставки в ~/.newsboat/urls: recsel -P Feed < links.rec | sed /^$/d. Кроме того, на Go написал программу преобразующую этот .rec файл в XBEL и OPML форматы обмена ссылками. Мне так понравился формат, что применяю его для хранения состояния зависимостей в goredo, для журналов NNCP и некоторых рабочих проектах.

Hjson



Раз уж идёт речь про форматы, то не могу не упомянуть про любовь к Hjson. Это вечная holywar тема о том какой формат лучше для конфигурации ПО. Всё сильно зависит от сложности данных внутри. Где-то может и .ini подойти, и CSV, и termcap-like формат (много где используемый в FreeBSD).

Я точно ярый противник YAML: чрезвычайно переусложнён и библиотеки для работы с ним чаще превосходят по размерам саму основную программу. Мог бы привести вагон примеров когда чёрт ногу сломит человеку понять что же там написано и попытаться интерпретировать все эти структуры с кучей способов записи.

Я и противник TOML, опять же, на практике пробовав в своих проектах. Очень простые структуры в нём хорошо читаются. Но чуть более сложные, типа списков внутри словарей — увольте, но я, даже зная наизусть структуры своего NNCP, не мог самостоятельно корректно прописать это всё с первого раза.

А чем так плох JSON, если забыть про сложность (относительно termcap, ini) его парсинга? Невозможностью добавить комментарии, унылыми кавычками в ключах словарей, запятой в конце списков. Хочется тривиального небольшого синтаксического сахара. Hjson именно его и добавляет. Библиотека парсинга Hjson убирает этот сахар, превращая Hjson в обычный JSON, который можно отпарсить родной библиотекой. Золотая середина для конфигов со сложными структурами!

*tex*



Что думаю про формат документации? Однозначно GNU Texinfo и Info! man подходит только для простых страниц — для кратких сводок опций. Попробуйте попутешествовать по man-у zsh! Вариант с man -P «less +/^EXAMPLES» считаю костылём. Отсутствие ссылок, разделения на секции по которым можно было бы перемещаться. Info — гипертекстовый документ, с разделением на секции/ноды. Плюс .info файл можно смотреть даже без специализированного броузера. Можно иметь ссылки и на картинки, которые в GUI Emacs бы отобразились. Похожая идея в CHM файлах Windows.

----- ~/.infokey -----
#info
j next-line
k prev-line
^E next-line
^Y prev-line
^W split-window
^N next-window

#var
link-style=yellow
active-link-style=yellow,bold
match-style=underline,bold,nocolour
scroll-behavior=Page Only


Чуть больше синтаксической подсветки, отключение автоматического перехода на следующие секции (это предоставляется как фича, но мне ужасно неудобное поведение), работоспособность колеса прокрутки и немного vi-like клавиш управления.

Это всё речь про форматы которые просматриваются. А формат исходного текста уже может отличаться. И man (mdoc) и и HTML тоже можно набирать сразу же от руки. А можно использовать какой-нибудь reStructured Text (reST), который и HTML и даже .info в состоянии сгенерировать. Уйму лет web-сайты делал на reST.

Как отношусь к Markdown? Никак не могу относиться к тому чего нет: нету никакого Markdown формата — есть только куча диалектов, частично совместимых между собой. И их общая часть — крайне скудна и не достаточна. К AsciiDoc у меня негативного отношения нет, но и каких-то плюсов относительно reST не вспомню — оба даже написаны на Python.

Но от reST в итоге отошёл (применяю разве что для Python проектов, ибо это родной формат в этой экосистеме). Не всегда хватает возможностей «самовыражения», типа «сделать жирную ссылку». Чуть более сложные вложенные структуры уже проблематично прописывать и генерировать автоматизированно, ибо требуется быть очень аккуратным с отступами и это становится сложно воспринимать с экрана.

Texinfo оказался золотой серединой между по человечески выглядящему reST и полного SGML тэгами HTML! Легко читать, легко писать, низкий порог входа, легко генерировать, свобода самовыражения и гибкость. Он действительно более близок и похож на TeX. Плюс написан на Си/Perl — мало зависимостей и отличная скорость работы. Почти все web-сайты генерирую из Texinfo кода. Единственное, что иногда создаёт неудобства, так это его заточенность под иерархичную структуру документов.

Ещё есть свойство спорной значимости: Texinfo может генерировать Docbook, очень приличного качества форматирования. Открыв каким-нибудь LibreOffice, можно сохранить в Office Open XML документ, почти без потерь форматирования задуманного автором .texi.

reST из коробки умеет читать docstring-и Python кода. Doxygen умеет из Си кода. Texinfo из коробки нет, но не проблема: docstringer.pl Perl скрипт находит в файлах комментарии такого вида, в которых есть пометка TEXINFO: XXX:

----- .../app.h -----
[...]
// TEXINFO: CrickAppSignVerify
// @deftypefun CrickErr CrickAppSignVerify @
//     (CrickApp *app, const CrickFileId fileId, @
//      const uint8_t *pub, const size_t pubLen, @
//      const uint8_t *dgst, const size_t dgstLen, @
//      const uint8_t *sign, const size_t signLen)
// Verify @var{sign} signature made on @var{dgst} digest with @var{pub}
// 34.10-2012-256 public key. @ref{CrickErrInvalidMACSignature} error is
// returned if signature is wrong.
// @end deftypefun
CrickErr
CrickAppSignVerify(
    CrickApp *,
    const CrickFileId,
    const uint8_t *pub,
    const size_t pubLen,
    const uint8_t *dgst,
    const size_t dgstLen,
    const uint8_t *sign,
    const size_t signLen);
[...]


А в исходном Texinfo коде, содержащим @DOCSTRING CrickAppSignVerify@, будет сделана подстановка тела комментария.

Как бы много чего монструозного не было в GNU проекте, но их обязательное требование использования Texinfo/Info всецело одобряю.

Рабочие отчёты для печати создаю в LaTeX, используя TeX Live дистрибутив, чтобы не было раздумий какой CTAN пакет и как мне надо устанавливать. В нём же писал все курсовые, диплом и всё подобное. Схемы, картинки ни разу в жизни не рисовал «от руки» (мышкой что то там расставлять на экране) — только PGF/TikZ, GraphViz и Gnuplot. Только чертежи выполнял в QCAD. Почти все презентации выполняю в Beamer пакете LaTeX.

У меня стоит OpenJDK исключительно ради запуска PlantUML. Меня коробит от столь тяжёлой зависимости (Java), но PlantUML настолько хорош и прост в использовании, создавая и прекрасные растровые и Unicode текстовые диаграммы! Созданные им диаграммы у меня встречаются практически в каждой программе.

File transfer



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

----- ~/bin/mirror-site.sh -----
#!/bin/sh
user_agent="Mozilla/5.0 ..." # некоторые Wget считают роботом и не дают доступа
name=$1
shift
mkdir -p $name
cd $name
wget \
    --page-requisites \
    --convert-links \
    --adjust-extension \
    --restrict-file-names=ascii \
    --span-hosts \
    --random-wait \
    --execute robots=off \
    --recursive \
    --timestamping \
    -l inf \
    --no-remove-listing \
    --no-parent \
    --user-agent "$user_agent" \
    --reject '*.woff*,*.ttf,*.eot,*.js' \
    --tries 10 \
    $@


wget умеет создавать и Web ARChive .warc файлы, которые затем можно просматривать через WARC-прокси:

----- ~/bin/mirror-site-warc.sh -----
#!/bin/sh
mirror-site.sh "$1" \
    --warc-file $name-$(date '+%Y%M%d%H%m%S') \
    --no-warc-compression --no-warc-keep-log $@


Если Web-сервер предоставляет WebDAV, или есть FTP зеркало (не говоря про rsync), то использую LFTP, как например:

echo "mirror --delete --continue . if-archive" | lftp -d ftp.ifarchive.org


ZFS тут может здорово помочь в создании атомарных обновлений раздаваемых директорий. Сам HTTP/FTP сервер может смотреть на snapshot по /path/.zfs/snapshot/SNAPNAME пути, а долгое rsync/lftp обновление идёт просто в /path. Когда зеркалирование завершено, то новую версию snapshot над обновлённым состоянием можно переименовать в /path/.zfs/snapshot/SNAPNAME.

Ещё бы рекомендовал aria2 менеджер скачивания. Использую его как BitTorrent клиент, а также когда нужно скачивать множество файлов, с возможностью докачки, в несколько потоков.

----- ~/.aria2/aria2.conf -----
max-concurrent-downloads=200
ca-certificate=/etc/ssl/cert.pem
bt-max-open-files=1000
bt-max-peers=0
bt-save-metadata=true
enable-dht=true
enable-dht6=true
seed-ratio=0.0
disk-cache=0
file-allocation=trunc
force-save=true


Важно переопределить file-allocation, который по умолчанию полностью создаёт пустой файл перед скачиванием, что бессмысленно на современных copy-on-write файловых системах.

Из BitTorrent клиентов пробовал ctorrent — нравился, но нет DHT и UDP поддержки. Transmission — ресурсоёмкий, не нравился. Rtorrent долго использовал, пока не наткнулся на его hard-coded ограничения на максимальные размеры файлов/торрентов. К aria2 у меня вообще никаких нареканий. С ней использую немного изменённый (добавил команды просмотра подключённых peer-ов и их прогресс) diana frontend.

Games



Последние лет десять, играю время от времени (отдых же нужен) в:

  • FreeCiv стратегию.
  • NetHack RPG, в которой только недавно узнал про существенное удобство curses интерфейса:

    ----- ~/games/nethack/.nethackrc -----
    OPTIONS=gender:male
    OPTIONS=windowtype:curses,color,menucolors
    OPTIONS=boulder:0
    OPTIONS=!autoopen,autopickup,pickup_types:$
    OPTIONS=hitpointbar,statushilites=3,showexp,force_invmenu,perm_invent,statuslines:3
    OPTIONS=hilite_pet,hilite_pile,lit_corridor
    OPTIONS=pettype:none
    
    MENUCOLOR="blessed"=green
    MENUCOLOR=" cursed"=red
    
    OPTION=hilite_status: gold/up/yellow/down/brown
    OPTION=hilite_status: characteristics/up/green/down/red
    OPTION=hilite_status: hitpoints/100%/gray&normal
    OPTION=hilite_status: hitpoints/<100%/green&normal
    OPTION=hilite_status: hitpoints/<66%/yellow&normal
    OPTION=hilite_status: hitpoints/<50%/orange&normal
    OPTION=hilite_status: hitpoints/<33%/red&bold
    OPTION=hilite_status: hitpoints/<15%/red&inverse
    OPTION=hilite_status: condition/major/orange&inverse
    OPTION=hilite_status: condition/lev+fly/red&inverse
    WIZKIT=/home/stargrave/games/nethack/.wizkit # да, ни разу честно не победил
    


    Эта игра мне не нравится только одним: на неё не получится отвлечься на пару часов — придётся убить хотя бы день, даже с учётом использования wizard-режима и wizkit.
  • ScummVM машина в которой идут сотни потрясающих игр! Все квесты от LucasArts (поклонник именно их творений, а не Sierra) прошёл в этом эмуляторе. А недавно в него встроили ResidualVM и в этом году прошёл The Longest Journey. Одни из самых лучших моментов жизни связаны именно с ScummVM и ResidualVM.
  • Не так часто, как хотелось бы, но запускаю interactive fiction игры, почти всегда в Frotz Z-машине.

    Одним из первых шагов, при прохождении больших Infocom-овских игр, было рисование карт от руки. Через какое-то время сдался (рисовать/чертить не люблю) и попробовал это сделать через GraphViz. Далее написал Perl скрипт который декларативные инструкции по рисованию переводил в Dot язык. И только после задался вопросом о существовании готовых инструментов для этой задачи. С тех пор использую Interactive Fiction Mapper.


БД



Всё написанное выше: про базовую работу в ОС, общение и развлечения. Про саму работу речи ещё не было. А это написание кучи кода, тестов, скриптов и документации. А также работа с СУБД:

----- ~/.sqliterc -----
.bail on
.mode column
.headers on


----- ~/.psqlrc -----
CREATE OR REPLACE FUNCTION ppj(jsonb) RETURNS TEXT AS $$
    SELECT jsonb_pretty($1);
$$ LANGUAGE SQL;
CREATE OR REPLACE FUNCTION ppj(jsonb[]) RETURNS SETOF TEXT AS $$
    SELECT jsonb_pretty(unnest) FROM (SELECT unnest($1)) AS dummy;
$$ LANGUAGE SQL;

\pset linestyle unicode
\pset footer off
\timing on
\set HISTFILE ~/secure/.psql_history


Из интересного тут можно отметить только функции pretty printing JSONB полей PostgreSQL. Для редактирования SQL запросов активно применяю внешний редактор через \e вызов.

ЯП



Начинал программировать с Perl, имел многолетний опыт с Ruby, PHP, Lua. Но основная работа была проделана на Python. А знакомство с Go коренным образом поменяло моё отношение ко всему остальному. Считаю этот язык неким Святым Граалем: продуманный до мелочей, крайне простой и лёгкий в поддержке и понимании кода — это самое главное. Всё остальное, если не имеет простоты и понятности — не играет роли. Это лучший язык для промышленного программирования из существующих, заточенный не для выпендрёжа, а для решения задач. Go (и его авторы) даёт не то что люди хотят, а то что им действительно нужно.

Недавно заметил, что на Python перестал писать даже простейшие скрипты типа преобразования моего .rec файла с ссылками в другие форматы: всё равно без раздумий использую Go. И на серверах у меня полностью исчезли интерпретаторы Python.

К Ruby, PHP и Lua уже не притрагиваюсь. Ничего против них не имею, но просто нет задач. Python до сих пор используется из-за большой кодовой базы на нём написанной. Мне не нравится путь развития и самого интерпретатора и в целом всей Python экосистемы, где даже просто установить пакет из PyPI может быть тем ещё приключением из-за несовместимых версий зависимостей и ОС-специфичных setup.py файлах.

Чуть более года назад, начал программировать на Си. И это дело мне очень нравится! Go, безусловно, остаётся «Си каким он должен был бы быть».

Не проходит и недели чтобы не было бы что-то написано на Perl. Крайне удручает существование множества негативных мифов о нём. Насколько понимаю, рождённых исключительно из голов не сильно квалифицированных системных администраторов, которым чаще всего проходилось иметь с ним дело в 90-х годах, так как не больно много других интерпретируемых языков было. Человечество ещё не придумало что могло бы занять его нишу. Писать большие проекты наверное я бы не рекомендовал, но программы до 2-х экранов кода, особенно связанных с обработкой текста — самое оно.

Я негативно отношусь к использованию awk. Во-первых, как и в случае с sed и grep — есть разные диалекты: код написанный для gawk не будет работать на *BSD системах, где его нет и не будет из коробки. Проблема совместимости и портируемости. Во-вторых, в 99.99% случаев люди используют 0.1% его возможностей, не уходящих дальше {print $NF, $1}. awk люди не знают. Знаком только с парой человек умеющих на нём писать полноценные программы. Зависеть от полноценного ЯП только для печати столбцов?

Perl же, для преобладающего большинства случаев, везде будет одинаково работать. Работа с текстом, сокетами, файлами и процессами (fork/exec, как минимум) не требует внешних .pm библиотек и встроена в интерпретатор. Который занимает несколько сот килобайт и безболезненно встраивается даже в OpenWRT дистрибутивы.

Он имеет и все средства из коробки для удобной замены awk oneliner задач. Например awk пример выше, на Perl мог бы быть таким: perl -lane 'print "@F[$#F, 0]"' — работающим и в GNU и BSD и OpenWRT системе, требуя довольно компактный единственный исполняемый файл. К тому же, работающий очень и очень шустро с текстом. Достаточно знать только основы Perl, вместо нескольких диалектов (чтобы помнить разницу между ними) sed, awk, cut (тоже имеющий некоторые полезные опции (-w) не везде присутствующие), tr и подобных.

Python



Кроме Vim-специфичных вещей, для Python имеется только мизерный конфиг pdb отладчика:

----- ~/.pdbrc -----
import os
alias hare os.system("kill %d" % os.getpid())
alias hare9 os.system("kill -9 %d" % os.getpid())
alias pp1 import pyderasn ;; print(pyderasn.pprint(%1, \
    oid_maps=(locals().get("OID_STR_TO_NAME", {}),)))


hare алиас используется для убийства текущего процесса. Так уж вышло, что частенько быстрее прибить процесс, чем нажимать кучу Ctrl-D/C. А pp1 используется для pretty printing-а ASN.1 структур.

----- ~/.zshenv -----
export PYTHONDONTWRITEBYTECODE=1


Позволит не создавать .py[co] файлы, смысла в которых не вижу, только мешаясь на диске.

Я не использую PyPI в PIP напрямую. Написал GoCheese PyPI-совместимый сервер для кэширующего проксирования и отдачи локально загруженных пакетов. Удивительно, но на момент его написания (до сих пор?), на Python не было ни одного удовлетворившего меня простого прокси. pyshop катастрофически медленный. Благодаря GoCheese абсолютно всё, что когда либо устанавливал — надёжно сохранено на диске, не требуя Интернет. И работает стремглав быстро. А для хранения состояния использует директории и файлы, проверяя и запоминая контрольные суммы.

Ещё одна программа исключительно для Python исходного кода, написанная не на Python:

----- ~/work/pyimportcan/pyimportcan.pl -----
my $buf; my $con; my @imports; my %parsed;

# Collect strings and aggregate the splitted ones
while(<>){
    next if /^[<=>]{2,}/;
    next if /^\s*#/;
    chop;
    $buf = ($con ? $buf : "") . $_;
    $con = /[\\,\(]\s*$/ ? 1 : 0;
    next if /^\s*\)*\s*$/;
    push @imports, $buf;
};

# Consolidate information from where what is imported
foreach (@imports) {
    s/[\\\(\)]//g;
    s/  */ /g;
    next if /import\s*$/;
    /^(.*)\s*import\s*(.*)$/;
    my ($where, $what) = ($1, $2);
    map { $parsed{$where}->{$_}++ } split /\s*,\s*/, $what;
};

foreach my $where (sort keys %parsed){
    map { print $where . "import $_\n" } sort keys %{$parsed{$where}};
};


Она преобразует import-ы, имеющие кучу вариаций написания:

from foo import bar
from foo import bar, baz
from foo import (
  bar,
  baz
)
from foo import (
  baz,
  bar,
)
from foo import bar, \
    baz


к единому «каноничному» виду, очень дружелюбному для разрешения git конфликтов, и простому для работы в редакторах. Чёткий и детерминированный:

from foo import bar
from foo import baz


Go



----- ~/.zshenv -----
export GOCACHE=/tmp/go-cache
export GOPROXY=off
export GOSUMDB=off
export GOPATH=$HOME/work/gopath


Кэш для промежуточных файлов сборки не хочу хранить на диске, так как это занимает много места, быстро становясь не актуальным, плюс потенциально несущим ценную информацию о коде. Не использую ни sum.golang.org, ни proxy.golang.org, так как это утечка приватных данных. Текущая GOPROXY=off настройка вообще отключает лазанье в Интернет — например вызов go doc частенько туда хочет. Когда мне надо явно разрешить скачивание кода, то добавляю GOPROXY=direct перед командой go get.

Так как Go штатно может иметь множество версий одной и той же библиотеки в $GOPATH/pkg/mod, то размещаю $GOPATH директории на ZFS dataset с включённой дедупликацией. Результат: dedupratio=1.26x.

Из must-have утилит для работы с Go могу отметить:

  • golang.org/x/tools/cmd/goimports, автоматически форматирующую код и обновляющую список import-ов, экономя кучу времени.
  • golang.org/x/tools/gopls LSP сервер. Самый приятный и положительный опыт работы с LSP у меня только с Go, в котором он работает с невероятной скоростью. Что в Python, что в Си (на больших проектах) — можно ждать по несколько секунд когда обновится linter или получу подсказку дополнения.
  • github.com/goware/modvendor помощник для создания vendor директории, включающейся в release tarball.


Я яростный поклонник Go модулей! Не скажу что мне их идея сразу же понравилась и что всё шло как по маслу и было понятно. Но в Go всегда так: спустя время ты понимаешь гениальную простоту и продуманность. Очень много людей, судя по блогам, не понимают как использовать vendor директорию и что $GOPATH, действительно, становится не нужен для сборки софта из чётко предоставленного исходного кода в tarball-е. Сам сторонник того, что в tarball всё должно быть, никакой зависимости от Интернета. Но $GOPATH deprecation ни капли не мешает.

C



Для Си разработки применяю LLVM, Clang, LLDB, clang-analyzer, clang-tidy, clangd LSP сервер, clang-format и include-what-you-use (IWYU). Сплошная LLVM/Clang экосистема. LSP сервер действительно помогает для linting-а. Статические анализаторы уйму раз ткнули меня носом в проблемы которые бы заметил неизвестно когда. А с IWYU забыл про геморрой корректного включения нужных #include-ов, хотя приходится рядом иметь mapping-файл с коррекциями его решений.

Из-за clang-format не форматировал ни единого кусочка (ну кроме таблиц констант) исходного кода, вызывая cfmt.sh. Стиль формата не полностью меня удовлетворяет, но пока наименее раздражающий:

----- ~/bin/cfmt.sh -----
#!/bin/sh
clang-format -style="`cat ~/.clang-format`" $@


----- ~/.clang-format -----
{
    BasedOnStyle: llvm,
    ColumnLimit: 88,
    IndentWidth: 4,
    AlignAfterOpenBracket: AlwaysBreak,
    AlignConsecutiveAssignments: true,
    AllowAllParametersOfDeclarationOnNextLine: false,
    AllowShortBlocksOnASingleLine: true,
    AlwaysBreakAfterReturnType: TopLevel,
    BinPackArguments: false,
    BinPackParameters: false,
    BreakBeforeTernaryOperators: false,
    BreakStringLiterals: false,
    BreakBeforeBraces: Custom,
    BraceWrapping: {
        AfterFunction: true,
    },
}


Опыта на Си у меня совсем немного, но среди всех отладчиков с которыми работал в разных языках: pdb, ipdb, delve, gdb — LLDB показался самым удобным и в нём хочется путешествовать по всему что происходит в дебрях ОС. Единственный алиас для печати участка памяти:

----- ~/.lldbinit -----
command alias mr memory read --size 1 --format x --count %1 --


Во время разработки на Си, ощутил и понял удобство DTrace. И прежде что-то на нём иногда запускал и смотрел, но USDT (userspace defined tracing) пробы встраиваются очень просто и с минимальным overhead-ом для программы. Позволяя потом трассировать что в ней происходит. Причём USDT код может как для включения DTrace проб использоваться, так и SystemTap в GNU/Linux, далее используя eBPF framework для трассировки. Таким образом, исходный код пригоден без изменений под разные системы трассировки.

redo



Есть три небольшие HTML странички, которые существенно изменили всё в моей жизни программиста. Это предложение DJB о системе слежения за зависимостями, о системе сборки redo. Гениальная простота этого предложения, да и сама реализация, которую можно на Go написать за один день (подтверждено на практике), впечатлили меня как никогда. Об этом я уже писал. Сейчас у меня не осталось ни одного личного или рабочего проекта, где бы использовался Make или autoconf. У них просто нет никакого смысла существования.

Я был не совсем удовлетворён redo-c реализацией и был удручён скоростью работы apenwarr/redo реализации на Python. В итоге написал свою версию на Go, используя десятки и сотни раз на дню. Но в рабочих проектах проверяю совместимость .do файлов и с другими реализациями, где их может быть под сотню, включая default*.do.

----- ~/.zshenv -----
export REDO_JOBS=0 REDO_NO_SYNC=1


Здесь разрешаю распараллеливать задачи сборки и не делать fsync вызов, дабы потенциально ещё больше ускорить работу.

TAI64



Ещё я поклонник TAI64 формата времени, познакомившись с ним в daemontools утилитах. В UTC время всегда идёт вперёд, но не монотонно из-за високосных секунд (leap seconds). TAI время их учитывает и идёт равномерно. А TAI64 это предложение от DJB для формата хранения этого TAI значения: с сохранением секунд, наносекунд или аттосекунд. В своих программах начал использовать TAI64N. Хотя это и потребует наличия обновляемой базы данных високосных секунд, чтобы точно переводить TAI в UTC.

Vim



Перехожу к самому важному в своей системе: текстовому редактору. В нём провожу преобладающую часть всего времени пока не сплю. Всё что касается работы, ввода чего бы то ни было на web-сайтах, когда-то переписки с девушками (много тысяч писем!), выполнение работ в институте, создание статей/документов/отчётов и презентаций — всё делалось и делается в редакторе. Работа с SQL в клиенте СУБД, редактирование длинных строк командного интерпретатора, полей ввода броузера — всё использует внешний редактор (в моём случае это Vim). Это самая важная программа на компьютере, без которой он и не нужен.

Есть люди спрашивающие почему Vim, а не «модное Electron-driven творение или монстр от Microsoft/Apple»? Не знаю что им ответить. Подозреваю что там могут быть лучше интегрированы отладчики и подсказки связанные с языком программирования, но какой в этом смысл, когда сама правка кода там выполняется редактором другой весовой категории? «Почему ты используешь Unix или Windows NT, а не MS-DOS?» — для меня вопрос такого же уровня. В Vim несколько сотен команд редактирования и движения, сотни команд для командной строки, сотни переменных для настроек мельчайших нюансов поведения, плюс ещё возможность и скриптовать это всё. О каком сравнении может идти речь?

Есть люди считающие, что скорость редактирования не важна, ибо они бОльшую часть времени тратят «на раздумья». Не спорю что сама работа руками может занимать меньше времени, вот только частенько проще что-то попробовать сделать, попробовать изменить, порефакторить и посмотреть что выйдет на деле, чем в «виртуальной машине» в голове. Возможно преувеличивая лишь самую малость, мне со стороны кажется, глядя на людей работающих в чём-то простом типа Блокнота/Word/Nano/mcedit, что они могли бы 75% времени, ими проводимым за экраном, сэкономить просто более эффективно работая в редакторе и shell-е. Да зачастую всё программирование это сплошное редактирование уже имеющихся кусков текста! Убеждён, что люди просто не понимают и не замечают как много времени они тратят на «материализацию» своих мыслей (что они хотят увидеть на экране).

Я очень уважаю Emacs, но всё же как редактор он будет чуть менее эффективным. Но признаю что разница между ними настолько несущественна, что profit от единой «экосистемы» Emacs будет запросто перевешивать эффективность Vim, даже обвешанного скриптами и интегрированного с терминалами. Но Emacs не буду пробовать: его постоянная долбёжка по Ctrl вредна для здоровья (статистика говорит о большем количестве проблем с руками у Emacs-еров) и я не сторонник функционального программирования, так как оно требует другого склада ума.

В Vim плохо одно: постоянное обучение и изучение (no pain, no gain!). 20+ лет с ним работаю, но до пор раз в пару месяцев узнаю какую-нибудь очередную интересную команду или трюк. Нет, всё есть в документации, во встроенном :help, но понять значимость, понять use-case-ы и ценность команды или способа совмещения команд между собой не выходит сразу. Время от времени, беру и читаю его документацию со случайной позиции, стараясь что-то новое отметить и начать это отрабатывать на практике. Кажется что вершина мастерства Vim недостижима, но уже не первый год замечаю, что из 99% статей про Vim, что открываю с заголовками «ultra advanced powerful tips and tricks», не узнаю ничего нового, ни бита полезной информации.

С Vim может быть другая проблема: вредные привычки. Самое убийственное для производительности труда в Vim что наблюдал со стороны — использование визуального режима (v) не к месту. Я это называю «блокнот-mode», когда можно и рукоятью электрической дрели забивать гвозди. Визуальный режим, означает что штатные motion-ы не выполняются (motion будет на визуальное выделение), а это приводит к невозможности создания макросов, использования normal команд в какой-нибудь :g команде, банального повторения (.) действия к отличающемуся куску текста. Это как пытаться бегать, но связав себе ноги вместе — передвижение будет, но не выше заданной планки.

Есть ещё мнение что Vim требует кучи плагинов. Только если для совершенно специфичных задач, как и любому бы другому редактору надо было. А в общем случае: полнейшая чушь, говорящая только о непонимании родных возможностей. И сам грешил этим: когда-то ставя себя пару десятков плагинов и удивляясь почему они не встроены из коробки. С годами, количество плагинов только уменьшалось. Либо оставались совсем task-specific, либо несколько must-have творений Tim Pope (об этом ниже).

И ещё бытует миф что для работы в Vim нужно много строчек в .vimrc. Пухлые конфиги, как мне кажется, возникают по двум причинам:

  • Новички пытаются изменить штатное поведение Vim чтобы он стал похож на других. Не понимают как надо использовать. И таких конфигов видел большинство. На своём опыте убедился что .vimrc со временем сильно похудел.
  • У опытных пользователей накапливается огромный багаж привычек и более дружелюбного поведения по умолчанию именно для их задач и, так сказать, особенностей анатомии (рабочие пальцы, восприятие цветов). Пользователям не «настоящих» редакторов это сложно понять, так как в них мало чего настраивается.


Рассказывать про команды и трюки в Vim не смогу, как и в случае с zsh: это слишком объёмная тема. Так что ограничусь только рассмотрением своих конфигов.

----- ~/.vimrc -----
set t_Co=16
syntax on
filetype on
filetype plugin on
set nocompatible
set encoding=utf-8
set mouse=""


Включение синтаксической подсветки, filetype-специфичных плагинов, отключение совместимости с vi (без этого куча всего не будет работать), UTF-8 как кодировка по умолчанию и не пытаться перехватывать управление мышкой (чтобы в терминале выделять текст можно было).

Я форсированно ограничиваю количество цветов до 16 — это делает их насыщенными и контрастными, а не блёклыми и бледными, еле отличимыми друг от друга. Никогда не использовал цветовую схему отличную от default. На чёрном фоне с 16 цветами всё очень хорошо различимо.

----- ~/.vimrc -----
set viminfo-=h
set viminfo+=f1,%,n~/secure/vim/info
set viewdir=~/secure/vim/view
set directory=~/secure/vim/tmp
set undodir=~/secure/vim/undo
set undofile


  • Из viminfo (управление информацией сохраняемой в .viminfo файле) убираю забывание о hlsearch и требую сохранять метки на файлы (заглавные 'A-'Z) и список буферов. .viminfo находится на зашифрованном разделе, дабы в открытом виде нечаянно не осела важная информация из регистров.
  • Директории для временных файлов, undo-файлов, view (сохраняющих информацию о текущей «сессии») тоже находятся на зашифрованном разделе.


----- ~/.vimrc -----
set autoindent
set tabstop=4
set shiftwidth=4
set smarttab
set expandtab
set nojoinspaces
set scrolloff=2
set backspace=indent,eol
set shortmess=aoOtI
set highlight-=v:Visual
set highlight+=vr
set highlight+=sr
set cpoptions+=$
set showcmd
set showmatch
set completeopt-=preview
set diffopt+=indent-heuristic,algorithm:histogram


  • autoindent включает автоматическую вставку отступов во время ввода текста.
  • tabstop, shiftwidth, expandtab говорят о размерах табуляции и о том, чтобы табуляцию превращать в пробелы.

    Я сторонник использования символов табуляции для indenation. Но только
    для indentation! Пробелы должны использоваться для всего остального:
    например выравнивания относительно скобок. С этим не будет проблем даже когда
    размеры табуляции у людей отличаются.

    Но, к сожалению, люди (из за редакторов?) не в состоянии это делать грамотно и сдался и смирился с тем, что проще и надёжнее везде использовать пробелы. Это просто меньшее из зол. К тому же всё равно для indentation-а нажимаю tab, и команды изменения indent тоже прекрасно работают и с пробелами.
  • nojoinspaces предотвращает вставку лишних пробелов после символов пунктуации во время объединения строк (J). У нас не принято так писать, да и в программировании это точно не нужно.
  • scrolloff показывает две строки над/под курсором когда он находится на самом верху/низу страницы. Так проще понять контекст где нахожусь.
  • backspace опция говорит где может работать backspace. Может ли он «удалять» отступ и удалять перевод между строками (объединять их). У меня убрано значение start: это значит что не могу удалять ничего перед местом где начал вставку. К такому поведению у меня привычка со времён (n)vi.
  • shortmess настраивает использование коротких информационных сообщений и отключение таких вещей как приветствие при запуске Vim. Например "[noeol]" вместо "[Incomplete last line]", «999L, 888C» вместо «999 lines, 888 characters», и т.д..
  • highlight заменяет стиль подсветки визуальных выделений с блёклого серого подсвечивания на инвертирование цветов. Просто сильно заметнее и ярче.
  • В cpoptions добавлено указание на то, что когда изменяю (change действие) кусок текста, то не удалять всё что было написано, а просто пометить $ в конце редактируемого куска. Гораздо удобнее видеть что за текст там был. Это же поведение по умолчанию в (n)vi.
  • showcmd показывает в углу частично введённую, но ещё не законченную команду. Во-первых, это полезно при парном программировании (коллега видит что ты вбиваешь). Во-вторых, чётко даёт понять в каком ты сейчас режиме: ввёл ли ты что или ещё нет.
  • showmatch при закрытии скобочки быстро прыгает курсором на соответствующую открывающую скобку, показывая что конкретно мы закрываем.
  • Из completeopt опций регулирующих поведение предложения completion убран показ preview окна. Оно меня сбивает и визуально раздражает.
  • diffopt настраивает более ресурсоёмкий, но приятный для человека алгоритм вычисления diff.


----- ~/.vimrc -----
set cursorline
set cursorcolumn
set colorcolumn=80


  • cursorline, cursorcolumn включают вертикальную и горизонтальную подсветку текущего положения курсора. Делая некое перекрестие. Позволяют визуально моментально найти где находится курсор.
  • cursorcolumn рисует вертикальную черту на границе 80-го символа, чтобы было понятно когда захожу за неё при наборе. 80 (±) символов это отличная ширина для восприятия человеком текста, не заставляющая горизонтально бегать глазами по ширине экрана.


----- ~/.vimrc -----
set listchars=trail:·,tab:>→,nbsp:%
set list
highlight ExtraWhitespace ctermbg=green ctermfg=blue
match ExtraWhitespace /\s\+$/


  • listchars, list подсвечивают символы, штатно невидимые, типа пустующих whitespace-ов в конце строки, символов табуляции и неразделяемого пробела (бывает появляется при вставке) указанным символом. Очень полезно лицезреть лишний невидимый мусор.
  • ExtraWhitespace highlight ярко подсвечивает whitespace-ы в конце строки, которые в 99.99% случаев не нужны, являясь мусором.


----- ~/.vimrc -----
set relativenumber
set numberwidth=3


relativenumber — ультраважнейшая штука, ради которой когда-то обновлял Vim из исходников, минуя пакеты. Существенно повышающая эффективность работы штука. Суть тривиальна: на каждой строчке показывается смещение относительно текущей. Строка которая на десять строк выше/ниже от текущей — будет иметь номер 10. Может ли человек быстро удалить две строки в Vim? 2dd — без проблем. А пять или десять? Уже нет, так как глазом он уже не в состоянии быстро посчитать и оценить сколько тут строк и какое число надо ввести. Относительный номер строки буквально покажет расстояние. Хочется прыгнуть «вот на эту» строку где-то на полэкрана выше? Относительный номер покажет что надо ввести например 23k.

Я часто хочу скопировать, переместить или удалить строку из какого-то далёкого места на экране: можно туда прыгнуть, нажать копирование/удаление, потом вернутся назад и вставить её. Я же сделаю :-27t. чтобы 27-ую строку выше скопировать под текущую. Это возможно сделать и без относительной нумерации, но абсолютные адреса строк чаще длиннее, просто потому что относительных на экране много не влезает и они состоят из 2-х цифр.

numberwidth уменьшает ширину столбца для отображения номеров строк. Так как используется относительная нумерация, то значений из трёх цифр там не будет и ширина в 4 символа (по умолчанию) излишня.

----- ~/.vimrc -----
set ignorecase
set smartcase
set hlsearch
set incsearch
set gdefault
map <F4> :nohlsearch<CR>:MarkClear<CR>


  • ignorecase и smartcase игнорируют регистр символов при поиске, если запрос был введён исключительно маленькими буквами. Если появляется хоть одна заглавная, то поиск будет с учётом регистра. Крайне удобная штука: не отвлекаешься на корректный регистр при поиске, и не теряешь возможность точно поиска.
  • hlsearch подсвечивает на экране все найденные элементы. А incsearch будет их подсвечивать по мере ввода запроса. Кроме того, вас могут и переместить на совершенно другое место в файле, показывая где и что найдено в первую очередь. Очень удобное поведение!
  • gdefault инвертирует поведение флага /g в командах замены :s/.../.... Это флаг глобальной замены во всей строке, требующийся чаще всего. С этой опцией отсутствие флага означает глобальную замену, а присутствие — единичную. Must-have!
  • F4 клавиша отключает подсветку найденных слов. Когда всё нашлось, то подсветка может мешать. Набирать :nohl? Долго! Кроме того, использую плагин Mark для множественной разноцветной подсветки слов — F4 уберёт подсветку и для его результатов.


tags



----- ~/.vimrc -----
set wildmode=list:longest
set tags=.tags/**/tags;
set wildignore=**/.git,**/.tags


  • wildmode задаёт поведение дополнения путей и имён файлов. longest режим дополняет только до наибольшей общей части строки, как это происходит в shell-е. Более привычно. list покажет список из возможных дополнений.
  • wildignore игнорирует при поиске всё что касается файлов с тэгами (они могут быть большого размера и регулярно включаться в результат поиска) и git-а.
  • tags указывает где искать файлы с тэгами.


Тэги — архиважнейшая функция! И удручает как мало людей даже не знают об их существовании. Python-исты часто используют сторонние плагины для навигации по исходному коду и ни разу не видел чтобы это было так же эффективно как работа с тэгами. Физический смысл тэгов банален: это база данных ключейзначений, где ключами являются эти самые тэги, а значениями информация об их местоположении. Текстовый индекс. Как правило, каждая запись ещё сопровождается информацией о типе тэга (переменная, функция, класс, и т.д.) и возможно полной сигнатурой функции.

Названия тэгов (название переменных, функций) могут пересекаться, поэтому везде имеется функционал чтобы перемещаться между тэгами одного названия. Вот такими могут быть предложения тэгов для Go, Си и Python кода:

:tselect Addr
   # Prio Art Tag              Datei
  1 F   m    Addr              /home/stargrave/work/nncp/src/call.go
               struct:nncp.Call typeref:typename:*string
               Addr           *string
  2 F   m    Addr              /home/stargrave/work/nncp/src/cfg.go
               struct:nncp.CallJSON typeref:typename:*string
               Addr           *string `json:"addr,omitempty"`

:tselect ASN1_BIT_STRING_free
   # Prio Art Tag                  Datei
  1 F   f    ASN1_BIT_STRING_free  .../libressl-2.7.4/crypto/asn1/tasn_typ.c
               typeref:typename:void
               ASN1_BIT_STRING_free(ASN1_BIT_STRING *a)
  2 F   p    ASN1_BIT_STRING_free  .../libressl-2.7.4/include/openssl/asn1.h
               typeref:typename:void
               void ASN1_BIT_STRING_free(ASN1_BIT_STRING *a);
  3 F   p    ASN1_BIT_STRING_free  .../libressl/include/openssl/asn1.h
               typeref:typename:void
               void ASN1_BIT_STRING_free(ASN1_BIT_STRING *a);

:tselect Certificate
   # Prio Art Tag              Datei
  1 F   c    Certificate       .../x509/cert.py
               class Certificate(Sequence):
  2     v    certificate       .../xx/__init__.py
               class:XXEntryType
               certificate = 'certificate'


Даже nvi (не Vim!) в FreeBSD имеет навигацию по тэгам, как и less пейджер. Самое главное (для меня) что это очень быстрый способ навигации, в отличии от систем которые парсят исходный код на лету. ctags генератор файлов с тэгами даже идёт из коробки в FreeBSD. Но использую Universal Ctags генератор, так как он знает про Go язык, создавая vi/less-совместимые tags файлы.

Работая с проектом который использует сторонние библиотеки, хочется чтобы были тэги и по самому проекту и по зависимым библиотекам. Недавно просто добавлял символические ссылки к исходному коду зависимых библиотек в код проекта. Время от времени файл с тэгами необходимо обновлять, чтобы в нём отразились изменения. И с огромными сторонними библиотеками (например OpenSSL) это может занять ощутимое время, тогда как сама зависимая библиотека не обновляется вовсе.

Сейчас использую такой удобный подход: в проекте создаётся .tags директория:

----- ~/bin/mktags -----
#!/bin/sh -e
mkdir -p .tags
touch .tags/tags


В которой находится tags файл и возможно поддиректории со своими tags файлами. Проект с LibreSSL и libtasn1 зависимостями будет иметь следующую .tags директорию:

.tags/tags
.tags/libressl/tags => /path/to/libressl/.tags/tags
.tags/tasn1/tags    => /path/to/tasn1/.tags/tags


.tags/tags отвечает за тэги только локального проекта. У меня не вышло (возможно это бага моей версии Vim) сделать так, чтобы Vim воспринимал тэги сторонних проектов без поддиректорий.

Ну и ладно! tags настройка в .vimrc отлично работает с этим use-case. Для генерирования файла с тэгами использую vimscript :Ctags команду, которой передаю языки программирования для индексирования. Она самостоятельно находит местоположение .tags/tags файла, находящегося в корне проекта, от которого и будет запущена uctags команда индексирования:

----- ~/.vim/plugin/ctags.vim -----
function! s:ctags(lang)
    let dst = tagfiles()
    if len(dst) == 0
        echohl WarningMsg | echomsg "No tagfiles" | echohl None
        return
    endif
    let dst = sort(dst, {a, b -> len(a) > len(b) ? 1 : len(a) == len(b) ? 0 : -1})[0]
    let src = "/" . join(split(dst, "/")[:-3], "/")
    if dst[0] != "/"
        let dst = getcwd() . "/.tags/tags"
        let src = getcwd()
    endif
    let cmdline = [
        \"uctags",
        \"--languages=" . a:lang,
        \"--python-kinds=-i",
        \"--c++-kinds=+p",
        \"--recurse",
        \"-f", dst, src,
    \]
    execute "!" . join(cmdline, " ")
    redraw!
endfunction

command! -nargs=1 Ctags silent call s:ctags(<f-args>)


Чтобы подготовить к работе новый проект, делаю mktags, а далее внутри Vim, например :Ctags C,C++. Зависимые библиотеки, интересующие меня, руками добавляю через mkdir и ln -s.

.vimrc



----- ~/.vimrc -----
set exrc
set secure


exrc опция позволяет Vim-у автоматически исполнять .vimrc/.exrc файл в текущей директории. Например в корне рабочего проекта туда можно заносить какие-то project/language специфичные настройки. secure строже будет относится к дозволенным действиям из этого файла, на всякий пожарный (мало ли кто подложит этот файл в git коммите). Когда-то планировал в этом .exrc файле указывать пути до файлов тэгов к другим проектам, но решил что хранить эту информацию в состоянии ФС будет проще.

----- ~/.vimrc -----
set foldmethod=indent
set foldnestmax=9
set foldenable
set foldcolumn=1
set foldlevel=0

autocmd BufWinEnter * normal zR
highlight FoldColumn ctermfg=cyan ctermbg=black
highlight CursorColumn ctermfg=cyan ctermbg=red


Включение автоматического folding-а по отступам, с отображением в колонке слева уровня fold-а. При открытии окна, все fold-ы будут раскрыты (выполняется zR команда). Folding (схлопывание множества строк в одну) бывает очень полезным, когда хочется лицезреть кучу кода, но убирая тела циклов или целых функций. Часто схлопываю все fold-ы чтобы видеть в файле только названия функций/классов.

----- ~/.vimrc -----
set laststatus=2
set statusline=%F\ %m%r%h%w%k
set statusline+=%{len(getqflist())?'[Q]':''}
set statusline+=%{len(getloclist(0))?'[L]':''}
set statusline+=\ %=
set statusline+=%n\ %#Question#%Y
set statusline+=%#DiffChange#%{strlen(&fenc)?&fenc:&enc}%{&bomb?'-bom':''}\ %{&ff}
set statusline+=%#DiffAdd#%l/%L
set statusline+=%#Todo#%c%V:0x%B
set statusline+=%#Comment#%o
set statusline+=%#Error#%{LintStatus()}
set statusline+=%*\ %P

function! LintStatus() " it is overrided in ftplugins
    return ""
endfunction


Настройка строки состояния, которая выглядеть может вот так:

options.txt [-][RO][Hilfe] ^^^^2 HELP utf-8 unix 6370/8966 7-49:0x2A 272384 71%
                                     ^          ^         ^         ^      ^


Тут нельзя передать цвета, поэтому в местах с "^" указал границу между двумя разными цветами, между которыми нет пробела. То есть «HELP» и «utf...» написаны слитно. Стараюсь экономить место, которого не хватало бы для всего что хочется отобразить когда открыто много split-ов.

  • В начале пишется имя файла.
  • Затем вереница из флагов: есть ли изменения в файле ([+]/[-]), только ли он для чтения ([RO]), является ли он help-буфером ([help], но у меня Vim перевёл на немецкий), является ли он preview-окном ([preview]), существует ли сейчас в принципе quickfix или location списки ([Q], [L]).
  • Далее отображается множество "^"-символов если текущее окно активно. Игрался с цветами строки состояния чтобы подсветить активное окно, но так и не нашёл которые бы одновременно и были заметны и не сбивали восприятие. Вереница из "^" хорошо заметна, чтобы понять в каком сейчас окне нахожусь.
  • Далее пишется номер буфера, за который это окно отвечает. Может быть полезно если хочется удалить буфер и сразу виден его номер для :bd X команды.
  • Далее показывается тип файла (filetype).
  • Далее его кодировка и наличие BOM (:set bomb!).
  • Далее «формат» файла, а если точнее, то способ перевода строк: CRLF (dos), LF (unix) или CR (mac). Довольно часто полезно видеть это, чтобы понимать что за файл под рукой. Как и видеть кодировку отличную от UTF-8.
  • Номер текущей строки и всего строк.
  • Байтовая позиция курсора в строке и символьная. Например точка в этом предложении сейчас имеет значение 91-51, означая, что курсор на ней находится на 91-ом байте, но 51-ом символе на экране. Кириллический символ в UTF-8 занимает два байта, но один символ экрана. Табуляция может занимать 8 символов экрана, но один байт. Оба этих значения полезны.
  • Unicode значение символа под курсором. Очень полезно, как минимум, чтобы понимать что у нас Unicode символ похожий на ASCII.
  • Абсолютная байтовая позиция в файле где сейчас находится курсор.
  • Ярко красное отображение вывода функции LintStatus().
  • Процентаж того где мы находимся относительно всего файла.


Для Python использую самописный асинхронный вызов linter-а. Его код тут не привожу, ибо занимает больше экрана. Суть работы проста: в фоне запустить pylint/pyflake, получить весь вывод, отпарсить и преобразовать в формат quickfix списка, загрузить в quickfix окно. Пока он работает, то LintStatus() возвращает строку LN, предупреждая что linter сейчас работает.

В Go у меня есть скрипт вызывающий внешнюю gogetdoc команду, которая в LintStatus() напишет GD. Строка состояния — динамичная штука.

----- ~/.vimrc -----
autocmd BufWinLeave ?* silent! if &ft !=# "gitcommit" | mkview | endif
autocmd BufWinEnter ?* silent loadview


Эти hook-и создают view-файлы при выходе из буфера и загружают их при открытии, если есть имя файла (?). Вообще в Vim есть .viminfo, но view является куда более полноценной информацией о состоянии рабочей «сессии», вплоть до последнего местоположения курсора.

Плюс не стоит забывать, что мой Vim запоминает и список буферов при выходе. Если просто запущу vim, то увижу пустой экран, однако на самом деле все буферы загруженные при выходе будут уже присутствовать. По сути не сохраняется только местоположение и размеры окон смотрящих на эти буферы. Но это можно сделать через штатный функционал сессий (:mksession, vi -S Session.vim).

----- ~/.vimrc -----
nnoremap <C-b> <C-W>
noremap <C-j> <C-w>j
noremap <C-k> <C-w>k
noremap <C-l> <C-w>l
noremap <C-h> <C-w>h
nmap <leader>- <C-w>\|<C-w>_
nmap <leader>= <C-w>=
nmap <Del> :close<CR>
autocmd VimResized * wincmd =


  • Я аллергичен к Ctrl-сочетаниям клавиш для частых действий. Это всё для Emacs оставьте. С редкой вставкой слова под курсором (C-R+C-W) ещё готов мириться, но не частым перемещением между окнами. Ctrl+[hjkl] клавишами сразу перемещаемся между окнами.
  • Так как Ctrl-B могу послать одним нажатием Menu клавиши, и в Vim оно ни с чем не конфликтует, то Ctrl-B (одно!) нажатие является и C-W. Почему не оставить только Ctrl-B алиас с которым переход между окнами тоже можно осуществить двумя нажатиями? Потому что при использовании Ctrl+[hjkl], Ctrl можно оставить зажатым.
  • \- (корректнее писать «leader»-клавиша + "-", но у меня leader является обратным слэшом (как по умолчанию), так что буду уж писать настоящую клавишу) делает «максимизирование» размеров окна: заполняет всё вертикальное и горизонтальное пространство. От остальных окон остаются только строки состояния. Как бы аналог Prefix+Z из Tmux. Частенько бывает нужно временно удобно посмотреть на текст не ютясь в маленьком окне. А \= выравнивает размеры всех окон обратно. Есть отдельные плагины для Vim позволяющие полностью запоминать все точные размеры окон чтобы их восстановить обратно — но либо иметь ровно одну строчку алиаса, либо скрипт на десятки строк? Меня удовлетворяют и окна равного размера и остающийся statusline.
  • Клавиша Del закрывает окно. Сверхчасто используемая штука. Ну не набирать же :bd!
  • Из-за tiling-ового оконного менеджера dwm, любое появление новых окон изменяет и размер уже существующих на экране. Поэтому размеры X11 окон на практике может часто «колбасить», а в Vim при этом сбиваются размеры внутренних окон. VimResized событие вызывает команду выравнивания размеров.


----- ~/.vimrc -----
set titleold = ""
set title
autocmd BufEnter * let &titlestring = expand("%:t")


Эти настройки заставляют Vim посылать escape последовательности изменения заголовка окон (в tmux и X11). В качестве заголовка используется последний элемент пути (название файла).

----- ~/.vimrc -----
set comments-=mb:*
set formatlistpat=^\\s*\\*\ \\s*
set formatoptions+=onj


Один из примеров за что так люблю Vim. Настройка поведения форматирования комментариев и списков. Удовлетворяющего всех поведения быть не может — ничего не бывает сразу удобного для всех.

  • В formatoptions добавляется: требование добавлять символ комментария (оценивая текущий контекст и известные символы комментариев) при добавлении новой строки командами o/O; включение распознавания нумерованных списков; отбрасывание символов комментариев во время объединения строк (J) (Vim начнёт воспринимать текст в комментариев как будто в нём нет лидирующих символов комментариев).
  • По умолчанию звёздочки в начале строк распознаются как символы комментариев (в Си языках часто используются). Но я не использую /*… */ комментарии совсем, а звёздочки у меня для списков. Соответственно, хочу чтобы к звёздочкам применялись «правила» работы со списками. Из comments убираю знание о звёздочках как символах комментариев. А в formatlistpat добавлен шаблон распознавания звёздочек как элементов списка.


----- ~/.vimrc -----
set spelllang=ru,en_gb
highlight SpellBad cterm=inverse ctermfg=red ctermbg=black
highlight SpellLocal term=reverse ctermfg=red ctermbg=cyan


Тут указывается какие словари использовать для проверки орфографии. Настройки цветов изменены из-за моих правок для создания inverse цветов и форсированного ограничения палитры.

По возможности стараюсь использовать британские слова, поэтому и en_gb словарь. А русский словарь (~/.vim/spell/ru.utf-8.spl) генерировал самостоятельно, так как противник противников использования буквы «ё», а дистрибутивы из коробки подкладывают словари без неё. Поэтому пришлось конвертировать rus-myspell-yo-0.99f7 в формат словаря Vim.

Раздражающие опечатки:

----- ~/.vimrc -----
map q: :
command! W w
nmap <F1> :help!<CR>
imap <F1> <C-O>:help!<CR>
iabbrev итд и т.д.
iabbrev итп и т.п.


  • q: запускает редактирование истории командной строки. Очень часто опечатываюсь, набирая :q, после чего приходится выходить из появившегося окна и снова набирать :q. Когда редактирование истории мне действительно нужно, то можно вызвать командую строку и нажать Ctrl-F.
  • Для сохранения файла чаще всего использую :w, где при наборе двоеточия могу не успеть отпустить Shift при наборе «w» — ещё одна частая опечатка.
  • Бывает нажимаю F1, в том числе из-за привычки проверки ею почты в zsh. И по умолчанию это откроет мне помощь. Just for fun вызываю при этом встроенную пасхалку из «Автостопом по галактике», показывающую мне «Nur keine Panik!». Зато не открывающую назойливого окна помощи.


----- ~/.vimrc -----
set keywordprg=
let g:sh_no_error = 1
set termwinkey=<C-B>


  • При нажатии K, по умолчанию, вызывается man для слова под курсором. Бывает нечаянно нажимаю эту клавишу, получая выговор о том что такого man нет. Для конкретных языков программирования эта функция полезна, но не в общем случае (пустой keywordprg).
  • sh_no_error заставляет игнорировать ошибки (с точки зрения syntax/sh.vim) shell-скриптов. Конкретику уже не помню, но sh.vim бывает ошибается в понимании что написано в скрипте, сильно раздражая безумной подсветкой на экране.
  • termwinkey задаёт клавишу управления окнами терминала (встроенный терминал в Vim… который тоже запускается под мультиплексором терминалов Tmux, в свою очередь запускаемый в эмуляторе терминала X11). Опять же, просто использую Ctrl-B, посылаемый у меня одним нажатием Menu клавиши. Но терминалы, сколько не пробовал, не выходит использовать для пользы совсем.


----- ~/.vimrc -----
nmap <space> f<space>
nmap _ f_l


Как часто люди перемещаются по «большим» словам (W)? Очень! Но также очень часто хочется чтобы курсор находился не на самом слове, а на пробеле перед ним. А штатно на пробеле нет никакой команды. У меня теперь есть. Плюс «большие» прыжки с большими словами (W/E) выполняются с нажатием Shift-а, что медленнее. Один из тех самых трюков которые люблю: просты и эффективны. А "_" заставляет перемещаться по подчёркиваниям, которых полно в Python коде бывает, чтобы прыгать по словам внутри названий функций и переменных.

----- ~/.vimrc -----
nmap <leader>] "*yiw
nmap <leader>p "_diwP


\] копирует в X11 буфер обмена слово под курсором. \p заменяет слово под курсором, не загрязняя регистры, значением регистра по умолчанию. Удалили/скопировали где-то слово и им хочется заменить другое, но чтобы от прошлого не осталось и следа.

----- ~/.vimrc -----
nnoremap <leader>d "_d
vnoremap <leader>d "_d


\d производит удаляет в "/dev/null" регистр. После, конечно же, нужно указать motion. Очень часто использую.

----- ~/.vimrc -----
map Q gq
cmap ][ '[,']
imap <C-b> <C-x><C-o>
nmap <Tab> :buffers<CR>:b<Space>
nmap <silent> <Home> :registers<CR>
nmap <leader>' yiwciw"<C-r>""<ESC>
nmap <leader>h1 yypVr=o
nmap <leader>sc 024i-<ESC>a >8 <ESC>24a-<ESC>
nmap <leader><C-]> :vertical wincmd ]<CR>
nnoremap <C-P> <C-I>


  • Q более быстро позволяет написать qg, который используется, как минимум, для переформатирования параграфа gqap.
  • ][ набор в командной строке раскрывается в указание диапазона от метки [ до метки ] — они автоматически ставятся после вставки текста. Архичасто используемый функционал, так как после вставки какого-то куска ему хочется изменить indentation: ][< и Enter!
  • Ctrl-B в режиме ввода вызывает LSP completion функционал. В чужих статьях вроде бы видел, что часто используют Shift+Space комбинацию, но Ctrl-B у меня нажимается одной клавишей.
  • Когда-то для переключения буферов ставил плагины типа bufexplorer, в которых Tab использовался для показа списка буферов и их выбора. Позже дошёл до простейшего тривиального решения: показать список буферов и набрать :b с пробелом, чтобы оставалось только указать название/номер буфера для переключения и нажать Enter. Причём Vim из коробки может понять указание буфера через часть его имени.
  • Home клавиша покажет содержимое всех регистров. Частенько проще их посмотреть, чем понять по косвенным признакам (вставке, применению макроса) что там за значение.
  • \' одна из самых частых команд в Python. Просто обрамляет слово под курсором в двойные кавычки. Прежде она использовала функционал surround плагина (о нём ниже), но независимость всё же лучше. А если хочется обрамить слово в одинарные? Или скобочки? Обрамляю в двойные, а дальше использую surround возможности для смены двойных кавычек на нужные мне. Звучит геморройно, но на деле всё это всё делается достаточно быстро: \'cs"'.
  • \h1 подчёркивает всю текущую строку "=" символом. Использовалось, в первую очередь, в reST файлах для создания заголовка (отсюда и «h1», как header в HTML). Интересно как она это делает: копирует всю строку, вставляет ниже (чтобы появилась строка нужно длины), визуально полностью выделяет, делает замену (автоматически применяя ко всем символам) на "=". Если нужно использовать другой символ для заголовка, то проще и быстрее выполнить изменение "=" символов, например \h1:s/=/-. Вижу колоссальное количество коммитов в разных репозиториях на исправление некорректной длины подчёркивания заголовка — явно людям редактор не помогает.
  • \sc создаёт длинный "-- >8 --" разделитель. Сочетание символов похожих на ножницы увидел в git. Очень часто в письмах ставлю этот хорошо визуально различимый разделитель.
  • \+Ctrl+] переходит на тэг (слово под курсором) в вертикально открытом новом окне. Аналог встроенного Ctrl-W+Ctrl-], но не в горизонтально разбитом окне.
  • По причинам которых не помню, Tab является и Ctrl-I. Tab перебил на выбор буфера. Соответственно и Ctrl-I перестаёт работать для перехода по jump list-у «вперёд» (Ctrl-O работает «назад»). Чтобы не терять функционал хождения по jump list-у в любую сторону, использую Ctrl-P, который тоже удобно находится рядом с Ctrl-O, симметрично его дополняя.


----- ~/.vimrc -----
command! E Explore
command! Ch cd %:p:h
command! -bar -nargs=? -bang Tmp :silent vnew<bang>|
    setlocal buftype=nofile bufhidden=hide noswapfile buflisted
    filetype=<args> modifiable


  • В старых версиях Vim :E команда открывала файловый броузер на текущей директории. В новых версиях пришлось бы набирать :Ex. А я привык к короткой записи.
  • :Ch команда меняет текущую рабочую директорию Vim на ту, в которой находится текущий файл.
  • :Tmp XXX команда создаёт окно для временных данных. Без хранения чего бы то ни было на файловой системе. XXX позволяет задавать тип файла в нём, чтобы хотя бы синтаксическая подсветка работала.


~/.vim/pack



Всё остальное у меня вынесено в директории и отдельные скрипты. В Vim8 появились packages: возможность подключения дополнительных «chroot»-ов Vim директорий. Причём как с автоматическим запуском, так и опциональным. Например плагины Tim Pope у меня установлены так:

~/.vim/pack/tpope/start/fugitive          # установлено из .zip
~/.vim/pack/tpope/start/vim-abolist/.git  # git репозиторий смотрящий на github
[...]


А мой изредка используемый (поэтому не нужно его загружать и тратить ресурсы) плагин Codecomm включается когда нужно командой :packadd codecomm:

~/.vim/pack/codecomm/opt/codecomm


Смысла в пакетных менеджерах после этого нет никакого. Фичи типа «пропиши github/foo/bar строчку, автоматически всё скачаю и запущу» ужаснут любого кто задумывается о безопасности и компрометации своего компьютера. И не только я так считаю: все плагины Tim Pope тоже советуют именно такой способ установки. Но стоит давать скидку на то, что многие плагины были написаны до Vim8, поэтому они с чистой совестью могут упоминать «альтернативные» пакетные менеджеры.

Vim разработка регулярно славится тем, что она годами ничего не привносит существенного, как мне показалось. Потом появляются какие-нибудь сторонние проекты типа NeoVim. После чего Мууленаар в новой версии выкатывает и асинхронные задачи, и каналы, и пакетную систему. Так было и во времена nvi, в какой-той момент перевешивавший по возможностям Vim. Вот только, на мой взгляд, в Vim всё гораздо проще и продуманнее делается в итоге.

Сразу расскажу про несколько убер-полезнейших плагинов. Всё творение Tim Pope:

  • surround. Первое что однозначно бы советовал ставить. Позволяет добавлять, удалять и изменять «обрамления» участков текста. Заменять одни кавычки/скобочки на другие и всё в таком духе. Это всё чрезвычайно эффективно: проще набрать слово, а потом просто добавить к нему нужное обрамление. Это настолько популярная и полезная штука, что даже для zsh делают плагины повторяющие этот функционал в vi-режиме редактирования.
  • repeat. Явно этот плагин никак не виден, но он позволяет повторять команды surround плагина (и с полдюжины других творений Tim Pope) штатной командой повторения (.). Обязательное дополнение к surround.
  • unimpaired. Даже сам Tim Pope пишет что этот плагин просто является выжимкой mapping-ов из его .vimrc. Но так уж вышло, что и у меня многие mapping-и были похожи и часто используемы, типа ]q/[q для перехода к следующему/предыдущему элементу quickfix списка (не набирать же :cnext!).

    Этот плагин полон подобных быстрых переходов, а также переключателей орфографии, wrapping и прочих мелочей. Плагин сконсолидировал множество всего что у меня было в .vimrc и я переделал переключение textwidth чтобы оно походило на unimpaired:

    ----- ~/.vimrc -----
    nmap [ob :set textwidth=72<CR>
    nmap ]ob :set textwidth=0<CR>
    

  • abolish много чего умеет на тему похожих и схожих замен. Команда замены :Subvert бесценна во время программирования и рефакторинга. Например у вас есть строчка FooBar foo_bar FOO_BAR. Применив :S/Foo/Baz/ вы получите BazBar baz_bar BAZ_BAR. Одной :S командой можно делать глобальный рефакторинг для всего файла!

    А также abolish умеет менять «coercion» у слова: применив cr_ к FooBarBaz, он изменится на foo_bar_baz. Применив crm («m» от «mixed» case) к этому, вы получите FooBarBaz снова. В языке, где регулярно присутствует мешанина из mixed, camel, kebab, snake case написания одного и того же слова — это бесценнейший плагин.
  • commentary позволяет gc командой добавлять или убирать символы комментариев. Крайне просто и эффективно работает.
  • tbone позволяет выполнять команды в tmux. Использую частенько :Tyank и :Tpaste для вставки текста из буферов tmux или копирования в них. Удобная интеграция.
  • Ну и конечно же fugitive. Как пишет сам Tim Pope в описании, "(этот) git wrapper настолько крутой, что должен быть вне закона" (отсюда и fugitive название). И он не преувеличивает: выполняю тьму действий в git-е только через него. В первую очередь это :Gdiff — только ради него можно бы было ставить этот плагин.

    ----- ~/.vimrc -----
    nmap <F7> :Gvsplit <C-R><C-W><CR>zR
    nmap <F9> :diffupdate<CR>:syntax off<CR>:syntax on<CR>
    


    F7 позволяет открыть окно с просмотром (в fugitive) коммита находящегося под курсором. Постоянно использую во время интерактивного rebase. А F9 «передёргивает» синтаксическую подсветку, которая (вина Vim) может иногда съезжать, особенно во время работы с diff-ом. Костыль, но терпимый.


Отдельно скажу своё фи про NERD* семейство плагинов: это пример того, что не надо пытаться делать с Vim-ом. NERDTree только скрывает, и так достаточно удобный и хороший, функционал, не давая пользователю его познать. А в NERDCommenter достаточно увидеть hard-coded знания о символах комментариях для разных языков, в противовес commentary, использующим штатную commentstring настройку. Честно, если в статье про Vim вижу совет использования NERD*, то сразу закрываю.

А также другие плагины:

  • Mark позволяет подсвечивать разными цветами разные слова. Просто нажимая \m будет другой цвет очередного подсвечиваемого слова. Помню что мне это сильно помогало в огромных SQL запросах, где полно слов отличающихся только окончанием и тяжело визуально ориентироваться и дифференцировать их.
  • traces в реальном времени показывает какое изменение будет производить набираемая команда :s. На деле оказалось очень и очень удобным: сразу виден корявый результат!
  • IndentWise добавляет motion-ы по уровням indent-ов. С ним можно «переместиться на ближайшую строку с меньшим/большим indent-ом». Можно и другие команды (например удаление) применять к indent-блокам. Очень часто использую как в Python, так и в Си для навигации. Как мне сказать чтобы я переместился на начало внешнего for-цикла где-то за пределами экрана? Indent motion-ом это делается мгновенно.
  • ViewPort выглядит как хак, но вполне себе неплохой. Он позволяет участок буфера редактировать в отдельном окне. Он запоминает начало/конец редактируемой части и вешает hook-и чтобы, при сохранении ViewPort-буфера, его содержимое вставилось назад между метками. В Python коде это очень часто помогало, когда есть масса визуально похожих частей кода и можно ошибиться, начав менять что-то не то. Или поиск будет регулярно выходить на пределы редактируемой функции/класса, сбивая с толку.
  • vim-lsp LSP клиент. Корявый, регулярно сбивающий :Changes плагин. Но… всё же приносящий пользу. LSP идея мне нравится, но лишь бы оно быстро работало. С Go вообще никаких проблем. clangd LSP сервер для Си кода медленно работает с completion на больших проектах. С Python польза LSP мне ещё не ясна, когда и так есть асинхронный linter, заполняющий, к тому же, quickfix.

    ----- ~/.vim/plugin/lsp.vim -----
    let g:lsp_auto_enable = 1
    let g:lsp_diagnostics_echo_cursor = 1
    let g:lsp_diagnostics_echo_delay = -1
    let g:lsp_signature_help_enabled = 0
    
    function! s:on_lsp_buffer_enabled() abort
        setlocal omnifunc=lsp#complete
        nmap <buffer> [g <Plug>(lsp-previous-diagnostic)
        nmap <buffer> ]g <Plug>(lsp-next-diagnostic)
        nmap <buffer> gd <plug>(lsp-definition)
        nmap <buffer> K <plug>(lsp-hover)
    endfunction
    
    augroup lsp_install
        autocmd!
        autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
    augroup END
    


    Вот показ диагностических сообщений (linter) полезен: пускай даже и не быстро работает, но не отвлекает и подсказывает о примитивных проблемах (невалидный синтаксис). [g и ]g прыгают по ближайшим диагностическим проблемам. gd (goto definiton) из-за тэгов не особо нужен, но пускай будет. А K команда показывает hover окно с сигнатурой функций. Причём автоматически показ сигнатуры (g:lsp_signature_help_enabled) во время ввода отключён — слишком уж мелькает на экране всё при этом. Плюс отключено автоматическое omni-дополнение из-за тормозов (явно вызываю только когда нужно через Ctrl-B).


Особняком стоит мой самописный CodeComm. История его началась почти десять лет назад, когда, работая в одной компании, надо было делать code review и специализированного инструментария для этого не было — review писался в виде комментариев к задаче в трэкере. Открывая код для review в fugitive, визуально выделял интересующие куски кода, нажимал \cc, открывалось окно с этим куском кода, где оставлял свои замечания. Всё это агрегировалось в файле на диске, который затем вставлял в трэкер.

На другой работе использовался Gerrit. Но не буду же сидеть в броузере для review!? Модифицировал плагин: он в итоге создавал JSON, пригодный для вставки в API Gerrit-а. А дальше вынес основной функционал комментирования в CodeComm, не привязанный к какой-либо системе.

Выделив какой-то блок внутри fugitive (чтобы был виден хэш коммита и путь), нажав \cc, будет создаваться такой вот блок:

-----# 2 [                            ec77b1f6b | src/pyderasn.py ]-----
  79 def tag_encode(num, klass=TagClass.universal, form=TagForm.primitive):
  80     if num < 31:
  81         # [XX|X|.....]
  82         return int2byte(klass.value | form.value | num)
  83     # [XX|X|11111][1.......][1.......] ... [0.......]
  84     return int2byte(klass.value | form.value | 31) + ...
  85
---------------------------------- >8 ----------------------------------
Here goes my comment.


Номера строк, номер комментария (чтобы сослаться), хэш, путь. Агрегирование комментариев в файл на диске. Для ревью кода использую сейчас только его.

~/.vim/plugin



По сути вся ~/.vim/plugin директория может находится внутри ~/.vimrc, как и наоборот. Принципа разделения у меня нет никакого — просто ощущения что надо выносить, а что по мелочам оставить в общей куче. Только один раз вставлю тут строки проверяющие загружен ли плагин — все они могут чуть-чуть ускорить загрузку Vim, но интереса более не представляют. Рассмотрю содержание директории без какого-либо порядка:

----- ~/.vim/plugin/buftabline.vim -----
if exists('*<SID>BufTabLine') | finish | endif
function! BufTabline()
    redir => bufsRaw
    silent buffers
    redir END
    let bufs = []
    for buf in split(bufsRaw, "\n")[:16]
        let leftIdx = stridx(buf, '"')
        let rightIdx = strridx(buf, '"')
        let filename = substitute(buf[leftIdx + 1 : rightIdx - 1], "%", "%%", "g")
        let linenum = split(buf[rightIdx:], " ")[-1]
        let attrs = split(buf[:leftIdx-1], '\s\+')
        let attrs = map(attrs, 'substitute(v:val, "%", "%%", "g")')
        let attrs = (len(attrs) == 1) ? attrs[0] : attrs[0] . ":" . join(attrs[1:], "")
        let attrs = "%#TabLine#" . attrs . "%*"
        if len(filename) > 20 | let filename = "<" . filename[-(20-1):] | endif
        let hl = (match(attrs, "%%") == -1) ? "%#TabLine#:" : "%#TabLineSel#"
        let bufs = add(bufs, attrs . hl . filename . ":" . linenum . "%*")
    endfor
    return join(bufs, "  ")
endfunction
set showtabline=2
set tabline=%!BufTabline()


Одна из немногих фич которую совершенно не использую в Vim это tab-ы. Не сложилась дружба с ними, даже не пытаюсь более. Но раз для них выделена целая строка tabline, то почему бы не использовать её для информации? Вот её и заполняю списком буферов, аналогично выдаче :buffers, только в одну строку. Сейчас она выглядит вот так:

1:a:mysetup.html:6040  2:~/.vimrc:118  17:<mm/doc/codecomm.txt:34  20:%a<ugin/buftabline.vim:1


Она буквально перехватывает вывод :buffers и парсит его. Почему нельзя использовать функции vimscript? Потому что не нашёл возможностей для получения ряда интересующих меня атрибутов буферов! Или плохо искал.

----- ~/.vim/plugin/chnglstnav.vim -----
function! s:Chng(...)
    if a:0 == 0 | return | endif
    execute "normal " . a:1 . (a:1 > 0 ? "g;" : "g,")
endfunction
command! -nargs=? Chng call s:Chng(<args>)
nmap <End> :changes<CR>:Chng<Space>


End клавиша показывает список :changes (вынес на отдельную клавишу, потому что часто использую) и подготавливает :Chng команду к вводу. Ей можно передать номер change на который надо прыгнуть назад. Указав отрицательное значение, можно прыгнуть «вперёд». Использую это всё когда нужно совершить прыжок не на 2-3 шага назад/вперёд, а на сколько именно не помню.

Defsplit плагин изначально писался для тривиальной задачи: разбить сигнатуру Python функции по строчкам:

    def foobar(self, foo: str, bar: Some[thing, too]) -> None:
превращается в:
    def foobar(
          self,
          foo: str,
          bar: Some[thing, too],
    ) -> None:

    foo(bar, baz)[0]
превращается в:
    foo(
        bar,
        baz,
    )[0]


То ли совсем не нашёл готовых средств для этого, то ли все они занимали тьму кода. Если для такой (простой) задачи вижу плагин на несколько экранов — что-то тут не так, иду пробовать писать своё решение. :Defsplit делает разбиение сигнатуры по строчкам. Можно указать порядковый номер скобочки с которой начать перенос. :Undefsplit схлопнет всё выражение в одну строчку: например уменьшилось количество аргументов или надо переразбить по другой скобочке. Далее появился Brsplit на том же самом «движке» для разбиения любых скобочек. Во время разработки на Python это всё использовалось сотни раз в час! Для Go, очевидно, Defsplit не применяется, так как в нём собственная жёсткая система автоматического форматирования.

----- ~/.vim/plugin/disables.vim -----
let g:loaded_2html_plugin = 1
let g:loaded_getscriptPlugin = 1
let g:loaded_logipat = 1
let g:loaded_rrhelper = 1
let g:loaded_spellfile_plugin = 1
let g:loaded_vimballPlugin = 1
let g:loaded_vimball = 1


Отключение загрузки кучи родных плагинов, которые мне штатно не нужны. Сокращает время запуска Vim.

----- ~/.vim/plugin/exted.vim -----
function! s:exted(ext)
    execute "edit %<." . a:ext
endfunction
command! -nargs=1 Ee silent call s:exted(<f-args>)


Ee h позволяет отредактировать файл с таким же названием как и текущий, но «h» расширением. Использую для переключения между .c и .h.

----- ~/.vim/plugin/fileline.vim -----
function! s:gotoline()
    let file = bufname("%")
    if filereadable(file) | return | endif
    let names = matchlist(file, '\(.\{-1,}\):\%(\(\d\+\)\%(:\(\d*\):\?\)\?\)\?$')
    if empty(names) | return | endif
    let file_name = names[1]
    let line_num = names[2] == "" ? "0" : names[2]
    let col_num = names[3] == "" ? "0" : names[3]
    if !filereadable(file_name) | return | endif
    let nr = bufnr("%")
    exec "keepalt edit +" . line_num . " " . file_name
    exec "normal! " . col_num . "|"
    exec "bdelete " . nr
endfunction
autocmd! BufNewFile *:* nested call s:gotoline()
autocmd! BufRead *:* nested call s:gotoline()


Это must-have возможность открытия файла с указанием номера строки через двоеточие. foo:12 имя файла откроет foo файл на 12-ой строке. Мой вариант это упрощённый file:line, в котором происходила замена текущих открытых окон на новое, что мне было не удобно. Огромное количество утилит и linter-ов выдаёт адреса строк в файлах именно в этом формате.

----- ~/.vim/plugin/pastemode.vim -----
if &term =~ "screen.*"
    let &t_ti = &t_ti . "\e[?2004h"
    let &t_te = "\e[?2004l" . &t_te
    function! XTermPasteBegin(ret)
        set pastetoggle=<Esc>[201~
        set paste
        return a:ret
    endfunction
    map <expr> <Esc>[200~ XTermPasteBegin("i")
    imap <expr> <Esc>[200~ XTermPasteBegin("")
    cmap <Esc>[200~ <nop>
    cmap <Esc>[201~ <nop>
endif


Очередная must-have вещь, которая встроена в NeoVim по умолчанию: поддержка bracketed paste вставки, про которую писал выше. Тут чётко видно, что когда Vim встречает escape-последовательность начинающую bracketed paste, то он переключает paste опцию.

----- ~/.vim/plugin/whereami.vim -----
function! s:pwdLoad()
    let g:mein_pwdL=trim(system("pwd -L"))
    let g:mein_pwdP=trim(system("pwd -P"))
endfunction
autocmd VimEnter * call s:pwdLoad()

function! WhereAmI(fmt)
    let fullpath = expand("%:p")
    if fullpath[:len(g:mein_pwdP)-1] ==# g:mein_pwdP
        let fullpath = g:mein_pwdL . fullpath[len(g:mein_pwdP):]
    endif
    if a:fmt == "gnu"
        let where = fullpath . ":" . line(".")
    elseif a:fmt == "lldb"
        let where = "breakpoint set --file " . fullpath . " --line " . line(".")
    else
        let where = "unknown fmt"
    endif
    let @* = where
    echomsg where
endfunction
nmap <leader>w :call WhereAmI("gnu")<CR>
nmap <leader>W :call WhereAmI("lldb")<CR>


\w команда показывает и копирует в X11 буфер обмена путь к редактируемому файлу и номер строки где находится курсор. Используется в первую очередь для работы с отладчиками. Нужно установить breakpoint вот на этом участке кода: \w, переходим в отладчик, Shift-Insert, Enter. Но формат у LLDB и GDB команд отличается, поэтому для LLDB используется \W.

Начало скрипта очень важно: при запуске Vim он запоминает значение «физической» и «логической» текущей директории. У меня есть проекты в которых символические ссылки библиотеки ведут во вне дерева исходных кодов (например на зашифрованный раздел). Go при сборке программы видит путь ~/work/go/src/XXX, а Vim при редактировании файла в этом XXX будет видеть его физический путь до /somewhere/outside/XXX. И я не нашёл способа получить «логический» путь до файла/директории средствами самого Vim. И засада в том, что при нажатии \w передам в отладчик физический путь, про который ни он, ни компилятор ничего не знают. В своём плагине проверяю отличаются ли физические и логические пути и делаю замену физической части пути на логическую. Выглядит грязно, но работает is good enough.

----- ~/.vim/plugin/grep.vim -----
function! s:Vim(pattern)
    let ignorecase_bak=&ignorecase
    set noignorecase
    execute "vimgrep /" . a:pattern . "/ **/*"
    copen
    let &ignorecase=ignorecase_bak
    let g:pylint_disable=1
endfunction
command! -nargs=* -complete=file Vim call s:Vim(<q-args>)


В Vim есть встроенный поиск по файловой системе: vimgrep. Мне почти всегда нужно указать рекурсивный поиск по всему дереву, добавляя **/* путь. А также не игнорировать регистр. Писать каждый раз :vimgrep /whatever/ **/*, да ещё и с запоминанием ignorecase настройки — не вариант. Поэтому используется :Vim whatever команда.

----- ~/.vim/plugin/ggrep.vim -----
function! s:Vmg(pattern)
    silent execute 'Ggrep "' . a:pattern . '"'
    copen
    redraw!
    let g:pylint_disable=1
endfunction
command! -nargs=* -complete=file Vmg call s:Vmg(<q-args>)


Позже познал скорость git grep и начал использовать :Ggrep команду из fugitive. Но мне не нравится что quickfix список с результатами поиска не открывается автоматом. Кроме того, нужно помнить про экранирование искомого запроса. Тут родилась :Vmg обёртка, схожая по названию с :Vim, чтобы меньше запоминать.

----- ~/bin/qq -----
#!/bin/sh
tmp=`mktemp`
tmux capture-pane -J
tmux save-buffer $tmp
tmux delete-buffer
perl -ne '/^([^:]+:\d+:.*[^\s])\s*$/ and print "$1\n"' < $tmp > $tmp.err
rm $tmp
vim -c 'let g:pylint_disable=1' -c copen -q $tmp.err
rm $tmp.err


Часто хочу открыть результат grep, запущенного из командной строки zsh, уже постфактум. Да, Vim поддерживает file:line формат файлов, но это если мне надо открыть один файл. qq скрипт сохраняет tmux экран, находит всё что похоже на file:line и запускает Vim, загружая в него quickfix список. Выглядит грязно, но вполне себе сносно работает! Ведь не всегда же я сижу в Vim и провожу поиск через :Vim и :Vmg.

----- ~/.vim/plugin/zshfe.vim -----
if !exists("g:zshfe_path") | let g:zshfe_path=expand("~/.vim/plugin/zshfe.zsh") | endif

function! s:zshfe(query, opencmd)
    silent let result = systemlist(g:zshfe_path . " " . a:query)
    if len(result) == 0 | return | endif
    exec a:opencmd . " " . result[0][:-2]
endfunction

command! -nargs=1 Fe call s:zshfe(<f-args>, "edit")
command! -nargs=1 Fsp call s:zshfe(<f-args>, "split")
command! -nargs=1 Fvs call s:zshfe(<f-args>, "vsplit")

nmap <Leader>e :Fe         <- тут пробел в конце строки
nmap <Leader><space> :Fsp  <- тут пробел в конце строки
nmap <Leader>v :Fvs        <- тут пробел в конце строки


----- ~/.vim/plugin/zshfe.zsh -----
#!/usr/bin/env zsh
set -e
zmodload zsh/zpty
zpty zshfe zsh
zpty -w zshfe "print ZSHFE $*"$'\t EFHSZ\nexit\n'
zpty -r zshfe zshfe "*exit"
print -- $zshfe | sed -n "s/^ZSHFE \(.*\) EFHSZ/\1/p"


Я так привык к fuzzy-like дополнению путей в zsh (когда f/b/baz раскрывается в foo/2bar/somebaz), что хотел бы и в Vim задавать пути к открываемым файлам в этом же виде. Не могу описать как к этому пришёл, но лучше чем запускать zsh, внутри которого эмулировать pty терминал, отсылать нажатия клавиш (ввод пути) и ловить вывод, не нашёл.

\eПУТЬ выполняет :Fe ПУТЬ команду, которая откроет найденный ПУТЬ (zsh-like completed) в текущем окне. \v сделает это в вертикальном split, а \ПРОБЕЛ в горизонтальном.

~/.vim/ftplugin


Filetype-специфичные настройки, не требующие комментариев:

----- ~/.vim/ftdetect/conf.vim -----
autocmd BufRead,BufNewFile *.conf setlocal noexpandtab

----- ~/.vim/ftdetect/dtrace.vim -----
autocmd BufRead,BufNewFile *.d setlocal filetype=dtrace

----- ~/.vim/ftdetect/hjson.vim -----
autocmd BufNewFile,BufRead *.hjson setlocal shiftwidth=2
autocmd BufNewFile,BufRead *.hjson setlocal commentstring=#\ %s

----- ~/.vim/ftdetect/python.vim -----
autocmd BufRead,BufNewFile *.pyi setlocal filetype=python

----- ~/.vim/ftdetect/redo.vim -----
autocmd BufNewFile,BufRead *.do setlocal filetype=sh

----- ~/.vim/ftdetect/sql.vim -----
autocmd BufRead /tmp/psql.edit* setlocal filetype=sql

----- ~/.vim/ftplugin/gitcommit/autos.vim -----
setlocal spell spelllang=ru,en_gb
setlocal textwidth=72

----- ~/.vim/ftplugin/yaml/autos.vim -----
setlocal shiftwidth=2

----- ~/.vim/ftplugin/texinfo/autos.vim -----
abbreviate \t @tab
setlocal commentstring=@c\ %s

----- ~/.vim/ftplugin/python/ignores.vim -----
set wildignore+=**/_build/*,**/.hypothesis

----- ~/.vim/ftplugin/c/lsp.vim -----
au User lsp_setup call lsp#register_server({
\    "name": "clangd",
\    "cmd": ["clangd"],
\    "allowlist": ["c", "cpp", "objc", "objcpp"],
\})

----- ~/.vim/ftplugin/go/lsp.vim -----
au User lsp_setup call lsp#register_server({
\    "name": "gopls",
\    "cmd": ["gopls"],
\    "allowlist": ["go"],
\})

----- ~/.vim/ftplugin/python/lsp.vim -----
if executable("pyls")
    " pip install 'python-language-server[all]'
    au User lsp_setup call lsp#register_server({
    \    "name": "pyls",
    \    "cmd": {server_info->["pyls"]},
    \    "allowlist": ["python"],
    \})
endif


----- ~/.vim/ftdetect/mutt.vim -----
function! s:KillSignature()
    call cursor(1, 1)
    call search('^[>|] \?-- \?$')
    if getpos(".")[1] != 1
        normal d}
    endif
endfunction
autocmd BufRead /tmp/mutt-* call s:KillSignature()
autocmd BufRead /tmp/mutt-* setlocal textwidth=72


Во время написания ответа на письмо, эта функция вырезает подпись из процитированного письма. К сожалению, бывают неопытные пользователи, не знающие что подпись отделяется тремя символами .

----- ~/.vim/ftdetect/tex.vim -----
autocmd BufNewFile,BufRead *.tex abbreviate framedo
    \begin{frame}<CR>\frametitle{TODO}<CR>\end{frame}<ESC>k>>wwlcw
autocmd BufNewFile,BufRead *.tex abbreviate itemdo
    \begin{itemize}<CR>\item <CR>\end{itemize}<ESC>k>>$a
autocmd BufNewFile,BufRead *.tex abbreviate cnter
    \begin{center}<CR>\end{center}<ESC>k$o


Ввод framedo, itemdo или cnter с последующим Ctrl-O отобразят на экране:

\begin{frame}
    \frametitle{КУРСОР}
\end{frame}

\begin{itemize}
    \item КУРСОР
\end{itemize}

\begin{center}
КУРСОР
\end{center}


где курсор сразу будет находятся в КУРСОР позиции в режиме ввода. Создал это из-за презентаций (в Beamer), где чаще всего применяется. Знаю что есть отдельные плагины для создания, так называемых, code snippet. Пробовал SnipMate (может быть что-то ещё), но не нашёл где бы мне не хватило штатного функционала.

----- ~/.vim/ftplugin/c/autos.vim -----
setlocal commentstring=//\ %s

abbreviate UCC unsigned char
abbreviate u8 uint8_t *
abbreviate U8 (uint8_t *)
let @e = "ywoassert(^[pA!= NULL);^["

setlocal equalprg=cfmt.sh
command! -buffer Fmt normal mtgg=G'tz.


Вызов @e макроса копирует название переменной под курсором на следующую строку, оборачивая в assert(XXX != NULL); вызов. А :Fmt команда: запоминает текущую позицию, переходит в начало файла, применяет к нему команду выравнивания, возвращается назад, центрирует экран. Команда «выравнивания» тут применяется исключительно как способ прогона всего исходного кода через внешнюю утилиту, форматирующую код. Для Go применяется fmt.vim плагин, поставляемый, если не путаю, в Go дистрибутиве. Он тоже использует:Fmt (на самом деле это Си сделан по образу и подобию).

----- ~/.vim/ftplugin/go/motion.vim -----
nnoremap <silent> <buffer> ]] :call <SID>Go_jump('/^\(func\\|type\)')<cr>
nnoremap <silent> <buffer> [[ :call <SID>Go_jump('?^\(func\\|type\)')<cr>
nnoremap <silent> <buffer> ]m :call <SID>Go_jump('/^\s*\(func\\|type\)')<cr>
nnoremap <silent> <buffer> [m :call <SID>Go_jump('?^\s*\(func\\|type\)')<cr>
fun! <SID>Go_jump(motion) range
    let cnt = v:count1
    let save = @/    " save last search pattern
    mark '
    while cnt > 0
        silent! exe a:motion
        let cnt = cnt - 1
    endwhile
    call histdel('/', -1)
    let @/ = save    " restore last search pattern
endfun


Насколько помню, всё это было взято из Python filetype файла из дистрибутива Vim и переделано для Go. Команды [[/]] в vi позволяли перемещаться между функциями (поэтому в Си коде важно чтобы открывающая фигурная скобка начала функции располагалась в начале строки), а [m/]m между методами. Здесь [[/]] используются, аналогично, для прыжков между функциями и объявлением типов. А [m/]m разрешают иметь indentation, позволяя прыгать на анонимные функции, объявляемые внутри других функций.

----- ~/.vim/ftplugin/go/autos.vim -----
set noexpandtab
let g:defsplit_shift="ТАБУЛЯЦИЯ"

let @e = "^iif err = ^[A; err != nil {^[oТАБУЛЯЦИЯ"
let @r = "oif err != nil {^M}^[OТАБУЛЯЦИЯ"


@e макрос при применении на foo(bar) строчке сделает:

if err = foo(bar); err != nil {
    КУРСОР


А @r добавит на следующей строке:

if err != nil {
    КУРСОР
}


----- ~/.vim/ftplugin/go/gogetdoc.vim -----
function! LintStatus()
    if exists("b:gogetdoc_job") && job_status(b:gogetdoc_job) == "run" | return "GD" | endif
    return ""
endfunction

function! GoGetDocGot(ch)
    let msgs = []
    while ch_status(a:ch) == "buffered"
        let msgs = add(msgs, ch_read(a:ch))
    endwhile
    if exists("b:godocid") | call popup_close(b:godocid) | endif
    if len(msgs) == 0
        echohl WarningMsg | echomsg "No go doc" | echohl None
        return
    endif
    let msgs = msgs[2:]
    let b:godocid = popup_atcursor(msgs[2:-2],
        {"wrap": 0, "title": msgs[0], "move": "word"})
endfunction

function! s:GoGetDoc()
    if exists("b:gogetdoc_job") && job_status(b:gogetdoc_job) == "run"
        return
    endif
    let pos = line2byte(line(".")) + col(".") - 2
    let cmdline = "gogetdoc -pos " . expand("%p") . ":#" . pos
    echomsg cmdline
    let b:gogetdoc_job = job_start(cmdline, {
        \"in_mode": "nl",
        \"err_io": "null",
        \"close_cb": "GoGetDocGot",
    \})
endfunction

nmap <buffer> <silent> <CR> :call <SID>GoGetDoc()<CR>


Изначально это был просто proof-of-concept работы popup окон, но в итоге стало функцией показа документации (используя внешнюю gogetdoc утилиту) по слову под курсором. Просто нажимаем на нём Enter и асинхронная задача в фоне будет получать документацию, отобразив в popup окне.

----- ~/.vim/ftplugin/python/autos.vim -----
iabbrev #u # coding: utf-8
iabbrev tt # type:
iabbrev tti # type: ignore
iabbrev trace import pdb ; pdb.set_trace()<CR>pass
iabbrev embed import code ; code.interact(local=locals())
iabbrev kargs *args, **kwargs
iabbrev pyldis # pylint: disable=
iabbrev deff def () -> None:<ESC>F(i
iabbrev """ """<ESC>o"<ESC>2i"<ESC>kA
nmap <leader>ss :set lazyredraw<CR>vip:sort u<CR>:'<,'>sort i<CR>:set nolazyredraw<CR>
let @b = ">gvctry:^[<<oexcept Exception as err:^Mimport pdb ;
    pdb.set_trace()^Mpass^[>>k>>kP"
let @n = "ddV/except.*:^M<n3dd"


  • Большая часть аббревиатур и так ясна. deff с последующим Ctrl-O создаёт: def (КУРСОР) -> None:. Ввод тройных открывающих docstring кавычек сразу создаёт и закрывающие.
  • \ss используется для сортировки абзаца с import-ами и удаления дублей. Почему нельзя делать :sort ui? Потому что бывают случаи, когда игнорирование регистра приведёт к схлопыванию, ставших «одинаковыми», import-ов. А lazyredraw исключительно для производительности.
  • @b и @n макросы — самое ценное что есть у меня для Python. Визуально выделяем какой-то участок кода (например пару строчек с foo и bar), применяем @b и получаем:

    try:
        foo
        bar
    except Exception as err:
        import pdb ; pdb.set_trace()
        pass
    


    И это всё будет работать независимо от начального indentation. Применяем @n на try: и вся эта обёртка убирается назад. Колоссальнейшая экономия времени во время отладки тестов и кода. И всё без vimscript.


----- ~/.vim/ftplugin/python/importcompl.vim -----
let s:git_grep_cmd = "git grep -H --line-number --ignore-case --no-color "

function! SortByLen(s1, s2)
    if len(a:s1) == len(a:s2) | return a:s1 > a:s2 | endif
    return 1 ? len(a:s1) > len(a:s2) : -1
endfunction

function! ImportCompl()
    normal diw
    let output = system(s:git_grep_cmd . '"^from .* import .*' . @" . '" -- "*.py" "**/*.py"')
    let suggestions = []
    for line in split(output, "\n")
        if stridx(line, "unused-import") != -1 | continue | endif
        let m = matchlist(line, '^.*:\d\+:\(.*\)$')
        if len(m) == 0 | continue | endif
        call insert(suggestions, m[1])
    endfor
    call sort(suggestions, "SortByLen")
    call uniq(suggestions)
    call reverse(suggestions)
    call complete(col('.'), suggestions)
    return ''
endfunction

inoremap <F3> <C-R>=ImportCompl()<CR>

function! AllImportCompl()
    let output = system(s:git_grep_cmd . '"^from .* import" -- "*.py" "**/*.py"')
    let imports = {}
    for line in split(output, "\n")
        if stridx(line, "unused-import") != -1 | continue | endif
        for regexp in [
            \'^.*:\d\+:\(from .* import \(\w\+\).*\)$',
            \'^.*:\d\+:\(from .* import \w\+ as \(\w\+\).*\)$',
        \]
            let m = matchlist(line, regexp)
            if len(m) == 0 | break | endif
            let imports[m[2]] = m[1]
        endfor
    endfor
    let lines = getloclist(winnr())
    if len(lines) == 0 | let lines = getqflist() | endif
    let result = []
    for line in lines
        let m = matchlist(line.text, '\(E0602\|F821\).*' . "'" . '\(\w\+\)' . "'$")
        if len(m) == 0 || !has_key(imports, m[2]) | continue | endif
        call insert(result, imports[m[2]])
    endfor
    call sort(result, "i")
    call uniq(result)
    call append(".", result)
endfunction


Код из серии «хак на хаке». Но при этом вполне работающий в преобладающем большинстве случаев. Занимается автоматической вставкой import-ов. Повторюсь: экосистема Python меня удручает и, на момент написания, не встретил удовлетворивших меня инструментов (тогда как в Go вон всё прекрасно).

Например я набрал слово Certificate и нажал на нём F3. Будет запущен git grep с поиском import-ов связанных с Certificate по всему проекту и сортировкой по самой длинной строке. Будет предложено меню с найденными предложениями. Конечно это не сработает когда проект совсем голый и import ещё ни разу не был написан. А ещё это будет работать только с «каноничными» import-ами (уже писал о них для pyimportcan.pl скрипта).

Позже был рождён ещё больший хак в виде :AllImportCompl функции. Предварительно нужно запустить Pylint или Pyflakes linter-ы, загрузив результат их работы в quickfix список (это автоматом делает мой pylint.vim). Функция найдёт в этом списке ошибок все предупреждения об отсутствующих import-ах и для каждого из них она запустит аналог F3-функции. Дальше останется только применить сортировку и разбиение по группам. До сих пор удивительно для меня, но этот огромный хак отрабатывает очень и очень хорошо: с ростом проекта покрывая (автоматически успешно добавляя) преобладающее большинство всех import-ов.

Примеры кода тут привожу чтобы продемонстрировать что в vimscript нет ничего сложного и страшного. Это штука которую может использовать обычный пользователь, а не hardcore разработчик.

----- ~/.vim/ftplugin/python/unused_remover.vim -----
function! UnusedImportsRemover()
    call setqflist(filter(getqflist(), "stridx(v:val.text, \"unused-import\") != -1 ||
        stridx(v:val.text, \"imported but unused\") != -1"))
    cdo d
endfunction


Это дополнение к хаку :AllImportCompl — функция удаляющая неиспользуемые import-ы (тоже на основе quickfix данных linter-а).

----- ~/.vim/ftplugin/python/testname.vim -----
function! TestName()
    normal mm
    normal ?.*\s*def .*[Tt]est^M
    normal ^f(Byw
    let postfix = @"
    normal [[f(Byw
    let postfix = @" . "." . postfix
    normal `m
    let base = join([""] + split(getcwd(), "/")[:-1], "/")
    let prefix = substitute(expand("%:p:r")[len(base)+1:], "/", ".", "g")
    let name = prefix . ":" . postfix
    let @* = name
    echomsg name
endfunction
nmap <leader>t :call TestName()<CR>


Функция создания Python-пути до теста в котором мы сейчас находимся. На удивление не маленькая, так как у нас только текст, а не AST дерево кода. Находясь в теле теста, нажимаем \t и в буфер обмена будет скопирована строка типа foo.bar.tests:TestBaz.test_whatever. Часто использую когда нужно быстренько запустить только этот метод: что происходит постоянно при написании новых тестов.

Вот и всё! Надеюсь что кто-нибудь да почерпнёт что-нибудь полезное для себя!
Теги:
Хабы:
Всего голосов 9: ↑8 и ↓1+7
Комментарии1

Публикации

Истории

Ближайшие события

2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань