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
      [all:vars]
      ansible_python_interpreter=/usr/bin/python3

      Комментарий: всё явно — логин <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#*[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: максимально «прямой» и читаемый

        Ни циклов, ни переменных — всё в явном виде. Примечание: замените <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. Чтобы у bash никогда не было желания «исполнить» ваш .zshrc,zshrc, поставьте самую первую строку-сторож:

        [ -n "$ZSH_VERSION" ] || return

        Также в Remote-настройках VS Code выберите zsh:

        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В начало

        .zshrc

        :zshrc: `[ -n "$ZSH_VERSION" ]


        return`; выбрать zsh в настройках VS Code

        UnsupportedCodeUnsupported parameters … warn

        в Ansible

        НеподдерживаемыйAnsibleНеподдерживаемый параметр

        warn

        для вашей версии

        shell/command

        УдалитьcommandУдалить

        warn

        ;warn; команда: `command -v zsh


        true

        (zshtrue(или вообще не искать путь и задать

        /задать/usr/bin/zsh` явно)

        «conflicting action statements … user, shell»

        Ошибка отступов YAML

        ПараметрыYAMLПараметры модуля должны быть под ним, с двумя пробелами



        ВсёпробеламиВсё «уехало» к

        root

        ,root, а не к пользователю

        become:пользователюbecome: yes

        + упование на «текущего» пользователя

        ВпользователяВ явном плейбуке использовать фиксированный

        name: user

        и абсолютные пути

        /home/user/...



        Нет

        eza

        /

        eza/bat

        в репозитории

        Дистрибутив/репозиторииДистрибутив/версия пакетов

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

        ignore_errors: yes

        ,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,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,remove, apk del,del, pacman -R,R, и т. п.)


        10) Заключение

        Мы получили прозрачный, воспроизводимый способ развернуть одинаковую zsh-среду на нескольких серверах. Без «магии» и «переменных-шаблонов»: только явные пути, конкретные имена и последовательные шаги. Такой подход удобен для проверки, аудита и быстрого дебага — особенно когда нужно массово наводить порядок на узлах, не опасаясь «побочек» в VS Code и сервисах.

        Где нужно подставляйте ваши реальные хосты, ключи и домены (YOU.DOMAIN)DOMAIN). И — приятной работы в красивом, тихом и предсказуемом терминале!