Simplifying advanced Bash prompt variable (PS1) code - bash

So I've found the following cool Bash prompt:
..with the very basic logic of:
PS1="\[\033[01;37m\]\$? \$(if [[ \$? == 0 ]]; then echo \"\[\033[01;32m\]\342\234\223\"; else echo \"\[\033[01;31m\]\342\234\227\"; fi) $(if [[ ${EUID} == 0 ]]; then echo '\[\033[01;31m\]\h'; else echo '\[\033[01;32m\]\u#\h'; fi)\[\033[01;34m\] \w \$\[\033[00m\] "
However, this is not very basic and happens to be an incredible mess. I'd like to make it more readable.
How?

Use PROMPT_COMMAND to build the value up in a sane fashion. This saves a lot of quoting and makes the text much more readable. Note that you can use \e instead of \033 to represent the escape character inside a prompt.
set_prompt () {
local last_command=$? # Must come first!
PS1=""
# Add a bright white exit status for the last command
PS1+='\[\e[01;37m\]$? '
# If it was successful, print a green check mark. Otherwise, print
# a red X.
if [[ $last_command == 0 ]]; then
PS1+='\[\e[01;32m\]\342\234\223 '
else
PS1+='\[\e[01;31m\]\342\234\227 '
fi
# If root, just print the host in red. Otherwise, print the current user
# and host in green.
# in
if [[ $EUID == 0 ]]; then
PS1+='\[\e[01;31m\]\h '
else
PS1+='\[\e[01;32m\]\u#\h '
fi
# Print the working directory and prompt marker in blue, and reset
# the text color to the default.
PS1+='\[\e[01;34m\] \w \$\[\e[00m\] '
}
PROMPT_COMMAND='set_prompt'
You can define variables for the more esoteric escape sequences, at the cost of needing some extra escapes inside the double quotes, to accommodate parameter expansion.
set_prompt () {
local last_command=$? # Must come first!
PS1=""
local blue='\[\e[01;34m\]'
local white='\[\e[01;37m\]'
local red='\[\e[01;31m\]'
local green='\[\e[01;32m\]'
local reset='\[\e[00m\]'
local fancyX='\342\234\227'
local checkmark='\342\234\223'
PS1+="$white\$? "
if [[ $last_command == 0 ]]; then
PS1+="$green$checkmark "
else
PS1+="$red$fancyX "
fi
if [[ $EUID == 0 ]]; then
PS1+="$red\\h "
else
PS1+="$green\\u#\\h "
fi
PS1+="$blue\\w \\\$$reset "
}

Related

bash if "$1" == "0" is always false when running function for bash prompt

I have been struggling with this for a long time.
Trying to change colour as part of my prompt depending on the exit code of the last command.
I have reduced my prompt to a minimal example:
Red="\[\033[31m\]"
Green="\[\033[32m\]"
Reset="\[\033[0m\]"
statColour(){
if [[ "$1" == "0" ]]; then
echo -e "${Green} $1 "
else
echo -e "${Red} $1 "
fi
}
export PS1="$(statColour \$?)What Colour? $Reset"
And results in red always being used despite the fact the number is clearly 0 in the first instance.
I have tried [ and $1 -eq 0 with no success. Why isn't this working?
Try this:
Red="\033[35m"
Green="\033[32m"
Reset="\033[0m"
statColour(){
if [[ $1 = 0 ]]; then
echo -e "${Green} $1 "
else
echo -e "${Red} $1 "
fi
}
export PS1="\$(statColour \$?)What Colour? $Reset"
# ^
Color definitions changed
Call of statColour is now done every time, and not only once.
if [[ ]] optimized
For an explanation why you always take the false branch:
You are calling statColour with \$? as argument. The backslash ensures, that the $ is taken literally (and not as the start of a parameter expanson), so you have in effect the literal string $?. Since ? is a wildcard character, it is undergoing filename generation, i.e. the parameter is replaced by all files where the name is a $, followed by a single character. If there are no such files in your directory (which is probably the case), the string $? is passed literally to statColour.
Inside statColour, you wrote
[[ "$1" == "0" ]]
which means that you ask, whether the string $? is equal to the string 0. This is never the case, hence the comparision is always false.
For your problem, you could try this approach (not tested, so you may have to debug it a bit):
statColour() {
# Fetch the exit code of the last program
local last_exit_code=$?
if ((last_exit_code == 0)) # Numeric comparision
then
.....
else
...
fi
# Preserve the exit code
return $last_exit_code
}
and set the prompt as
PS1='$(statColour) '"$Reset"
The single quotes ensure that statColour is evaluated dynamically, while $Reset is in double quotes since it is OK to evaluate it statically.

Simulating command history and editing inside a looping bash script

I'd like to have a bash script that implements some of the functionality of the bash command line itself: namely, command history and vi-style command editing.
The script would loop forever (until crtl/d) and read input from the user in terminal, treating each line as a command. The commands are actually a set of shell scripts that I have already written which are designed to support a photo work flow. The same edit and recall functionality should be available in this interpreted environment.
Having bash command history and command editing functions in this script would be very desirable.
Was looking for a way to mimic command history within script as well, couldn't find much on it online, so built a simple one myself. Not exacly what you asked for, but might give you, or anyone else some references.
It really just is one big function that does nothing else than handle the prompt like behaviour and return the on screen string from pressing enter. It allows for browsing of appointed history file, while saving new input, to go back to. Auto indent or moving the marker is not implemented below. I think the script require bash version 4, with the arithmetic shells, but change to an older syntax and bash 3 should work. It's not fully tested yet.
Use it as:
./scriptname.sh /optional/path/to/history_file
The script
#!/bin/bash
# vim: ts=4:
function getInput() {
local hist_file="${1:-.script_hist}";
local result="";
local escape_char=$(printf "\u1b")
local tab=$(echo -e "\t");
local backspace=$(cat << eof
0000000 005177
0000002
eof
);
local curr_hist=0;
local curr_cmd="";
function browseHistory() {
! test -s "$hist_file" && return 1;
local max_hist="$(cat "$hist_file" | wc -l || echo 0)";
curr_hist=$((curr_hist + "$1"));
(( curr_hist > max_hist )) && curr_hist=$max_hist;
if (( curr_hist <= 0 )); then
curr_hist=0;
return 1;
fi
result="$(sed -n "$((max_hist - curr_hist + 1))p" < "$hist_file")";
return 0;
}
ifs=$IFS;
while true; do
# empty IFS, read one char
IFS= read -rsn1 input
if [[ $input == $escape_char ]]; then
# read two more chars, this is for non alphanumeric input
read -rsn2 input
fi
# check special case for backspace or tab first
# then move onto arrow keys or anything else in case
if [[ $(echo "$input" | od) = "$backspace" ]]; then
# delete last character of current on screen string
result=${result%?};
elif [ "$input" = "$tab" ]; then
# replace with function call for autofill or something
# it's unused but added in case it would be useful later on
continue;
else
case $input in
'[A')
! browseHistory '1' && result=$curr_cmd;
;;
'[B')
! browseHistory '-1' && result=$curr_cmd;
;;
'[D') continue ;; # left, does nothing right now
'[C') continue ;; # right, this is still left to do
*)
# matches enter and returns on screen string
[[ "$input" == "" ]] && break;
result+=$input
;;
esac
fi
# store current command, for going back after browsing history
(( curr_hist == 0 )) && curr_cmd="$result";
echo -en "\r\033[K";
echo -en "${result}"
done
IFS=$ifs;
test -n "$result" && echo "$result" >> "$hist_file";
return 0;
}
getInput $1

Trying to escape the backslash in this bash script

The following bash script takes any input of numbers between 0-100 and prints out an average of the numbers to the screen. The script also has input validation that won't allow anything but a number between 0-100 and a q or Q. Once you enter a q or Q it computes the results and outputs to the screen. Input validation also checks to make sure there are no null values entered, special characters entered and that there are no number/letter combinations, special character/number combinations etc. entered.
The only problem I have is with the backslash character. The backslash is escaped in this script and when I run the script and enter a backslash it pauses and requires you to press return for the script to continue. Seems like the script still works but I'm curious about why it pauses. Most of the recommendations I've seen on this on this site have been to escape the backslash with more backslashes but that doesn't work.
#! /bin/bash
AVERAGE="0"
SUM="0"
NUM="0"
clear
while true; do
echo -n "Enter your score [0-100%] ('q' for quit): "; read SCORE;
if [[ "$SCORE" == *[a-pA-pr-zR-Z]* ]] ||
[[ "$SCORE" == *['!'\\##\$%^\&*()_+~\`\-=\[\]\{\}\|:\;\'\"\<\>,.?/\\]* ]] ||
[[ -z "$SCORE" ]] ||
(( "$SCORE" < "0" )) || (( "$SCORE" > "100" ))
then
echo "Be serious. Come on, try again: "
elif [[ "$SCORE" == [qQ] ]]; then
echo "Average rating: $AVERAGE%."
break
else
SUM=$[$SUM + $SCORE]
NUM=$[$NUM + 1]
AVERAGE=$[$SUM / $NUM]
fi
done
echo "Exiting."
Use read -r to disable backslash escaping, which is enabled by default.
Options:
-r do not allow backslashes to escape any characters

bash script if elif statement

keep getting this error when I run the script, not sure what went wrong, this is if elif with or condition statement
line xx: ((: WEST - Very Big=EAST - BIG: syntax error in expression (error token is "WEST - Very Big")
echo "$yn"
if (($yn=EAST - BIG)) || (($yn=EAST - SMALL))
then
echo "---------------------------------------------------------------------------" >> /tmp/"$HOSTNAME".log
elif (($yn=WEST - Very Big)) || (($yn=WEST - Very Small))
then
echo "---------------------------------------------------------------------------" >> /tmp/"$HOSTNAME".log
else
echo "---------------------------------------------------------------------------" >> /tmp/"$HOSTNAME".log
fi
Several issues. The check for equality inside (( )) is == (a single = is an assignment). This is common to many languages.
You are not allowed whitespace inside a variable name (assuming those are variable names). The characters allowed in a variable name are ASCII alphanumerics or an underscore, and the first character cannot be a number.
It is also a bad idea to use all UPPERCASE for your own variable names. The shell sets and uses a large number of UPPERCASE variables itself, and you could stomp on each other's values.
Here is my test version of your code:
yn=42
EAST=52
BIG=100
WEST=45
Very_Big=3
Very_Small=1
HOSTNAME='fred'
# Here I used a variable to avoid repeating myself
# that makes it easier to change the filename later
outfile="/tmp/$HOSTNAME.log"
> "$outfile" # Zero the file
echo "$yn"
if (($yn == EAST - BIG )) || (($yn == EAST - SMALL ))
then
echo "---------------------------------------------------------------------------" >> "$outfile"
elif (($yn == WEST - Very_Big )) || (($yn == WEST - Very_Small))
then
echo "---------------------------------------------------------------------------" >> "$outfile"
else
echo "---------------------------------------------------------------------------" >> "$outfile"
fi
Code is much easier to read when you use consistent indentation. To trace how a bash program is running, use -x, for example:
bash -x myscript

Bash Shell Scripting - detect the Enter key

I need to compare my input with Enter/Return key...
read -n1 key
if [ $key == "\n" ]
echo "###"
fi
But this is not working.. What is wrong with this code
Several issues with the posted code. Inline comments detail what to fix:
#!/bin/bash
# ^^ Bash, not sh, must be used for read options
read -s -n 1 key # -s: do not echo input character. -n 1: read only 1 character (separate with space)
# double brackets to test, single equals sign, empty string for just 'enter' in this case...
# if [[ ... ]] is followed by semicolon and 'then' keyword
if [[ $key = "" ]]; then
echo 'You pressed enter!'
else
echo "You pressed '$key'"
fi
Also it is good idea to define empty $IFS (internal field separator) before making comparisons, because otherwise you can end up with " " and "\n" being equal.
So the code should look like this:
# for distinguishing " ", "\t" from "\n"
IFS=
read -n 1 key
if [ "$key" = "" ]; then
echo "This was really Enter, not space, tab or something else"
fi
I'm adding below code just for reference if someone will want to use such solution containing countdown loop.
IFS=''
echo -e "Press [ENTER] to start Configuration..."
for (( i=10; i>0; i--)); do
printf "\rStarting in $i seconds..."
read -s -N 1 -t 1 key
if [ "$key" = $'\e' ]; then
echo -e "\n [ESC] Pressed"
break
elif [ "$key" == $'\x0a' ] ;then
echo -e "\n [Enter] Pressed"
break
fi
done
read reads a line from standard input, up to but not including the new line at the end of the line. -n specifies the maximum number of characters, forcing read to return early if you reach that number of characters. It will still end earlier however, when the Return key is pressed. In this case, its returning an empty string - everything up to but not including the Return key.
You need to compare against the empty string to tell if the user immediately pressed Return.
read -n1 KEY
if [[ "$KEY" == "" ]]
then
echo "###";
fi
None of these conditions worked for me and so I've came up with this one:
${key} = $'\0A'
Tested on CentOS with Bash 4.2.46.

Resources