Why does function call from PS1 require escaping? - bash

I'm setting my prompt inside of .bash_profile like this
export PS1="\w\$(getBranchName)\n ---->"
My getBranchName function exists, and this works, fine.
My question is, why do I need to escape the call to getBranchName like this \$(getBranchName).
In other words, why doesn't this code work, instead?
export PS1="\w$(getBranchName)\n ---->"
If curious, this is what the getBranchName function looks like
esc="\033"
redf="${esc}[31m"
green="${esc}[32m"
purple="${esc}[35m"
cyanf="${esc}[36m"
reset="${esc}[0m"
getBranchName() {
if [[ "$(__git_ps1 '%s')" == "master" ]]
then
echo -e "${redf}$(__git_ps1)${reset}";
else
echo -e "${cyanf}$(__git_ps1)${reset}";
fi
}
export PS1="\w\$(getBranchName)\n ---->"

You need to escape the dollar because you want to store this exact text in your variable.
Try it by typing echo "$PS1". You should see the exact text : \w$(getBranchName)\n ---->
If you didn't escape it, the function would be evaluated only once, during the allocation.
The bottom line is that PS1 is a special variable : every time you display a new line in the console, the variable is evaluated to extract the display settings.

The PS1 variable is basically a template string (which might contain function calls) which is evaluated each time the prompt is shown.
If you want to evaluate a function each time, so that each prompt shows the result of this new execution, you need to escape the call.
If you would embed the function call directly in the string, the function would be called once immediately (i.e. likely during login) and your PS1 will contain the result of this single function call as evaluated during your initial login. Thus, the value won't be updated again since the function is not called anymore (since the PS1 doesn't contain the function call anymore but only the static result of one).

It's escaped because you want it to run when the shell evaluates $PS1 each time it's displayed, not just during the assignment.
The other expansions (which should be using tput unless you actually like random control codes all over your non-ANSI terminals) you want to be expanded just once, when you assign to PS1.

Related

Concat to prompt if env var exists

I'm trying to add () around my Python virtual environment name like this:
(my-env) my-user#my-machine:%
and if the env is not set, it will only show:
my-user#my-machine:%
Right now I have:
MYPS1+='($PYENV_VERSION) '
which will show the () if the virtual env is not set:
() my-user#my-machine:%
Is there away I can do something like this:
MYPS1+='($PYENV_VERSION) ' if $PYENV_VERSION exists else ''
So the important thing when setting the prompt like this, is that you (probably) want to reevaluate it every time it prints the prompt. That way if and when the envvar changes, the prompt changes. That's why you have ' characters around what you're adding to the prompt -- it prevents any vars or code in there from being evaluated when you set it. While you could do something like you suggest (with an if in the shell) that would not be reevaluated when the prompt was printed, so could be "stale".
Instead you want to do it all in the variable expansion. sh/bash comes with a variety of ways of expanding variables but the one you want is
${parameter:+word}
Use Alternative Value. If parameter is unset or null, null is substituted; otherwise, the expansion of word is substituted.
That means you want something like
MYPS1+='${PYENV_VERSION:+($PYENV_VERSION) }'

Export serialized array in bash

I'm trying to serialize an array in bash and then export it:
function serialize
{
for i in ${1[#]}; do
ret+=$i" "
done
return ${ret::-1}
}
MEASUREMENT_OUTPUT_FILES=( t1DQ.txt t1CS.txt t2RXe.txt t2e.txt )
export MEASUREMENT_OUTPUT_FILES=${serialize MEASUREMENT_OUTPUT_FILES[#]}
The code produces the following error:
MEASUREMENT_OUTPUT_FILES=${serialize MEASUREMENT_OUTPUT_FILES[#]}: bad
substitution
Any ideas what the correct syntax (error in the last line starting with export) would be?
I believe you want:
export MEASUREMENT_OUTPUT_FILES=$(serialize "${MEASUREMENT_OUTPUT_FILES[#]}")
(where $(...) is a notation for command substitution).
That said, your command is actually equivalent to
export MEASUREMENT_OUTPUT_FILES="${MEASUREMENT_OUTPUT_FILES[*]}"
so you don't need the serialize function unless you want to improve your serialization logic. (Which you should consider doing, IMHO: just joining with a space is error-prone, because what if one of the arguments includes a space?)
Edited to add: Also, I don't know how we all missed this before, but this:
return ${ret::-1}
actually needs to be this:
echo "${ret::-1}"
or this:
printf %s "${ret::-1}"
since return is for setting the exit status of a function, which must be an integer. (It's intended for indicating success, zero, vs. failure, nonzero, though some commands assign special meanings to multiple nonzero values.) What you want is for your function to "print" the files, so you can capture them.

Bash functions returning values meanwhile altering global variables

I'm just struggling with bash functions, and trying to return string values meanwhile some global variable is modified inside the function. An example:
MyGlobal="some value"
function ReturnAndAlter () {
MyGlobal="changed global value"
echo "Returned string"
}
str=$(ReturnAndAlter)
echo $str # prints out 'Returned value' as expected
echo $MyGlobal # prints out the initial value, not changed
This is because $(...) (and also `...` if used instead) cause the function to have its own environment, so the global variable is never affected.
I found a very dirty workaround by returning the value into another global variable and calling the function only using its name, but think that there should be a cleaner way to do it.
My dirty solution:
MyGlobal="some value"
ret_val=""
function ReturnAndAlter () {
ret_val="Returned string"
MyGlobal="changed value"
}
ReturnAndAlter # call the bare function
str=$ret_val # and assign using the auxiliary global ret_val
echo $str
echo $MyGlobal # Here both global variables are updated.
Any new ideas? Some way of calling functions that I'm missing?
Setting global variables is the only way a function has of communicating directly with the shell that calls it. The practice of "returning" a value by capturing the standard output is a bit of a hack necessitated by the shell's semantics, which are geared towards making it easy to call other programs, not making it easy to do things in the shell itself.
So, don't worry; no, you aren't missing any cool tricks. You're doing what the shell allows you to do.
The $(…) (command expansion) is run in a sub-shell.
All changes inside the sub-shell are lost when the sub-shell close.
It is usually a bad idea to use both printing a result and changing a variable inside a function. Either make all variables or just use one printed string.
There is no other solution.

How to generate PS1 in a function (escaping issues)

I'd like to move my bash prompt's construction into a function that can build it up modularly. The problem is that I cannot figure out how to get the function's result to be interpreted.
Example:
function build_prompt {
echo "\#"
}
export PS1="\$(build_prompt)"
My prompt always shows as \#, but should be the current time.
Sure there are ways around this particular example, but I'd like a general solution so I can use it for other escaped components, such as colours.
This is one use case for the PROMPT_COMMAND variable: running a function just before displaying the prompt that updates the value of PS1.
function build_prompt {
PS1='\#'
}
PROMPT_COMMAND='build_prompt'

Shell scripting return values not correct, why?

In a shell script I wrote to test how functions are returning values I came across an odd unexpected behavior. The code below assumes that when entering the function fnttmpfile the first echo statement would print to the console and then the second echo statement would actually return the string to the calling main. Well that's what I assumed, but I was wrong!
#!/bin/sh
fntmpfile() {
TMPFILE=/tmp/$1.$$
echo "This is my temp file dude!"
echo "$TMPFILE"
}
mainname=main
retval=$(fntmpfile "$mainname")
echo "main retval=$retval"
What actually happens is the reverse. The first echo goes to the calling function and the second echo goes to STDOUT. why is this and is there a better way....
main retval=This is my temp file dude!
/tmp/main.19121
The whole reason for this test is because I am writing a shell script to do some database backups and decided to use small functions to do specific things, ya know make it clean instead of spaghetti code. One of the functions I was using was this:
log_to_console() {
# arg1 = calling function name
# arg2 = message to log
printf "$1 - $2\n"
}
The whole problem with this is that the function that was returning a string value is getting the log_to_console output instead depending on the order of things. I guess this is one of those gotcha things about shell scripting that I wasn't aware of.
No, what's happening is that you are running your function, and it outputs two lines to stdout:
This is my temp file dude!
/tmp/main.4059
When you run it $(), bash will intercept the output and store it in the value. The string that is stored in the variable contains the first linebreak (the last one is removed). So what is really in your "retval" variable is the following C-style string:
"This is my temp file dude!\n/tmp/main.4059"
This is not really returning a string (can't do that in a shell script), it's just capturing whatever output your function returns. Which is why it doesn't work. Call your function normally if you want to log to console.

Resources