Массовая доставка Zsh с Oh-My-Zsh и темой Jovial на несколько серверов: практика, нюансы, анти-грабли
Готово! Ниже — полноценный «ready-to-publish» Markdown. Я также положил его в файл:Download the Markdown
Массовая доставка Zsh с Oh-My-Zsh и темой Jovial на несколько серверов: практический разбор без магии
Всё в этой статье обезличено. Где в реальной жизни у вас встречаются ваши домены — мысленно подставляйтеYOU.DOMAIN. Во всех примерах используютсяявные пути и имена, без автоподстановок и без «шаблонной магии». Конфигkeychainполностью исключён.
TL;DR
Сводим существующие хост-группы Ansible в одну общую через
:children.Разворачиваем zsh, Oh-My-Zsh, тему Jovial и плагины (
zsh-autosuggestions,zsh-syntax-highlighting,zsh-history-enquirer, а также встроенныйbgnotify) на всех целевых хостах.Кладём единый
.zshrc, в котором:первая строка-сторож: исполнять только под zsh,аккуратные алиасы подezaиbat/batcatс безопасными fallback’ами,безkeychain.Для VS Code (Remote-SSH) выбираем
zshкак дефолтный терминал и не позволяемbashисполнять.zshrc.
1) Задача и подводные камни
Задача: получить на нескольких серверах единообразный интерактивный опыт в терминале: zsh + Oh-My-Zsh + тема Jovial + нужные плагины и алиасы. Сделать это повторяемо и прозрачно через Ansible.
Подводные камни, которые встретились на практике:
От имени какого пользователя выполняется конфигурация: из-заbecome: yesможно ненароком раскатывать всё в/rootвместо/home/user.VS Code Remote-SSH может поднимать процессы bash; если.zshrcвдруг исполняется bash’ем — посыплются ошибки.В Ansible легко поймать «мелкие» ошибки: несовместимые параметры модулей, неверные отступы YAML.Порядок подключения плагинов важен:zsh-syntax-highlightingдолжен быть последним.Дальше — минималистичное и явное решение без переменных-шаблонов.
2) Inventory: одна общая группа из нескольких
Объединяем ваши темы хостов (
docker,vpn,zud) в общую группуzsh_targets:# inventories/inventory.ini [docker] n-1-dsw ansible_host=203.0.113.11 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_dsw1 n-2-dsw ansible_host=203.0.113.12 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_dsw2 n-3-dsw ansible_host=203.0.113.13 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_dsw3 [vpn] srv-1 ansible_host=198.51.100.21 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_vpn [zud] zud-1 ansible_host=192.0.2.41 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_zud1 zud-2 ansible_host=192.0.2.42 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_zud2 # Общая группа для «раскатки» zsh [zsh_targets:children] docker vpn zud [all:vars] ansible_python_interpreter=/usr/bin/python3Комментарий: всё явно — логин
user, приватные ключи в конкретных путях.
3)
.zshrc: чистый, безопасный, без keychainКлючевые особенности:
Строчка-сторож в самом верху: исполняем файл только в zsh (это предотвращает ошибки, когдаbashслучайно «подхватывает».zshrc, например в VS Code).Плагинzsh-syntax-highlighting— строго последним.Никакогоkeychain.# /home/user/.zshrc # 1) СТОРОЖ: выполнять этот файл только в zsh (важно для VS Code и любых bash-сценариев) [ -n "$ZSH_VERSION" ] || return # 2) Oh-My-Zsh + тема Jovial export ZSH="/home/user/.oh-my-zsh" # явный путь без $HOME ZSH_THEME="jovial" # тема zthxxx/jovial # 3) Плагины (последним — zsh-syntax-highlighting) plugins=( git # стандартный набор git-команд и алиасов autojump # быстрые переходы по каталогам bgnotify # уведомления о завершении долгих задач zsh-history-enquirer # «поисковик» истории команд zsh-autosuggestions # «серые подсказки» на основе истории jovial # доп. функции темы zsh-syntax-highlighting # должен быть ПОСЛЕДНИМ ) # 4) Подключение Oh-My-Zsh source "/home/user/.oh-my-zsh/oh-my-zsh.sh" # 5) Порог показа времени выполнения команды (Jovial) typeset -g JOVIAL_EXEC_THRESHOLD_SECONDS=1 # 6) Цвета терминала (явно) export TERM="xterm-256color" export COLORTERM="truecolor" # 7) eza: красивые списки файлов (fallback на ls, если eza нет) if command -v eza >/dev/null 2>&1; then alias ls='eza --icons=auto --group-directories-first --git' alias ll='eza -l --icons=auto --group-directories-first --git' alias la='eza -la --icons=auto --group-directories-first --git' alias lt='eza --tree --level=2 --icons=auto' alias lsize='eza -l --sort=size --icons=auto' else alias ls='ls --color=auto' alias ll='ls -l --color=auto' alias la='ls -la --color=auto' fi # 8) bat/batcat: «красивый cat» без пейджера (оба варианта) if command -v bat >/dev/null 2>&1; then alias cat='bat --paging=never --style=plain' elif command -v batcat >/dev/null 2>&1; then alias cat='batcat --paging=never --style=plain' fi export BAT_PAGER="less -FR" export BAT_THEME="ansi" # 9) Тема Jovial: «стрелка» меняет «эмоцию» по типу команды typeset -g JOV_ARROW_DEFAULT='%(?.(◕‿◕).(╥﹏╥%))' # успех/ошибка по статусу typeset -g JOV_ARROW_KUBE='%(?.(\_(ツ)_/¯).(╥﹏╥%))' # kubectl/helm typeset -g JOV_ARROW_CONT='%(?.(⌐■_■).(╥﹏╥%))' # docker/podman и т. п. autoload -Uz add-zsh-hook # 10) Хук: сохранить «базовую» команду (игнорируя sudo/doas) typeset -g JOV_LAST_CMD_BASE='' jov_store_last_cmd() { local line="${2:-$1}" # OMZ часто кладёт команду во 2-й аргумент emulate -L zsh -o extendedglob line="${line##[[:space:]]##}" # срезать лидирующие пробелы local base="${line%%[[:space:]]*}" # первое слово if [[ "$base" == (sudo|doas) ]]; then # поддержка sudo/doas local rest="${line#*[[:space:]]}" base="${rest%%[[:space:]]*}" fi JOV_LAST_CMD_BASE="$base" } preexec_functions=(${preexec_functions:#jov_store_last_cmd}) preexec_functions+=('jov_store_last_cmd') # 11) Хук: выбрать «эмоцию» для стрелки перед прорисовкой промпта jov_apply_context_face() { local expr="$JOV_ARROW_DEFAULT" case "$JOV_LAST_CMD_BASE" in kubectl|k|helm) expr="$JOV_ARROW_KUBE" ;; docker|docker-compose|podman|nerdctl) expr="$JOV_ARROW_CONT" ;; *) expr="$JOV_ARROW_DEFAULT" ;; esac # И обычная стрелка, и git-варианты — одной логикой JOVIAL_SYMBOL[arrow]="$expr" JOVIAL_SYMBOL[arrow.git-clean]="$expr" JOVIAL_SYMBOL[arrow.git-dirty]="$expr" } precmd_functions=(${precmd_functions:#jov_apply_context_face}) precmd_functions+=('jov_apply_context_face') # 12) Редакторы (по желанию) export EDITOR="code --wait --reuse-window" export VISUAL="code --wait --reuse-window" export SUDO_EDITOR="code --wait --reuse-window" export SYSTEMD_EDITOR="code --wait --reuse-window" export KUBE_EDITOR="code --wait --reuse-window"
4) Плейбук Ansible: максимально «прямой» и читаемый
Ни циклов, ни переменных — всё в явном виде. Примечание: замените
userна реальный логин. Все пути — конкретные и абсолютные.# playbooks/zsh_deploy.yml --- - name: Install & configure Zsh with Oh-My-Zsh + Jovial on multiple hosts hosts: zsh_targets gather_facts: yes become: yes tasks: # 1) Убедиться, что пользователь существует, и сразу поставить логин-шелл zsh - name: Ensure user exists and set default shell to /usr/bin/zsh ansible.builtin.user: name: user shell: /usr/bin/zsh # 2) Базовые пакеты - name: Install Zsh, Git, curl, ca-certificates, autojump ansible.builtin.package: name: - zsh - git - curl - ca-certificates - autojump state: present # 3) eza — если доступен в репозитории - name: Install eza if available ansible.builtin.package: name: eza state: present ignore_errors: yes # 4) bat — если доступен (на Debian бывает batcat) - name: Install bat if available ansible.builtin.package: name: bat state: present ignore_errors: yes # 5) Каталог Oh-My-Zsh - name: Create Oh-My-Zsh directory ansible.builtin.file: path: /home/user/.oh-my-zsh state: directory owner: user group: user mode: "0755" # 6) Клон самого Oh-My-Zsh - name: Clone Oh-My-Zsh ansible.builtin.git: repo: https://github.com/ohmyzsh/ohmyzsh.git dest: /home/user/.oh-my-zsh version: master update: yes become_user: user # 7) Подготовка каталогов для плагинов и тем - name: Create custom plugins directory ansible.builtin.file: path: /home/user/.oh-my-zsh/custom/plugins state: directory owner: user group: user mode: "0755" - name: Create custom themes directory ansible.builtin.file: path: /home/user/.oh-my-zsh/custom/themes state: directory owner: user group: user mode: "0755" # 8) Плагины — по одному, явно - name: Clone zsh-autosuggestions ansible.builtin.git: repo: https://github.com/zsh-users/zsh-autosuggestions.git dest: /home/user/.oh-my-zsh/custom/plugins/zsh-autosuggestions version: master update: yes become_user: user - name: Clone zsh-syntax-highlighting ansible.builtin.git: repo: https://github.com/zsh-users/zsh-syntax-highlighting.git dest: /home/user/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting version: master update: yes become_user: user - name: Clone zsh-history-enquirer ansible.builtin.git: repo: https://github.com/zthxxx/zsh-history-enquirer.git dest: /home/user/.oh-my-zsh/custom/plugins/zsh-history-enquirer version: master update: yes become_user: user - name: Clone jovial (plugin + theme) ansible.builtin.git: repo: https://github.com/zthxxx/jovial.git dest: /home/user/.oh-my-zsh/custom/plugins/jovial version: master update: yes become_user: user # 9) Симлинк темы Jovial - name: Symlink jovial theme into custom themes ansible.builtin.file: src: /home/user/.oh-my-zsh/custom/plugins/jovial/jovial.zsh-theme dest: /home/user/.oh-my-zsh/custom/themes/jovial.zsh-theme state: link owner: user group: user # 10) Установить наш .zshrc (из раздела выше) - name: Install .zshrc ansible.builtin.copy: dest: /home/user/.zshrc owner: user group: user mode: "0644" content: | [ -n "$ZSH_VERSION" ] || return export ZSH="/home/user/.oh-my-zsh" ZSH_THEME="jovial" plugins=( git autojump bgnotify zsh-history-enquirer zsh-autosuggestions jovial zsh-syntax-highlighting ) source "/home/user/.oh-my-zsh/oh-my-zsh.sh" typeset -g JOVIAL_EXEC_THRESHOLD_SECONDS=1 export TERM="xterm-256color" export COLORTERM="truecolor" if command -v eza >/dev/null 2>&1; then alias ls='eza --icons=auto --group-directories-first --git' alias ll='eza -l --icons=auto --group-directories-first --git' alias la='eza -la --icons=auto --group-directories-first --git' alias lt='eza --tree --level=2 --icons=auto' alias lsize='eza -l --sort=size --icons=auto' else alias ls='ls --color=auto' alias ll='ls -l --color=auto' alias la='ls -la --color=auto' fi if command -v bat >/dev/null 2>&1; then alias cat='bat --paging=never --style=plain' elif command -v batcat >/dev/null 2>&1; then alias cat='batcat --paging=never --style=plain' fi export BAT_PAGER="less -FR" export BAT_THEME="ansi" typeset -g JOV_ARROW_DEFAULT='%(?.(◕‿◕).(╥﹏╥%))' typeset -g JOV_ARROW_KUBE='%(?.(\_(ツ)_/¯).(╥﹏╥%))' typeset -g JOV_ARROW_CONT='%(?.(⌐■_■).(╥﹏╥%))' autoload -Uz add-zsh-hook typeset -g JOV_LAST_CMD_BASE='' jov_store_last_cmd() { local line="${2:-$1}" emulate -L zsh -o extendedglob line="${line##[[:space:]]##}" local base="${line%%[[:space:]]*}" if [[ "$base" == (sudo|doas) ]]; then local rest="${line#*[[:space:]]}" base="${rest%%[[:space:]]*}" fi JOV_LAST_CMD_BASE="$base" } preexec_functions=(${preexec_functions:#jov_store_last_cmd}) preexec_functions+=('jov_store_last_cmd') jov_apply_context_face() { local expr="$JOV_ARROW_DEFAULT" case "$JOV_LAST_CMD_BASE" in kubectl|k|helm) expr="$JOV_ARROW_KUBE" ;; docker|docker-compose|podman|nerdctl) expr="$JOV_ARROW_CONT" ;; *) expr="$JOV_ARROW_DEFAULT" ;; esac JOVIAL_SYMBOL[arrow]="$expr" JOVIAL_SYMBOL[arrow.git-clean]="$expr" JOVIAL_SYMBOL[arrow.git-dirty]="$expr" } precmd_functions=(${precmd_functions:#jov_apply_context_face}) precmd_functions+=('jov_apply_context_face') export EDITOR="code --wait --reuse-window" export VISUAL="code --wait --reuse-window" export SUDO_EDITOR="code --wait --reuse-window" export SYSTEMD_EDITOR="code --wait --reuse-window" export KUBE_EDITOR="code --wait --reuse-window" # 11) На всякий случай — выровнять владельцев/права на дереве OMZ - name: Ensure ownership on .oh-my-zsh tree ansible.builtin.file: path: /home/user/.oh-my-zsh state: directory recurse: yes owner: user group: userЗапуск плейбука:
ansible-playbook -i inventories/inventory.ini playbooks/zsh_deploy.yml -l zsh_targets
5) VS Code Remote-SSH: как избежать конфликтов с bash
Когда VS Code поднимает интегрированный терминал или сервер, он нередко стартует с bash. Чтобы у
bashникогда не было желания «исполнить» ваш.zshrc, поставьте самую первую строку-сторож:[ -n "$ZSH_VERSION" ] || returnТакже в Remote-настройках VS Code выберите
zsh:{ "terminal.integrated.profiles.linux": { "zsh": { "path": "/usr/bin/zsh" } }, "terminal.integrated.defaultProfile.linux": "zsh" }Проверка на хосте:
getent passwd user | cut -d: -f7 # ожидается /usr/bin/zsh echo $SHELL # в интерактивном окне VS Code — /usr/bin/zsh
6) Типичные ошибки и быстрые решения
Симптом
Причина
Решение
«Oh My Zsh can't be loaded from: bash»
Bash пытается исполнить
.zshrcВ начало
.zshrc: `[ -n "$ZSH_VERSION" ]
return`; выбрать zsh в настройках VS Code
Unsupported parameters … warnв Ansible
Неподдерживаемый параметр
warnдля вашей версии
shell/commandУдалить
warn; команда: `command -v zsh
true
(или вообще не искать путь и задать/usr/bin/zsh` явно)
«conflicting action statements … user, shell»
Ошибка отступов YAML
Параметры модуля должны быть под ним, с двумя пробелами
Всё «уехало» к
root, а не к пользователю
become: yes+ упование на «текущего» пользователя
В явном плейбуке использовать фиксированный
name: userи абсолютные пути
/home/user/...
Нет
eza/
batв репозитории
Дистрибутив/версия пакетов
Задачи помечены
ignore_errors: yes, в
.zshrcесть безопасные fallback’ы
7) Проверки после раскатки
# 1) Логин-шелл пользователя getent passwd user | cut -d: -f7 # 2) Наличие Oh-My-Zsh и владельцы ls -ld /home/user/.oh-my-zsh # 3) Сторож в .zshrc на первой строке head -n 1 /home/user/.zshrc # 4) Алиас ls (если eza установлен) type ls
8) Почему
zsh-syntax-highlightingдолжен быть последнимЭтот плагин подсвечивает ввод команды по мере набора. Ему нужна «финальная» форма строки после всех модификаций других плагинов/тем. Если подключить его раньше — подсветка может вести себя нестабильно или ломаться. Поэтому он — последний и в
.zshrc, и в нашей автоматизации.
9) Откат (деинсталляция)
# Вернуть логин-шелл bash: sudo chsh -s /bin/bash user # Удалить Oh-My-Zsh и .zshrc: sudo rm -rf /home/user/.oh-my-zsh sudo rm -f /home/user/.zshrc # (опционально) удалить пакеты: sudo apt remove --purge -y zsh eza bat autojump sudo apt autoremove -y(Если у вас другой менеджер пакетов — используйте его явные команды:
dnf remove,apk del,pacman -R, и т. п.)
10) Заключение
Мы получили прозрачный, воспроизводимый способ развернуть одинаковую zsh-среду на нескольких серверах. Без «магии» и «переменных-шаблонов»: только явные пути, конкретные имена и последовательные шаги. Такой подход удобен для проверки, аудита и быстрого дебага — особенно когда нужно массово наводить порядок на узлах, не опасаясь «побочек» в VS Code и сервисах.
Где нужно подставляйте ваши реальные хосты, ключи и домены (YOU.DOMAIN). И — приятной работы в красивом, тихом и предсказуемом терминале!