Пятница, 19.04.2024, 18:04
Мой персональный сайт Добрым людям smart & sober

Главная Регистрация Вход
Приветствую Вас, Гость · RSS
Калькулятор


Меню сайта
Календарь
«  Июнь 2012  »
ПнВтСрЧтПтСбВс
    123
45678910
11121314151617
18192021222324
252627282930


Форма входа


Архив записей
Мини-чат


Категории раздела


Наш опрос
В чем заключается ваш смысл жизни
Всего ответов: 154
 
Главная » 2012 » Июнь » 2 » Yet another cool story about bash prompt
00:12
Yet another cool story about bash prompt
Я программист. По крайней мере так написано в трудовой книжке. Почти всё своё рабочее время я провожу в консоли и текстовом редакторе. Мне очень нравится 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):
# 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 "\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 меня не удовлетворила. Во-первых, мне хотелось видеть не только название ветки, но и состояние репозитория, ещё и подсвечивая это состояние разными цветами. Во-вторых, для синхронизации своих конфигов между разными машинами/серверами я использую гит-репозиторий, и состояние этого репозитория мне хочется видеть только в домашней директории, но не во всех вложенных папках независимо от их глубины.

Собственно, функция, вычитывающая состояние гита выглядит следующим образом:
# 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 "\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
 ...

 # 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).

UPD: Вот этот код:
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 "\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 {
 # get cursor position and add new line if we're not in first column
 echo -en "\033[6n" && read -sdR CURPOS
 [[ ${CURPOS##*;} -gt 1 ]] && echo "${color_error}${color_error_off}"
}
PROMPT_COMMAND=prompt_command
Просмотров: 782 | Добавил: Breger | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Copyright MyCorp © 2024