I am sure that I have seen someone have a part of their prompt aligned to the right in their terminal window and then have the actual cursor start on a second line. I know that I can achieve the second line with a "\n" in the PS1, but I cannot figure out how to align part of it to the right. Was what I saw just whitespace added between the two strings?
9 Answers
What you want can fairly easily be done by displaying the first line before displaying the prompt. For example, the following displays a prompt of \w
on the left of the first line and a prompt of \u@\h
on the right of the first line. It makes use of the $COLUMNS
variable which contains the width of the terminal and the $PROMPT_COMMAND
parameter which is evaluated before bash displays the prompt.
print_pre_prompt ()
{
PS1L=$PWD
if [[ $PS1L/ = "$HOME"/* ]]; then PS1L=\~${PS1L#$HOME}; fi
PS1R=$USER@$HOSTNAME
printf "%s%$(($COLUMNS-${#PS1L}))s" "$PS1L" "$PS1R"
}
PROMPT_COMMAND=print_pre_prompt
-
3Note that things get significantly more complicated if you want a colored left prompt, since the non-printing characters mean that string length is not the same as the number of characters displayed.– Mu MindSep 1, 2012 at 6:49
-
1Both this and the highest voted answer don't work correctly if
.inputrc
hasset show-mode-in-prompt on
. Both don't calculate the length of the non-prinable ANSI CSI codes, and don't properly enclose them in\[
and\]
as mentioned by @Mu Mind. See this answer for a resolution.– Tom HaleApr 26, 2017 at 10:58
Based on the information I found here I was able to discover a simpler solution to right align while accommodating variable length content on the right or left including support for colour. Added here for your convenience...
Note on colours: using the \033
escape in favour of alternatives, without \[\]
groupings, proves most compatible and therefor recommended.
The trick is to write the right hand side first, then use carriage return (\r
) to return to start of line and continue to overwrite the left hand side content on top of that, as follows:
prompt() {
PS1=$(printf "%*s\r%s\n\$ " "$(tput cols)" 'right' 'left')
}
PROMPT_COMMAND=prompt
I am using tput cols
on Mac OS X to retrieve the terminal/console width from terminfo
since my $COLUMNS
var is not populated in env
but you may substitute the replaceable "*
" value in %*s
, by providing "${COLUMNS}
", or any other value you prefer, instead.
The next example uses $RANDOM
to generate different length content includes colours and shows how you might extract functions to refactor the implementation to reusable functions.
function prompt_right() {
echo -e "\033[0;36m$(echo ${RANDOM})\033[0m"
}
function prompt_left() {
echo -e "\033[0;35m${RANDOM}\033[0m"
}
function prompt() {
compensate=11
PS1=$(printf "%*s\r%s\n\$ " "$(($(tput cols)+${compensate}))" "$(prompt_right)" "$(prompt_left)")
}
PROMPT_COMMAND=prompt
Since printf
assumes the length of string to be the # of characters we need to compensate for the amount of characters required to render the colours, you will find it always short of the end of screen because of the non printed ANSI characters without compensation. The characters required for colour remains constant and you will find that also printf takes into account the change in length, as returned by $RANDOM
for example', which keeps our right alignment in tact.
This is not the case with special bash prompt escape sequences (ie. \u
, \w
, \h
, \t
) though, as these will only record a length of 2 because bash will only translate them when the prompt is displayed, after printf has rendered the string. This does not affect the left hand side but best to avoid them on the right.
Of no consequence if the generated content will remain at constant length though. Like with the time \t
option which will always render the same amount of characters (8) for 24 time. We only need to factor in the compensation required to accommodate for the difference between 2 characters counted which results to 8 characters when printed, in these cases.
Keep in mind that you may need to triple escape \\\
some escape sequences which otherwise hold meaning to strings. As with the following example the current working directory escape \w
holds no meaning otherwise so it works as expected but the time \t
, which means a tab character, does not work as expected without triple escaping it first.
function prompt_right() {
echo -e "\033[0;36m\\\t\033[0m"
}
function prompt_left() {
echo -e "\033[0;35m\w\033[0m"
}
function prompt() {
compensate=5
PS1=$(printf "%*s\r%s\n\$ " "$(($(tput cols)+${compensate}))" "$(prompt_right)" "$(prompt_left)")
}
PROMPT_COMMAND=prompt
nJoy!
Using printf
with $COLUMNS
worked really well, something like:
printf "%${COLUMNS}s\n" "hello"
It right justified it perfectly for me.
The following will put the current date and time in RED on the RHS of the terminal.
# Create a string like: "[ Apr 25 16:06 ]" with time in RED.
printf -v PS1RHS "\e[0m[ \e[0;1;31m%(%b %d %H:%M)T \e[0m]" -1 # -1 is current time
# Strip ANSI commands before counting length
# From: https://www.commandlinefu.com/commands/view/12043/remove-color-special-escape-ansi-codes-from-text-with-sed
PS1RHS_stripped=$(sed "s,\x1B\[[0-9;]*[a-zA-Z],,g" <<<"$PS1RHS")
# Reference: https://en.wikipedia.org/wiki/ANSI_escape_code
local Save='\e[s' # Save cursor position
local Rest='\e[u' # Restore cursor to save point
# Save cursor position, jump to right hand edge, then go left N columns where
# N is the length of the printable RHS string. Print the RHS string, then
# return to the saved position and print the LHS prompt.
# Note: "\[" and "\]" are used so that bash can calculate the number of
# printed characters so that the prompt doesn't do strange things when
# editing the entered text.
PS1="\[${Save}\e[${COLUMNS:-$(tput cols)}C\e[${#PS1RHS_stripped}D${PS1RHS}${Rest}\]${PS1}"
Advantages:
- Works correctly with colours and ANSI CSI codes in the RHS prompt
- No subprocesses.
shellcheck
clean. - Works correctly if
.inputrc
hasset show-mode-in-prompt on
. - Correctly encapsulates the non-prompt-length-giving characters in
\[
and\]
so that editing text entered at the prompt doesn't cause the prompt to reprint strangely.
Note: You'll need to ensure that any colour sequences in the $PS1
before this code is exeucted are properly enclosed in \[
and \]
and that there is no nesting of them.
-
while i do like this approach in theory, in practice it doesn't work out of the box (ubuntu 18.04, GNU bash 4.4.19): appending the code directly into .bashrc first gives the error
bash: local: can only be used in a function
, which is trivial to fix, and after that, it doesn't show anything becauseCOLUMNS
is not defined: it has to be substituted with$(tput cols)
. same outcome if the snippet is saved on a different file, and then sourced into.bashrc
. Sep 3, 2018 at 17:56 -
1Thanks @Polentino. I've updated the code to run
tput cols
if$COLUMNS
is unset. And yes, this code should be inside a function. I usePROMPT_COMMAND='_prompt_bash_set'
and name the function_prompt_bash_set
.– Tom HaleSep 4, 2018 at 10:50
I just thought I would throw mine in here. It's almost exactly the same as the GRML zsh prompt (except zsh updates it's prompt a little better on new lines and back spaces - which is impossible to replicate in bash ... well very difficult at this point in time, at least).
I spent a good three days on this (only tested on a laptop running arch), so here's a screenshot and then the stuff that goes in my ~/.bashrc :)
warning - it's a little crazy
important aside - every ^[
(such as ^[[34m
) is really the escape character (char)27
. The only way I know how to insert this is to enter ctrl+([v) (i.e. hit both [ and v while ctrl is held down.
# grml battery?
GRML_DISPLAY_BATTERY=1
# battery dir
if [ -d /sys/class/power_supply/BAT0 ]; then
_PS1_bat_dir='BAT0';
else
_PS1_bat_dir='BAT1';
fi
# ps1 return and battery
_PS1_ret(){
# should be at beg of line (otherwise more complex stuff needed)
RET=$?;
# battery
if [[ "$GRML_DISPLAY_BATTERY" == "1" ]]; then
if [ -d /sys/class/power_supply/$_PS1_bat_dir ]; then
# linux
STATUS="$( cat /sys/class/power_supply/$_PS1_bat_dir/status )";
if [ "$STATUS" = "Discharging" ]; then
bat=$( printf ' v%d%%' "$( cat /sys/class/power_supply/$_PS1_bat_dir/capacity )" );
elif [ "$STATUS" = "Charging" ]; then
bat=$( printf ' ^%d%%' "$( cat /sys/class/power_supply/$_PS1_bat_dir/capacity )" );
elif [ "$STATUS" = "Full" ] || [ "$STATUS" = "Unknown" ] && [ "$(cat /sys/class/power_supply/$_PS1_bat_dir/capacity)" -gt "98" ]; then
bat=$( printf ' =%d%%' "$( cat /sys/class/power_supply/$_PS1_bat_dir/capacity )" );
else
bat=$( printf ' ?%d%%' "$( cat /sys/class/power_supply/$_PS1_bat_dir/capacity )" );
fi;
fi
fi
if [[ "$RET" -ne "0" ]]; then
printf '\001%*s%s\r%s\002%s ' "$(tput cols)" ":( $bat " "^[[0;31;1m" "$RET"
else
printf '\001%*s%s\r\002' "$(tput cols)" "$bat "
fi;
}
_HAS_GIT=$( type 'git' &> /dev/null );
# ps1 git branch
_PS1_git(){
if ! $_HAS_GIT; then
return 1;
fi;
if [ ! "$( git rev-parse --is-inside-git-dir 2> /dev/null )" ]; then
return 2;
fi
branch="$( git symbolic-ref --short -q HEAD 2> /dev/null )"
if [ "$branch" ]; then
printf ' \001%s\002(\001%s\002git\001%s\002)\001%s\002-\001%s\002[\001%s\002%s\001%s\002]\001%s\002' "^[[0;35m" "^[[39m" "^[[35m" "^[[39m" "^[[35m" "^[[32m" "${branch}" "^[[35m" "^[[39m"
fi;
}
# grml PS1 string
PS1="\n\[\e[F\e[0m\]\$(_PS1_ret)\[\e[34;1m\]${debian_chroot:+($debian_chroot)}\u\[\e[0m\]@\h \[\e[01m\]\w\$(_PS1_git) \[\e[0m\]% "
I'm still working on making the colors configurable, but I am happy with the colors as they are now.
Currently working on a fix for the crazy ^[
character and easy color switching :)
-
It's not Ctrl + [ and v simultaneously, it's Ctrl + v followed by Ctrl + [.– MayaFeb 25, 2018 at 13:45
You can use printf
to do right alignment:
$ printf "%10s\n" "hello"
hello
$ PS1='$(printf "%10s" "$somevar")\w\$ '
Adding on Giles' answer, I wrote something to handle colors better (provided they're properly enclosed in \[
and \]
. It's case-by-case and doesn't handle every case, but it lets me set my PS1L in the same syntax as PS1 and uses the (uncolored) date as PS1R.
function title {
case "$TERM" in
xterm*|rxvt*)
echo -en "\033]2;$1\007"
;;
*)
;;
esac
}
print_pre_prompt() {
PS1R=$(date)
PS1L_exp="${PS1L//\\u/$USER}"
PS1L_exp="${PS1L_exp//\\h/$HOSTNAME}"
SHORT_PWD=${PWD/$HOME/~}
PS1L_exp="${PS1L_exp//\\w/$SHORT_PWD}"
PS1L_clean="$(sed -r 's:\\\[([^\\]|\\[^]])*\\\]::g' <<<$PS1L_exp)"
PS1L_exp=${PS1L_exp//\\\[/}
PS1L_exp=${PS1L_exp//\\\]/}
PS1L_exp=$(eval echo '"'$PS1L_exp'"')
PS1L_clean=$(eval echo -e $PS1L_clean)
title $PS1L_clean
printf "%b%$(($COLUMNS-${#PS1L_clean}))b\n" "$PS1L_exp" "$PS1R"
}
Here it is on github: dbarnett/dotfiles/right_prompt.sh. I use it in my .bashrc like this:
source $HOME/dotfiles/right_prompt.sh
PS1L='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]'
PS1='\[\033[01;34m\]\w\[\033[00m\]\$ '
PROMPT_COMMAND=print_pre_prompt
Note: I also added a newline after PS1R, which makes no visual difference, but seems to keep the prompt from getting garbled if you scroll back through certain commands in your command history.
I'm sure someone else can improve on this, and maybe generalize some of the special-case-iness.
Here is a solution based on PROMPT_COMMAND
and tput
:
function __prompt_command() {
local EXIT="$?" # This needs to be first
history -a
local COL=$(expr `tput cols` - 8)
PS1="💻 \[$(tput setaf 196)\][\[$(tput setaf 21)\]\W\[$(tput setaf 196)\]]\[$(tput setaf 190)\]"
local DATE=$(date "+%H:%M:%S")
if [ $EXIT != 0 ]; then
PS1+="\[$(tput setaf 196)\]\$" # Add red if exit code non 0
tput sc;tput cuu1; tput cuf $COL;echo "$(tput setaf 196)$DATE"; tput rc
else
PS1+="\[$(tput setaf 118)\]\$"
tput sc;tput cuu1; tput cuf $COL;echo "$(tput setaf 118)$DATE"; tput rc
fi
PS1+="\[$(tput setaf 255)\] "
}
PROMPT_COMMAND="__prompt_command"
The magic is performed by:
tput sc;tput cuu1; tput cuf $COL;echo "$(tput setaf 196)$DATE"; tput rc
Which is broken down by:
tput sc # saved the cursor position
tput cuu1 # up one line
tput cuf $COL # move $COL characters left
echo "$(tput setaf 196)$DATE" # set the colour and print the date
tput rc # restore the cursor position
In PS1, tput
is escaped with \[ \] so that it is not counted in displayed length.
Instead of using \T
,\u
,\H
or any other PS1
escape sequences,
I use built-in commands like date +%T
,whoami
,cat /proc/sys/kernel/hostname
to avoid inaccurate sequence length calculations
$RPROMPT
in zsh or$rprompt
in tcsh.