79

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

keflavich
  • 18,278
  • 20
  • 86
  • 118
  • 1
    I'm not following your problem here tbh. why not just `colorstr="$Green someword $Red someotherword"` ? – favoretti Dec 27 '12 at 03:45
  • 1
    For the more general case where your string contains multiple `$variable`, use `envsubst`, see https://stackoverflow.com/a/31926346/6770384 – Socowi Jan 28 '22 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 `\]`. – Dave Sep 09 '22 at 00: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')` – Charlie Parker Nov 23 '22 at 20:42

5 Answers5

93

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.)

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • 6
    Someone [suggested](http://stackoverflow.com/review/suggested-edits/1244205) to use [`xxd`](http://linuxcommand.org/man_pages/xxd1.html) instead of `odx`. – Paolo Moretti Dec 27 '12 at 13:27
  • 2
    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.) – Jonathan Leffler Dec 27 '12 at 14:58
  • An associative array should be preferred over indirection. – ormaaj Dec 29 '12 at 01:20
  • 6
    One just had to use `xxd -g1` to like the output the same as from `odx`. – Ruslan Oct 03 '18 at 07:13
  • 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. – Mark Berry Jan 22 '21 at 02:10
  • 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')` – Charlie Parker Nov 23 '22 at 20:41
  • @CharlieParker: It isn't clear what your trouble is, and trying to squeeze it into a comment isn't doing yourself any favours. You seem to be creating an array with 3 elements — `eval`, `echo`, and a long single-quoted string including what might be I/O redirections. What's the problem? I recommend asking a new question; cross-reference this one, by all means, though at the moment I don't see where the ideas here are relevant to your problem. – Jonathan Leffler Nov 24 '22 at 02:29
21

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.

perreal
  • 94,503
  • 21
  • 155
  • 181
  • 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. – keflavich Dec 27 '12 at 04:41
  • `eval` is evil, especially when it's not needed. `-1`. – gniourf_gniourf Dec 27 '12 at 13:47
  • 1
    @gniourf_gniourf Eval is the way to transform $colorstr into green. If so, it is needed. –  Dec 17 '15 at 06:19
  • 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! – jontsai Sep 19 '19 at 17:27
  • 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')` – Charlie Parker Nov 23 '22 at 20:41
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

ormaaj
  • 6,201
  • 29
  • 34
  • Ooooh, neat, didn't know bash had dicts! – keflavich Dec 29 '12 at 01:20
  • `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 Dec 29 '12 at 10:07
  • @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. – ormaaj Dec 29 '12 at 13:36
  • 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 Dec 21 '18 at 23:26
  • @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. – ormaaj Dec 23 '18 at 03:46
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>"
  • 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. – keflavich Dec 27 '12 at 04:44
  • edited my answer, hopefully this is closer to what you where asking –  Dec 27 '12 at 06:59
0

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>"
  • 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')` – Charlie Parker Nov 23 '22 at 20:41