Improvements on Janos's answer
- using
tput
to yield the escape codes for cross-platform compatibility
- wrapping the escape codes in
\[
and \]
for proper shell calculation of length of immutable text
- unquoting
EOF
and escaping the parts I wanted lazily evaluated to eagerly evaluate expensive operations only needed once
- those things adding up to a faster PS1.
Thanks to Janos for getting me started, though, please give him an upvote.
The long story
If you're going to plan on throwing an implementation away, may as well code it up in Python - the perfect prototyping language. But I was concerned about the performance, and launching a separate Python process, perhaps multiple times.
Janos's answer does most of what I want, but in retrospect, it seems flawed - I'm concerned about proper escaping and closing of the escapes. In particular, I have a problem where the final line of the PS1, where I print the exit code when there's an error, but it the terminal won't let me backspace over any text about the length of the escape code + the exit signal on some systems.
While I was looking for the right solution, I discovered a program called tput
which should provide a platform independent way of triggering the escape code instead of hard-coding them. But it still does not enclose the escape in a best-practice sort of way.
The tput
man page is quite bare and actually makes no mention of this usage. man terminfo
should tell you what numbers correspond to what colors though - and note that the background mappings are not the same as the foreground.
Also, I am concerned about running tput
multiple times. I wanted to close over the results (like a closure) when the string was parsed, not execute them every time.
I found that, in order to eval only part of the heredoc, without executing the other parts, I would need to protect the parts I want lazily evaluated by escaping all relevant '
, \
, and $
symbols. Removing the "
from around the delimiter word, in this case, EOF
, allows the expansion to take place.
Also important are the \[
and \]
around the escape sequences - this tells the shell to not count the escape sequences when determining what you can backspace over or not.
This left me with a very snappy PS1 that seems to pop up before I can take my finger off the Enter key:
PS1=$(cat << EOF
\$(
LAST_RET=\$?
WHOAMI=\u
HOSTNAME=\h
RESET='\[$(tput sgr0 )\]'
BOLD='\[$(tput bold )\]'
RED='\[$(tput setaf 1 )\]'
GREEN='\[$(tput setaf 2 )\]'
YELLOW='\[$(tput setaf 3 )\]'
BLUE='\[$(tput setaf 4 )\]'
MAGENTABG='$(tput setab 5 )'
CYAN='\[$(tput setaf 6 )\]'
DATE=\$(date +"%Y-%m-%d %H:%M:%S")
echo "\$YELLOW\$DATE \$GREEN\$WHOAMI\$RED@\$CYAN\$HOSTNAME \$BLUE\$BOLD\$PWD\$RESET"
test \$LAST_RET = 0 || printf "\$MAGENTABG\$LAST_RET\$RESET"
printf "\\\$ "
)
EOF
)
Again, note, I escaped everything that I wanted lazily evaluated (especially, and perhaps easiest to miss, the \\\$
- which is basically an escaped backslash followed by an escaped dollar sign), and left everything open for shell expansion that I wanted to be eagerly evaluated on parsing.
(No big deal, but I also got one fewer RESET
expansion by moving them to the end of the sequences.)
Things I wanted eagerly evaluated are:
- name (WHOAMI)
- HOSTNAME
- escape sequences
- The actual heredoc
Things I wanted lazily evaluated are:
- the containing subshell inside the heredoc
- LAST_RET
- DATE
- all printf and echos
Ideas for further improvement that need more exploration
It's conceivable that I could avoid lots of escaping by using multiple heredocs. That would have the effect of making the code a little more readable and maintainable.
Also - the Python looked much nicer. Perhaps I could compile a kind of cython executable that would run much faster.
Another point - I worry that the color for the background isn't quite right.
I had to install ncurses on my android termux app to get tput
. It was a simple exercise, but makes me worry about cross-compatibility again.
Conclusion
Assuming one can maintain this code, this may be arguably the best way to go for performance and long-run cross-compatibility. Please enjoy.
Update
Since switching to NixOS I have wanted my nix-shell to use my standard PS1 instead of simply telling me I'm in the nix-shell, so I have added that to my PS1:
programs.bash.promptInit = ''
set_PS1()
{
local RESET=$(tput sgr0 )
local BOLD=$(tput bold )
local RED=$(tput setaf 1 )
local GREEN=$(tput setaf 2 )
local YELLOW=$(tput setaf 3 )
local BLUE=$(tput setaf 4 )
local MAGENTA=$(tput setaf 5 )
local MAGENTABG=$(tput setab 5 )
local CYAN=$(tput setaf 6 )
local WHOAMI='\u'
local WHERE='\w'
local HOSTNAME='\h'
local DATE='\D{%Y-%m-%d %H:%M:%S}'
local EXECUTE=$
local LAST_RET={?#0}
local NIX=$( if [[ -n "$IN_NIX_SHELL" ]];
then echo "nix-shell";
else echo ""; fi )
local LINE_1a="$YELLOW$DATE $GREEN$WHOAMI$MAGENTA@$CYAN$HOSTNAME"
local LINE_1b="$BLUE$BOLD$WHERE$RESET"
local LINE_2="\\[$MAGENTA\\]$EXECUTE$LAST_RET\\[$RESET\\]$NIX"'\$ '
PS1="$LINE_1a $LINE_1b\n$LINE_2"
}
set_PS1
'';
```