Skip to main content

Массовая доставка Zsh с Oh-My-Zsh и темой Jovial на несколько серверов: практика, нюансы, анти-грабли

  • Сводим существующие хост-группы 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). И — приятной работы в красивом, тихом и предсказуемом терминале!