Skip to main content

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

    Сводим существующие хост-группы Ansible в одну общую через <span class="editor-theme-code">:children</span>. Разворачиваем zsh, Oh-My-Zsh, тему Jovial и плагины (<span class="editor-theme-code">zsh-autosuggestions</span>, <span class="editor-theme-code">zsh-syntax-highlighting</span>, <span class="editor-theme-code">zsh-history-enquirer</span>, а также встроенный <span class="editor-theme-code">bgnotify</span>) на всех целевых хостах. Кладём единый <span class="editor-theme-code">.zshrc</span>, в котором:
      первая строка-сторож: исполнять только под zsh, аккуратные алиасы под <span class="editor-theme-code">eza</span> и <span class="editor-theme-code">bat/batcat</span> с безопасными fallback’ами, без <span class="editor-theme-code">keychain</span>. Для VS Code (Remote-SSH) выбираем <span class="editor-theme-code">zsh</span> как дефолтный терминал и не позволяем <span class="editor-theme-code">bash</span> исполнять <span class="editor-theme-code">.zshrc</span>.

      1) Задача и подводные камни

      Задача: получить на нескольких серверах единообразный интерактивный опыт в терминале: zsh + Oh-My-Zsh + тема Jovial + нужные плагины и алиасы. Сделать это повторяемо и прозрачно через Ansible.

      Подводные камни, которые встретились на практике:

        От имени какого пользователя выполняется конфигурация: из-за <span class="editor-theme-code">become: yes</span> можно ненароком раскатывать всё в <span class="editor-theme-code">/root</span> вместо <span class="editor-theme-code">/home/user</span>. VS Code Remote-SSH может поднимать процессы bash; если <span class="editor-theme-code">.zshrc</span> вдруг исполняется bash’ем — посыплются ошибки. В Ansible легко поймать «мелкие» ошибки: несовместимые параметры модулей, неверные отступы YAML. Порядок подключения плагинов важен: <span class="editor-theme-code">zsh-syntax-highlighting</span> должен быть последним.

        Дальше — минималистичное и явное решение без переменных-шаблонов.


        2) Inventory: одна общая группа из нескольких

        Объединяем ваши темы хостов (<span class="editor-theme-code">docker</span>, <span class="editor-theme-code">vpn</span>, <span class="editor-theme-code">zud</span>) в общую группу <span class="editor-theme-code">zsh_targets</span>:

        # 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
        

        Комментарий: всё явно — логин <span class="editor-theme-code">user</span>, приватные ключи в конкретных путях.


        3) <span class="editor-theme-code">.zshrc</span>: чистый, безопасный, без keychain

        Ключевые особенности:

          Строчка-сторож в самом верху: исполняем файл только в zsh (это предотвращает ошибки, когда <span class="editor-theme-code">bash</span> случайно «подхватывает» <span class="editor-theme-code">.zshrc</span>, например в VS Code). Плагин <span class="editor-theme-code">zsh-syntax-highlighting</span> — строго последним. Никакого <span class="editor-theme-code">keychain</span>.
          # /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) Редакторы (по желанию)
          

          4) Плейбук Ansible: максимально «прямой» и читаемый

          Ни циклов, ни переменных — всё в явном виде. Примечание: замените <span class="editor-theme-code">user</span> на реальный логин. Все пути — конкретные и абсолютные.

          # 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 &gt;/dev/null 2&gt;&amp;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 &gt;/dev/null 2&gt;&amp;1; then
                  alias cat='bat --paging=never --style=plain'
                elif command -v batcat &gt;/dev/null 2&gt;&amp;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 CodeUnsupported parameters … warn в AnsibleНеподдерживаемый параметр warn для вашей версии shell/commandУдалить warn; команда: `command -v zshtrue(или вообще не искать путь и задать/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). И — приятной работы в красивом, тихом и предсказуемом терминале!