Трюки, облегчающие жизнь в zsh

    Zsh — одна из лучших командных оболочек, обладающая впечатляющим набором возможностей. Однако, из‐за большого количества возможностей нет ничего удивительного в том, что некоторые из них проходят мимо внимания или возможность их применения для решения повседневных задач неочевидна. В этой статье будут рассмотрены как несколько «встроенных» возможностей zsh, так и примеры непростого кода, облегчающие жизнь.

    1. Использование READNULLCMD


    Переменная READNULLCMD определяет команду, которая будет вызвана, если перенаправление stdin использовать без ввода команды: <file.txt. Так можно вызывать less, вводя намного меньше символов: просто установите READNULLCMD=less.

    2. Вставка открывающих и закрывающих скобок


    С редакторами вроде Vim часто используются дополнения, занимающиеся автоматических закрытием скобок при их вводе. Т.е. при вводе [ вы получаете [] с курсором посередине. В оболочках такое тоже возможно (даже в bash): просто нужно использовать что‐то вроде binkey -s "[" $'\C-v[]\C-b': эквивалент этой команды вполне может быть помещён в .inputrc. Более универсальное решение для zsh предполагает использование ZLE widget’ов:
    insert-double-brackets() {
        LBUFFER="${LBUFFER}[[ "
        RBUFFER=" ]]${RBUFFER}"
    }
    zle -N insert-double-brackets
    bindkey ',H' insert-double-brackets
    
    Здесь в переменной LBUFFER содержится вся командная строка до курсора, а в переменной RBUFFER — вся после. Вторая команда создаёт widget, третья назначает его на сочетание ,H: таким образом ввод ,H превращается в [[ ]] с курсором посередине.

    3. Глобальные alias’ы


    Вы все, наверное, знаете, что такое alias в оболочке и, возможно, использовали что‐то вроде alias hp='hg push'. Alias’ы в zsh имеют две дополнительные возможности: т.н. суффиксные alias’ы, позволяющие автоматически открывать файлы, не вводя программу (пример: alias -s txt=vim превратит команду foo.txt в vim foo.txt) и глобальные. Первые я никогда не использовал, а вторые нахожу весьма полезными.

    Глобальные alias’ы используются для замены отдельно стоящих слов на своё значение. В отличие от суффиксных и обычных alias’ов, заменяемое слово не обязано находится в положении команды (т.е. первого слова в командной строке, или первого слова после разделителя команд). Так как alias’ы обрабатываются до того, как сработает основной парсер, то вы вполне можете иметь в глобальном alias’е всё, что угодно: перенаправление, if, разделители команд.

    С моей точки зрения наиболее полезно перенаправление различных видов:
    alias -g NN='&>/dev/null'
    alias -g L='|less'
    alias -g G='|grep'
    В данном примере определяются три alias’а: один замалчивает команду, другой использует less для показа вывода команды, третий фильтрует ввод. Пример использования: запись hg cat -r default file.csv G 42 L эквивалентна hg cat -r default file.csv | grep 42 | less, но гораздо короче. Для подачи на вход команды G буквально необходимо использование экранирования: \G или 'G'. Замечу, что \G и 'G' также формируют слова, и на них тоже могут быть alias’ы: alias -g "'G'=|grep", но, надеюсь, вы находитесь в достаточно здравом уме, чтобы не использовать этот факт.

    Несмотря на своё удобство, из‐за некоторых особенностей zsh глобальные alias’ы весьма опасны тем, что могут испортить дополнения zsh. Я видел в одном скрипте case, где было в том числе условие вида L), и оно не срабатывало из‐за превращение в совсем другое условие. Поэтому глобальные alias’ы должны определяться самыми последними, после того, как вы уже загрузили все дополнения. Для загрузки дополнений после определения, отключайте настройку ALIASES: используйте что‐то вроде
    source() {
        setopt localoptions
        setopt noaliases
        builtin source "${@[@]}"
    }
    .() {
        setopt localoptions
        setopt noaliases
        builtin . "${@[@]}"
    }
    
    И так для каждого варианта загрузки дополнений (помимо source и . есть ещё, как минимум, autoload, насчёт действенности именно таких функций для которого я совершенное не уверен). Глобальные alias’ы, впрочем, опасны только в интерактивной сессии, скрипты с #!/bin/zsh затронуты не будут.

    4. Возврат настроек терминала


    Ни для кого не секрет, что если написать cat /bin/test (точнее, cat any-binary-file), то можно получить различные странные эффекты: например, замену части вводимых далее символов на символы для рисования графики. Большинство эффектов устраняются написанием вслепую echo $'\ec', но это та вещь, которую хотелось бы автоматизировать. В этом нам поможет hook precmd, позволяющий запускать вашу функцию прямо перед отображением оболочки. Проблемы, которые я иногда вижу, если случайно вывожу в терминал бинарный файл, у меня валится редактор (Vim) или же я просто запускаю wine (он зачем‐то переключает режим ввода (keyboard transmit mode) и не возвращает обратно): графические символы вместо нормальных, alternate screen становится основным (= отсутствует scrollback (история ввода)), перестают работать как надо стрелки (именно здесь отметился keyboard transmit), не отображается курсор. Для их решения была создана следующая функция:
    _echoti() {
        emulate -L zsh
        (( ${+terminfo[$1]} )) && echoti $1
    }
    term_reset() {
        emulate -L zsh
        [[ -n $TTY ]] && (( $+terminfo )) && {
            _echoti rmacs  # Отключает графический режим
            _echoti sgr0   # Убирает цвет
            _echoti cnorm  # Показывает курсор
            _echoti smkx   # Включает «keyboard transmit mode»
            echo -n $'\e[?47l' # Отключает alternate screen
            # See https://github.com/fish-shell/fish-shell/issues/2139 for smkx
        }
    }
    zmodload zsh/terminfo && precmd_functions+=( term_reset )
    
    ttyctl -f
    
    . После её введения набирать echo $'\ec' мне больше практически не приходится.

    Также отмечу ttyctl -f: эта встроенная возможность zsh блокирует некоторые изменения настроек терминала: тех настроек, которые устанавливаются с помощью stty, а не тех, что можно установить с помощью специальных последовательностей (escape sequences).

    5. Функция zmv


    Вы, возможно, сталкивались с командой rename для автоматического переименования множества файлов. Она существует даже в двух экземплярах: написанный на perl вариант и написанный на C. Zsh имеет что‐то подобное, но только более мощное: во‐первых, вы можете таким образом копировать файлы или запускать hg mv вместо простого перемещения по типу mv. Во‐вторых, можно использовать «интуитивно понятный» вариант вроде noglob zmv -W *.c *.cpp (чтобы избавиться от noglob, используйте alias; в дальнейших примерах noglob подразумевается). Zmv для работы использует не регулярные выражения, а более подходящие под задачу glob выражения. Также в качестве второго аргумента можно использовать фактически любое выражение: zmv -w test_*.c 'test/${1/_foo/_bar}' превратит test_foo_1.c в test_bar_1.c. Здесь параметры вида $N предоставляют доступ к аналогу «capturing groups» из регулярных выражений, а -w превращает test_*.c в test_(*).c.

    Все аргументы:
    • -f: игнорирование наличия файла‐цели. Т.е. если файл test.cpp существует, то команда zmv -W *.c *.cpp откажется перемещать какие‐либо файлы, если среди них есть test.c. -f заставит zmv это сделать, но, однако, не передаст аргумент -f для mv.
    • -i: уточнение необходимости перед каждым перемещением. Для утвердительного ответа нужно нажать y или Y, для отказа нужно нажать что‐либо ещё. Внимание: нажать нужно только y или Y. Нажимать ввод не нужно, он будет воспринят как отказ для следующего файла.
    • -n: печать всех команды, которые zmv будет выполнять, без собственно выполнения.
    • -Q: включение glob qualifier’ов. В связи с тем, что glob qualifier легко перепутать с capturing group, они отключены по‐умолчанию. Glob qualifier — это часть glob, которая уточняет результат: существуют qualifier’ы для определения порядка сортировки, включения некоторых настроек для одного glob’а, а также наиболее полезные в данных обстоятельствах фильтры вроде «раскрывать только символические ссылки».
    • -s: передача дополнительного аргумента -s в команду. Используется в связке с -L, либо эквивалентным испольванием zln вместо zmv.
    • -v: печать выполняемых команд по мере их выполнения.
    • -oarg: указание дополнительных аргументов для команды. Так, чтобы передать mv аргумент --force нужно использовать zmv -o--fore. Может быть использована только один раз.
    • -pprog: использование данной программы вместо mv. Команда должна понимать --: она будет выполняться как prog -- source target.
    • -Pprog: аналогично предыдущему аргументу, но для команд, не понимающих --. Программа будет вызываться как prog source target.
    • -w: автоматическое добавление capturing groups для всех wildcard’ов, описанная выше.
    • -W: тоже, что и предыдущий аргумент, но использование параметров $N, созданных для capturing groups происходит автоматически для wildcard’ов в правом аргументе.
    • -C, -L и -M: аналогично -pcp, -pln и -pmv соответственно: позволяет использовать копирование, создание символических ссылок или перемещение независимо от названия функции (по‐умолчанию есть две дополнительных функции, использующих тот же код, что и zmv: zcp и zln).

    6. Запуск mpv с автоматически найденными субтитрами


    Если вы когда‐либо качали сериалы с внешними субтитрами с torrent’ов, то, несомненно, заметили, что каждый человек, их выкладывающий, имеет собственное мнение относительно того, где должны находится субтитры. Основных вариантов два: в собственном каталоге и непосредственно рядом с видео, но под «собственным каталогом» может скрываться любое название каталога, и даже различные глубины вложения: я видел каталоги вида «subs {sub group}», «субтитры {sub group}», «subs/{sub group}» и даже просто «{sub group}». Дополнительной проблемой служит использование нестандартных шрифтов в субтитрах, с распространением их вместе с субтитрами.

    Для того, чтобы субтитры были‐таки подхвачены и использовали корректные шрифты можно использовать разные способы. Я предпочёл создать функцию, которая автоматически делает нужную работу практически во всех случаях:
    aplayer() {
        emulate -L zsh
        setopt extendedglob
        setopt nullglob
        local -a args
        args=()
        local -A mediadirs
        mediadirs=()
        for arg in $@ ; do
            if [[ ${arg[0]} == '-' ]] ; then
                continue
            fi
            if test -f $arg ; then
                mediadirs[${arg:A:h}]=1
            fi
        done
        local d
        local -i found=0
        for d in ${(k)mediadirs} ; do
            local tail=$d:t
            test -d ~/.fonts/aplayer/${tail}-1 && continue
            local f
            for f in $d/**/(#i)font* ; do
                if test -d $f ; then
                    (( found++ ))
                    ln -s $f ~/.fonts/aplayer/${tail}-${found}
                elif [[ $f == (#i)*.rar ]] || [[ $f == (#i)*.zip ]] ; then
                    (( found++ ))
                    mkdir ~/.fonts/aplayer/${tail}-${found}
                    pushd -q ~/.fonts/aplayer/${tail}-${found}
                        7z x $f
                    popd -q
                fi
            done
        done
        if (( found )) ; then
            fc-cache -v ~/.fonts
        fi
        local -aT subpaths SUBPATHS
        local -A SUBPATHS_MAP
        SUBPATHS=( ${(k)^mediadirs}/(#i)*(sub|суб)*{,/**/*}(/) )
        for sp in $SUBPATHS ; do
            SUBPATHS_MAP[$sp]=1
        done
        local -a subarr
        for d in ${(k)mediadirs} ; do
            for subd in $d/**/ ; do
                if ! test -z $SUBPATHS_MAP[$d] ; then
                    continue
                fi
                subarr=( $subd/*.(ass|ssa|srt) )
                if (( $#subarr )) ; then
                    SUBPATHS_MAP[$subd]=1
                    SUBPATHS+=( $subd )
                fi
            done
        done
        if (( ${#SUBPATHS} )) ; then
            args+=( --sub-paths $subpaths )
        fi
        mpv $args $@ &>/dev/tty
    }
    
    Наличие в zsh вещей вроде ассоциативных массивов очень помогает при создании таких функций.

    Здесь первая часть функции проходится по всем аргументам и забивает каталоги, в которых находятся произведения в ассоциативный массив mediadirs. Он сделан ассоциативным исключительно, чтобы избежать дубликатов.

    Далее функция проходится по всем каталогам с произведениями и находит в них или подкаталогах шрифты, которые могут находиться как в архиве, так и в подкаталоге. Шрифты определяются по характерному названию (наличию font в начале названия), setopt nullglob позволяет не беспекоиться об их отсутствии (по‐умолчанию отсутствие вызвало бы ошибку). Использование setopt extendedglob вкупе с (#i) позволяет не беспокоиться о регистре: (#i) позволяет шрифтам находиться как в каталоге FONTS, так и в Fonts. После нахождения и установки шрифтов в ~/.fonts обновляются индексы с помощью fc-cache: иначе даже скопированные в правильный каталог шрифты не будут использованы. ${(k)ASSOCIATIVE_ARRAY} превращает ассоциативный массив в простой массив, состоящий из ключей.

    В третьем цикле находятся и забиваются в ассоциативный массив каталоги с субтитрами, имеющие «простые» названия вроде «subs» или «субтитры». Опять использовано игнорирование регистра и отдельно (/) в конце, ограничивающее glob только каталогами (пример glob qualifier’а). ${^array} используется для того, чтобы array=( a b c ); echo ${^array}* было эквивалентом echo {a,b,c}*.

    Последний цикл находит каталоги с субтитрами, названные нестандартным способом. Каталогом с субтитрами считается любой подкаталог (по отношению к каталогам с видео), содержащий хотя бы один файл с расширением ass, ssa или srt.

    Нужно отметить наличие довольно странного кода: переменную subpaths никто, вроде, не трогает, но в качестве аргумента --sub-paths используется именно её значение. Дело в том, что в zsh отметили довольно частый шаблон, когда массив значений (обычно, каталогов) является простой строкой, где различные значения отделяются друг от друга разделителем (обычно, двоеточием): примером такого «массива» может быть переменная PATH. Однако программистам было бы удобно работать с такими массивами именно как с массивами, поэтому были созданы «связанные» переменные, где одна из переменных массив (пример: path), а другая строка с заданным (по‐умолчанию двоеточие) разделителем (пример: PATH), и изменение одной из переменных автоматически отражается на другой. Именно таким способом был связан массив SUBPATHS со строкой subpaths.

    7. Создание команд с автоматическим экранированием аргументов


    Аргументами некоторых команд никогда не являются файлы. Однако этот факт не останавливает zsh от раскрытия шаблонов. В обычном случае достаточно написать alias mycmd='noglob mycmd' и mycmd *.foo станет эквивалентным mycmd '*.foo'. Но что, если вы хотите создать команду, на вход которой вы собираетесь подавать $VAR буквально и не хотите писать '$VAR'? Здесь я приведу пример кода, который делает запись zpy import zsh; print(zsh.getvalue("PATH")) эквивалентной zpython 'import zsh; print(zsh.getvalue("PATH"))'; разумеется, только в интерактивном режиме:
    zshaddhistory() {
        emulate -L zsh
        if (( ${+_HISTLINE} && ${#_HISTLINE} )) ; then
            print -sr -- "${_HISTLINE}"
            unset _HISTLINE
        elif (( ${#1} )) ; then
            print -sr -- "${1%%$'\n'}"
        fi
        fc -p
    }
    accept-line() {
        emulate -L zsh
        if [[ ${BUFFER[1,4]} == "zpy " ]] ; then
            _HISTLINE=$BUFFER
            BUFFER="zpython ${(qqq)BUFFER[5,-1]}"
        fi
        zle .accept-line
    }
    zle -N accept-line
    
    Основная часть функции: при вызове widget’а accept-line (вызывается, когда вы нажимаете ввод) определяется, не начинается ли строка с zpy и, если да, строка заменяется на zpython …, где  — экранированная часть строки после zpy и пробела. Функция zshaddhistory используется, чтобы в истории оказалась исходная строка, а не её замена.
    Таким способом можно добавлять в zsh любой нестандартный синтаксис.

    8. Автоматическое исключение файлов из glob’ов


    Представьте, что у вас есть редактор Vim и вы хотите использовать его, чтобы открыть все файлы из каталога (использовать шаблон *). Но помимо простых текстовых файлов в каталоге есть много бинарных вроде *.o (объектных) файлов, которые вы открывать не хотите. Для этого вы можете вместо просто звёздочки написать несколько шаблонов, соответствующих нужным файлам. Или использовать шаблон‐исключение (*~*.o, требует setopt extendedglob). Но с помощью относительно простого трюка это можно автоматизировать:
    filterglob () {
        local -r exclude_pat="$2"
        shift
        local -r cmd="$1"
        shift
        local -a args
        args=( "${@[@]}" )
        local -a new_args
        local -i expandedglobs=0
        local first_unexpanded_glob=
        for ((I=1; I<=$#args; I++ )) do
            if [[ $args[I] != ${${args[I]}/[*?]} ]]
            then
                local initial_arg=${args[I]}
                args[I]+="~$exclude_pat(N)"
                new_args=( $~args[I] )
                if (( $#new_args )) ; then
                    expandedglobs=1
                else
                    if [[ $options[cshnullglob] == off
                          && $options[nullglob] == off ]] ; then
                        if [[ $options[nomatch] == on ]] ; then
                            : ${~${args[I]%\(N\)}}  # Will error out.
                        else
                            new_args=( "$initial_arg" )
                        fi
                    fi
                    if [[ -z $first_unexpanded_glob ]] ; then
                        first_unexpanded_glob=${args[I]%\(N\)}
                        readonly first_unexpanded_glob
                    fi
                fi
                args[I,I]=( "${new_args[@]}" )
                (( I += $#new_args - 1 ))
            fi
        done
        if [[ $options[cshnullglob] == on && $options[nullglob] == off ]] ; then
            if (( !expandedglob )) ; then
                : $~first_unexpanded_glob  # Will error out.
            fi
        fi
        "$cmd" "${args[@]}"
    }
    alias vim='noglob filterglob "*.o" vim'
    
    Здесь определяется alias, который запрещает раскрытие шаблона самой zsh (noglob), но использует для запуска vim функцию, которая раскрывает шаблоны сама (filterglob). Но не просто раскрывает их, а ещё и дополняет шаблоном‐исключением так, что vim * будет работать как vim *~*.o.
    В функции задействованы следующие возможности zsh: ${~var} заставляет zsh использовать раскрытие шаблона применительно к значению переменной var и подставляет результат раскрытия шаблона вместо самой переменной. array[idx1,idx2]=( $new_array ) удаляет часть массива от idx1 до idx2 включительно, вставляя на место удалённых элементов значения массива new_array. При этом размер массива array может измениться. Конструкции вида : $~var с комментарием «Will error out» нужны, чтобы zsh показал ожидаемую ошибку. При этом выполнение функции завершиться. Особых причин использовать именно этот вариант вместо echo … >&2 нет, хотя мой вроде должен поддерживать перехват ошибки с использованием always (что вы вряд ли используете в интерактивной сессии).
    Метки:
    • +22
    • 12,4k
    • 5
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 5
    • 0
      Кто‐нибудь знает, как мой набор ссылок из «содержание» превратить из ведущих в никуда в действующие? Я точно видел оглавление в некоторых статьях.
      • 0
        Попробуйте добавить ссылки вида <a name="parenthesis"></a> перед заголовками
        • 0
          Не работает. Во всяком случае, не работало в черновике и предпросмотре.
          • +6
            Я тоже как-то искал, и вот только сейчас (попытки с третьей) увидел как не очевидно это сделано (взято из справки «html-теги»):
            <anchor>Example1</anchor>
            Тег для указания якоря. Для вызова используйте тег вставки ссылок (<a href="#Example1">Текст</a>).
            • 0
              А я смотрел habrahabr.ru/info/help/posts и думал, что там перечислены все тёги. Оказывается, как минимум anchor там нет. Спасибо!

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.