How can I clean a string in bashrc? - bash

I'd like to know how to "clean up" a line of code in bash. I've created a string to colour the command line in my ~/.bashrc which looks like this:
PS1='\[\033[1;36m\]\u\[\033[1;31m\]#\[\033[1;32m\]\h:\[\033[1;35m\]\w\[\033[1;31m\]\$\[\033[0m\] '
I'd like to make it easier to discern. More something along these lines:
PS1='\[\033[1;36m\]
\u\[\033[1;31m\]
#\[\033[1;32m\]
\h:\[\033[1;35m\]
\w\[\033[1;31m\]
\$\[\033[0m\] '
I've tried putting these {} brackets but that didn't seem to work, either.
EDIT: thanks to tripleee. The code that works is as follows:
tprompt () {
local bold=$(tput bold)
local red=$(tput setaf 1)
local green=$(tput setaf 2)
local magenta=$(tput setaf 5)
local cyan=$(tput setaf 6)
local plain=$(tput sgr0)
printf -v PS1 "%s" "$bold" "$cyan" '\u' \
"$red" "#" \
"$green" '\h:' \
"$magenta" '\w' \
"$red" '\$' \
"$plain"
}
tprompt

A better overall approach might be to use tput to generate these codes, rather than hard-code them for a specific terminal. This also allows you to assign mnemonic names to them.
tprompt () {
local bold=$(tput bold)
local red=$(tput setaf 1)
local green=$(tput setaf 2)
local magenta=$(tput setaf 5)
local cyan=$(tput setaf 6)
local plain=$(tput sgr0)
printf -v PS1 "%s" "$bold" "$cyan" '\u' \
"$red" "#" \
"$green" '\h:' \
"$magenta" '\w' \
"$red" '\$' \
"$plain" ' '
}
tprompt
(Not sure I managed to correctly map back the escape codes to their meanings, but you should at least be able to see how this is more readable and maintainable than what you had. The sole reason to define a function for this is to create a local scope so the helper variables don't pollute the global namespace. We only call the function once; you could even undefine the function when you have called it, too.)
As an aside, notice also how printf lets you easily paste together multiple strings. Even if you don't adopt this approach wholesale, you might want to refactor to
printf -v PS1 '%s' \
'\[\033[1;36m\]' \
'\u\[\033[1;31m\]' \
'#\[\033[1;32m\]' \
'\h:\[\033[1;35m\]' \
'\w\[\033[1;31m\]' \
'\$\[\033[0m\] '
As a final aside, non-printing sequences in the prompt should be wrapped in \[ ... \] to tell Bash to disregard them when calculating the line length. Maybe wrap tput with a function, too:
tput () {
printf '\\['
command tput "$#"
printf '\\]'
}

Related

tput is very slow on MinTTY (Git Bash)

Git Bash is pretty sluggish overall (compare 1.082s of average runtime under WSL/Ubuntu vs 4.460s in MinTTY). I've narrowed down a whopping 1.479s to the following chunk of code:
# Determine if this terminal supports colors
if test -t 1; then
if [[ -n "$(tput colors)" ]] && [[ "$(tput colors)" -ge 8 ]]; then
MY_APP_FMT_SUPPORTED=true
MY_APP_FMT_BOLD="$(tput bold)"
MY_APP_FMT_UNDERLINE="$(tput smul)"
MY_APP_FMT_INVERSE="$(tput smso)"
MY_APP_FMT_BLACK="$(tput setaf 0)"
MY_APP_FMT_RED="$(tput setaf 1)"
MY_APP_FMT_GREEN="$(tput setaf 2)"
MY_APP_FMT_YELLOW="$(tput setaf 3)"
MY_APP_FMT_BLUE="$(tput setaf 4)"
MY_APP_FMT_MAGENTA="$(tput setaf 5)"
MY_APP_FMT_CYAN="$(tput setaf 6)"
MY_APP_FMT_WHITE="$(tput setaf 7)"
MY_APP_FMT_CODE=$MY_APP_FMT_CYAN
# placing it down below so that option -x doesn't cause bad highlighting
# to persist
MY_APP_FMT_CLEAR="$(tput sgr0)"
fi
fi
Given my understanding of the performance of *nix tools on Windows, I suspect the slowdown is from all the subshells.
Should these subshells explain the entire slowdown? If not, I'll need to continue researching why Git Bash is still sluggish.
Is there a more performant way to do this while maintaining terminal compatibility?
You can group tput calls by using -S option:
#!/usr/bin/env bash
tkeys=(bold smul "setaf 0" "setaf 1") # You can add the rest
tvalues_s=$(tput -S < <(printf "%s\n" "${tkeys[#]}"))
declare -a tvalues=( ${tvalues_s//$'\e'/ $'\e'} )
declare -p tvalues
Now that you have values in tvalues, which you can assign to MY_APP_FMT_...

Bash and Zsh prompt that ring a bell and do display error code of last command

I want to have the same prompt in both Bash and Zsh. And I want it to:
bell a ring, if the last command failed, and
display its error code.
In Bash, I do have:
BLK="\[$(tput setaf 0; tput bold)\]"
RED="\[$(tput setaf 1; tput bold)\]"
grn="\[$(tput setaf 2)\]"
GRN="\[$(tput setaf 2; tput bold)\]"
yel="\[$(tput setaf 3)\]"
reset_color="\[$(tput sgr0)\]"
PS1='\n\
`if [[ $? -gt 0 ]]; then printf "\[\033[01;31m\]$?"; tput bel; else printf "\[\033[01;32m\]0"; fi`\
\[\033]0;$PWD\007\] \
\[\033[0;32m\]\u#\h\
\[\033[01;30m\]:\
\[\033[;;33m\]\w\
\[\033[36m\]`__git_ps1`\
\[\033[0m\]\n$ '
In Zsh, that's my config:
BLK=$(tput setaf 0; tput bold)
RED=$(tput setaf 1; tput bold)
grn=$(tput setaf 2)
GRN=$(tput setaf 2; tput bold)
yel=$(tput setaf 3)
reset_color=$(tput sgr0)
PROMPT="
%(?.$GRN.$RED)%?$reset_color $grn%n#%m$BLK:$reset_color$yel%~ $reset_color
%(!.#.$) "
And this is how it looks like in the terminal:
Both prompts do ring the bell when there is an error with the last command.
But, in Bash, it prints 0 instead of the right return code of the command that
failed.
How to fix that?
PS- Any better way to improve the above code is welcomed!
The command to test $? itself resets $? to the result of the test. You need to save the value you want to display first.
PS1='\n\
$(st=$?; if [[ $st -gt 0 ]]; then printf "\[\033[01;31m\]$st"; tput bel; else printf "\[\033[01;32m\]0"; fi)\
\[\033]0;$PWD\007\] \
\[\033[0;32m\]\u#\h\
\[\033[01;30m\]:\
\[\033[;;33m\]\w\
\[\033[36m\]`__git_ps1`\
\[\033[0m\]\n$ '
I would recommend building up the value of PS1 using PROMPT_COMMAND, instead of embedding executable code. This gives you more flexibility
for commenting and separating any computations you need from the actual
formatting. make_prompt doesn't need quite so many lines, but it's
just a demonstration.
set_title () {
printf '\033]0;%s%s' "$1" "$(tput bel)"
}
make_prompt () {
local st=$?
local c bell
bell=$(tput bel)
# Green for success, red and a bell for failure
if [[ $st -gt 0 ]]; then
c=31
else
c=32 bell=
fi
win_title=$(set_title "$PWD")
git_status=$(__git_ps1)
PS1="\n"
PS1+="\[\e[01;${c}m$bell\]" # exit status color and bell
PS1+=$st
PS1+="\[$win_title\]" # Set the title of the window
PS1+="\[\e[0;32m\]" # Color for user and host
PS1+="\u#\h"
PS1+="\[\e[01;30m\]" # Color for : separator
PS1+=":"
PS1+="\[\e[;;33m\]" # Color for directory
PS1+="\w"
PS1+="\[\e[36m\]" # Color for git branch
PS1+=$git_status
PS1+="\[\e[0m\]" # Reset to terminal defaults
PS1+="\n$ "
}
PROMPT_COMMAND=make_prompt
zsh already has terminal-agnostic escape sequences for adding color.
PROMPT="%B%(?.%F{green}.%F{red}$(tput bel))%?%f%b %F{green}%n#%m%F{black}%B:%b%F{yellow}%~ %f%(!.#.$) "
Any external command (such as printf or tput) resets the value of $?. You need to capture it in a variable before that happens.
rc=$?
if [ $rc -gt 0 ]; then
printf "\[\033[01;31m\]$rc"
tput bel
else
printf "\[\033[01;32m\]0"
fi
(Unwrapped for legibility; this will replace the code inside the backticks.)
Notice that this will still overwrite the value of $?; perhaps add exit $? at the end to properly preserve the value.

How to print a string in color, when a Variable has the actual color name?

I am trying to print a string with Specific color depending on the error code. Color varies on each error code, I am storing the actual color name in variable and using it in printf.
BLACK=$(tput setaf 0)
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
BLUE=$(tput setaf 4)
CYAN=$(tput setaf 5)
BOLD=$(tput bold)
NORMAL=$(tput sgr0)
# this color varies depending on error code
color=GREEN
printf "\${$color} This is a String ${NORMAL} \n"
But I get output as
${GREEN} This is a String
Expected output(In actual Green color)
This is a String
I can get this using
printf "${GREEN} This is a String ${NORMAL} \n"
But I want this output using color variable
Bash doesn't treat ${<variable>} recursively inside strings, as you tried to do.
You can do color=$<colorVariable>, ie color=$GREEN before the printf, and then in the printf string doing "$color This is a String ${NORMAL} \n"
So, final result:
BLACK=$(tput setaf 0)
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
BLUE=$(tput setaf 4)
CYAN=$(tput setaf 5)
BOLD=$(tput bold)
NORMAL=$(tput sgr0)
# Event-dependant color
color=$GREEN
# And finally,
printf "$color This is a String ${NORMAL} \n"
There's another way you can do it.
# Define all colors, only GREEN and NORMAL here for brevity reasons
GREEN=$(tput setaf 2)
NORMAL=$(tput sgr0)
# Notice the missing $, as the original question
color=GREEN
# And then, use Bash's variable reference method (here notice both ! instead of $ and the missing \ at the beginning
printf "${!color} This is a String ${NORMAL} \n"
How about:
color=$GREEN
And then:
printf "$color This is a String ${NORMAL} \n"
Gives me:
This is a String
in green.

Efficiently reusing color codes in a printf %s substitution in bash

biwhite=$(tput bold)$(tput setaf 7)
color_off=$(tput sgr0)
printf "%s$USER%s at %s$HOME%s has path %s$PATH%s" "$biwhite" "$color_off" "$biwhite" "$color_off" "$biwhite" "$color_off"
Is there a printf shortcut to avoid having to define every %s when I want to add color to only certain parts of a statement?
Entering "$biwhite" "$color_off" 3 times seems redundant.
It's good practice to avoid putting parameter expansions in the format string of printf, in case they contain percent signs as well. That said, parameter substitution offers a way around some of the repetitiveness.
w="${biwhite}X${color_off}"
printf "%s at %s has path %s" "${w/X/$USER}" "${w/X/$HOME}" "${w/X/$PATH}"
It's not foolproof, but it's fairly unlikely that X will appear in the output of tput. You can pick a longer string instead, at the cost of more typing.
I'm afraid, though, that adding color codes to a string is inherently painful.
I think you could write a bash function:
biwhite=$(tput bold)$(tput setaf 7)
color_off=$(tput sgr0)
whiten() {
echo "$biwhite$1$color_off"
}
echo "$(whiten "$USER") at $(whiten "$HOME") has path $(whiten "$PATH")"
Also, why use printf if you're not using any of it's formatting capabilities?
Provided below is a function which colorizes every other argument, starting with the second. (Thus, a non-colorized prefix can be provided by passing a non-empty string in this first position, or a colorized one by leaving it empty, as here).
Notably, no subshells are involved in this code's execution except in the tput invocations used for demonstrative purposes. It is thus, while verbose, very low-overhead to execute.
biwhite=$(tput bold)$(tput setaf 7)
color_off=$(tput sgr0)
# colorize every other argument, starting from the 2nd.
colorize_words() {
local start_color end_color
: "${start_color:=$biwhite}" "${end_color:=$color_off}"
while (( $# )); do
printf '%s' "$1"
shift || break
printf '%s%s%s' "$start_color" "$1" "$end_color"
shift || break
done
printf '\n'
}
colorize_words "" "$USER" " at " "$HOME" " has path " "$PATH"
This can be customized by passing start_color and end_color values to an individual invocation; for example:
# this prints every other argument in red
start_color=$(tput setaf 1) colorize_words "hello " "cruel " world

How to right align and left align text strings in Bash

I'm creating a bash script and would like to display a message with a right aligned status (OK, Warning, Error, etc) on the same line.
Without the colors, the alignment is perfect, but adding in the colors makes the right aligned column wrap to the next line, incorrectly.
#!/bin/bash
log_msg() {
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NORMAL=$(tput sgr0)
MSG="$1"
let COL=$(tput cols)-${#MSG}
echo -n $MSG
printf "%${COL}s" "$GREEN[OK]$NORMAL"
}
log_msg "Hello World"
exit;
I'm not sure why it'd wrap to the next line -- having nonprinting sequences (the color changes) should make the line shorter, not longer. Widening the line to compensate works for me (and BTW I recommend using printf instead of echo -n for the actual message):
log_msg() {
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NORMAL=$(tput sgr0)
MSG="$1"
let COL=$(tput cols)-${#MSG}+${#GREEN}+${#NORMAL}
printf "%s%${COL}s" "$MSG" "$GREEN[OK]$NORMAL"
}
You have to account for the extra space provided by the colors.
log_msg() {
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
NORMAL=$(tput sgr0)
MSG="$1"
STATUS="[OK]"
STATUSCOLOR="$GREEN${STATUS}$NORMAL"
let COL=$(tput cols)-${#MSG}+${#STATUSCOLOR}-${#STATUS}
echo -n $MSG
printf "%${COL}s\n" "$STATUSCOLOR"
}

Resources