I spend a great deal of time on the command line. That’s how I usually manipulate files, do little maintenance tasks, run tests from and etc. Because of this I like to make that environment nice to look at. I typically set up my console terminals and editors with the excellent and rather popular Solarized theme. I like it because it provides an unified look and feel to my command line regardless of the client or the platform. It works in Konsole, iTerm2 and even in PuTTY on windows via a neat registry hack. Not to mention that the Solarized Dark theme is actually one of the very few light text on dark background combinations that doesn’t make my eyes hurt.
Last week we talked about various ways to streamline and customize Bash. That post didn’t include an important area: the command prompt itself. That’s because it is a complex topic in and of itself.
There are prompt purists out there who swear by the standard, minimalistic prompt like:
PS1="\$ "
This is enough to distinguish whether or not you are logged in as root (though I don’t recall the last time I had to log in anywhere as root because of the magical properties of sudo. But, that’s way to simple for me. For example, I tend to forget where I am in the file system quite easily. Sure, I could always just run pwd to see my location, but why do that if I could simply display it in my prompt using the \w tag like this:
PS1="\w \$ "
While you’re adding crap to your prompt, you might as well go all the way and use the standard prompt string you commonly see on Ubuntu boxes:
PS1="\u@\h:\w \$"
This typically evaluates to the boring username@host:~ $ style prompt. Purists and unix Longbeards scoff at this. They are like “does your username and host change so often you need it in the prompt?” The answer to this is of course… Yes. Kinda. On a typical day I have bunch of terminals open, each of them logged into a different machine. Having my username and host on the prompt lets me ascertain at a glance weather I am logged into the production server, the test server, my home server, my other home server, my vagrant instance or if I’m just on the local prompt. So I actually really need these things there. I also need them not to be this ugly.
Bash lets you use ANSI color codes to colorize your output, using the rather ugly \033 sequence followed by a color code. Unfortunately using these color codes directly turns your prompt into a completely unreadable mess. Check this out:
PS1="\[\033[32m\]\u\[\033[0m\]@\[\033[32m\]\h\[\033[0m\]:\[\033[33m\]\ w\[\033[0m\] \$ "
I absolutely hate this. I edit my prompt only every once in a blue moon, so I don’t memorize these codes. Every time I have to make a change to the prompt I have to make effort to actually parse this ungodly string. So I like to define my color codes like this:
##-ANSI-COLOR-CODES-##
Color_Off="\[\033[0m\]"
###-Regular-###
Red="\[\033[0;31m\]"
Green="\[\033[0;32m\]"
Purple="\[\033[0;35\]"
####-Bold-####
BRed="\[\033[1;31m\]"
BPurple="\[\033[1;35m\]"
Then I can just write my prompt with readable variable names. Please tell me that this is not much, much better than the thorn-bush-tangle looking line from paragraph above:
PS1+="$BRed\u$Color_Off@$BRed\h$Color_Off:$BPurple\w$Color_Off \$"
This gives you a prompt that looks like the one you might have seen in my PHP or bash tutorials from the past – a red username/host part and purple path. It is simple, aesthetic and useful. Lately though I’ve been kinda jealous of the people with tricked out prompts that return the status of the last command or the status of their git repository. So I decided to be fancy and write my own.
The status of the last command was pretty easy. You just check the $? variable and set the colors appropriately. I wanted to do it nicely, so I wrote it as a function.
# Status of last command (for prompt)
function __stat() {
if [ $? -eq 0 ]; then
echo -en "$Green[✔]$Color_Off"
else
echo -en "$Red[✘]$Color_Off"
fi
}
The git part was little bit more difficult because while most systems have a __git_ps1 function that is to be used specifically in the prompts, it only returns the name of the current branch, but not it’s status. What I really wanted was to have prompt that could tell me whether or not my index is “dirty” or not.
Most of the time when I cd into a git repository, I run git status to see if there are any uncommited or untracked files in there. Wouldn’t it be nice to have that information in the prompt? But how?
Well, I tried couple of different things and settled on a solution I mostly stole from here. It turns out that there is no way to avoid running git status, but you can force your prompt to do it automatically and then parse the results like this:
# Display the branch name of git repository
# Green -> clean
# purple -> untracked files
# red -> files to commit
function __git_prompt() {
local git_status="`git status -unormal 2>&1`"
if ! [[ "$git_status" =~ Not\ a\ git\ repo ]]; then
if [[ "$git_status" =~ nothing\ to\ commit ]]; then
local Color_On=$Green
elif [[ "$git_status" =~ nothing\ added\ to\ commit\ but\ untracked\ files\ present ]]; then
local Color_On=$Purple
else
local Color_On=$Red
fi
if [[ "$git_status" =~ On\ branch\ ([^[:space:]]+) ]]; then
branch=${BASH_REMATCH[1]}
else
# Detached HEAD. (branch=HEAD is a faster alternative.)
branch="(`git describe --all --contains --abbrev=4 HEAD 2> /dev/null || echo HEAD`)"
fi
echo -ne "$Color_On[$branch]$Color_Off "
fi
}
This is not a perfect solution. It parses what is commonly known as the porcelain – the user readable output that is bound to change. So upgrading git to the next release is likely to break if they decide to change the wording of these prompts a bit. Still, this is by far the fastest solution which utilizes only a single external command (other than bash built-ins).
Now you put it all together like this:
PS1=""
# command status (shows check-mark or red x if last command failed)
PS1+='$(__stat) '$Color_Off
# debian chroot stuff (take it or leave it)
PS1+="${debian_chroot:+($debian_chroot)}"
# basic information (user@host:path)
PS1+="$BRed\u$Color_Off@$BRed\h$Color_Off:$BPurple\w$Color_Off "
# add git display to prompt
PS1+='$(__git_prompt)'$Color_Off
# prompt $ or # for root
PS1+="\$ "
export PS1
In a perfect world you ought to have a prompt that looks like this:
Unfortunately, it does not work. Or rather it outputs something that looks like this:
\[\][✔]\[\] luke@firmin:~ \[\][master]\[\] $
The stuff in the middle is fine, but the non-printable character escape codes \[ and \] get printed from within the function calls. Why? Well, internally Bash recognizes these as escape codes when it parses the PS1 prompt variable but the echo command does not recognize them as such. So you can’t use these characters when you echo.
You could always set up your color codes without these characters like this:
Color_Off="\033[0m"
Red="\033[0;31m"
Green="\033[0;32m"
Purple="033[0;35"
Now my functions will work correctly but this causes another issue. If you don’t use the \[ and \] escape codes, Bash treats the ASNI codes as printable characters when it calculates the column count. If your column count is off, then your commands won’t wrap to the next line. Instead they will wrap around and overwrite your prompt. This is endlessly frustrating and annoying. Unfortunately it is a catch 22 – for PS1 prompt to work correctly you must escape color codes. But if you use functions you must echo the results and can’t escape properly.
I spent countless hours fighting with this issue, until I realized that using functions was just not a practical idea. In most scripting environments this would be a sound choice – modular code, encapsulation, etc. But Bash just doesn’t work that way.
So I rewrote it using the PROMPT_COMMAND variable. Normally when you set up your PS1 it gets evaluated when a Bash shell is instantiated. The trick I was using was injecting function literals into that variable, to trick Bash into running them each time it evaluates the prompt. This is hackish, but that’s how most people do it.
There is actually a proper way to set up Bash to build your prompt at run time. What you do is you assign a callback function to PROMPT_COMMAND variable. This function then gets called every time Bash is about to print out your PS1. What’s more important is that this function can manipulate and update the PS1 variable before it gets printed. So as long as you do all your magic in that function, you can append variables directly to PS1 without the need to echo them.
Here is my solution:
# set up command prompt
function __prompt_command()
{
# capture the exit status of the last command
EXIT="$?"
PS1=""
if [ $EXIT -eq 0 ]; then PS1+="\[$Green\][\!]\[$Color_Off\] "; else PS1+="\[$Red\][\!]\[$Color_Off\] "; fi
# if logged in via ssh shows the ip of the client
if [ -n "$SSH_CLIENT" ]; then PS1+="\[$Yellow\]("${$SSH_CLIENT%% *}")\[$Color_Off\]"; fi
# debian chroot stuff (take it or leave it)
PS1+="${debian_chroot:+($debian_chroot)}"
# basic information (user@host:path)
PS1+="\[$BRed\]\u\[$Color_Off\]@\[$BRed\]\h\[$Color_Off\]:\[$BPurple\] \w\[$Color_Off\] "
# check if inside git repo
local git_status="`git status -unormal 2>&1`"
if ! [[ "$git_status" =~ Not\ a\ git\ repo ]]; then
# parse the porcelain output of git status
if [[ "$git_status" =~ nothing\ to\ commit ]]; then
local Color_On=$Green
elif [[ "$git_status" =~ nothing\ added\ to\ commit\ but\ untracked\ files\ present ]]; then
local Color_On=$Purple
else
local Color_On=$Red
fi
if [[ "$git_status" =~ On\ branch\ ([^[:space:]]+) ]]; then
branch=${BASH_REMATCH[1]}
else
# Detached HEAD. (branch=HEAD is a faster alternative.)
branch="(`git describe --all --contains --abbrev=4 HEAD 2> /dev/null || echo HEAD`)"
fi
# add the result to prompt
PS1+="\[$Color_On\][$branch]\[$Color_Off\] "
fi
# prompt $ or # for root
PS1+="\$ "
}
PROMPT_COMMAND=__prompt_command
The end result looks like this:
As you can see I sort of abandoned the check-mark and x notation in lieu of command history numbers. Why? Mostly because these characters were causing line wrapping issues in some terminals (notably PuTTY). But also because the history numbers are actually somewhat more useful than simple ok/fail which is effectively communicated by color alone.
In addition I also added a little yellow tag that shows your remote IP if you happen to be connected via SSH like this:
Why do I need that on my prompt? Well, sometimes it is useful to know your IP – for example not to firewall yourself off from a remote system by accident. It also helps when you do port forwarding or have to play SSH-INCEPTION to get somewhere. For example, if I’m at work I can’t really SSH out of the network because of draconian firewall rules. The only outbound ports that are open to end users are 80 and 443. So if I for some reason I need to log in to one of my Linode boxes or to a university server I first have to SSH home. I run a tiny server listening for SSH connections on port 80 at home for the sole purpose of being my “relay” when I’m at work. So seeing the IP on the prompt gives me an idea where the hell am I logged in from when I suddenly find an open terminal with a live ssh session from 4 hours ago.
How about you? What is on your prompt?
I used to have roughly the same prompt as you in Bash, but I’ve switched over to Zsh and Oh-My-Zsh!, so now mine looks like below. The (red, but you can’t see it here) asterisk indicates that the Git repo has uncommited things, and the number right after it is the exit code from last command.
fx@computer-name:/full/path/.../.../ (*) [1]
> ls -lrt master [c22c6d5] modified untracked
I’m also using Dropbox to sync my dotfiles between computers, so they are always relatively painless to modify ;)
Heh, mine is very simple…
export PS1='[\u@\h:\w]$ '
Wow, I really like the idea of changing the color of the prompt based on the exit status of the previous command. Clever idea.
Since I work in the world of Solaris zones, I got in the habit of also adding the zone name to the prompt. This was especially handy when working with Trusted Extensions. I also change the prompt color to represent the security level. I found the prompt getting a little too long sometimes to I also added a carriage return.
If you work on multiple platforms like myself, you can also add a case statement to allow you to have multiple environments based on uname.
Hello from consoleland! Mine is pretty simple:
PS1=”`whoami`@`hostname`$ ”
Oh and I’m using mksh instead of that god-awful bash.
I’ve played around with having more information in my prompts before, but it always seems to end up making it harder for me to see the things I really use. Hence I opted for a slight modification of the default: http://i.imgur.com/Dcry9.png
I find this two-line variant quite handy – it makes it easier to read long cwd paths, has lots of space to type in a command, and makes prompts easier to pick out if you’re scrolling back up through a ream of output. The 0 in front of the prompt is the exit code of the last command – turns red if nonzero.
Source on github: https://github.com/reacocard/dotfiles/blob/master/.zshrc#L116
I worked out my bash prompt about 6 months ago when establishing my dotfiles repository. I hadn’t bothered customizing it before because propagating the configuration was too much of a hassle, but the repository solved that.
Here’s what it’s looked like the last 6 months,
http://i.imgur.com/7YXSU.png
It’s fairly minimal: user/host/cwd. With heavy SSH usage, this is everything I need to know on the fly. I mostly drive Git from within Emacs using Magit, so I don’t need any Git prompt fanciness. It’s colorful so that it doesn’t need delimiters between the prompt items (@, :, etc.), and so that it’s easy to spot the command lines among a bunch of output.
I actually went the minimal route for the prompt:
export PS1=”> ”
Primarily I did this because I wanted to maximise the horizontal space. However I like having more information available quickly (e.g. time, where am I, host etc.) so I created an alias in my .alias file that includes detailed information:
alias s=’echo $(date) : $(whoami)@$(hostname) : $(ps axu | grep “paul” | wc -l) ps : $(pwd)’
This lets me in two key strokes get out the detailed information I want.
I found a nifty little trick that will only print the full prompt if you hit enter twice in a row or when opening a new terminal. Gives more room on the line for the rest of your commands.
.bashrc snippet here: http://sprunge.us/OQjU
git-completion-plus.sh: http://sprunge.us/SbRZ
This adds a nice indication as to what git branch you’re on, how many uncommitted changes and committed changes there are and the current path/hostname. The branch name and the number of changes all change color depending on the status. I like your idea of adding the IP address for remote servers. I only access a few different machines, so I just use different colors for my tmux status bar depending on which machine I’m logged in to (95% of the time I have tmux running).
firecat53@scotty:~/projects/company/inventory/src (master) 1 1 $
Scott
I’m often confronted with a lack of screen space, so I tend to go with the minimalistic:
PS1="\$ "
as it means you don’t have anything taking up valuable console width.
Checking for exit status 0 isn’t enough; when you type Ctrl+Z to push a command into the background (“stop” it, in POSIX terms), $? gets set to a non-zero value. The solution is to check for
[ $? -eq 0 -o `kill -l $?` = TSTP ]
Why don’t you use some of the other git ps1 vars like:
GIT_PS1_SHOWDIRTYSTATE
GIT_PS1_SHOWUPSTREAM
Check them out here
Hey!
I’ve fighted hours with the bad wrapping of lines due to the use of colors in PS1.
The best solution I found:
https://bugs.r-project.org/bugzilla3/show_bug.cgi?id=14800
And yes the solution is as simple as using 01 and 02 instead of \[ and \]!!!!
Unfortunately, it is very difficult to find, since it is not discussed in bash texts/blogs/articles/…, but in an R lang bug report!!!
Hope this helps!
Brilliant look for the PS1, love the idea.
Just an FYI for people ending up here using Ubuntu.
When I tried to use the above it didn’t work for me (Ubuntu 13.10). This is because Ubuntu has been using a different shell since 6.10 (https://wiki.ubuntu.com/DashAsBinSh).
Basically it means that some of the things (like the substitutions) wont work. I haven’t figured out how to get it to adjust the above code to make it work. But if someone does: post it!
Color me unimpressed. The lengths some hipsters will go to in order to feel unique. A prompt is supposed to be functional, not distracting.
Thanks for posting this; saved me inordinate amounts of time. However, the SSH_CLIENT line creates a “bad substitution” error on every prompt. Removing the extra dollar sign (and the unescaped quotation marks) fixed it for me:
if [ -n "$SSH_CLIENT" ]; then PS1+="\[$Yellow\](${SSH_CLIENT%% *})\[$Color_Off\]"; fi
Sorry, one more… I was getting weird linewrap issues using the above code (illustrative GIF: http://i.stack.imgur.com/fccjc.gif). Because I had my color codes defined as:
##-ANSI-COLOR-CODES-##
Color_Off="\[33[0m\]"
###-Regular-###
Red="\[33[0;31m\]"
Green="\[33[0;32m\]"
Purple="\[33[0;35\]"
Yellow="\[33[1;33m\]"
####-Bold-####
BRed="\[33[1;31m\]"
BPurple="\[33[1;35m\]"
lines like
PS1+="\[$Green\][\!]\[$Color_Off\] "
were causing the escape sequences to be double quoted. The article’s a little unclear as to the final color definitions you arrived at (it also doesn’t define $Yellow despite its use in the SSH_CLIENT code). If somebody’s reading this and defines their colors as I do, I’ll save you the trouble of removing the extra escape sequences:
@ captnjlp:# set up command prompt
function __prompt_command()
{
# capture the exit status of the last command
EXIT="$?"
PS1=""
if [ $EXIT -eq 0 ]; then PS1+="$Green[\!]$Color_Off "; else PS1+="$Red[\!]$Color_Off "; fi
# if logged in via ssh shows the ip of the client
if [ -n "$SSH_CLIENT" ]; then PS1+="$Yellow(${SSH_CLIENT%% *})$Color_Off"; fi
# debian chroot stuff (take it or leave it)
PS1+="${debian_chroot:+($debian_chroot)}"
# basic information (user@host:path)
PS1+="$BRed\u$Color_Off@$BRed\h$Color_Off:$BPurple\w$Color_Off "
# check if inside git repo
local git_status="`git status -unormal 2>&1`"
if ! [[ "$git_status" =~ Not\ a\ git\ repo ]]; then
# parse the porcelain output of git status
if [[ "$git_status" =~ nothing\ to\ commit ]]; then
local Color_On=$Green
elif [[ "$git_status" =~ nothing\ added\ to\ commit\ but\ untracked\ files\ present ]]; then
local Color_On=$Purple
else
local Color_On=$Red
fi
if [[ "$git_status" =~ On\ branch\ ([^[:space:]]+) ]]; then
branch=${BASH_REMATCH[1]}
else
# Detached HEAD. (branch=HEAD is a faster alternative.)
branch="(`git describe --all --contains --abbrev=4 HEAD 2> /dev/null || echo HEAD`)"
fi
# add the result to prompt
PS1+="$Color_On[$branch]$Color_Off "
fi
# prompt $ or # for root
PS1+="\$ "
}
PROMPT_COMMAND=__prompt_command
What I should have is:
PS1=”: \u@\h\$ ;”
Because that means I can simply cut&paste a whole line and it is still a valid command. Triple-clicking (in xterm, at least) is an easy-ish way to get a whole line, so…