Skip to main content

Автоматизация установки Zsh с Oh-My-Zsh и темой Jovial через Ansible

  • Сводим

    Полное существующиерешение хост-группыдля Ansibleмассовой в одну общую через

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

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

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

Подводные

Структура камни,проекта

которые
ansible-zsh/
встретились├── наinventories/
практике:
  • От└── имениinventory.ini какого├── пользователяgroup_vars/ выполняется конфигурация:└── из-заzsh_targets.yml become:├── yesplaybooks/ │ └── zsh_deploy.yml └── README.md можно ненароком раскатывать всё в /root вместо /home/user.
  • VS Code Remote-SSH может поднимать процессы bash; если .zshrc вдруг исполняется bash’ем — посыплются ошибки.
  • В Ansible легко поймать «мелкие» ошибки: несовместимые параметры модулей, неверные отступы YAML.
  • Порядок подключения плагинов важен: zsh-syntax-highlighting должен быть последним.

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


2)1. Inventory: одна общая группа из несколькихinventories/inventory.ini

Объединяем ваши темы хостов (docker, vpn, zud) в общую группу zsh_targets:

# === INVENTORY ===
# Замените примеры на реальные данные ваших серверов

[docker]
n-1-dsw ansible_host=203.0.113.11 ansible_user=myuser ansible_ssh_private_key_file=~/.ssh/id_rsa_dsw1
n-2-dsw ansible_host=203.0.113.12 ansible_user=myuser ansible_ssh_private_key_file=~/.ssh/id_rsa_dsw2
n-3-dsw ansible_host=203.0.113.13 ansible_user=myuser ansible_ssh_private_key_file=~/.ssh/id_rsa_dsw3

[vpn]
srv-1   ansible_host=198.51.100.21 ansible_user=myuser ansible_ssh_private_key_file=~/.ssh/id_rsa_vpn

[zud]
zud-1   ansible_host=192.0.2.41 ansible_user=myuser ansible_ssh_private_key_file=~/.ssh/id_rsa_zud1
zud-2   ansible_host=192.0.2.42 ansible_user=myuser ansible_ssh_private_key_file=~/.ssh/id_rsa_zud2

# Объединяем все группы в одну для удобства
[zsh_targets:children]
docker
vpn
zud

[all:vars]
ansible_python_interpreter=/usr/bin/python3

2. Переменные: group_vars/zsh_targets.yml

---
# === ПЕРЕМЕННЫЕ (CHANGEИЗМЕНИТЕ MEПОД гдеСЕБЯ) отмечено)===

# Пользователь для которого настраивается zsh
zsh_user: myuser

# Домашняя директория (будет определена автоматически, но можно задать явно)
# zsh_home: /home/myuser

# Путь к zsh (определяется автоматически)
# zsh_shell_path: /usr/bin/zsh

# Создавать ли бэкап существующего .zshrc
zsh_backup_config: true

# Версии плагинов (ветки в git)
zsh_omz_version: master
zsh_plugins_version: master

# Редактор по умолчанию
zsh_editor: nano
# Для VS Code раскомментируйте:
# zsh_editor: "code --wait --reuse-window"

3. Playbook: playbooks/zsh_deploy.yml

---
- name: Install and configure Zsh with Oh-My-Zsh and Jovial theme
  hosts: zsh_targets
  gather_facts: yes
  become: yes

  vars:
    # Домашняя директория пользователя (определяется автоматически)
    zsh_home: "{{ ansible_facts['user_dir'] | default('/home/' + zsh_user) }}"

  tasks:
    # ======================================
# Везде, где указано user и /home/user — замени на своего реального пользователя
# и его домашний каталог. Примеры ниже — ЭТО ШАБЛОН.

[docker]
n-1-dsw ansible_host=203.0.113.11 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_dsw1   # CHANGE ME: user → твой логин; /home/user → твой HOME; путь к ключу → твой ключ
n-2-dsw ansible_host=203.0.113.12 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_dsw2   # CHANGE ME
n-3-dsw ansible_host=203.0.113.13 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_dsw3   # CHANGE ME

[vpn]
srv-1   ansible_host=198.51.100.21 ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_vpn   # CHANGE ME

[zud]
zud-1   ansible_host=192.0.2.41    ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_zud1  # CHANGE ME
zud-2   ansible_host=192.0.2.42    ansible_user=user ansible_ssh_private_key_file=/home/user/.ssh/id_rsa_zud2  # CHANGE ME

# Общая группа-«зонтик» для раскатки zsh
[zsh_targets:children]
docker
vpn
zud

[all:vars]
ansible_python_interpreter=/usr/bin/python3

Комментарий: всё явно — логин user, приватные ключи в конкретных путях.


3) .zshrc пример:

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

  • Строчка-сторож в самом верху: исполняем файл только в zsh (это предотвращает ошибки, когда bash случайно «подхватывает» .zshrc, например в VS Code).
  • Плагин zsh-syntax-highlighting — строго последним.
# === ~/.zshrc (CHANGE ME где отмечено) =======================================
    # СразуЭТАП после1: вставкиПРОВЕРКИ поменяйИ ПОДГОТОВКА
    # ============================================================================

    - name: Ensure user exists
      ansible.builtin.user:
        name: "{{ zsh_user }}"
        state: present
        create_home: yes
      register: user_info

    - name: Get user home directory
      ansible.builtin.getent:
        database: passwd
        key: "{{ zsh_user }}"
      register: user_getent

    - name: Set zsh_home fact
      ansible.builtin.set_fact:
        zsh_home: "{{ user_getent.ansible_facts.getent_passwd[zsh_user][4] }}"

    - name: Display configuration info
      ansible.builtin.debug:
        msg:
          - "Target user: {{ zsh_user }}"
          - "Home directory: {{ zsh_home }}"
          - "OS family: {{ ansible_os_family }}"

    # ============================================================================
    # ЭТАП 2: УСТАНОВКА ПАКЕТОВ
    # ============================================================================

    - name: Install base packages (Debian/Ubuntu)
      ansible.builtin.apt:
        name:
          - zsh
          - git
          - curl
          - ca-certificates
          - autojump
        state: present
        update_cache: yes
      when: ansible_os_family == "Debian"

    - name: Install base packages (RedHat/CentOS/Fedora)
      ansible.builtin.dnf:
        name:
          - zsh
          - git
          - curl
          - ca-certificates
          - autojump-zsh
        state: present
      when: ansible_os_family == "RedHat"

    - name: Install base packages (Arch Linux)
      community.general.pacman:
        name:
          - zsh
          - git
          - curl
          - ca-certificates
          - autojump
        state: present
      when: ansible_os_family == "Archlinux"

    # eza installation
    - name: Check if eza is available in repositories (Debian/Ubuntu)
      ansible.builtin.shell: apt-cache show eza
      register: eza_available
      failed_when: false
      changed_when: false
      when: ansible_os_family == "Debian"

    - name: Install eza from package manager (Debian/Ubuntu)
      ansible.builtin.apt:
        name: eza
        state: present
      when:
        - ansible_os_family == "Debian"
        - eza_available.rc == 0
      ignore_errors: yes

    - name: Install eza (RedHat/Fedora)
      ansible.builtin.dnf:
        name: eza
        state: present
      when: ansible_os_family == "RedHat"
      ignore_errors: yes

    - name: Install eza (Arch Linux)
      community.general.pacman:
        name: eza
        state: present
      when: ansible_os_family == "Archlinux"
      ignore_errors: yes

    # bat installation
    - name: Install bat (Debian/Ubuntu)
      ansible.builtin.apt:
        name: bat
        state: present
      when: ansible_os_family == "Debian"
      ignore_errors: yes

    - name: Create symlink for batcat (Debian/Ubuntu)
      ansible.builtin.file:
        src: /home/usr/bin/batcat
        dest: /usr/local/bin/bat
        state: link
      when:
        - ansible_os_family == "Debian"
      ignore_errors: yes

    - name: Install bat (RedHat/Fedora)
      ansible.builtin.dnf:
        name: bat
        state: present
      when: ansible_os_family == "RedHat"
      ignore_errors: yes

    - name: Install bat (Arch Linux)
      community.general.pacman:
        name: bat
        state: present
      when: ansible_os_family == "Archlinux"
      ignore_errors: yes

    # ============================================================================
    # ЭТАП 3: ОПРЕДЕЛЕНИЕ ПУТИ К ZSH И УСТАНОВКА КАК SHELL ПО УМОЛЧАНИЮ
    # ============================================================================

    - name: Find zsh binary path
      ansible.builtin.command: command -v zsh
      register: zsh_path_result
      changed_when: false

    - name: Set zsh_shell_path fact
      ansible.builtin.set_fact:
        zsh_shell_path: "{{ zsh_path_result.stdout }}"

    - name: Display zsh path
      ansible.builtin.debug:
        msg: "Zsh binary found at: {{ zsh_shell_path }}"

    - name: Set zsh as default shell for user
      наansible.builtin.user:
        твойname: реальный"{{ home-каталог!zsh_user }}"
        shell: "{{ zsh_shell_path }}"

    # ============================================================================
    # ЭТАП 4: УСТАНОВКА OH-MY-ZSH
    # ============================================================================

    - name: Backup existing .zshrc if exists
      ansible.builtin.copy:
        src: "{{ zsh_home }}/.zshrc"
        dest: "{{ zsh_home }}/.zshrc.backup.{{ ansible_date_time.epoch }}"
        remote_src: yes
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
        mode: "0644"
      when: zsh_backup_config | bool
      failed_when: false

    - name: Check if Oh-My-Zsh is already installed
      ansible.builtin.stat:
        path: "{{ zsh_home }}/.oh-my-zsh"
      register: omz_dir

    - name: Create Oh-My-Zsh directory
      ansible.builtin.file:
        path: "{{ zsh_home }}/.oh-my-zsh"
        state: directory
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
        mode: "0755"
      when: not omz_dir.stat.exists

    - name: Clone Oh-My-Zsh repository
      ansible.builtin.git:
        repo: https://github.com/ohmyzsh/ohmyzsh.git
        dest: "{{ zsh_home }}/.oh-my-zsh"
        version: "{{ zsh_omz_version }}"
        update: yes
        force: no
      become_user: "{{ zsh_user }}"
      register: omz_clone

    # ============================================================================
    # ЭТАП 5: СОЗДАНИЕ ДИРЕКТОРИЙ ДЛЯ КАСТОМНЫХ ПЛАГИНОВ И ТЕМ
    # ============================================================================

    - name: Create custom plugins directory
      ansible.builtin.file:
        path: "{{ zsh_home }}/.oh-my-zsh/custom/plugins"
        state: directory
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
        mode: "0755"

    - name: Create custom themes directory
      ansible.builtin.file:
        path: "{{ zsh_home }}/.oh-my-zsh/custom/themes"
        state: directory
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
        mode: "0755"

    # ============================================================================
    # ЭТАП 6: УСТАНОВКА ПЛАГИНОВ
    # ============================================================================

    - name: Clone zsh-autosuggestions plugin
      ansible.builtin.git:
        repo: https://github.com/zsh-users/zsh-autosuggestions.git
        dest: "{{ zsh_home }}/.oh-my-zsh/custom/plugins/zsh-autosuggestions"
        version: "{{ zsh_plugins_version }}"
        update: yes
        force: no
      become_user: "{{ zsh_user }}"

    - name: Clone zsh-syntax-highlighting plugin
      ansible.builtin.git:
        repo: https://github.com/zsh-users/zsh-syntax-highlighting.git
        dest: "{{ zsh_home }}/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting"
        version: "{{ zsh_plugins_version }}"
        update: yes
        force: no
      become_user: "{{ zsh_user }}"

    - name: Clone zsh-history-enquirer plugin
      ansible.builtin.git:
        repo: https://github.com/zthxxx/zsh-history-enquirer.git
        dest: "{{ zsh_home }}/.oh-my-zsh/custom/plugins/zsh-history-enquirer"
        version: "{{ zsh_plugins_version }}"
        update: yes
        force: no
      become_user: "{{ zsh_user }}"

    - name: Clone jovial plugin/theme
      ansible.builtin.git:
        repo: https://github.com/zthxxx/jovial.git
        dest: "{{ zsh_home }}/.oh-my-zsh/custom/plugins/jovial"
        version: "{{ zsh_plugins_version }}"
        update: yes
        force: no
      become_user: "{{ zsh_user }}"

    # ============================================================================
    # ЭТАП 7: УСТАНОВКА ТЕМЫ JOVIAL
    # ============================================================================

    - name: Create symlink for jovial theme
      ansible.builtin.file:
        src: "{{ zsh_home }}/.oh-my-zsh/custom/plugins/jovial/jovial.zsh-theme"
        dest: "{{ zsh_home }}/.oh-my-zsh/custom/themes/jovial.zsh-theme"
        state: link
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
        force: yes

    # ============================================================================
    # ЭТАП 8: СОЗДАНИЕ .ZSHRC
    # ============================================================================

    - name: Install .zshrc configuration
      ansible.builtin.copy:
        dest: "{{ zsh_home }}/.zshrc"
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
        mode: "0644"
        content: |
          # === GUARD: выполнять только в zsh ===
          [ -n "$ZSH_VERSION" ] || return

          # не=== исполнятьOh-My-Zsh .zshrc в bash/VS Code===
          export ZSH="{{ zsh_home }}/home/user/.oh-my-zsh"
          # CHANGE ME: /home/user → твой HOME
ZSH_THEME="jovial"

          # === Плагины ===
          # ВАЖНО: zsh-syntax-highlighting должен быть последним!
          plugins=(
            git
            autojump
            bgnotify
            zsh-history-enquirer
            zsh-autosuggestions
            jovial
            zsh-syntax-highlighting
          # должен быть ПОСЛЕДНИМ
)

          source "/home/user/.oh-my-zsh/$ZSH/oh-my-zsh.sh"

          # CHANGE=== ME:Настройки /home/userJovial → твой HOME===
          typeset -g JOVIAL_EXEC_THRESHOLD_SECONDS=1

          # === Терминал ===
          export TERM="xterm-256color"
          export COLORTERM="truecolor"

          # === Локаль (для корректного отображения emoji) ===
          export LC_ALL=en_US.UTF-8
          export LANG=en_US.UTF-8

          # === История команд ===
          HISTFILE=~/.zsh_history
          HISTSIZE=10000
          SAVEHIST=10000
          setopt SHARE_HISTORY
          setopt HIST_IGNORE_DUPS
          setopt HIST_FIND_NO_DUPS

          # === Автодополнения (case-insensitive) ===
          zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}'

          # === Алиасы для eza (еслис нет — нижебезопасным fallback на ls) ===
          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

          # bat=== илиАлиасы для bat/batcat (еслис нетfallback — обычныйна 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"

          # Jovial:=== «эмоции»Контекстные "эмоции" стрелки по контексту команды
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}"  # OMZ часто кладёт команду во 2-й аргумент
  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')

# Опционально: замени "code" на свой редактор, если VS Code не используешь
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 на реальный логин. Все пути — конкретные и абсолютные.

#Jovial === PLAYBOOK (CHANGE ME где отмечено) =======================================
# Везде, где указан user и /home/user — замени на своего реального пользователя
# и его домашний каталог. Если путь к zsh не /usr/bin/zsh — замени тоже.

---
- 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          # CHANGE ME: укажи ТВОЁ имя пользователя (должно совпадать с тем, под которым ты работаешь на сервере)
        shell: /usr/bin/zsh # CHANGE ME при необходимости: на некоторых ОС путь может быть /bin/zsh. Проверь: `command -v zsh`

    # 2) Базовые пакеты (под Debian/Ubuntu). Для других дистрибутивов замени на их аналоги (dnf/yum/apk/pacman).
    - 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  # OK, если пакета нет — .zshrc имеет fallback на ls

    # 4) bat — если доступен (в Debian пакет может называться batcat; fallback есть в .zshrc)
    - 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  # CHANGE ME: /home/user → твой HOME
        state: directory
        owner: user                  # CHANGE ME: user → твой пользователь
        group: user                  # CHANGE ME: user → твой пользователь
        mode: "0755"

    - name: Clone Oh-My-Zsh
      ansible.builtin.git:
        repo: https://github.com/ohmyzsh/ohmyzsh.git
        dest: /home/user/.oh-my-zsh  # CHANGE ME: /home/user → твой HOME
        version: master
        update: yes
      become_user: user              # CHANGE ME: user → твой пользователь (важно для владельцев)

    - name: Create custom plugins directory
      ansible.builtin.file:
        path: /home/user/.oh-my-zsh/custom/plugins   # CHANGE ME: /home/user → твой HOME
        state: directory
        owner: user                                  # CHANGE ME
        group: user                                  # CHANGE ME
        mode: "0755"

    - name: Create custom themes directory
      ansible.builtin.file:
        path: /home/user/.oh-my-zsh/custom/themes    # CHANGE ME: /home/user → твой HOME
        state: directory
        owner: user                                  # CHANGE ME
        group: user                                  # CHANGE ME
        mode: "0755"

    # 6) Плагины — каждый отдельной задачей (читабельно и без «магии»)
    - 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  # CHANGE ME: /home/user → твой HOME
        version: master
        update: yes
      become_user: user  # CHANGE ME

    - 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  # CHANGE ME
        version: master
        update: yes
      become_user: user  # CHANGE ME

    - 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   # CHANGE ME
        version: master
        update: yes
      become_user: user  # CHANGE ME

    - name: Clone jovial (plugin + theme)
      ansible.builtin.git:
        repo: https://github.com/zthxxx/jovial.git
        dest: /home/user/.oh-my-zsh/custom/plugins/jovial                 # CHANGE ME
        version: master
        update: yes
      become_user: user  # CHANGE ME

    # 7) Симлинк темы Jovial
    - name: Symlink jovial theme into custom themes
      ansible.builtin.file:
        src:  /home/user/.oh-my-zsh/custom/plugins/jovial/jovial.zsh-theme   # CHANGE ME
        dest: /home/user/.oh-my-zsh/custom/themes/jovial.zsh-theme           # CHANGE ME
        state: link
        owner: user    # CHANGE ME
        group: user    # CHANGE ME

    # 8) Устанавливаем .zshrc (с guard-строкой и без keychain)
    - name: Install .zshrc
      ansible.builtin.copy:
        dest: /home/user/.zshrc  # CHANGE ME: /home/user → твой HOME
        owner: user              # CHANGE ME
        group: user              # CHANGE ME
        mode: "0644"
        content: |
          [ -n "$ZSH_VERSION" ] || return  # не исполнять .zshrc в bash/VS Code

          export ZSH="/home/user/.oh-my-zsh"  # CHANGE ME: /home/user → твой HOME
          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"  # CHANGE ME: /home/user → твой HOME

          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')

          # Опционально:=== еслиРедакторы не используешь VS Code CLI — удали/замени на свой редактор===
          export EDITOR="code{{ --waitzsh_editor --reuse-window"}}"
          export VISUAL="code{{ --waitzsh_editor --reuse-window"}}"
          {% if zsh_editor.startswith('code') %}
          export SUDO_EDITOR="code{{ --waitzsh_editor --reuse-window"}}"
          export SYSTEMD_EDITOR="code{{ --waitzsh_editor --reuse-window"}}"
          export KUBE_EDITOR="code{{ --waitzsh_editor --reuse-window"}}"
          {% endif %}

          # 9)=== ФинальноДополнительные алиасы убедиться,===
          чтоalias дерево..='cd OMZ..'
          принадлежитalias пользователю...='cd ../..'
          alias ....='cd ../../..'

    # ============================================================================
    # ЭТАП 9: ФИНАЛЬНЫЕ НАСТРОЙКИ ПРАВ
    # ============================================================================

    - name: Ensure ownership on entire .oh-my-zsh directory
      ansible.builtin.file:
        path: "{{ zsh_home }}/.oh-my-zsh"
        state: directory
        recurse: yes
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"

    - name: Ensure ownership on .oh-my-zsh treezshrc
      ansible.builtin.file:
        path: "{{ zsh_home }}/home/user/.zshrc"
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
        mode: "0644"

    # ============================================================================
    # ЭТАП 10: ПРОВЕРКА И ТЕСТИРОВАНИЕ
    # ============================================================================

    - name: Test zsh configuration
      ansible.builtin.shell: |
        {{ zsh_shell_path }} -c 'echo $ZSH_VERSION'
      register: zsh_test
      become_user: "{{ zsh_user }}"
      changed_when: false

    - name: Display zsh version
      ansible.builtin.debug:
        msg: "Zsh version: {{ zsh_test.stdout }}"

    - name: Verify default shell is set
      ansible.builtin.shell: |
        getent passwd {{ zsh_user }} | cut -d: -f7
      register: shell_check
      changed_when: false

    - name: Display configured shell
      ansible.builtin.debug:
        msg:
          - "User: {{ zsh_user }}"
          - "Default shell: {{ shell_check.stdout }}"
          - "Expected: {{ zsh_shell_path }}"

    - name: Final summary
      ansible.builtin.debug:
        msg:
          - "✓ Zsh installation completed successfully"
          - "✓ Oh-My-Zsh installed at: {{ zsh_home }}/.oh-my-zshzsh"
          #- CHANGE"✓ MEConfiguration state:file: directory{{ recurse:zsh_home yes}}/.zshrc"
          owner:- user"✓ #Theme: CHANGEJovial"
          ME- group:"✓ userPlugins: #git, CHANGEautojump, MEbgnotify, zsh-autosuggestions, jovial, zsh-syntax-highlighting, zsh-history-enquirer"
          - ""
          - "⚠ User must re-login or run 'source {{ zsh_home }}/.zshrc' to apply changes"


4. Запуск плейбука:плейбука

Базовый запуск

ansible-playbook -i inventories/inventory.ini playbooks/zsh_deploy.yml

Запуск для конкретной группы

ansible-playbook -i inventories/inventory.ini playbooks/zsh_deploy.yml -l zsh_targetsdocker

Запуск с проверкой (dry-run)

ansible-playbook -i inventories/inventory.ini playbooks/zsh_deploy.yml --check

Запуск с подробным выводом

ansible-playbook -i inventories/inventory.ini playbooks/zsh_deploy.yml -v

Запуск для одного хоста

ansible-playbook -i inventories/inventory.ini playbooks/zsh_deploy.yml -l n-1-dsw

5)5. VSПроверка Codeпосле Remote-SSH:установки

как

Подключаемся избежатьк конфликтовсерверу

с
ssh bashmyuser@203.0.113.11

Проверяем shell

echo $SHELL
# Ожидается: /usr/bin/zsh или /bin/zsh

Проверяем тему

echo $ZSH_THEME
# Ожидается: jovial

Проверяем плагины

# Начните вводить команду — должны появиться серые автодополнения
# Введите команду — она должна подсвечиваться

6. Обновление конфигурации

КогдаЕсли VSнужно Codeобновить поднимает интегрированный терминал или сервер, он нередко стартует с bash. Чтобы у bash никогда не было желания «исполнить» ваштолько .zshrc, поставьтебез самуюпереустановки первую строку-сторож:всего:

[ansible-playbook -ni "$ZSH_VERSION"inventories/inventory.ini ]playbooks/zsh_deploy.yml ||--tags returnconfig

ТакжеДля вэтого Remote-настройкахдобавьте тег к задаче установки .zshrc:

- name: Install .zshrc configuration
  ansible.builtin.copy:
    # ... параметры ...
  tags:
    - config

7. Откат изменений (деинсталляция)

Создайте файл playbooks/zsh_uninstall.yml:

---
- name: Uninstall Zsh and Oh-My-Zsh
  hosts: zsh_targets
  gather_facts: yes
  become: yes

  tasks:
    - name: Get user home directory
      ansible.builtin.getent:
        database: passwd
        key: "{{ zsh_user }}"
      register: user_getent

    - name: Set zsh_home fact
      ansible.builtin.set_fact:
        zsh_home: "{{ user_getent.ansible_facts.getent_passwd[zsh_user][4] }}"

    - name: Restore default shell to bash
      ansible.builtin.user:
        name: "{{ zsh_user }}"
        shell: /bin/bash

    - name: Remove Oh-My-Zsh directory
      ansible.builtin.file:
        path: "{{ zsh_home }}/.oh-my-zsh"
        state: absent

    - name: Remove .zshrc
      ansible.builtin.file:
        path: "{{ zsh_home }}/.zshrc"
        state: absent

    - name: Find .zshrc backups
      ansible.builtin.find:
        paths: "{{ zsh_home }}"
        patterns: ".zshrc.backup.*"
      register: zshrc_backups

    - name: Restore latest .zshrc backup if exists
      ansible.builtin.copy:
        src: "{{ (zshrc_backups.files | sort(attribute='mtime') | last).path }}"
        dest: "{{ zsh_home }}/.zshrc"
        remote_src: yes
        owner: "{{ zsh_user }}"
        group: "{{ zsh_user }}"
      when: zshrc_backups.matched > 0

    - name: Remove packages (Debian/Ubuntu)
      ansible.builtin.apt:
        name:
          - zsh
          - eza
          - bat
          - autojump
        state: absent
        purge: yes
      when: ansible_os_family == "Debian"

    - name: Remove packages (RedHat/Fedora)
      ansible.builtin.dnf:
        name:
          - zsh
          - eza
          - bat
          - autojump-zsh
        state: absent
      when: ansible_os_family == "RedHat"

    - name: Autoremove unused packages (Debian/Ubuntu)
      ansible.builtin.apt:
        autoremove: yes
      when: ansible_os_family == "Debian"

Запуск деинсталляции:

ansible-playbook -i inventories/inventory.ini playbooks/zsh_uninstall.yml

8. Настройка VS Code выберитеRemote-SSH

Для корректной работы с VS Code добавьте в zshsettings.json:

{
  "terminal.integrated.profiles.linux": {
    "zsh": {
      "path": "/usr/bin/zsh",
      "args": ["-l"]
    }
  },
  "terminal.integrated.defaultProfile.linux": "zsh",
  "terminal.integrated.inheritEnv": false
}

9. Типичные проблемы и решения

Проблема: "Permission denied" при клонировании репозиториев

Решение: Проверьте что become_user указан правильно и пользователь существует

- name: Clone repository
  ansible.builtin.git:
    # ...
  become_user: "{{ zsh_user }}"  # Важно!

Проблема: Git показывает изменения даже когда их нет

Решение: Используйте force: no чтобы не перезаписывать локальные изменения

- name: Clone Oh-My-Zsh repository
  ansible.builtin.git:
    # ...
    force: no  # Не перезаписывать

Проблема: Владельцы файлов некорректные

Решение: Всегда явно указывайте владельца через become_user при создании файлов

- name: Create directory
  ansible.builtin.file:
    path: "{{ zsh_home }}/.oh-my-zsh"
    owner: "{{ zsh_user }}"
    group: "{{ zsh_user }}"

10. Преимущества этого решения

Кросс-платформенность — поддержка Debian, Ubuntu, CentOS, Fedora, Arch
Автоматическое определение путей — не нужно хардкодить /usr/bin/zsh
Идемпотентность — можно запускать многократно без проблем
Бэкапы — автоматическое сохранение существующих конфигов
Проверки — тестирование после установки
Безопасность — корректная работа с правами и владельцами
Прозрачность — все параметры в переменных, легко настроить
Откат — простая деинсталляция при необходимости


11. Дополнительные возможности

Добавление собственных алиасов через переменные

В group_vars/zsh_targets.yml:

zsh_custom_aliases:
  - { alias: 'update', command: 'sudo apt update && sudo apt upgrade -y' }
  - { alias: 'gs', command: 'git status' }
  - { alias: 'dps', command: 'docker ps' }

Проверка на хосте:

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) Проверь, что ВЕЗДЕ заменил user и /home/user
grep -R --line-number -E '(^|/)(home/user|user\b)' inventories/ playbooks/ ~/.zshrc

# 2) На целевом хосте: логин-шелл
getent passwd $USER | cut -d: -f7   # ДОЛЖНО быть /usr/bin/zsh (или /bin/zsh, если так и решил)

# 3) Пути OMZ:
ls -ld /home/$USER/.oh-my-zsh       # ВЛАДЕЛЕЦ — твой пользователь
head -n 1 /home/$USER/.zshrc        # ДОЛЖНА быть строка-guard: [ -n "$ZSH_VERSION" ] || return

8) Почему zsh-syntax-highlighting должен быть последним

Этот плагин подсвечивает ввод команды по мере набора. Ему нужна «финальная» форма строки после всех модификаций других плагинов/тем. Если подключить его раньше — подсветка может вести себя нестабильно или ломаться. Поэтому он — последний идобавьте в .zshrc,:

и
{% for item in zsh_custom_aliases | default([]) %}
alias {{ item.alias }}='{{ item.command }}'
{% endfor %}

Установка специфичных настроек для групп

Создайте group_vars/docker.yml:

zsh_custom_plugins:
  - docker
  - docker-compose

И подключайте их условно в нашей автоматизации.плейбуке.


9) Откат (деинсталляция)Заключение

#

Это Вернутьполное, логин-шеллproduction-ready bash:решение sudoдля chshавтоматизации -sустановки /bin/bashZsh user # Удалитьс Oh-My-Zsh и .zshrc:темой sudoJovial. 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 и сервисах.окружениях.