Я программист. По крайней мере так написано в трудовой книжке. Почти всё своё рабочее время я провожу в консоли и текстовом редакторе. Мне очень нравится bash. Почти год я жил в zsh, прислушавшись к советам своих многочисленных коллег и знакомых, но в итоге я вернулся в bash и ни капельки об этом не жалею.
Zsh красив, приятен, чертовски функционален, но, признаюсь честно, я не смог совладать со всеми его многочисленными настройками. Я хочу работать, а не бороться со своим рабочим окружением. Простой пример: пару раз из-за автодополнения zsh я удалял все директории и файлы в текущей директории — zsh просто ставил пробел между автодополненной директорией и введённой мною звёзочкой (я хотел удалить всё в выбранной папке). Помните тот эпичный баг с пробелом и удалении директории /usr? У меня было то же самое. Спасибо гиту, выручил в который раз.
Впрочем, дело не в zsh — будь я чуточку умнее я бы с ним обязательно справился бы, и всё было бы хорошо, но мы, суровые программисты, будем использовать bash и vim, а гламурные zsh и textmate оставим хипстерам и прочим модникам ;)
Я не напишу ничего оригинального и универсального решения я не приведу, но мне всегда нравилось читать конфиги и описания других людей, а если к ним были приложены интересные картинки, так я вообще перечитывал эти статьи несколько раз. Надеюсь, вам тоже будет интересно.
Если вдруг что-то из написанного мною можно решить проще, или в баше уже есть описанный функционал — напишите в комментариях. Ну и на всякий случай, моя где я живу:
GNU bash, version 4.2.28(2)-release (i386-apple-darwin11.3.0)
Добавляем перевод строки перед приглашением
Итак, первое, с чем я сталкиваюсь каждый день и что мне не нравится в баше — команды, которые не завершают свой вывод переводом строки при завершении. Вот простой пример (эмуляция подобного поведения):
Конечно, ничего страшного не произошло, но тот же zsh корректно обрабатывает эту ситуацию, научим же и баш такому трюку.
Для этого нам нужно при каждом выводе приглашения командной строки (PS1) смотреть на позицию курсора, и если курсор находится не на первом символе в строке — выводить перевод строки (символ "n"). Позицию курсора можно определить с помощью escape-последоваельности:
echo -en "33[6n" && read -sdR CURPOS
В результате в переменной CURPOS будет находиться что-то вроде этого: "^[[4;12R", где 4 — номер строки, а 12 — номер символа в строке. Добавляем соответствующий код в наш конфиг баша (~/.bashrc или ~/.bash_profile):
# setup color variables
color_is_on=
color_red=
color_green=
color_yellow=
color_blue=
color_white=
color_gray=
color_bg_red=
color_off=
if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
color_is_on=true
color_red="[$(/usr/bin/tput setaf 1)]"
color_green="[$(/usr/bin/tput setaf 2)]"
color_yellow="[$(/usr/bin/tput setaf 3)]"
color_blue="[$(/usr/bin/tput setaf 6)]"
color_white="[$(/usr/bin/tput setaf 7)]"
color_gray="[$(/usr/bin/tput setaf 8)]"
color_off="[$(/usr/bin/tput sgr0)]"
color_error="$(/usr/bin/tput setab 1)$(/usr/bin/tput setaf 7)"
color_error_off="$(/usr/bin/tput sgr0)"
fi
function prompt_command {
# get cursor position and add new line if we're not in first column
exec < /dev/tty
local OLDSTTY=$(stty -g)
stty raw -echo min 0
echo -en "33[6n" > /dev/tty && read -sdR CURPOS
stty $OLDSTTY
[[ ${CURPOS##*;} -gt 1 ]] && echo "${color_error}↵${color_error_off}"
}
PROMPT_COMMAND=prompt_command
PROMPT_COMMAND — это функция, которая вызывается при каждой отрисовке приглашения командной строки. Здесь был использован небольшой хак, подсмотренный мною в комментарии на stackoverflow, без этого хака значение переменной $CURPOS в некоторых случаях выводилось на экран. На кучу цветов не обращайте внимание — ниже они все нам пригодятся. Результат работы нашего конфига:
Красный фон был добавлен специально, чтобы отличать этот символ от того, что может вывести команда. И, да, на дворе 21 век, поэтому мы используем utf-ную локаль. В случае с устаревшими локалями. символ "↵", скорее всего, придётся заменить на что-нибудь попроще, например, символ "%", как в zsh.
Выводим состояние git-репозитория
При работе с гитом из консоли (только не нужно говорить про гуй — мы же суровые разработчики старой закалки!) удобно видеть текущую ветку гита и общее состояние репозитория — есть ли изменённые файлы, или всё закоммичено. Уже на этом этапе я пришёл к выводу, что мне будет удобней работать с приглашением командной строки, состоящим из двух строк — в первой строке выводится информация о текущем окружении (пользователь, сервер, рабочая директория, информация о репозитории и вообще всё, что мы пожелаем), а во второй строке — непосредственно команда, которую мы вводим. Первое время было непривычно, сейчас же я не готов возвращаться к прежней схеме.Для того, чтобы добавить информацию о гите, мы можем воспользоваться специально обученной функцией "__git_ps1", которая появляется вместе с bash-completion для гита:
или же написать свой «костыль». Я пошёл по второму пути, т.к. функция __git_ps1 меня не удовлетворила. Во-первых, мне хотелось видеть не только название ветки, но и состояние репозитория, ещё и подсвечивая это состояние разными цветами. Во-вторых, для синхронизации своих конфигов между разными машинами/серверами я использую гит-репозиторий, и состояние этого репозитория мне хочется видеть только в домашней директории, но не во всех вложенных папках независимо от их глубины.
Собственно, функция, вычитывающая состояние гита выглядит следующим образом:
# get git status
function parse_git_status {
# clear git variables
GIT_BRANCH=
GIT_DIRTY=
# exit if no git found in system
local GIT_BIN=$(which git 2>/dev/null)
[[ -z $GIT_BIN ]] && return
# check we are in git repo
local CUR_DIR=$PWD
while [ ! -d ${CUR_DIR}/.git ] && [ ! $CUR_DIR = "/" ]; do CUR_DIR=${CUR_DIR%/*}; done
[[ ! -d ${CUR_DIR}/.git ]] && return
# 'git repo for dotfiles' fix: show git status only in home dir and other git repos
[[ $CUR_DIR == $HOME ]] && [[ $PWD != $HOME ]] && return
# get git branch
GIT_BRANCH=$($GIT_BIN symbolic-ref HEAD 2>/dev/null)
[[ -z $GIT_BRANCH ]] && return
GIT_BRANCH=${GIT_BRANCH#refs/heads/}
# get git status
local GIT_STATUS=$($GIT_BIN status --porcelain 2>/dev/null)
[[ -n $GIT_STATUS ]] && GIT_DIRTY=true
}
Раньше я ещё парсил и отдельно выводил изменённые (modified) файлы, файлы, находящиеся в индексе для коммита (cached), и файлы, не принадлежащие репозиторию (untracked), но со временем я понял, что это лишняя информация для меня. Собственно, функция простая: смотрим, что гит вообще стоит в системе, проверяем, что мы находимся в гитовом репозитории, рекурсивно обходя все директории наверх до корня файловой системы и ища папку ".git", получаем название текущей ветки и смотрим, есть ли хоть какие-нибудь незакоммиченные файлы. Добавляем вызов этой функции в нашу prompt_command и строим приглашение:
function prompt_command {
local PS1_GIT=
local PWDNAME=$PWD
...
# beautify working firectory name
if [ $HOME == $PWD ]; then
PWDNAME="~"
elif [ $HOME == ${PWD:0:${#HOME}} ]; then
PWDNAME="~${PWD:${#HOME}}"
fi
# parse git status and get git variables
parse_git_status
# build b/w prompt for git
[[ ! -z $GIT_BRANCH ]] && PS1_GIT=" (git: ${GIT_BRANCH})"
local color_user=
if $color_is_on; then
# set user color
case `id -u` in
0) color_user=$color_red ;;
*) color_user=$color_green ;;
esac
# build git status for prompt
if [ ! -z $GIT_BRANCH ]; then
if [ -z $GIT_DIRTY ]; then
PS1_GIT=" (git: ${color_green}${GIT_BRANCH}${color_off})"
else
PS1_GIT=" (git: ${color_red}${GIT_BRANCH}${color_off})"
fi
fi
fi
# set new color prompt
PS1="${color_user}${USER}${color_off}@${color_yellow}${HOSTNAME}${color_off}:${color_white}${PWDNAME}${color_off}${PS1_GIT}n➜ "
}
Вот как это выглядит в итоге:
Пара слов про переменную PWDNAME. Да, я знаю, что можно написать "w" и всё будет так же, но у меня в функции «prompt_command» ещё и выставляется заголовок терминала:
# set title
echo -ne "33]0;${USER}@${HOSTNAME}:${PWDNAME}"; echo -ne "07"
а вот там уже "w" не работает.
Показываем название виртуального окружения python
Последнее время основным языком, на котором я пишу, является Python. Для него есть очень удобная штука под названием virtualenv. Не буду вдаваться в подробности — это тема отдельной статьи, но видеть текущее виртуальное окружение в консоли баша тоже крайне удобно.
На самом деле скрипт virtualenv добавляет название текущего venv в приглаашение баша, но уж очень некрасиво это выглядит:
Запрещаем vertualenv'у вмешиваться в наше приглашение командной строки:
export VIRTUAL_ENV_DISABLE_PROMPT=1
И выводим название venv сами, так, как нам нравится:
function prompt_command {
local PS1_VENV=
...
[[ ! -z $VIRTUAL_ENV ]] && PS1_VENV=" (venv: ${VIRTUAL_ENV#$WORKON_HOME})"
if $color_is_on; then
...
# build python venv status for prompt
[[ ! -z $VIRTUAL_ENV ]] && PS1_VENV=" (venv: ${color_blue}${VIRTUAL_ENV#$WORKON_HOME}${color_off})"
fi
# set new color prompt
PS1="${color_user}${USER}${color_off}@${color_yellow}${HOSTNAME}${color_off}:${color_white}w${color_off}${PS1_GIT}${PS1_VENV}n➜ "
}
Собственно, тут всё достаточно банально.
Отделяем визуально команды друг от друга
При достаточно активной работе с терминалом вводимые команды и результат их выполнения сливаются друг с другом, особенно когда мы в выводе присутствует цвет:
Конечно, пример не совсем показателен, но все, кто работал в консоли, понимают, о чём я.
Я решил выводить горизонтальную черту в каждом приглашении на всю ширину терминала и эта идея себя оправдала — пользоваться консолью стало гораздо удобней:
Вот как я это делаю:
function prompt_command {
...
# build b/w prompt for git and vertial env
[[ ! -z $GIT_BRANCH ]] && PS1_GIT=" (git: ${GIT_BRANCH})"
[[ ! -z $VIRTUAL_ENV ]] && PS1_VENV=" (venv: ${VIRTUAL_ENV#$WORKON_HOME})"
# calculate fillsize
local fillsize=$(($COLUMNS-$(printf "${USER}@${HOSTNAME}:${PWDNAME}${PS1_GIT}${PS1_VENV} " | wc -c | tr -d " ")))
local FILL=$color_gray
while [ $fillsize -gt 0 ]; do FILL="${FILL}─"; fillsize=$(($fillsize-1)); done
FILL="${FILL}${color_off}"
...
# set new color prompt
PS1="${color_user}${USER}${color_off}@${color_yellow}${HOSTNAME}${color_off}:${color_white}${PWDNAME}${color_off}${PS1_GIT}${PS1_VENV} ${FILL}n➜ "
}
Сначала находим длину строки приглашения без цвета, вычитаем её из ширины терминала (переменная $COLUMNS) и делаем строку такой длины, состоящую из символа ASCII-графики "─" (опять же, если локаль не юникодная — можно использовать любой другой символ).
Больше цветов
Если терминал поддерживает 256 цветов, не обязательно ограничиваться стандартными:
color_pink="[$(/usr/bin/tput setaf 99)]"
Но для совместимости с устаревшими терминалами лучше этого избегать.
Послесловие или «Cool story, bro!»
Рабочее окружение должно быть удобным, неважно, чем ты пользуешься. Bash может быть уютненьким — нужно только постараться. Кстати, ещё одной причиной, почему я не использую zsh — его отсутствие на некоторых серверах, на которые я хожу и невозможность его туда поставить.
Очень хочется услышать комментарии, а так же примеры настройки вашего терминала, несмотря на то, что подобной информации в интернете — вагон, и каждый, кто открывает для себя различные шеллы в первую (ну ладно, во вторую) очередь лезет настраивать себе приглашение командной строки. И, да, ШГ, знаю ;) Терминуса под мак нормального я так и не нашёл =( Печаль..
Для экономии места я не привожу весь свой конфиг целиком — желащие могут посмотреть на него тут: github.com/dreadatour/dotfiles (файл .bash_profile).
Автор: Dreadatour