Update variable in bash function and use it in prompt (PS1) - bash

What I want to do is to call a function in PS1 to update a variable inside the function. Then I like to use that variable to add another line to PS1. Like bellow:
my_func(){
var="_RED_"
echo "hello in red"
}
PS1="\[\033]0;\w\007\]"
PS1+='$(my_func)'
if [ $var = "_RED_" ]; then # here I want to use that var
PS1+="\[$(tput setaf 124)\] red"
fi
The reason for doing this is to bring non-printable characters \[ and \] out of the function to prevent from overlapping long lines that is caused by \[ \]

You can absolutely update global variables inside a shell function -- all assignments inside functions modify global variables unless local or declare variables were used to create a new scope.
The problem here, however, is that you aren't running your function in the same shell process as the code that later tries to read var (though whether it is in fact "later" or not is a separate issue)! When you use command substitution -- the $() in $(my_func) -- you're creating a new forked-off subprocess to run that function. When that subprocess exits, all changes to variable values it's made are lost with it.
However, you can work around that by not using command substitution at all. Consider the below, which uses the PROMPT_COMMAND hook to assign PS1:
# code to run before each time a prompt is printed
PROMPT_COMMAND='build_ps1_func'
# rewrite your function to write to a named variable, not stdout
my_func(){
local outvar=$1; shift # named variable to write stdout to
var="_RED_" # hardcoded global to update, per question
printf -v "$outvar" '%s' "hello in red"
}
build_ps1_func() {
local your_str # define your_str as a local variable
my_func your_str # call my_func, telling it to write output to your_str
PS1="\[\033]0;\w\007\]"
PS1+="$your_str"
if [ $var = "_RED_" ]; then # using that variable here
PS1+="\[$(tput setaf 124)\] red"
fi
}

What I want to do is to call a function in PS1 to update a variable inside the function. Then I like to use that variable to add another line to PS1.
From your sample code, I suppose you mean that you want to perform a command expansion involving a shell function in the process of assigning a value to PS1. [update:] Since you've tagged the question [bash], we'll presume that you are specifically interested in the behavior of GNU Bash, which differs from that of a fully-conforming POSIX shell in this area. It is important in that case to recognize that that the command expansion will be performed once, at the time the value of PS1 is set / modified, not each time PS1 is displayed. Your wording and specific syntax make me suspect that you have a different expectation.
Consider this part of your code:
PS1="\[\033]0;\w\007\]"
PS1+='$(my_func)'
Because it appears in single quotes, the $(my_func) is not subject to command expansion or any other expansion at the time that it is appended to your prompt string. Although the single quotes are removed before the value is appended to PS1, that does not mean it will be subject to expansion later. Unlike a fully-conforming POSIX shell, however, Bash will perform command substitution on the prompt string before printing it.
Now, because the function body is a curly-braced compound command, if it is executed then it will indeed set the value of var in the current shell, and that effect will be visible after the function returns. However,
(update, per #CharlesDuffy:) the command in a command substitution is run in a subshell. Although a variable update in that subshell would persist past the function return, its scope is limited to the subshell, which exits almost immediately thereafter.
Again, nothing in the code you presented results in your function ever being called, and
even if it were called, and it did set var in the current shell, you test for a value of $var different from the one your function would set.
In contrast, consider this other fragment of your code:
PS1+="\[$(tput setaf 124)\] red"
Because the whole string is double-quoted in this case, the contents are unequivocally subject to command expansion. If this were executed, the $(tput setaf 124) would be replaced with the output from running tput setaf 124. That would happen at the time that PS1's value is modified, not every time its value is displayed.
Although you can generate a prompt that contains ANSI escape sequences, you cannot do it quite the way you're trying to do. Inasmuch as your specific needs are unclear, I hesitate to suggest a particular alternative.

Related

Why does `export` fail on bad substitutions but not command failures?

It’s well known that export masks the return value of command
substitutions in its variable assignments. But, interestingly,
export does not mask the return value of failed substitutions:
$ (set -eu; export FOO="$(bad_command)"); echo $?
bash: bad_command: command not found
0
$ (set -eu; export FOO="${bad_variable}"); echo $?
bash: bad_variable: unbound variable
1
$ (set -eu; export FOO="${}"); echo $? # bad substitution
bash: ${}: bad substitution
1
(Similar behavior in dash.)
What part of the specification indicates that failure of command
substitution does not propagate through export, but failure of
parameter expansion does?
Relevant sections from man bash (GNU Bash 4.4):
set -u
Treat unset variables and parameters other than the
special parameters "#" and "*" as an error when performing parameter
expansion. If expansion is attempted on an unset variable or
parameter, the shell prints an error message, and, if not interactive,
exits with a non-zero status.
and
export [-fn] [name[=word]] ...
export -p
The supplied names are marked for automatic export to the
environment of subsequently executed commands. If the -f option is
given, the names refer to functions. If no names are given, or if the
-p option is supplied, a list of names of all exported variables is
printed. The -n option causes the export property to be removed from
each name. If a variable name is followed by =word, the value of
the variable is set to word. export returns an exit status of 0
unless an invalid option is encountered, one of the names is not a
valid shell variable name, or -f is supplied with a name that is
not a function.
—I don’t see anything here that would distinguish between the two cases.
In particular, export just says that the value of the variable “is set
to” word, which suggests that it goes through the normal expansion
process (which it does) without special treatment.
POSIX specification references:
Shell Command Language
The export special builtin
The set special builtin
Neither case is really up to export
A command substitution just becomes text (empty or not) in a command's argument array. The Unix process model does not have any mechanism for relaying whether the text came from a program, or whether that program was successful.
This means that it's not possible to write an external command that behaves differently when you run foo var="$(true)" vs foo var="$(false)" vs foo var="" and shell builtins like export traditionally follow the same behavior for ease of implementation.
With set -u and unset variables, the command never runs at all. The shell simply skips execution if it encounters this condition while building the argument array, and reports failure instead. A command can't choose to ignore such a failure, since it's never consulted.
It would certainly be possible to implement a new shell mode that similarly skips execution and reports failure if command substitutions fail during the construction of the argument array, but this has not been a traditional feature so it's not in the POSIX spec.

Echo-ing an environment variable returns string literal rather than environment variable value

I have two bash scripts. The first listens to a pipe "myfifo" for input and executes the input as a command:
fifo_name="myfifo"
[ -p $fifo_name ] || mkfifo $fifo_name;
while true
do
if read line; then
$line
fi
done <"$fifo_name"
The second passes a command 'echo $SET_VAR' to the "myfifo" pipe:
command='echo $SET_VAR'
command_to_pass="echo $command"
$command_to_pass > myfifo
As you can see, I want to pass 'echo $SET_VAR' through the pipe. In the listener process, I've set a $SET_VAR environment variable. I expect the output of the command 'echo $SET_VAR' to be 'var_value,' which is the value of the environment variable SET_VAR.
Running the first (the listener) script in one bash process and then passing a command via the second in another process gives the following result:
$SET_VAR
I expected to "var_value" to be printed. Instead, the string literal $SET_VAR is printed. Why is this the case?
Before I get to the problem you're reporting, I have to point out that your loop won't work. The while true part (without a break somewhere in the loop) will run forever. It'll read the first line from the file, loop, try to read a second line (which fails), loop again, try to read a third line (also fails), loop again, try to read a fourth line, etc... You want the loop to exit as soon as the read command fails, so use this:
while read line
do
# something I'll get to
done <"$fifo_name"
The other problem you're having is that the shell expands variables (i.e. replaces $var with the value of the variable var) partway through the process of parsing a command line, and when it's done that it doesn't go back and re-do the earlier parsing steps. In particular, if the variable's value included something like $SET_VAR it doesn't go back and expand that, since it's just finished the bit where it expands variables. In fact, the only thing it does with the expanded value is split it into "words" (based on whitespace), and expand any filename wildcards it finds -- no variable expansions happen, no quote or escape interpretation, etc.
One possible solution is to tell the shell to run the parsing process twice, with the eval command:
while read line
do
eval "$line"
done <"$fifo_name"
(Note that I used double-quotes around "$line" -- this prevents the word splitting and wildcard expansion I mentioned from happening before eval goes through the normal parsing process. If you think of your original code half-parsing the command in $line, without double-quotes it gets one and a half-parsed, which is weird. Double-quotes suppress that half-parsing stage, so the contents of the variable get parsed exactly once.)
However, this solution comes with a big warning, because eval has a well-deserved reputation as a bug magnet. eval makes it easy to do complex things without quite understanding what's going on, which means you tend to get scripts that work great in testing, then fail incomprehensibly later. And in my experience, when eval looks like the best solution, it probably means you're trying to solve the wrong problem.
So, what're you actually trying to do? If you're just trying to execute the lines coming from the fifo as shell commands, then you can use bash "$fifo_name" to run them in a subshell, or source "$fifo_name" to run them in the current shell.
BTW, the script that feeds the fifo:
command='echo $SET_VAR'
command_to_pass="echo $command"
$command_to_pass > myfifo
Is also a disaster waiting to happen. Putting commands in variables doesn't work very well in the shell (I second chepner's recommendation of BashFAQ #50: I'm trying to put a command in a variable, but the complex cases always fail!), and putting a command to print another command in a variable is just begging for trouble.
bash, by it's nature, reads commands from stdin. You can simply run:
bash < myfifo

How to export bash variable from function execed in prompt to parent shell?

Oh... Probably title is not really easy to interpret. So let's describe it a little bit:
In .bashrc file I set PS1 to get custom prompt. In this prompt I need to have some additional info, which I get from other specific function. This function takes some time to exec so it is not cool to wait 1 sec after push enter in console. But I have idea to cached returned value from this specific function.
I need to check cache flag in every prompt print so I cant use variable, I must use function in printing prompt, because sourceing .bashrc is only one times, but if I pass function to PS1 it will be execed every time.
prompt_fun(){
export CACHE_YES=1
export PROMPT_CACHE="Something"
echo "$PROMPT_CACHE"
#in real case here will be checking if cache is turned on.
#If yes, we use cached value from exported variable in first time.
#If no, we exec specific function and export values to env variables.
}
PS1="$(prompt_fun): "`
Of course, variable CACHE_YES and PROMPT_CACHE are not set in console, so I am unable to control caching be changing CACHE_YES. I know when cache should change so I can type in console to change CACHE_YES=0 but my script don't rechange it to CACHE_YES=1 after cache new values.
How to make that export in prompt_fun have global effect?
The short answer is, you can't. Since prompt_fun is called in a command substitution, any changes made to variables in that subshell disappear when the subshell exits.
Instead, you'll want to set the value of PS1 inside prompt_fun, then call prompt_fun from the value of PROMPT_COMMAND, as the value of that parameter is executed in the current shell context prior to each prompt being displayed.
prompt_fun () {
if [[ -z $CACHE ]]; then
# Set value of $CACHE
fi
PS1=something
PS1+=something_else
PS1+=$CACHE
PS1+=final_value
}
PROMPT_COMMAND='prompt_fun' # Yes, single quotes

Line feed is being removed from echo when called in double-quotes

I'm trying to populate a shell variable called $recipient which should contain a value followed by a new-line.
$ set -x # force bash to show commands as it executes them
I start by populating $user, which is the value that I want to be followed by the newline.
$ user=user#xxx.com
+ user=user#xxx.com
I then call echo $user inside a double-quoted command substitution. The echo statement should create a newline after $user, and the double-quotes should preserve the newline.
$ recipient="$(echo $user)"
++ echo user#xxx.com
+ recipient=user#xxx.com
However when I print $recipient, I can see that the newline has been discarded.
$ echo "'recipient'"
+ echo ''\''recipient'\'''
'recipient'
I've found the same behaviour under bash versions 4.1.5 and 3.1.17, and also replicated the issue under dash.
I tried using "printf" rather than echo; this didn't change anything.
Is this expected behaviour?
Command substitution removes trailing newlines. From the standard:
The shell shall expand the command substitution by executing command in a subshell environment (see Shell Execution Environment ) and replacing the command substitution (the text of command plus the enclosing "$()" or backquotes) with the standard output of the command, removing sequences of one or more characters at the end of the substitution. Embedded characters before the end of the output shall not be removed; however, they may be treated as field delimiters and eliminated during field splitting, depending on the value of IFS and quoting that is in effect. If the output contains any null bytes, the behavior is unspecified.
You will have to explicitly add a newline. Perhaps:
recipient="$user
"
There's really no reason to use a command substitution here. (Which is to say that $(echo ...) is almost always a silly thing to do.)
All shell versions will react the same way, this is nothing new in scripting.
The new-line at the end of your original assignment is not included in the variable's value. It only "terminates" the current cmd and signals the shell to process.
Maybe user="user#xxx.com\n" will work, but without context about why you want this, just know that people usually keep variables values separate from the formatting "tools" like the newline.
IHTH.

What is the difference between PS1 and PROMPT_COMMAND?

While taking a look at this awesome thread I noticed that some examples use
PS1="Blah Blah Blah"
and some use
PROMPT_COMMAND="Blah Blah Blah"
(and some use both) when setting the prompt in a Bash shell. What is the difference between the two? A Stack Overflow search and even a bit of broader Google searching aren't getting me results, so even a link to the right place to look for the answer would be appreciated.
PROMPT_COMMAND can contain ordinary Bash statements whereas the PS1 variable can also contain the special characters, such as '\h' for hostname, in the variable.
For example, here is my Bash prompt that uses both PROMPT_COMMAND and PS1. The Bash code in PROMPT_COMMAND works out what Git branch you might be in and displays that at the prompt, along with the exit status of the last run process, hostname and basename of the pwd.
The variable RET stores the return value of the last executed program. This is convenient to see if there was an error and the error code of the last program I ran in the terminal. Note the outer ' surrounding the entire PROMPT_COMMAND expression. It includes PS1 so that this variable is reevaluated each time the PROMPT_COMMAND variable is evaluated.
PROMPT_COMMAND='RET=$?;\
BRANCH="";\
ERRMSG="";\
if [[ $RET != 0 ]]; then\
ERRMSG=" $RET";\
fi;\
if git branch &>/dev/null; then\
BRANCH=$(git branch 2>/dev/null | grep \* | cut -d " " -f 2);\
fi;
PS1="$GREEN\u#\h $BLUE\W $CYAN$BRANCH$RED$ERRMSG \$ $LIGHT_GRAY";'
Example output looks like this in a non-Git directory:
sashan#dhcp-au-122 Documents $ false
sashan#dhcp-au-122 Documents 1 $
And in a Git directory you see the branch name:
sashan#dhcp-au-122 rework mybranch $
Update
After reading the comments and Bob's answer, I think that writing it as he describes is better. It's more maintainable than what I originally wrote above, where the PS1 variable is set inside the PROMPT_COMMAND, which itself is a super complicated string that is evaluated at runtime by Bash.
It works, but it's more complicated than it needs to be. To be fair, I wrote that PROMPT_COMMAND for myself about 10 years ago and it worked and didn't think too much about it.
For those curious as to how I've amended my things, I've basically put the code for the PROMPT_COMMAND in a separate file (as Bob described) and then echo the string that I intend to be PS1:
GREEN="\[\033[0;32m\]"
CYAN="\[\033[0;36m\]"
RED="\[\033[0;31m\]"
PURPLE="\[\033[0;35m\]"
BROWN="\[\033[0;33m\]"
LIGHT_GRAY="\[\033[0;37m\]"
LIGHT_BLUE="\[\033[1;34m\]"
LIGHT_GREEN="\[\033[1;32m\]"
LIGHT_CYAN="\[\033[1;36m\]"
LIGHT_RED="\[\033[1;31m\]"
LIGHT_PURPLE="\[\033[1;35m\]"
YELLOW="\[\033[1;33m\]"
WHITE="\[\033[1;37m\]"
RESTORE="\[\033[0m\]" #0m restores to the terminal's default colour
if [ -z $SCHROOT_CHROOT_NAME ]; then
SCHROOT_CHROOT_NAME=" "
fi
BRANCH=""
ERRMSG=""
RET=$1
if [[ $RET != 0 ]]; then
ERRMSG=" $RET"
fi
if which git &>/dev/null; then
BRANCH=$(git branch 2>/dev/null | grep \* | cut -d " " -f 2)
else
BRANCH="(git not installed)"
fi
echo "${GREEN}\u#\h${SCHROOT_CHROOT_NAME}${BLUE}\w \
${CYAN}${BRANCH}${RED}${ERRMSG} \$ $RESTORE"
And in my .bashrc file:
function prompt_command {
RET=$?
export PS1=$(~/.bash_prompt_command $RET)
}
PROMPT_DIRTRIM=3
export PROMPT_COMMAND=prompt_command
From the GNU Bash documentation page (Bash Reference Manual):
PROMPT_COMMAND
If set, the value is interpreted as a command to execute before
the printing of each primary prompt ($PS1).
I never used it, but I could have used this back when I only had sh.
The difference is that PS1 is the actual prompt string used, and PROMPT_COMMAND is a command that is executed just before the prompt. If you want the simplest, most flexible way of building a prompt, try this:
Put this in your .bashrc file:
function prompt_command {
export PS1=$(~/bin/bash_prompt)
}
export PROMPT_COMMAND=prompt_command
Then write a script (Bash, Perl, or Ruby: your choice), and place it in file ~/bin/bash_prompt.
The script can use any information it likes to construct a prompt. This is much simpler, IMO, because you don't have to learn the somewhat baroque substitution language that was developed just for the PS1 variable.
You might think that you could do the same by simply setting PROMPT_COMMAND directly to ~/bin/bash_prompt, and setting PS1 to the empty string.
This at first appears to work, but you soon discover that the readline code expects PS1 to be set to the actual prompt, and when you scroll backwards in history, things get messed up as a result.
This workaround causes PS1 to always reflect the latest prompt (since the function sets the actual PS1 variable used by the invoking instance of the shell), and this makes readline and command history work fine.
From man bash:
PROMPT_COMMAND
If set, the value is executed as a command prior to issuing each primary prompt.
PS1
The value of this parameter is expanded (see PROMPTING below) and used as the primary prompt string. The default value is ''\s-\v\$ ''.
If you simply want to set the prompt string, using PS1 alone is enough:
PS1='user \u on host \h$ '
If you want to do something else just before printing the prompt, use PROMPT_COMMAND. For example, if you want to sync cached writes to disk, you can write:
PROMPT_COMMAND='sync'
Yeah, so to try to really nail this down:
PROMPT_COMMAND is a handy Bash convenience variable/function, but there is, strictly speaking, nothing that cannot also be done using PS1 alone, correct?
I mean, if one wants to set another variable with scope outside the prompt: depending on the shell, that variable would probably need to be declared first outside $PS1 or (worst case) one might have to get fancy with something waiting on a FIFO prior to calling $PS1 (and armed again at the end of $PS1); the \u \h might cause some trouble, particularly if you're using some fancy regex; but otherwise: one can accomplish anything PROMPT_COMMAND can by using command substitution within $PS1 (and, maybe in corner cases, explicit subshells)?
Right?
The difference is that
if you output an incomplete line from PROMPT_COMMAND, it will screw your Bash prompt
PS1 substitutes \H and friends
PROMPT_COMMAND runs its contents, and PS1 uses its contents as the prompt.
PS1 does variable expansion and command substitution at each prompt. There isn't any need to use PROMPT_COMMAND to assign a value to PS1 or to run arbitrary code. You can easily do export PS1='$(uuidgen) $RANDOM' once in file .bash_profile. Just use single quotes.
I spent so much time on this I just wanted to share what worked for me. I looked at a lot of the SO posts about PROMPT_COMMAND and PS1 and tried many combinations of single quotes, double quotes, calling functions... I could not get the prompt to update each time without printing control characters or the literal expanded but not processed prompt string, or without just setting PS1 in PROMPT_COMMAND as we are advised not to do. My problem was setting variables (colors) that contained control characters; these had to be hard-coded after the variable name in PS1. PROMPT_COMMAND is set to a function that sets variables and they are used (escaped) in a double-quoted PS1 string. This is for a powerline-style prompt that changes colors with each command.
icon1=#unicode powerline char like
#these: https://github.com/ryanoasis/powerline-extra-symbols#glyphs
icon2=#same
#array of ANSI colors. 2 for rgb mode then the rgb values
#then 'm' without '\]' control character. these are from
#the solarized theme https://ethanschoonover.com/solarized/
declare -a colors=(
"2;220;50;47m"
"2;203;75;22m"
"2;181;137;0m"
"2;133;153;0m"
"2;42;161;152m"
"2;38;139;210m"
"2;108;113;196m"
"2;211;54;130m"
"2;0;43;54m"
"2;7;54;66m"
"2;88;110;117m"
"2;101;123;131m"
"2;131;148;150m"
"2;147;161;161m"
)
#outside of vars set in PROMPT_COMMAND it's ok to have control chars
LEN=${#colors[#]}
BG="\[\e[48;"#set bg color
FG="\[\e[38;"#set fg color
TRANSP="1m\]"#transparency
BASE2="2;238;232;213m\]"#fg (text) color
myfunc(){
RAND=$(($RANDOM % $LEN))
COLOR1=${colors[$RAND]}
COLOR2=${colors[($RAND + 1) % $LEN]}
COLOR3=${colors[($RAND + 2) % $LEN]}
}
PROMPT_COMMAND=myfunc
#note double quotes and escaped COLOR vars followed by hard-coded '\]' control chars
PS1="$BG$TRANSP$FG\$COLOR1\]$icon1$BG\$COLOR1\]$FG$TRANSP$BG\$COLOR1\]$FG$BASE2
[username hard-coded in unicode] $BG\$COLOR2\]$FG\$COLOR1\]$icon2$BG\$COLOR2\]$FG$BASE2
\w $BG\$COLOR3\]$FG\$COLOR2\]$icon2$BG\$COLOR3\]$FG$BASE2 [more unicode]
\[\e[0m\]$FG\$COLOR3\]$icon2\[\e[0m\] "
That ought to get you going!

Resources