82

I'm trying to set up my PS1 prompt variable to dynamically choose a color. To do this, I've defined a bunch of local variables with color names:

$ echo $Green
\033[0;32m

but I was hoping to use those in dynamically assigning variables, but I can't figure out how to expand them properly:

> colorstr="\${$color}"
> echo $colorstr
${Green}

I've tried a dozen combinations of eval, echo, and double-quotes, and none seem to work. The logical way (I thought) to expand the variable results in an error:

> colorstr="${$color}"
-bash: ${$color}: bad substitution

(for clarity I've used > instead of $ for the prompt character, but I am using bash)

How can I expand that variable? i.e., somehow get the word "Green" to the value \033[0;32m? And prefereably, have bash or the terminal parse that \033[0;32m as the color green too.

EDIT: I was mis-using ${!x} and eval echo $x previously, so I've accepted those as solutions. For the (perhaps morbidly) curious, the functions and PS1 variable are on this gist: https://gist.github.com/4383597

4
  • 1
    I'm not following your problem here tbh. why not just colorstr="$Green someword $Red someotherword" ? Commented Dec 27, 2012 at 3:45
  • 1
    For the more general case where your string contains multiple $variable, use envsubst, see stackoverflow.com/a/31926346/6770384 Commented Jan 28, 2022 at 10:28
  • 1
    This is only indirectly related to the question, but when putting escape codes (or any non-printing sequences) in PS1, bash really needs to know whether to count them when working out the position of the cursor. Ensure when expanding $GREEN or ${!colour} you surround them in \[ and \]. Commented Sep 9, 2022 at 0:52
  • my command is more complicated. I've tried various options but it doesn't work. What is wrong. See my attempt with eval export RUN_CMD=(eval echo '$HOME/diversity-for-predictive-success-of-meta-learning/div_src/diversity_src/experiment_mains/main_diversity_with_task2vec.py --manual_loads_name diversity_ala_task2vec_delauny > $OUT_FILE 2> $ERR_FILE') Commented Nov 23, 2022 at 20:42

5 Answers 5

97

Using eval is the classic solution, but bash has a better (more easily controlled, less blunderbuss-like) solution:

  • ${!colour}

The Bash (4.1) reference manual says:

If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion.

For example:

$ Green=$'\033[32;m'
$ echo "$Green" | odx
0x0000: 1B 5B 33 32 3B 6D 0A                              .[32;m.
0x0007:
$ colour=Green
$ echo $colour
Green
$ echo ${!colour} | odx
0x0000: 1B 5B 33 32 3B 6D 0A                              .[32;m.
0x0007:
$

(The odx command is very non-standard but simply dumps its data in a hex format with printable characters shown on the right. Since the plain echo didn't show anything and I needed to see what was being echoed, I used an old friend I wrote about 24 years ago.)

Sign up to request clarification or add additional context in comments.

7 Comments

Someone suggested to use xxd instead of odx.
What does it matter? It displays data. Anyone can use any tool they like to see it. That's the beauty of Unix systems. You could use od, xxd, sed -l, vis, etc. I don't like the output of od or xxd or sed -l or vis for this purpose, but they'd all work. Since the display of the data is 100% tangential to the answer, it really doesn't matter. (And I had no hand in the rejecting the revision, but I'd probably have undone any such edit.)
An associative array should be preferred over indirection.
One just had to use xxd -g1 to like the output the same as from odx.
I'm sure this is correct, but when I see an exclamation point, I think "not" (negating a binary True value). Using it this way is confusing if you don't do a lot of bash scripting. Eval works for me.
|
23

Using eval should do it:

green="\033[0;32m"
colorstr="green"
eval echo -e "\$$colorstr" test           # -e = enable backslash escapes
test

The last test is in color green.

5 Comments

I swear I tried that one before.... but this time it worked. With a caveat, though: now my prompt has \033[0;32m in it. So, the string replacement I requested works, but bash isn't parsing the color code now.
eval is evil, especially when it's not needed. -1.
@gniourf_gniourf Eval is the way to transform $colorstr into green. If so, it is needed.
I came to this answer looking for a way to expand a string like "~/.myconfile.conf" into the full path, since the upstream source did not do expansion of the ~. eval echo -e $file did the trick nicely for me. Thanks!
my command is more complicated. I've tried various options but it doesn't work. What is wrong. See my attempt with eval export RUN_CMD=(eval echo '$HOME/diversity-for-predictive-success-of-meta-learning/div_src/diversity_src/experiment_mains/main_diversity_with_task2vec.py --manual_loads_name diversity_ala_task2vec_delauny > $OUT_FILE 2> $ERR_FILE')
2

Bash supports associative arrays. Don't use indirection when you could use a dict. If you don't have associative arrays, upgrade to bash 4, ksh93, or zsh. Apparently mksh is adding them eventually as well, so there should be plenty of choice.

function colorSet {
    typeset -a \
        clrs=(black red green orange blue magenta cyan grey darkgrey ltred ltgreen yellow ltblue ltmagenta ltcyan white) \
        msc=(sgr0 bold dim smul blink rev invis)

    typeset x

    while ! ${2:+false}; do
        case ${1#--} in
            setaf|setab)
                for x in "${!clrs[@]}"; do
                    eval "$2"'[${clrs[x]}]=$(tput "${1#--}" "$x")'
                done
                ;;
            misc)
                for x in "${msc[@]}"; do
                    eval "$2"'[$x]=$(tput "$x")'
                done
                ;;
            *)
                return 1
        esac
        shift 2        
    done
}

function main {
    typeset -A fgColors bgColors miscEscapes
    if colorSet --setaf fgColors --setab bgColors --misc miscEscapes; then
        if [[ -n ${1:+${fgColors[$1]:+_}} ]]; then
            printf '%s%s%s\n' "${fgColors[${1}]}" "this text is ${1}" "${miscEscapes[sgr0]}"
        else
            printf '%s, %s\n' "${1:-Empty}" 'no such color.' >&2
            return 1
        fi
    else
        echo 'Failed setting color arrays.' >&2
        return 1
    fi
}

main "$@"

Though we're using eval, it's a different type of indirection for a different reason. Note how all the necessary guarantees are made for making this safe.

See also: http://mywiki.wooledge.org/BashFAQ/006

5 Comments

Ooooh, neat, didn't know bash had dicts!
eval is not needed here, with bash >= 4.1 you can use printf with the -v option to assign values to array indices (this is a wonderful feature, you can abuse it). Also, the keyword function is deprecated (I suppose you're using it for the sake of some hypothetical portability, but this question is clearly tagged bash).
@gniourf_gniourf printf -v is nice, but can only assign a single element at a time. It would require a nested loop (actually there are some other tricks but they are nearly as ugly), and of course is slower. For any portability, a wrapper is needed. Alternatives don't offer much over eval. I use function for a number of reasons. All major shells that support typeset, arrays, and other "bashisms" like [[ also support the function keyword. I don't expect everyone use this style. Most shouldn't. It requires knowing a lot about scope and non-standard builtin differences between shells.
This answer is too limited in scope and too complex. There are reasons to want indirect expansion where an array is inappropriate, such as when you know the names of variables you want to expand but do not have control over setting them. Or when you are using a system that ships with bash version 3 and you don't have control over upgrading it (e.g. employer issued Mac), etc.. A simple for loop makes much more sense: for i in VAR1 VAR2; do echo "${!i}"; done
@user5359531 Sure indirect expansion can certainly be useful. Organizing data into a structure like an array is usually preferable to a Hungarian notation scheme however. There are quite a few other possible solutions that don't involve associative arrays too.
1

Your first result shows the problem:

$ echo $Green
\033[0;32m

The variable Green contains an string of a backlash, a zero, a 3, etc..

It was set by: Green="\033[0;32m". As such it is not a color code.
The text inside the variable needs to be interpreted (using echo -e, printf or $'...').

Let me explain with code:

$ Green="\033[0;32m"    ;     echo "  $Green   test   "
  \033[0;32m   test     

What you mean to do is:

$  Green="$(echo -e "\033[0;32m" )"    ;     echo "  $Green   test   "
 test   

In great color green. This could print the color but will not be useful for PS1:

$  Green="\033[0;32m"    ;     echo -e "  $Green   test   "
 test   

As it means that the string has to be interpreted by echo -e before it works.

An easier way (in bash) is :

$ Green=$'\033[0;32m'    ;     echo "  $Green   test   "
  test   

Please note the ` $'...' `

Having solved the issue of the variable Green, accesing it indirectly by the value of var colorstr is a second problem that could be solved by either:

$ eval echo \$$colorstr testing colors
testing colors
$ echo ${!colorstr} testing colors
testing colors

Note Please do not work with un-quoted values (as I did here because the values were under my control) in general. Learn to quote correctly, like:

$ eval echo \"\$$colorstr testing colors\"

And with that, you could write an PS1 equivalent to:

export PS1="${Green} welcome ${Red} user>"

with:

Green=$'\033[0;32m'    Red=$'\033[0;31m'
color1=Green           color2=Red
export PS1="${!color1} welcome ${!color2} user>"

1 Comment

my command is more complicated. I've tried various options but it doesn't work. What is wrong. See my attempt with eval export RUN_CMD=(eval echo '$HOME/diversity-for-predictive-success-of-meta-learning/div_src/diversity_src/experiment_mains/main_diversity_with_task2vec.py --manual_loads_name diversity_ala_task2vec_delauny > $OUT_FILE 2> $ERR_FILE')
0

You will want to write an alias to a function. Check out http://tldp.org/LDP/abs/html/functions.html, decent little tutorial and some examples.

EDIT: Sorry, looks like I misunderstood the issue. First it looks like your using the variables wrong, check out http://www.thegeekstuff.com/2010/07/bash-string-manipulation/. Also, what is invoking this script? Are you adding this to the .bash_profile or is this a script your users can launch? Using export should make the changes take effect right away without needed relog.

var Green="\[\e[32m\]"
var Red="\[\e41m\]"

export PS1="${Green} welcome ${Red} user>"

2 Comments

Why an alias to a function and not just a function? As it is, I'm using a function in my $PS1, which is probably part of the problem.
edited my answer, hopefully this is closer to what you where asking

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.