Массовая доставка 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’ами,
- Для 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:
# === INVENTORY (CHANGE ME где отмечено) ======================================
# Везде, где указано 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 где отмечено) =======================================
# Сразу после вставки поменяй /home/user на твой реальный home-каталог!
[ -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"
# 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 или 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"
# 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 на реальный логин. Все пути — конкретные и абсолютные.
# === 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 --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"
# 9) Финально — убедиться, что дерево OMZ принадлежит пользователю
- name: Ensure ownership on .oh-my-zsh tree
ansible.builtin.file:
path: /home/user/.oh-my-zsh # CHANGE ME
state: directory
recurse: yes
owner: user # CHANGE ME
group: user # CHANGE ME
Запуск плейбука:
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 пытается исполнить
| В начало
: `[ -n "$ZSH_VERSION" ] | return`; выбрать zsh в настройках VS Code |
в Ansible | Неподдерживаемый параметр
для вашей версии
| Удалить
; команда: `command -v zsh | true
/usr/bin/zsh` явно) |
«conflicting action statements … user, shell» | Ошибка отступов YAML | Параметры модуля должны быть под ним, с двумя пробелами | |
Всё «уехало» к
, а не к пользователю |
+ упование на «текущего» пользователя | В явном плейбуке использовать фиксированный
и абсолютные пути
| |
Нет
/
в репозитории | Дистрибутив/версия пакетов | Задачи помечены
, в
есть безопасные 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, и в нашей автоматизации.
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 и сервисах.