Bash expand variable in a variable - bash

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

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

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.

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

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>"

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>"

Related

Is it possible to combine bash variable search and replace with substring?

I have this function:
#! /usr/bin/env bash
function underline() {
U="${1//?/${2:--}}"
echo -e "\n$1\n${U:0:${#1}}\n"
}
underline "$1" "^-v-"
will work as expected:
$ ./u.sh "This is all you're going to see"
This is all you're going to see
^-v-^-v-^-v-^-v-^-v-^-v-^-v-^-v
It does what you expect. Originally it assumed that the underline character was just that, 1 character.
For "fun", I extended it to move from a single character underline to a repeating string underline (why? ... well ... because I can I suppose ... almost zero practical value in this exercise!).
And so, being the "let's make this so difficult that you need to write 2 pages of documentation to explain what's going on" sort of guy, I was wondering if the function could be written as a single line. And I don't mean:
function underline() { U="${1//?/${2:--}}"; echo -e "\n$1\n${U:0:${#1}}\n"; }
I don't think you can combine bash's variable search/replace with substring which is what is required.
I'm aware that this will work happily in zsh:
#! /usr/bin/env zsh
function underline() {
echo -e "\n$1\n${${1//?/${2:--}}:0:${#1}}\n"
}
underline $1 "^-v-"
e.g.
$ ./u.sh "This is all you're going to see"
This is all you're going to see
^-v-^-v-^-v-^-v-^-v-^-v-^-v-^-v
but not capable of this in bash (it seems).
No, you cannot combine those in a single parameter expansion in bash. But this single printf (a builtin in bash) should do the trick:
underline() { printf '%s\n%.*s\n' "$1" ${#1} "${1//?/${2:--}}"; }
underline "This is all you're going to see" "^-v-"
outputs
This is all you're going to see
^-v-^-v-^-v-^-v-^-v-^-v-^-v-^-v

Double Parameter substitution to get variable value [duplicate]

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

Can IFS be changed locally in a Bash function?

I have a function that needs to change IFS for its logic:
my_func() {
oldIFS=$IFS; IFS=.; var="$1"; arr=($var); IFS=$oldIFS
# more logic here
}
Can I declare IFS as local IFS inside the function so that I don't need to worry about backing its current value and restore later?
It appears to work as you desire.
#!/bin/bash
changeIFSlocal() {
local IFS=.
echo "During local: |$IFS|"
}
changeIFSglobal() {
IFS=.
echo "During global: |$IFS|"
}
echo "Before: |$IFS|"
changeIFSlocal
echo "After local: |$IFS|"
changeIFSglobal
echo "After global: |$IFS|"
This prints:
Before: |
|
During local: |.|
After local: |
|
During global: |.|
After global: |.|
Yes it can be defined!
As long as you define it local, setting of the value in the function does not affect the global IFS value. See the difference between the snippets below
addNumbers () {
local IFS='+'
printf "%s\n" "$(( $* ))"
}
when called in command-line as,
addNumbers 1 2 3 4 5 100
115
and doing
nos=(1 2 3 4 5 100)
echo "${nos[*]}"
from the command line. The hexdump on the above echo output wouldn't show the IFS value defined in the function
echo "${nos[*]}" | hexdump -c
0000000 1 2 3 4 5 1 0 0 \n
000000e
See in one of my answers, how I've used the localized IFS to do arithmetic - How can I add numbers in a bash script
I got confused because I changed the value of IFS to : inside the function (without using local) and then tried to display the value of IFS with this command, after calling the function:
echo $IFS
which displayed an empty line that made me feel the function wasn't changing IFS. After posting the question, I realized that word splitting was at play and I should have used
echo "$IFS"
or
printf '%s\n' "$IFS"
or, even better
set | grep -w IFS=
to accurately display the IFS value.
Coming back to the main topic of local variables, yes, any variable can be declared as local inside a function to limit the scope, except for variables that have been declared readonly (with readonly or declare -r builtin commands). This includes Bash internal variables like BASH_VERSINFO etc.
From help local:
local: local [option] name[=value] ...
Define local variables.
Create a local variable called NAME, and give it VALUE. OPTION can
be any option accepted by `declare'.
Local variables can only be used within a function; they are visible
only to the function where they are defined and its children.
Exit Status:
Returns success unless an invalid option is supplied, a variable
assignment error occurs, or the shell is not executing a function.
You can designate IFS as a local variable; the local version is still used as the field separator string.
Sometimes it is useful to run a function in a completely isolated environment, where no changes are permanent. (For example, if the function needs to change shell options.) This can be achieved by making the function run in a subshell; just change the {} in the function definition to ():
f() (
shopt -s nullglob
IFS=.
# Commands to run in local environment
)

Extending terminal colors to the end of line

I have a bash script which generates a motd. The problem is depending on some terminal settings which I am not sure about the color will extend to the end of the line. Othertimes it doesn't:
e.g.
v.s.
IIRC one is just the normal gnome-terminal and the other is my tmux term. So my question is how can I get this to extend to 80 character (or really to the terminal width). Of course I can pad to 80 chars but that really doesn't solve the problem.
Here is a snip of my code which generates the motd:
TC_RESET="^[[0m"
TC_SKY="^[[0;37;44m"
TC_GRD="^[[0;30;42m"
TC_TEXT="^[[38;5;203m"
echo -n "${TC_SKY}
... lots of printing..."
echo -e "\n Welcome to Mokon's Linux! \n"
echo -n "${TC_GRD}"
nodeinfo # Just prints the info seen below...
echo ${TC_RESET}
How can I programmatically from bash change the terminal settings or something change the color to the end of the line?
Maybe use the Escape sequence to clear-to-EOL
For some reason (on my MacOS terminal!) I only needed specify this sequence and then it worked for all the lines but for completeness I list it for all
TC_RESET=$'\x1B[0m'
TC_SKY=$'\x1B[0;37;44m'
TC_GRD=$'\x1B[0;30;42m'
TC_TEXT=$'\x1B[38;5;203m'
CLREOL=$'\x1B[K'
echo -n "${TC_SKY}${CLREOL}"
echo -e "\n ABC${CLREOL}\n"
echo -e "\n DEFG${CLREOL}\n"
echo -n "${TC_GRD}"
echo -e "\n ABC${CLREOL}\n"
echo -e "\n DEFG${CLREOL}\n"
echo ${TC_RESET}
Padding filter
Unfortunely, you have to pad each line with exact number of spaces for changing the color of the whole line's background.
As you're speaking about bash, my solution will use bashisms (Won't work under other shell, or older version of bash).
syntax printf -v VAR FORM ARGS assign to varianble VAR then result of sprintf FORM ARGS. That's bashism, under other kind of shell, you have to replace this line by TC_SPC=$(printf "%${COLUMNS}s" '')
You may try this:
... lots of printing..."
echo -e "\n Welcome to Mokon's Linux! \n"
echo -n "${TC_GRD}"
printf -v TC_SPC "%${COLUMNS}s" ''
nodeinfo |
sed "s/$/$TC_SPC/;s/^\\(.\\{${COLUMNS}\\}\\) */\\1/" # Just prints the info seen below...
echo ${TC_RESET}
Maybe you have to ensure that $COLUMNS is correctly setted:
COLUMNS=$(tput cols)
As you could see, only the result of command filtered by sed is fully colored.
you may
use same filter many times:
cmd1 | sed '...'
cmd2 | sed '...'
or group your commands to use only one filter:
( cmd1 ; cmd 2 ) | sed '...'
But there is an issue in case you try to filter ouptut that contain formatting escapes:
(
echo $'\e[33;44;1mYellow text on blue background';
seq 1 6;
echo $'\e[0m'
) | sed "
s/$/$TC_SPC/;
s/^\\(.\\{${COLUMNS}\\}\\) */\\1/"
Il the lines you have to pad to contain escapes, you have to isolate thems:
(
echo $'\e[33;44;1mYellow text on blue background';
seq 1 6;
echo $'\e[0m'
) | sed "
s/\$/$TC_SPC/;
s/^\\(\\(\\o33\\[[0-9;]*[a-zA-Z]\\)*\\)\\([^\o033]\\{${COLUMNS}\\}\\) */\\1\\3/
"
And finally to be able to fill terminate very long lines:
(
echo $'\e[33;44;1mYellow text on blue background';
seq 1 6;
echo "This is a very very long long looooooooooong line that contain\
more characters than the line could hold...";
echo $'\e[0m';
) | sed "
s/\$/$TC_SPC/;
s/^\\(\\(\\o33\\[[0-9;]*[a-zA-Z]\\)*\\)\\(\\([^\o033]\\{${COLUMNS}\\}\\)*\\) */\\1\\3/"
Nota: This only work if formating escapes are located at begin of line.
Try with this:
echo -e '\E[33;44m'"yellow text on blue background"; tput sgr0

right text align - bash

I have one problem.
My text should be aligned by right in specified width. I have managed to cut output to the desired size, but i have problem with putting everything on right side
Here is what i got:
#!/usr/local/bin/bash
length=$1
file=$2
echo $1
echo -e "length = $length \t file = $file "
f=`fold -w$length $file > output`
while read line
do
echo "line is $line"
done < "output"
thanks
Try:
printf "%40.40s\n" "$line"
This will make it right-aligned with width 40. If you want no truncation, drop .40 (thanks Dennis!):
printf "%40s\n" "$line"
For example:
printf "%5.5s\n" abc
printf "%5.5s\n" abcdefghij
printf "%5s\n" abc
printf "%5s\n" abcdefghij
will print:
abc
abcde
abc
abcdefghij
Your final step could be
sed -e :a -e 's/^.\{1,$length\}$/ &/;ta'
This is a very old question (2010) but it's the top google result, so might as well. Of the existing answers here, one is a guess that doesn't adjust for terminal width, and the other one invokes sed which is unnecessarily costly.
The printf solution is better as it's a bash builtin, so it vwon't slow things down, but instead of guessing - bash gives you $COLUMNS to tell you how wide the terminal window you're dealing with is.
so while you can explicitly align to, say the 40th column:
printf "%40s\n" "$the_weather"
You can size it for whatever your terminal width is with:
printf "%$COLUMNSs\n" "$the_weather"
(since we're mixing up syntax here, we have used the full form syntax for a bash variable i.e. ${COLUMNS} instead of $COLUMNS, so that bash can identify the variable from the other syntax
In action .. now that we've freed up all that sed processing time, we can use it for something else maybe:
the_weather="$(curl -sm2 'http://wttr.in/Dublin?format=%l:+%c+%f')"
printf "%${COLUMNS}s\n" "${the_weather:-I hope the weather is nice}"

Resources