Переход с bash на zsh

habralogo.jpg

Чтобы перейти с bash на zsh необходимо знать базовые отличия между ними — без этого будет сложно провести первоначальную настройку zsh в ~/.zshrc.


Я не нашёл краткого описания этих отличий когда переходил сам, и мне пришлось потратить немало времени на вычитывание документации zsh. Надеюсь, эта статья упростит вам переход на zsh.


Зачем переходить


Для начала —, а стоит ли вообще тратить своё время и внимание на переход? Учить ещё один диалект sh, менее распространённый чем POSIX sh или bash, заново заниматься настройкой рабочего окружения…


На мой взгляд, если вы проводите много времени в консоли, вам нравятся Vim или Emacs и вы уже потратили немало времени на их настройку «под себя» — однозначно стоит! Zsh по духу очень на них похожа: это очень сложная и гибкая программа, чьи возможности полностью мало кто знает, но потратив некоторое время на настройку можно получить очень удобную лично вам рабочую среду.


Что касается изучения нового диалекта sh… пользы от этого, скорее всего, действительно мало, но описанного в этой статье минимума должно быть достаточно чтобы настраивать zsh, а писать новые скрипты на диалекте zsh вам никто и не предлагает. В общем и целом это ничем не отличается от необходимости минимально знать VimL или Emacs Lisp исключительно для настройки Vim/Emacs.


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


  • Zsh не использует readline для ввода команд пользователем. Вместо этого используется собственный редактор ZLE (Zsh Line Editor). Это позволило реализовать множество фич: удобное редактирование многострочных команд, подсветку синтаксиса прямо в процессе ввода команды, особую обработку «paste» из clipboard чтобы не выполнить случайно вставленный текст, гибкое управление горячими клавишами, undo (в т.ч. отменяющее результат автодополнения и разворачивания glob-ов)… плюс интегрировать функционал редактора с zsh, что позволяет управлять его поведением через обычные функции zsh (например, подсветка синтаксиса так и реализована).
  • Невероятно сложный и гибкий механизм автодополнения команд. Он сильно зависит от контекста, поэтому при нажатии в разных местах командной строки будут дополняться разные вещи: имена команд, их параметры, файлы, имена пользователей и серверов, номера процессов, названия переменных, индексы массивов и ключи хешей, элементы синтаксиса zsh, названия цветов и шрифтов, сетевых интерфейсов, системных пакетов… короче, вообще всего что можно автодополнять. И его можно детально контролировать, вплоть до изменения логики автодополнения для конкретного контекста у конкретной команды.
  • Громадное количество (177 в zsh-5.2) опций, изменяющих поведение zsh. С их помощью можно, например, изменять поддерживаемый синтаксис и включать (в т.ч. частично) режимы совместимости с sh/bash/ksh/csh. Они позволяют настолько значительно влиять на работу, что в zsh пришлось сделать отдельный «режим совместимости с zsh», который многие функции обычно включают первой командой, потому что только это даёт им гарантию, что код этой функции будет понят zsh именно так, как ожидал его автор.
  • Предпочтение максимально сжатого, краткого синтаксиса — чтобы вам нужно было набирать как можно меньше текста для выполнения типичных, пусть даже довольно сложных, задач. В коде, где важна читабельность через месяц — это однозначно минус. Но в командной строке — однозначно плюс.
  • Модульная организация настроек через фреймворки (вроде oh-my-zsh и prezto), плагины, темы, etc. На самом деле здесь нет ничего специфичного для zsh, ровно то же самое можно сделать и для bash, но… почему-то для zsh всё это уже есть, а для bash — нет (а если и есть, то про это мало кто знает). А это даёт возможность относительно быстро собрать свой вариант настроек zsh из готовых «кубиков» (как пример, посмотрите видео менеджера плагинов zsh Аntigen), точно так же, как обвешивается плагинами Vim.

Отличия и совместимость


Ещё раз уточню, что я буду описывать именно отличия от bash, а не полный набор возможностей zsh. Большая часть привычного вам функционала работает в zsh точно так же, как и в bash. Но при этом часто есть специфичные для zsh способы делать примерно то же самое. Это связано с тем, что в zsh уделяется очень много внимания совместимости с другими шеллами, поэтому в zsh плюс к своим фичам перетащили очень многое из других шеллов — и в результате получили несколько альтернативных способов делать одно и то же.


Термины


  • Параметр: обычная переменная — скаляр (строка, целое, дробное), массив, ассоциативный массив (хеш). А переменными называют в основном переменные окружения, т.е. экспортированные скалярные параметры.
  • Аргумент: параметр (в традиционном смысле) вызываемой команды или функции (аргументы функции доступны через параметры $@, $1, …).
  • Шаблон: глоб. Как правило шаблоны подразумевают совпадение с реально существующими файлами, но в некоторых случаях они применяются к строке или значению параметра. Поддержка полноценных регулярок тоже есть, но в основном везде в качестве шаблонов для совпадения или поиска/замены используются глобы.
  • Флаги: задаются в круглых скобках перед тем, на что они должны влиять. Для параметров задаются между открывающей фигурной скобкой и именем параметра: ${(kv@)some_hash}. Для шаблонов могут быть в начале или середине: *CaseImportant(#i)CaseIgnored*.txt.
  • Квалификаторы: задаются в круглых скобках после шаблона, уточняя его свойствами не связанными с именем файла: *(/^F).
  • Модификаторы: задаются каждый после двоеточия, применяются по очереди изменяя текущее значение. Для параметров задаются после имени параметра: $PWD:h:t, ${some_param:h:t}. Для шаблонов задаются перед закрывающей круглой скобкой квалификаторов: *(:e).

Текущие настройки


Многие встроенные команды выводят текущее состояние при запуске без аргументов (плюс нередко у них есть аргумент, который оформляет вывод в стиле команд zsh, что довольно удобно).


# текущие опции
setopt
# полный список всех опций
setopt KSH_OPTION_PRINT; setopt

# список обрабатываемых кнопок в текущем режиме
bindkey
# список обрабатываемых кнопок во всех режимах, в формате команд zsh
for m in $(bindkey -l); bindkey -M $m -L

# текущие стили (контекстно-зависимые настройки)
zstyle
zstyle -L

# текущие алиасы (обычные плюс глобальные), в формате команд zsh
alias -L
# текущие алиасы для суффиксов, в формате команд zsh
alias -s -L

# текущие параметры (переменные)
typeset
# текущие параметры (переменные), в формате команд zsh
typeset -p

Это далеко не полный список, но для большинства задач в процессе (анализа) настройки zsh его должно хватить.


Ещё может быть полезным запуск zsh -f — это запускает zsh в состоянии по умолчанию (без выполнения любых стартовых скриптов кроме /etc/zshenv, которого в большинстве систем и так нет).


setopt и emulate


  • В именах опций регистр и подчёркивания значения не имеют, плюс перед любой опцией можно добавить префикс «no».
  • Вызов для одной и той же опции setopt с префиксом «no» и unsetopt без «no» (равно как и наоборот!) делают одно и то же.
  • В выводе setopt используются маленькие буквы без подчёркиваний, в документации используются большие буквы с подчёркиваниями. Это создаёт некоторое неудобство — при поиске в документации нужно догадаться, где вставлять подчёркивания чтобы найти нужную опцию.
  • Команда emulate позволяет массово установить группу опций в состояние совместимости с sh, ksh, csh или в состояние по умолчанию для zsh. Многие функции в zsh начинаются командой emulate -L zsh, что позволяет на время выполнения функции привести ключевые опции в состояние по умолчанию для zsh — без этого большинство нетривиальных функций может ломаться из-за выставленных пользователем опций (например, есть опция которая управляет тем, как индексируются массивы — от 1 или от 0).

# все эти команды делают одно и то же
setopt nonumericglobsort
setopt NO_numericglobsort
setopt NO_NUMERIC_GLOB_SORT
setopt _N_O_numERICglob_SORT_
unsetopt NUMERIC_GLOB_SORT
unsetopt numericglobsort

В начале использования zsh, для более привычной работы после bash, я бы рекомендовал следующие опции:


# традиционный стиль перенаправлений fd
unsetopt MULTIOS
# поддержка ~… и file completion после = в аргументах
setopt MAGIC_EQUAL_SUBST
# не обрабатывать escape sequence в echo без -e
setopt BSD_ECHO
# поддержка комментариев в командной строке
setopt INTERACTIVE_COMMENTS
# поддержка $(cmd) в $PS1 etc.
setopt PROMPT_SUBST

Ещё есть опция SH_WORD_SPLIT, и формально для привычной работы после bash её тоже надо включить, но я бы этого не рекомендовал: поведение zsh без этой опции более удобное и логичное, лучше сразу к нему привыкать. Она отвечает за то, как сработает cmd $PARAM если значение $PARAM это строка содержащая пробелы: в bash cmd получит несколько аргументов, а в zsh — один (как если бы вызвали cmd "$PARAM"). А если $PARAM это массив, то zsh передаст cmd по одному аргументу на каждый не пустой элемент массива (даже если эти элементы содержат пробелы).


(В основном, эта статья описывает поведение zsh с опциями по умолчанию, иначе каждое второе предложение пришлось бы уточнять в стиле «но вот при таких-то опциях всё это работает иначе».)


Параметры


  • Соглашение: для имён скалярных параметров (строки, целые и дробные числа) обычно используют $БОЛЬШИЕ буквы, а для массивов (обычных и ассоциативных) — $маленькие.
  • Через typeset -U можно объявить массив с уникальными элементами (попытки добавления уже существующих элементов будут игнорироваться).
  • Через typeset -T можно связать массив со скаляром в формате $PATH. Несколько таких связанных параметров уже созданы: $PATH и $path, $FPATH и $fpath, $MANPATH и $manpath, $CDPATH и $cdpath. Для связанных параметров не имеет значения какой из них мы изменяем — изменяются сразу оба. Поэтому в zsh с такими параметрами практически всегда работают через массивы ($path, $fpath, …) — это значительно удобнее.
  • Некоторые скалярные параметры так же связаны между собой, например $PS1, $PROMPT и $prompt (хотя, это скорее просто синонимы для одного параметра).

Массивы


  • Индексируются с 1.
  • Можно использовать отрицательные индексы (от конца массива).
  • Можно использовать срезы.
  • При использовании как скаляра — объединяют элементы через пробел.
  • Индексирование скаляра возвращает символы строки.
  • Глоб возвращает массив, так что индекс можно использовать как квалификатор глоба: *([2,-2]).

Шаблоны


  • **/ — совпадает с подкаталогом любого уровня вложенности, включая отсутствие подкаталога
  • <число1-число2> — совпадает с числом в заданном диапазоне в имени файла, и начало и конец диапазона можно не указывать
  • (шаблон1|шаблон2) — альтернатива (так же — группирующие скобки при использовании опции EXTENDED_GLOB)
  • если включить опцию EXTENDED_GLOB, то в шаблонах можно будет дополнительно использовать # (повтор предыдущего элемента), ~ и ^ (исключение из совпадения)

# показать файлы в текущем каталоге или его подкаталогах,
# которые содержат в имени число большее или равное 5 либо строку example,
# и у которых расширение .txt
ls -l **/*(<5->|example)*.txt

Флаги/Квалификаторы/Модификаторы


Квалификаторы есть только у шаблонов, они позволяют задать дополнительные условия отбора файла: по типу (файл/каталог/симлинк/etc.), правам, времени (изменения/etc.), размеру… Можно сортировать и индексировать отобранные файлы. Можно включить для конкретно этого шаблона совпадение начальной * с именами начинающимися на точку. Можно включить удаление этого шаблона из аргументов командной строки если он не совпал ни с одним файлом.


# до 5-ти подкаталогов текущего каталога,
# имена которых могут начинаться на точку и содержат "a",
# которые изменялись последними
ls -ld *a*(D/om[1,5])

Если включить опцию EXTENDED_GLOB, то в шаблонах можно будет использовать флаги: для файлов интерес представляет управление чувствительностью к регистру, а при совпадении с параметром/строкой есть и другие полезные флаги.


# эти команды идентичны
ls -ld .[cC][oO][nN][fF][iI][gG]*
setopt extendedglob; ls -ld .(#i)Config*

Для параметров доступно намного больше флагов: вывод всех (включая пустые) элементов массива даже в кавычках, выполнение join или split по заданной подстроке, вывод только ключей и/или значений ассоциативного массива, экранирование разными видами кавычек и обратная операция, etc.


# вывод ключей ассоциативного массива вместо значений
echo ${(k)some_hash}
# преобразовать $PATH в массив разделив на элементы по ":",
# после чего корректно взять каждый элемент в одинарные кавычки
echo ${(s<:>qq)PATH}

И для шаблонов и для параметров можно использовать модификаторы: удаление последнего элемента пути, удаление всех элементов пути кроме последнего, удалить/оставить расширение, экранирование и обратная операция, поиск и замена подстроки, etc.


# вывод имени родительского каталога (сначала отбрасываем последний
# элемент пути, потом отбрасываем все элементы пути кроме последнего)
echo $PWD:h:t
# вывести имена (без каталога) всех симлинков в любом подкаталоге,
# заменив в них подстроку "fil" на "FIL" (если такая подстрока есть)
echo **/*(@:t:s/fil/FIL/)

autoload -Uz


Помимо традиционного способа подгружать код через source /path/to/file.sh или . /path/to/file.sh в zsh активно используется автозагрузка кода в момент первого вызова функции.


Для поиска файла с нужной функцией используется $FPATH — переменная аналогичная по формату $PATH, содержащая список каталогов в которых выполняется поиск файла с именем, идентичным имени загружаемой функции.


При вызове autoload никаких файлов с диска не считывается, и даже не проверяется их наличие — всё это произойдёт при первом вызове функции. Практически всегда необходимо передавать autoload аргументы -U (отменяет эффект текущих alias-ов для загружаемого файла, потому что нередко alias-ы настроенные пользователем могут нарушать работу сторонних функций) и -z (необязательное уточнение что загружаемый файл — в формате zsh, но безопаснее его всегда задавать).


fpath=(~/my-zsh-functions $fpath)
autoload -Uz fn
fn

При этом содержимое файла ~/my-zsh-functions/fn может быть в одном из этих трёх форматов:


# Просто набор команд, без каких-либо функций:
echo "Я функция fn"

# Одна функция с именем совпадающим с именем файла:
fn() {
    echo "Я функция fn"
}

# Набор из любых команд и функций, включая fn:
fn() {
    fn2
}
fn2() {
    echo "Я хелпер функции fn"
}
echo "Выполнится перед первым запуском fn"
# Но файл должен содержать явный вызов fn:
fn "$@"
echo "Выполнится после первого запуска fn"

zkbd


При первом запуске zsh нередко оказывается, что часть кнопок вроде F1/Backspace/Delete/курсора работает некорректно. Это связано с тем, что абсолютное большинство консольных приложений использует readline и корректная настройка этих кнопок считывается из /etc/inputrc и ~/.inputrc, а zsh этого не делает.


Проблема решается в лоб — нужно посмотреть, какие escape-последовательности выдают нужные кнопки в вашем терминале и задать в ~/.zshrc нужные обработчики для этих escape-последовательностей. Примерно так:


bindkey '^[[A' up-line-or-history       # Up
bindkey '^[[B' down-line-or-history     # Down
# и т.д.

Смотреть выдаваемые кнопками последовательности можно запустив cat >/dev/null и нажимая Ctrl-V перед нужной кнопкой. (И таки да, занимаясь этим в 2017 я чувствовал себя немного странно…) Но в комплекте с zsh идёт вспомогательная утилита zkbd, которая автоматизирует этот процесс. Для этого необходимо подключить её в ~/.zshrc, после чего у вас появится ассоциативный массив $key содержащий нужные escape-последовательности:


autoload -Uz zkbd
[[ ! -f ~/.zkbd/$TERM-${${DISPLAY:t}:-$VENDOR-$OSTYPE} ]] && zkbd
source  ~/.zkbd/$TERM-${${DISPLAY:t}:-$VENDOR-$OSTYPE}

[[ -n $key[Up]   ]] && bindkey -- $key[Up]   up-line-or-history
[[ -n $key[Down] ]] && bindkey -- $key[Down] down-line-or-history
# и т.д.

Я не уточняю детально какие команды (вроде up-line-or-history) на какие кнопки назначать потому, что во-первых назначать надо не все подряд, а только те, которые у вас из коробки не заработают, и во-вторых если мнения насчёт того, что должны делать Home или Backspace у всех сходятся, то вот поиск в истории по Up и Down может выполняться довольно разными способами, и функции в этих случаях на эти кнопки надо назначать тоже разные.


(Кстати, задавать символ Escape (^[) в параметре bindkey можно и настоящим символом, вводя его через Ctrl-V, и двумя обычными символами ^[, и двумя символами \e.)


zstyle


Это встроенный способ использовать контекстно-зависимые настройки. Он во многом похож на обычные параметры, только помимо имени и значения параметра zstyle позволяет задать шаблон «контекста». А потом получать значения относящиеся к текущему контексту. Этот подход активно используется для настройки работы автодополнений, но им можно пользоваться и для своих скриптов.


# установим значение my-param=default для 3-х уровневого контекста,
# где на первом уровне идентификатор нашего приложения (у всех
# приложений общая база zstyle, так что свои настройки надо изолировать)
# а на следующих двух уровнях любые значения
% zstyle ':my-app:*:*' my-param default
# установим значение my-param=val-one для контекста, у которого на
# втором (более приоритетном) уровне будет значение "one"
% zstyle ':my-app:one:*' my-param val-one
# установим значение my-param=val-two для контекста, у которого на
# третьем (менее приоритетном) уровне будет значение "two"
% zstyle ':my-app:*:two' my-param val-two

# получаем значение my-param в переменную result для заданного контекста
% zstyle -s ':my-app:a:b' my-param result
% echo $result
default
% zstyle -s ':my-app:one:b' my-param result
% echo $result
val-one
% zstyle -s ':my-app:a:two' my-param result
% echo $result
val-two
% zstyle -s ':my-app:one:two' my-param result
% echo $result
val-one

zmodload


Часть дополнительного функционала zsh реализована не в обычных скриптах подгружаемых через autoload -Uz, а как системные библиотеки *.so. Они используются, например, для предоставления доступа к регулярным выражениям PCRE, математическим функциям, сокетам, etc. Такие библиотеки подгружаются через zmodload.


Разное


Для перехвата сигналов помимо стандартного trap '…;code;…' INT можно использовать функции с особыми именами: TRAPINT() { …;code;… }.


У многих конструкций вроде if, while, etc. есть сокращённая форма (пример есть выше, где выводилось значение всех режимов bindkey).


Внезапно, zsh-специфичный аналог echo — команда print — оказалась весьма удобной при изучении zsh. Она много чего умеет, но из самого полезного:


# вывод по одному аргументу на строку, удобно для массивов
print -l $path
# вывод по два аргумента на строку в столбцах,
# удобно для ключей и значений ассоциативных массивов
print -a -C 2 "${(kv@)ZSH_HIGHLIGHT_STYLES}" | sort
# вывод используя %-последовательности используемые в $PS1
print -P '%Bbold%b %F{red}current%f dir is: %~'

Если Когда решитесь переходить на zsh, то для принятия конкретных решений про фреймворки/модули/темы вам пригодится Awesome-коллекция всего для zsh.

Комментарии (0)

© Habrahabr.ru