Я программист. По крайней мере так написано в трудовой книжке. Почти всё
своё рабочее время я провожу в консоли и текстовом редакторе. Мне очень
нравится 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 "\033[6n" && read -sdR CURPOS
В результате в переменной CURPOS будет находиться что-то вроде этого:
"^[[4;12R", где 4 — номер строки, а 12 — номер символа в строке.
Добавляем соответствующий код в наш конфиг баша (~/.bashrc или
~/.bash_profile):
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 {
exec < /dev/tty
local OLDSTTY=$(stty -g)
stty raw -echo min 0
echo -en "\033[6n" > /dev/tty && read -sdR CURPOS
stty $OLDSTTY
[[ ${CURPOS##*;} -gt 1 ]] && echo "${color_error}↵${color_error_off}"
}
PROMPT_COMMAND=prompt_command
СМ. Update
PROMPT_COMMAND — это функция, которая вызывается при каждой отрисовке
приглашения командной строки. Здесь был использован небольшой хак,
подсмотренный мною в комментарии на stackoverflow,
без этого хака значение переменной $CURPOS в некоторых случаях
выводилось на экран. На кучу цветов не обращайте внимание — ниже они все
нам пригодятся. Результат работы нашего конфига:
Красный фон был добавлен специально, чтобы отличать этот символ от того,
что может вывести команда. И, да, на дворе 21 век, поэтому мы
используем utf-ную локаль. В случае с устаревшими локалями. символ "↵",
скорее всего, придётся заменить на что-нибудь попроще, например, символ
"%", как в zsh.
Выводим состояние git-репозитория
При работе с гитом из консоли (только не нужно говорить про гуй — мы же
суровые разработчики старой закалки!) удобно видеть текущую ветку гита и
общее состояние репозитория — есть ли изменённые файлы, или всё
закоммичено. Уже на этом этапе я пришёл к выводу, что мне будет удобней
работать с приглашением командной строки, состоящим из двух строк — в
первой строке выводится информация о текущем окружении (пользователь,
сервер, рабочая директория, информация о репозитории и вообще всё, что
мы пожелаем), а во второй строке — непосредственно команда, которую мы
вводим. Первое время было непривычно, сейчас же я не готов возвращаться к
прежней схеме.Для того, чтобы добавить информацию о гите, мы можем
воспользоваться специально обученной функцией "__git_ps1", которая
появляется вместе с bash-completion для гита:
или же написать свой «костыль». Я пошёл по второму пути, т.к. функция
__git_ps1 меня не удовлетворила. Во-первых, мне хотелось видеть не
только название ветки, но и состояние репозитория, ещё и подсвечивая это
состояние разными цветами. Во-вторых, для синхронизации своих конфигов
между разными машинами/серверами я использую гит-репозиторий,
и состояние этого репозитория мне хочется видеть только в домашней
директории, но не во всех вложенных папках независимо от их глубины.
Собственно, функция, вычитывающая состояние гита выглядит следующим образом:
function parse_git_status {
GIT_BRANCH=
GIT_DIRTY=
local GIT_BIN=$(which git 2>/dev/null)
[[ -z $GIT_BIN ]] && return
local CUR_DIR=$PWD
while [ ! -d ${CUR_DIR}/.git ] && [ ! $CUR_DIR = "/" ]; do CUR_DIR=${CUR_DIR%/*}; done
[[ ! -d ${CUR_DIR}/.git ]] && return
[[ $CUR_DIR == $HOME ]] && [[ $PWD != $HOME ]] && return
GIT_BRANCH=$($GIT_BIN symbolic-ref HEAD 2>/dev/null)
[[ -z $GIT_BRANCH ]] && return
GIT_BRANCH=
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
...
if [ $HOME == $PWD ]; then
PWDNAME="~"
elif [ $HOME == ${PWD:0:${#HOME}} ]; then
PWDNAME="~${PWD:${#HOME}}"
fi
parse_git_status
[[ ! -z $GIT_BRANCH ]] && PS1_GIT=" (git: ${GIT_BRANCH})"
local color_user=
if $color_is_on; then
case `id -u` in
0) color_user=$color_red ;;
*) color_user=$color_green ;;
esac
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
PS1="${color_user}${USER}${color_off}@${color_yellow}${HOSTNAME}${color_off}:${color_white}${PWDNAME}${color_off}${PS1_GIT}\n➜ "
}
Вот как это выглядит в итоге:
Пара слов про переменную PWDNAME. Да, я знаю, что можно написать "\w" и
всё будет так же, но у меня в функции «prompt_command» ещё и
выставляется заголовок терминала:
echo -ne "\033]0;${USER}@${HOSTNAME}:${PWDNAME}"; echo -ne "\007"
а вот там уже "\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
...
[[ ! -z $VIRTUAL_ENV ]] && PS1_VENV=" (venv: ${color_blue}${VIRTUAL_ENV#$WORKON_HOME}${color_off})"
fi
PS1="${color_user}${USER}${color_off}@${color_yellow}${HOSTNAME}${color_off}:${color_white}\w${color_off}${PS1_GIT}${PS1_VENV}\n➜ "
}
Собственно, тут всё достаточно банально.
Отделяем визуально команды друг от друга
При достаточно активной работе с терминалом вводимые команды и результат
их выполнения сливаются друг с другом, особенно когда в выводе
присутствует цвет:
Конечно, пример не совсем показателен, но все, кто работал в консоли, понимают, о чём я.
Я решил выводить горизонтальную черту в каждом приглашении на всю ширину
терминала и эта идея себя оправдала — пользоваться консолью стало
гораздо удобней:
Вот как я это делаю:
function prompt_command {
...
[[ ! -z $GIT_BRANCH ]] && PS1_GIT=" (git: ${GIT_BRANCH})"
[[ ! -z $VIRTUAL_ENV ]] && PS1_VENV=" (venv: ${VIRTUAL_ENV#$WORKON_HOME})"
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}"
...
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).
UPD: Вот этот код:
function prompt_command {
exec < /dev/tty
local OLDSTTY=$(stty -g)
stty raw -echo min 0
echo -en "\033[6n" > /dev/tty && read -sdR CURPOS
stty $OLDSTTY
[[ ${CURPOS##*;} -gt 1 ]] && echo "${color_error}↵${color_error_off}"
}
PROMPT_COMMAND=prompt_command
ломает ssh. Вернулся на старый вариант, без хака, ищу, в чём проблема:
function prompt_command {
echo -en "\033[6n" && read -sdR CURPOS
[[ ${CURPOS##*;} -gt 1 ]] && echo "${color_error}↵${color_error_off}"
}
PROMPT_COMMAND=prompt_command
|