Bash prompt multiple command substitution - bash

I am setting up my bash prompt in .bashrc using the following (simplified) function:
set_prompts() {
PS1="\u#\h in \w "
PS1+="\$(get_git_repo_details)"
PS1+="\n"
PS1+="\$(exit_status_prompt)"
}
Now the exit_status_prompt prints a different coloured prompt character, depending on whether the value of $? is 0 or not.
What I noticed though, is that with the code as above, the colour of the prompt character never updates. However, if I append the output of exit_status_prompt to $PS1 before I append the output of get_git_repo_details, or don't append the output of get_git_repo_details at all, then it does update.
Does anyone know what is causing this? Thanks.
Edit:
exit_status_prompt()
{
if [ $? -ne 0 ]
then
highlight 1 "❯ "
else
highlight 2 "❯ "
fi
}
The highlight function then just uses tput to prepend the string in the second parameter with the colour specified in the first parameter.

You need to call exit_status_prompt before doing anything else in set_prompts, or $? is going to be reset. Presumably, exit_status_prompt uses the exit status of the most recently executed command or assignment.
set_prompts() {
esp=$(exit_status_prompt)
PS1="\u#\h in \w "
PS1+="$(get_git_repo_details)"
PS1+="\n"
PS1+="$esp"
}
I've unescaped the command substitutions, because I assume that you are (and should be) running set_prompts as the first command in PROMPT_COMMAND.

Related

How to empty the value of $? variable in .bashrc? [duplicate]

This question already has answers here:
Detect empty command
(5 answers)
Closed 7 years ago.
I've been trying to customize my bash prompt so that it'll look like
┌─[error_code_if_not_zero]─[time_short]─[username]─[current_folder]─[git_branch]
└─▪
And here is my .bashrc:
# command completion
source /home/falcon/.bin/git-prompt.sh
GIT_PS1_SHOWDIRTYSTATE=1
GIT_PS1_SHOWSTASHSTATE=1
GIT_PS1_SHOWUNTRACKEDFILES=1
GIT_PS1_SHOWUPSTREAM="auto"
# function to generate the prompt
function __prompt_command() {
__exit_code="$?"
__error_int="";
if [ $__exit_code -ne 0 ]; then
__error_int="[\[\e[0;31m\]$__exit_code\[\e[0;37m\]]─"
fi
PS1="\[\e[0;37m\]┌─$__error_int[\A]─[\[\e[0;35m\]\u\[\e[0;37m\]]─[\[\e[0;33m\]\w\[\e[0;37m\]]\$(__git_ps1 '─[\[\e[0;31m\]%s\[\e[0;37m\]]')\n\[\e[0;37m\]└─▪ \[\e[0;m\]"
}
export PROMPT_COMMAND=__prompt_command
This configuration works fine, it is showing the error code when it's non-zero. But the trouble comes whem i'm just pressing enter in terminal (calling empty commands) - the returning value remains the same as the returning value of the last non-empty command. For example, this happened when i'm just pressing enter in terminal:
┌─[127]─[02:51]─[falcon]─[~]
└─▪
┌─[127]─[02:51]─[falcon]─[~]
└─▪
┌─[127]─[02:51]─[falcon]─[~]
└─▪
┌─[127]─[02:51]─[falcon]─[~]
└─▪
As you can see, the error code 127 remains even after an empty command.
But i'm expecting something like this:
┌─[127]─[02:51]─[falcon]─[~]
└─▪
┌─[02:51]─[falcon]─[~]
└─▪
┌─[02:51]─[falcon]─[~]
└─▪
┌─[02:51]─[falcon]─[~]
└─▪
So, my question is, how to empty the value of $? inside the function __prompt_command?
Got it. First, credit where it's due--anubhava in the mentioned "Detect Empty Command" question is the author of much of this code.
Still, it works the way you want (as far as I can tell).
# command completion
source /home/falcon/.bin/git-prompt.sh
GIT_PS1_SHOWDIRTYSTATE=1
GIT_PS1_SHOWSTASHSTATE=1
GIT_PS1_SHOWUNTRACKEDFILES=1
GIT_PS1_SHOWUPSTREAM="auto"
# function to generate the prompt
PS1="\[\e[0;37m\]┌─\$([[ -n \$_ret ]] && echo \"[\[\e[0;31m\]\$_ret\[\e[0;37m\]]-\")[\A]─[\[\e[0;32m\]\u\[\e[0;37m\]]─[\[\e[0;33m\]\w\[\e[0;37m\]]\$(__git_ps1 '─[\[\e[0;31m\]%s\[\e[0;37m\]]')\n\[\e[0;37m\]└─▪ \[\e[0;m\]"
trapDbg() {
local c="$BASH_COMMAND"
[[ "$c" != "pc" ]] && export _cmd="$c"
}
pc() {
local r=$?
if [[ $r == 0 ]]; then
r=''
fi
trap "" DEBUG
[[ -n "$_cmd" ]] && _ret="$r" || _ret=""
export _ret
export _cmd=
trap 'trapDbg' DEBUG
}
export PROMPT_COMMAND=pc
trap 'trapDbg' DEBUG
I combined your code and his, and modified the PS1. It now includes logic to only display the square brackets when $_ret is set. Also, anubhava's code always displayed a return code, including 0. I added the conditional bit to unset when return code was 0.
Anyhow, there you have it.
Note: I don't have whatever git-prompt.sh contains, so I tested without that bit. Hopefully that doesn't drastically change anything.
What if you "invoke" /bin/true as part of the function after you've used the $? value, that should always set it to 0

Detect empty command

Consider this PS1
PS1='\n${_:+$? }$ '
Here is the result of a few commands
$ [ 2 = 2 ]
0 $ [ 2 = 3 ]
1 $
1 $
The first line shows no status as expected, and the next two lines show the
correct exit code. However on line 3 only Enter was pressed, so I would like the
status to go away, like line 1. How can I do this?
Here's a funny, very simple possibility: it uses the \# escape sequence of PS1 together with parameter expansions (and the way Bash expands its prompt).
The escape sequence \# expands to the command number of the command to be executed. This is incremented each time a command has actually been executed. Try it:
$ PS1='\# $ '
2 $ echo hello
hello
3 $ # this is a comment
3 $
3 $ echo hello
hello
4 $
Now, each time a prompt is to be displayed, Bash first expands the escape sequences found in PS1, then (provided the shell option promptvars is set, which is the default), this string is expanded via parameter expansion, command substitution, arithmetic expansion, and quote removal.
The trick is then to have an array that will have the k-th field set (to the empty string) whenever the (k-1)-th command is executed. Then, using appropriate parameter expansions, we'll be able to detect when these fields are set and to display the return code of the previous command if the field isn't set. If you want to call this array __cmdnbary, just do:
PS1='\n${__cmdnbary[\#]-$? }${__cmdnbary[\#]=}\$ '
Look:
$ PS1='\n${__cmdnbary[\#]-$? }${__cmdnbary[\#]=}\$ '
0 $ [ 2 = 3 ]
1 $
$ # it seems that it works
$ echo "it works"
it works
0 $
To qualify for the shortest answer challenge:
PS1='\n${a[\#]-$? }${a[\#]=}$ '
that's 31 characters.
Don't use this, of course, as a is a too trivial name; also, \$ might be better than $.
Seems you don't like that the initial prompt is 0 $; you can very easily modify this by initializing the array __cmdnbary appropriately: you'll put this somewhere in your configuration file:
__cmdnbary=( '' '' ) # Initialize the field 1!
PS1='\n${__cmdnbary[\#]-$? }${__cmdnbary[\#]=}\$ '
Got some time to play around this weekend. Looking at my earlier answer (not-good) and other answers I think this may be probably the smallest answer.
Place these lines at the end of your ~/.bash_profile:
PS1='$_ret$ '
trapDbg() {
local c="$BASH_COMMAND"
[[ "$c" != "pc" ]] && export _cmd="$c"
}
pc() {
local r=$?
trap "" DEBUG
[[ -n "$_cmd" ]] && _ret="$r " || _ret=""
export _ret
export _cmd=
trap 'trapDbg' DEBUG
}
export PROMPT_COMMAND=pc
trap 'trapDbg' DEBUG
Then open a new terminal and note this desired behavior on BASH prompt:
$ uname
Darwin
0 $
$
$
$ date
Sun Dec 14 05:59:03 EST 2014
0 $
$
$ [ 1 = 2 ]
1 $
$
$ ls 123
ls: cannot access 123: No such file or directory
2 $
$
Explanation:
This is based on trap 'handler' DEBUG and PROMPT_COMMAND hooks.
PS1 is using a variable _ret i.e. PS1='$_ret$ '.
trap command runs only when a command is executed but PROMPT_COMMAND is run even when an empty enter is pressed.
trap command sets a variable _cmd to the actually executed command using BASH internal var BASH_COMMAND.
PROMPT_COMMAND hook sets _ret to "$? " if _cmd is non-empty otherwise sets _ret to "". Finally it resets _cmd var to empty state.
The variable HISTCMD is updated every time a new command is executed. Unfortunately, the value is masked during the execution of PROMPT_COMMAND (I suppose for reasons related to not having history messed up with things which happen in the prompt command). The workaround I came up with is kind of messy, but it seems to work in my limited testing.
# This only works if the prompt has a prefix
# which is displayed before the status code field.
# Fortunately, in this case, there is one.
# Maybe use a no-op prefix in the worst case (!)
PS1_base=$'\n'
# Functions for PROMPT_COMMAND
PS1_update_HISTCMD () {
# If HISTCONTROL contains "ignoredups" or "ignoreboth", this breaks.
# We should not change it programmatically
# (think principle of least astonishment etc)
# but we can always gripe.
case :$HISTCONTROL: in
*:ignoredups:* | *:ignoreboth:* )
echo "PS1_update_HISTCMD(): HISTCONTROL contains 'ignoredups' or 'ignoreboth'" >&2
echo "PS1_update_HISTCMD(): Warning: Please remove this setting." >&2 ;;
esac
# PS1_HISTCMD needs to contain the old value of PS1_HISTCMD2 (a copy of HISTCMD)
PS1_HISTCMD=${PS1_HISTCMD2:-$PS1_HISTCMD}
# PS1_HISTCMD2 needs to be unset for the next prompt to trigger properly
unset PS1_HISTCMD2
}
PROMPT_COMMAND=PS1_update_HISTCMD
# Finally, the actual prompt:
PS1='${PS1_base#foo${PS1_HISTCMD2:=${HISTCMD%$PS1_HISTCMD}}}${_:+${PS1_HISTCMD2:+$? }}$ '
The logic in the prompt is roughly as follows:
${PS1_base#foo...}
This displays the prefix. The stuff in #... is useful only for its side effects. We want to do some variable manipulation without having the values of the variables display, so we hide them in a string substitution. (This will display odd and possibly spectacular things if the value of PS1_base ever happens to begin with foo followed by the current command history index.)
${PS1_HISTCMD2:=...}
This assigns a value to PS1_HISTCMD2 (if it is unset, which we have made sure it is). The substitution would nominally also expand to the new value, but we have hidden it in a ${var#subst} as explained above.
${HISTCMD%$PS1_HISTCMD}
We assign either the value of HISTCMD (when a new entry in the command history is being made, i.e. we are executing a new command) or an empty string (when the command is empty) to PS1_HISTCMD2. This works by trimming off the value HISTCMD any match on PS1_HISTCMD (using the ${var%subst} suffix replacement syntax).
${_:+...}
This is from the question. It will expand to ... something if the value of $_ is set and nonempty (which it is when a command is being executed, but not e.g. if we are performing a variable assignment). The "something" should be the status code (and a space, for legibility) if PS1_HISTCMD2 is nonempty.
${PS1_HISTCMD2:+$? }
There.
'$ '
This is just the actual prompt suffix, as in the original question.
So the key parts are the variables PS1_HISTCMD which remembers the previous value of HISTCMD, and the variable PS1_HISTCMD2 which captures the value of HISTCMD so it can be accessed from within PROMPT_COMMAND, but needs to be unset in the PROMPT_COMMAND so that the ${PS1_HISTCMD2:=...} assignment will fire again the next time the prompt is displayed.
I fiddled for a bit with trying to hide the output from ${PS1_HISTCMD2:=...} but then realized that there is in fact something we want to display anyhow, so just piggyback on that. You can't have a completely empty PS1_base because the shell apparently notices, and does not even attempt to perform a substitution when there is no value; but perhaps you can come up with a dummy value (a no-op escape sequence, perhaps?) if you have nothing else you want to display. Or maybe this could be refactored to run with a suffix instead; but that is probably going to be trickier still.
In response to Anubhava's "smallest answer" challenge, here is the code without comments or error checking.
PS1_base=$'\n'
PS1_update_HISTCMD () { PS1_HISTCMD=${PS1_HISTCMD2:-$PS1_HISTCMD}; unset PS1_HISTCMD2; }
PROMPT_COMMAND=PS1_update_HISTCMD
PS1='${PS1_base#foo${PS1_HISTCMD2:=${HISTCMD%$PS1_HISTCMD}}}${_:+${PS1_HISTCMD2:+$? }}$ '
This is probably not the best way to do this, but it seems to be working
function pc {
foo=$_
fc -l > /tmp/new
if cmp -s /tmp/{new,old} || test -z "$foo"
then
PS1='\n$ '
else
PS1='\n$? $ '
fi
cp /tmp/{new,old}
}
PROMPT_COMMAND=pc
Result
$ [ 2 = 2 ]
0 $ [ 2 = 3 ]
1 $
$
I need to use great script bash-preexec.sh.
Although I don't like external dependencies, this was the only thing to help me avoid to have 1 in $? after just pressing enter without running any command.
This goes to your ~/.bashrc:
__prompt_command() {
local exit="$?"
PS1='\u#\h: \w \$ '
[ -n "$LASTCMD" -a "$exit" != "0" ] && PS1='['${red}$exit$clear"] $PS1"
}
PROMPT_COMMAND=__prompt_command
[-f ~/.bash-preexec.sh ] && . ~/.bash-preexec.sh
preexec() { LASTCMD="$1"; }
UPDATE: later I was able to find a solution without dependency on .bash-preexec.sh.

Bash IF not working as expected

I am trying to have a function, called from PS1 which outputs something in a different colour, depending on what that something is.
In this case it's $?, the exit status of a program.
I am trying to get this function to output red text if the exit status is anything other than 0.
I have tried all possible variations of this, ways of representing that variable in the conditions and so forth and it just isn't working.
Instead of outputting what I expect it's just either always $LRED in one variation of this IF, or always $HII in another variation of this IF.
All relevant BASH is posted below, can you guys offer any insight?
...
# Custom Colour Alias
NM="\[\033[0;38m\]" # No background and white lines
HI="\[\033[1;36m\]" # Username colour
HII="\[\033[0;37m\]" # Name colour
SI="\[\033[1;32m\]" # Directory colour
IN="\[\033[0m\]" # Command input color
LRED="\[\033[1;31m\]"
BRW="\[\033[0;33m\]"
...
exitStatus ()
{
if [ $? -ne 0 ]
then
echo "$LRED\$?"
else
echo "\$?"
fi
#echo \$?
}
...
export PS1="\n$HII[ $LRED\u $SI\w$NM $HII]\n[ \! / \# / $(exitStatus) $HII]$LRED $ $IN"
CODE BASED ON SOLUTION
This is what I did based on the accepted answer below.
# Before Prompt
export PROMPT_COMMAND='EXSO=$?;\
if [[ $EXSO != 0 ]];\
then\
ERRMSG="$LRED$EXSO";\
else\
ERRMSG="$EXSO";\
fi;\
PS1="\n$HII[ $LRED\u $SI\W$NM $HII\! / \# / $ERRMSG $HII] $SI$ $IN";'
Problem is that your assignment to PS1 is only evaluated once, thus exitStatus is only called once. As Nirk also mentions you should use PROMPT_COMMAND. Set it to the command you want executed before every new prompt is displayed. An example:
PROMPT_COMMAND='if [ $? -ne 0 ]; then echo -n FAIL:;fi'
Will yell FAIL: before every new prompt if the previous command failed:
mogul#linuxine:~$ date
Sun Sep 29 21:13:53 CEST 2013
mogul#linuxine:~$ rm crappy_on_existent_file
rm: cannot remove ‘crappy_on_existent_file’: No such file or directory
FAIL:mogul#linuxine:~$

How to set a conditional newline in PS1?

I am trying to set PS1 so that it prints out something just right after login, but preceded with a newline later.
Suppose export PS1="\h:\W \u\$ ", so first time (i.e., right after login) you get:
hostname:~ username$
I’ve been trying something like in my ~/.bashrc:
function __ps1_newline_login {
if [[ -n "${PS1_NEWLINE_LOGIN-}" ]]; then
PS1_NEWLINE_LOGIN=true
else
printf '\n'
fi
}
export PS1="\$(__ps1_newline_login)\h:\W \u\$ “
expecting to get:
# <empty line>
hostname:~ username$
A complete example from the the beginning would be:
hostname:~ username$ ls `# notice: no empty line desired above!`
Desktop Documents
hostname:~ username$
Try the following:
function __ps1_newline_login {
if [[ -z "${PS1_NEWLINE_LOGIN}" ]]; then
PS1_NEWLINE_LOGIN=true
else
printf '\n'
fi
}
PROMPT_COMMAND='__ps1_newline_login'
export PS1="\h:\W \u\$ "
Explanation:
PROMPT_COMMAND is a special bash variable which is executed every time before the prompt is set.
You need to use the -z flag to check if the length of a string is 0.
Running with dogbane's answer, you can make PROMPT_COMMAND "self-destruct", preventing the need to run a function after every command.
In your .bashrc or .bash_profile file, do
export PS1='\h:\W \u\$ '
reset_prompt () {
PS1='\n\h:\W \u\$ '
}
PROMPT_COMMAND='(( PROMPT_CTR-- < 0 )) && {
unset PROMPT_COMMAND PROMPT_CTR
reset_prompt
}'
When the file is processed, PS1 initially does not display a new-line before the prompt.
However, PROMPT_CTR is immediately decremented to -1 (it is implicitly 0 before) before the prompt is shown the first time. After the first command, PROMPT_COMMAND clears itself and the counter before resetting the prompt to include the new-line. Subsequently, no PROMPT_COMMAND will execute.
Of course, there is a happy medium, where instead of PROMPT_COMMAND clearing itself, it just resets to a more ordinary function. Something like
export PS1='\h:\W \u\$ '
normal_prompt_cmd () {
...
}
reset_prompt () {
PS1='\n\h:\W \u\$ '
}
PROMPT_COMMAND='(( PROMPT_CTR-- < 0 )) && {
PROMPT_COMMAND=normal_prompt_cmd
reset_prompt
unset PROMPT_CTR
}'
2018 Update (inspired by chepner's answer)
UPDATE: Fixed PROMPT_COMMAND issues caused by other answers
Changes:
No need to export PS1
I used "\n$PS1" instead of re-typing.
Other answers interfere with the PROMPT_COMMAND's default behavior (more info below)
Enter the following in ~/.bash_profile (substituting first line with your prompt):
PS1=YOUR_PROMPT_HERE
add_newline_to_prompt() {
is_new_login="true"
INIT_PROMPT_COMMAND="$PROMPT_COMMAND"
DEFAULT_PROMPT_COMMAND=update_terminal_cwd
PROMPT_COMMAND='{
if [ $is_new_login = "true" ]; then
is_new_login="false"
eval $INIT_PROMPT_COMMAND
else
PS1="\n$PS1"
PROMPT_COMMAND=$DEFAULT_PROMPT_COMMAND
fi
}'
}
add_newline_to_prompt
PROMPT_COMMAND
I noticed that my tab name in terminal wasn't updating to my current working directory and did some investigating. I realized that above solutions are messing with PROMPT_COMMAND. Try this out:
Comment out any modifications to PROMPT_COMMAND in your config files (.bash_profile etc.)
Add INIT_PROMPT_COMMAND="$PROMPT_COMMAND" to your config file
Now open a new shell:
$ echo $INIT_PROMPT_COMMAND
shell_session_history_check; update_terminal_cwd
$ echo $PROMPT_COMMAND
update_terminal_cwd
Notice that when you open a new shell, it runs both a "history check" and updates the name of the tab current working directory. Notice that it only runs the "history check" initially, and then never runs it again.
NOTE: I've only tested this on Mac's Terminal. May be different on other systems.
Insert this in your .bashrc:
PROMPT_COMMAND="export PROMPT_COMMAND=echo"
alias clear="clear; export PROMPT_COMMAND='export PROMPT_COMMAND='echo''"
This achieves exactly what you want. No need for \n in PS1 or any functions.

Bash Function is not getting called, unless I echo the return value

In my program I am trying to return a value from a function, the return value is string. Everything works fine(atleast some part), if I echo the value once it is returned, but it is not even calling the function, if I dont return.... Consider the code below....
#!/bin/bash
function get_last_name() {
echo "Get Last Name"
ipath=$1
IFS='/'
set $ipath
for item
do
last=$item
done
echo $last
}
main() {
path='/var/lib/iscsi/ifaces/iface0'
current=$(get_last_name "$path")
echo -n "Current="
echo $current
}
main
It gives me an output like this
OUTPUT
Current=Get Last Name iface0
If I comment the echo $current, then the I am not even seeing the "Get Last Name", which makes to come to conclusion, that it is not even calling the function. Please let me know what mistake I am making. But one thing I am sure, bash is the ugliest language I have ever seen.......
Functions do not have return values in bash. When you write
current=$(get_last_name "$path")
you are not assigning a return value to current. You are capturing the standard output of get_last_name (written using the echo command) and assigning it to current. That's why you don't see "Get last name"; that text does not make it to the terminal, but is stored in current.
Detailed explanation
Let's walk through get_last_name first (with some slight modifications to simplify the explanation):
function get_last_name () {
ipath=$1
local IFS='/'
set $ipath
for item
do
last=$item
done
echo "Get Last Name"
echo $last
}
I added the local command before IFS so that the change is confined to the body of get_last_name, and I moved the first echo to the end to emphasize the similarity between the two echo statements. When get_last_name is called, it processes its single argument (a string containing a file path), then echoes two strings: "Get Last Name" and the final component of the file path. If you were to run execute this function from the command line, it would appear something like this:
$ get_last_name /foo/bar/baz
Get Last Name
baz
The exit code of the function would be the exit code of the last command executed, in this case echo $last. This will be 0 as long as the write succeeds (which it almost certainly will).
Now, we look at the function main, which calls get_last_name:
main() {
path='/var/lib/iscsi/ifaces/iface0'
current=$(get_last_name "$path")
echo -n "Current="
echo $current
}
Just like with get_last_name, main will not have a return value; it will produce an exit code which is the exit code of echo $current. The function begins by calling get_last_name inside a command substitution ($(...)), which will capture all the standard output from get_last_name and treat it as a string.
DIGRESSION
Note the difference between the following:
current=$(get_last_name "$path")
sets the value of current to the accumulated standard output of get_last_name. (Among other things, newlines in the output are replaced with spaces, but the full explanation of how whitespace is handled is a topic for another day). This has nothing to do with return values; remember, the exit code (the closet thing bash has to "return values") is a single integer.
current=get_last_name "$path"
would not even call get_last_name. It would interpret "$path" as the name of a command and try to execute it. That command would have a variable current with the string value "get_last_name" in its environment.
The point being, get_last_name doesn't "return" anything that you can assign to a variable. It has an exit code, and it can write to standard output. The $(...) construct lets you capture that output as a string, which you can then (among other things) assign to a variable.
Back to main
Once the value of current is set to the output generated by get_last_name, we execute
two last echo statements to write to standard output again. The first writes "Current=" without a newline, so that the next echo statement produces text on the same line as the first. The second just echoes the value of current.
When you commented out the last echo of main, you didn't stop get_last_name from being executed (it had already been executed). Rather, you just didn't print the contents of the current variable, where the output of get_last_name was placed rather than on the terminal.

Resources