📜 ⬆️ ⬇️

Yet another cool story about bash prompt

I am a programmer. At least it is written in the workbook. Almost all of my work time I spend in the console and text editor. I really like bash. I lived in zsh for almost a year, listening to the advice of my many colleagues and acquaintances, but in the end I returned to bash and didn’t regret it.



Zsh is beautiful, pleasant, damn functional, but, I confess honestly, I could not cope with all its many settings. I want to work, not fight with my working environment. A simple example: a couple of times due to the zsh autocompletion, I deleted all the directories and files in the current directory - zsh just put a space between the autocompleted directory and the star I entered (I wanted to delete everything in the selected folder). Remember that epic bug with a space and the removal of the / usr directory? I had the same thing. Thanks to the gita, helped out again.
')
However, it's not about zsh - if I were a little smarter, I would definitely cope with it, and everything would be fine, but we, severe programmers, will use bash and vim, and glamorous zsh and textmate will leave to hipsters and other mods;)

I will not write anything original and universal solution, I will not give, but I always liked to read configs and descriptions of other people, and if they were accompanied by interesting pictures, so I generally re-read these articles several times. I hope you will be interested too.

If suddenly something from what I have written can be solved more simply, or there is already a described functionality in the bash - write in the comments. Well, just in case, my where I live:
GNU bash, version 4.2.28(2)-release (i386-apple-darwin11.3.0) 



Add line feed before prompt



So, the first thing that I come across every day and what I don’t like in bash is the commands that do not complete their output with a line break at the end. Here is a simple example (emulation of this behavior):

Of course, nothing terrible happened, but the same zsh correctly handles this situation, and we will teach bash such a trick.

To do this, we need to look at the cursor position at each output of the command line prompt (PS1), and if the cursor is not on the first character in the line, output a line break ("\ n" character). The position of the cursor can be determined using the escape sequence:
 echo -en "\033[6n" && read -sdR CURPOS 

As a result, the CURPOS variable will contain something like this: "^ [[4; 12R", where 4 is the line number and 12 is the number of the character in the line. Add the appropriate code to our bash config (~ / .bashrc or ~ / .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 

CM. Update
PROMPT_COMMAND is a function that is called whenever the command prompt is drawn. Here, a small hack was used, peeped by me in a comment on stackoverflow , without this hack the value of the $ CURPOS variable in some cases was displayed on the screen. Do not pay attention to a bunch of flowers - below they will all be useful to us. The result of our config:

A red background was added specifically to distinguish this symbol from what the command can output. And, yes, it's the 21st century, so we use the utf-local locale. In the case of obsolete locales. The symbol "↵" will most likely have to be replaced with something simpler, for example, the symbol "%", as in zsh.

Display the status of the git repository



When working with git from the console (just don’t need to talk about gui - we’re the harsh developers of the old school!) It’s convenient to see the current branch of the git and the general state of the repository - are there any modified files or everything is closed. Already at this stage, I came to the conclusion that it would be more convenient for me to work with the command line prompt consisting of two lines - the first line displays information about the current environment (user, server, working directory, information about the repository, and generally everything we want ), and in the second line - the command that we enter. The first time was unusual, but now I am not ready to go back to the previous scheme. In order to add information about the program, we can use the specially trained function "__git_ps1", which appears along with the bash-completion for the program:

or write your own “crutch”. I went the second way, because The __git_ps1 function did not satisfy me. Firstly, I wanted to see not only the name of the branch, but also the state of the repository, also highlighting this state with different colors. Secondly, I use the git repository to synchronize my configs between different machines / servers, and I want to see the status of this repository only in the home directory, but not in all subfolders, regardless of their depth.

Actually, the function reading the state of the gita is as follows:
 # 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 } 

Previously, I still parsed and separately displayed modified (modified) files, files that are in the index for a commit (cached), and files that do not belong to the repository (untracked), but over time I realized that this is too much information for me. Actually, the function is simple: we see that git is generally in the system, check that we are in the gitovy repository, recursively bypassing all directories up to the root of the file system and looking for the folder ".git", we get the name of the current branch and see if there is any any uncommitted files. Add a call to this function in our prompt_command and build the invitation:
 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âžś " } 

Here's what it looks like in the end:

A couple of words about the variable PWDNAME. Yes, I know that you can write "\ w" and everything will be the same, but in my function "prompt_command" the terminal title is also set:
 # set title echo -ne "\033]0;${USER}@${HOSTNAME}:${PWDNAME}"; echo -ne "\007" 

and there already "\ w" does not work.

Show the name of the python virtual environment.



Recently, the main language in which I write is Python. For him there is a very handy thing called virtualenv . I will not go into details - this is the topic of a separate article, but it is also extremely convenient to see the current virtual environment in the bash console.

Actually the virtualenv script adds the name of the current venv to the bash invitation, but it looks very ugly:

We prohibit vertualenv from interfering with our command line prompt:
 export VIRTUAL_ENV_DISABLE_PROMPT=1 

And we derive the name venv ourselves, the way we like it:
 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âžś " } 


Actually, everything is quite banal.

Separate the teams visually from each other



With a fairly active work with the terminal, the input commands and the result of their execution merge with each other, especially when there is a color in the output:

Of course, the example is not quite indicative, but everyone who worked in the console understands what I mean.

I decided to display a horizontal line in each invitation for the entire width of the terminal and this idea proved itself - it became much more convenient to use the console:

Here is how I do it:
 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➜ " } 

First, we find the length of the invitation string without a color, subtract it from the terminal width ($ COLUMNS variable) and make a string of that length consisting of the ASCII graphics character "─" (again, if the locale is not Unicode, you can use any other character).

More colors


If the terminal supports 256 colors, it is not necessary to be limited to the standard ones:
 color_pink="\[$(/usr/bin/tput setaf 99)\]" 


But for compatibility with legacy terminals, it is better to avoid this.

Afterword or “Cool story, bro!”



The work environment should be comfortable, no matter what you use. Bash can be cozy - you just need to try. By the way, another reason why I do not use zsh is its absence on some servers that I go to and the inability to put it there.

I really want to hear comments, as well as examples of setting up your terminal, despite the fact that there is a wagon on the Internet, and everyone who discovers various shells for the first (well, second) turns to customize the command line prompt. . And, yes, ShG, I know;) I did not find the terminus under the poppy normal = (Sadness ..

To save space, I do not cite all my config entirely - those who wish can look at it here: github.com/dreadatour/dotfiles ( .bash_profile file).

UPD : Here is the code:
 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 

breaks ssh. Returned to the old version, without a hack, looking for what the problem is:
 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 


UPD2 : Thank you all for the comments, - fixed the bugs, improved the code. Now the prompt_command function looks like this:
Hidden text
 # setup color variables color_is_on= color_red= color_green= color_yellow= color_blue= color_white= color_gray= color_bg_red= color_off= color_user= 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)" # set user color case `id -u` in 0) color_user=$color_red ;; *) color_user=$color_green ;; esac fi # some kind of optimization - check if git installed only on config load PS1_GIT_BIN=$(which git 2>/dev/null) function prompt_command { local PS1_GIT= local PS1_VENV= local GIT_BRANCH= local GIT_DIRTY= local PWDNAME=$PWD # beautify working directory name if [[ "${HOME}" == "${PWD}" ]]; then PWDNAME="~" elif [[ "${HOME}" == "${PWD:0:${#HOME}}" ]]; then PWDNAME="~${PWD:${#HOME}}" fi # parse git status and get git variables if [[ ! -z $PS1_GIT_BIN ]]; then # check we are in git repo local CUR_DIR=$PWD while [[ ! -d "${CUR_DIR}/.git" ]] && [[ ! "${CUR_DIR}" == "/" ]] && [[ ! "${CUR_DIR}" == "~" ]] && [[ ! "${CUR_DIR}" == "" ]]; do CUR_DIR=${CUR_DIR%/*}; done if [[ -d "${CUR_DIR}/.git" ]]; then # 'git repo for dotfiles' fix: show git status only in home dir and other git repos if [[ "${CUR_DIR}" != "${HOME}" ]] || [[ "${PWD}" == "${HOME}" ]]; then # get git branch GIT_BRANCH=$($PS1_GIT_BIN symbolic-ref HEAD 2>/dev/null) if [[ ! -z $GIT_BRANCH ]]; then GIT_BRANCH=${GIT_BRANCH#refs/heads/} # get git status local GIT_STATUS=$($PS1_GIT_BIN status --porcelain 2>/dev/null) [[ -n $GIT_STATUS ]] && GIT_DIRTY=1 fi fi fi fi # build b/w prompt for git and virtual env [[ ! -z $GIT_BRANCH ]] && PS1_GIT=" (git: ${GIT_BRANCH})" [[ ! -z $VIRTUAL_ENV ]] && PS1_VENV=" (venv: ${VIRTUAL_ENV#$WORKON_HOME})" # calculate prompt length local PS1_length=$((${#USER}+${#HOSTNAME}+${#PWDNAME}+${#PS1_GIT}+${#PS1_VENV}+3)) local FILL= # if length is greater, than terminal width if [[ $PS1_length -gt $COLUMNS ]]; then # strip working directory name PWDNAME="...${PWDNAME:$(($PS1_length-$COLUMNS+3))}" else # else calculate fillsize local fillsize=$(($COLUMNS-$PS1_length)) FILL=$color_gray while [[ $fillsize -gt 0 ]]; do FILL="${FILL}─"; fillsize=$(($fillsize-1)); done FILL="${FILL}${color_off}" fi if $color_is_on; then # 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 # 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}${PWDNAME}${color_off}${PS1_GIT}${PS1_VENV} ${FILL}\n➜ " # get cursor position and add new line if we're not in first column # cool'n'dirty trick (http://stackoverflow.com/a/2575525/1164595) # XXX FIXME: this hack broke ssh =( # exec < /dev/tty # local OLDSTTY=$(stty -g) # stty raw -echo min 0 # echo -en "\033[6n" > /dev/tty && read -sdR CURPOS # stty $OLDSTTY echo -en "\033[6n" && read -sdR CURPOS [[ ${CURPOS##*;} -gt 1 ]] && echo "${color_error}↵${color_error_off}" # set title echo -ne "\033]0;${USER}@${HOSTNAME}:${PWDNAME}"; echo -ne "\007" } # set prompt command (title update and color prompt) PROMPT_COMMAND=prompt_command # set new b/w prompt (will be overwritten in 'prompt_command' later for color prompt) PS1='\u@\h:\w\$ ' 


Source: https://habr.com/ru/post/145008/


All Articles