Assign system variables provided by the variable names in an array - bash

There is a bunch of variables to assign value. I was able to do it in a stupid way by copy-pasting the same piece of code, and then change the part that is different.
For example, I want to do the following:
export country="US"
export city="LA"
The stupid way, with a user-input interface, is:
printf "\nPlease assign country$ \n" ;
if [[ $country == nil ]] ; then
printf "Current value is nil\n"
else
printf "Current value is: $country\n"
fi ;
printf "country: " ;
read -e -i $country country_
export country=$country_
And for city, I just search-replace "country" with "city" and past the code, which is stupid, but works.
Now, I want to improve the readability, and also maintainability, of the code, buy putting the variable names in a list and then iterate over this list.
The half-worked-out code, after googling is:
declare -a var_list=("country" "city")
for var in ${var_list[*]}
do
printf "\nPlease assign $var \n" ;
if [[ ${!var} == nil ]] ; then
printf "Current value is nil\n"
else
printf "Current value is: ${!var}\n"
fi ;
printf "${bold}$var: ${normal}" ;
read -e -i ${!var} {$var_}
export $var={$var_}
done
The following 2 lines of codes are still not correct to do what I want them to do:
read -e -i ${!var} {$var_}
export $var={$var_}
I would like to get some help on that.

Make a function from it and pass variable name to it:
get() {
# descriptive variable names
local var previousvalue
var="$1"
previousvalue="${!1}"
# superfluous, servers as a documentation
# The string "$1" should be a global variable
declare -g "$var"
# Asking the real questions:
printf "\nPlease assign $var \n"
printf "Current value is '$previousvalue'\n"
read -e -p "$1: " -i "$previousvalue" "$var"
}
declare -a var_list=("country" "city")
for i in "${var_list[#]}"; do # or just `for i in country city; do`
get "$i"
done
echo
echo "country=$country"
echo "city=$city"
example exeuction:
Please assign country
Current value is ''
country: Poland
Please assign city
Current value is ''
city: Warsaw
country=Poland
city=Warsaw
Notes:
Don't use for var in ${var_list[*]}, it will improperly handle array elements with spaces inside them. Do for var in "${var_list[#]}". The "${...[#]}" will properly quote and pass all variables.
The export $var={$var_} line exports the variable named after expansion $var to the string consisting of { the expansion of var_ variable and }. I guess you don't want to include the { } in the value. I guess you wanted to write "${var}_" or "${var_}" - the { have to be after $.

I suggest you avoid resorting to variable indirection and use a function instead :
display_and_read() {
local item_type="$1" previous_value="$2"
printf "\nPlease assign $item_type$ \n" ;
if [[ $previous_value == nil ]] ; then
printf "Current value is nil\n"
else
printf "Current value is: $previous_value\n"
fi ;
printf "$item_type: " ;
}
display_and_read "country" "$country"
read -e -i $country country_
export country=$country_
display_and_read "city" "$city"
read -e -i $city city_
export city=$city_

Related

Bash check if array of variables have values or not

I have an array of variables. I want to check if the variables have a value using the for loop.
I am getting the values into loop but the if condition is failing
function check {
arr=("$#")
for var in "${arr[#]}"; do
if [ -z $var ] ; then
echo $var "is not available"
else
echo $var "is available"
fi
done
}
name="abc"
city="xyz"
arr=(name city state country)
check ${arr[#]}
For the above I am getting all as available
Expected output is
name is available
city is available
state is not available
country is not available
This is the correct syntax for your task
if [ -z "${!var}" ] ; then
echo $var "is not available"
else
echo $var "is available"
fi
Explanation, this method uses an indirect variable expansion, this construction ${!var} will expand as value of variable which name is in $var.
Changed check function a bit
check () {
for var in "$#"; do
[[ "${!var}" ]] && not= || not="not "
echo "$var is ${not}available"
done
}
And another variant using declare
check () {
for var in "$#"; do
declare -p $var &> /dev/null && not= || not="not "
echo "$var is ${not}available"
done
}
From declare help
$ declare --help
declare: declare [-aAfFgilnrtux] [-p] [name[=value] ...]
Set variable values and attributes.
Declare variables and give them attributes. If no NAMEs are given,
display the attributes and values of all variables.
...
-p display the attributes and value of each NAME
...
Actually all vars can be checked at once using this
check () {
declare -p $# 2>&1 | sed 's/.* \(.*\)=.*/\1 is available/;s/.*declare: \(.*\):.*/\1 is not available/'
}
While indirection is a possible solution, it is not really recommended to use. A safer way would be to use an associative array:
function check {
eval "declare -A arr="${1#*=}
shift
for var in "$#"; do
if [ -z "${arr[$var]}" ] ; then
echo $var "is not available"
else
echo $var "is available"
fi
done
}
declare -A list
list[name]="abc"
list[city]="xyz"
check "$(declare -p list)" name city state country
This returns:
name is available
city is available
state is not available
country is not available
The following question was used to create this answer:
How to rename an associative array in Bash?

how to echo out associative part of associative array bash [duplicate]

Using:
set -o nounset
Having an indexed array like:
myArray=( "red" "black" "blue" )
What is the shortest way to check if element 1 is set?
I sometimes use the following:
test "${#myArray[#]}" -gt "1" && echo "1 exists" || echo "1 doesn't exist"
I would like to know if there's a preferred one.
How to deal with non-consecutive indexes?
myArray=()
myArray[12]="red"
myArray[51]="black"
myArray[129]="blue"
How to quick check that 51 is already set for example?
How to deal with associative arrays?
declare -A myArray
myArray["key1"]="red"
myArray["key2"]="black"
myArray["key3"]="blue"
How to quick check that key2 is already used for example?
To check if the element is set (applies to both indexed and associative array)
[ "${array[key]+abc}" ] && echo "exists"
Basically what ${array[key]+abc} does is
if array[key] is set, return abc
if array[key] is not set, return nothing
References:
See Parameter Expansion in Bash manual and the little note
if the colon is omitted, the operator tests only for existence [of parameter]
This answer is actually adapted from the answers for this SO question: How to tell if a string is not defined in a bash shell script?
A wrapper function:
exists(){
if [ "$2" != in ]; then
echo "Incorrect usage."
echo "Correct usage: exists {key} in {array}"
return
fi
eval '[ ${'$3'[$1]+muahaha} ]'
}
For example
if ! exists key in array; then echo "No such array element"; fi
From man bash, conditional expressions:
-v varname
True if the shell variable varname is set (has been assigned a value).
example:
declare -A foo
foo[bar]="this is bar"
foo[baz]=""
if [[ -v "foo[bar]" ]] ; then
echo "foo[bar] is set"
fi
if [[ -v "foo[baz]" ]] ; then
echo "foo[baz] is set"
fi
if [[ -v "foo[quux]" ]] ; then
echo "foo[quux] is set"
fi
This will show that both foo[bar] and foo[baz] are set (even though the latter is set to an empty value) and foo[quux] is not.
New answer
From version 4.2 of bash (and newer), there is a new -v option to built-in test command.
From version 4.3, this test could address element of arrays.
array=([12]="red" [51]="black" [129]="blue")
for i in 10 12 30 {50..52} {128..131};do
if [ -v 'array[i]' ];then
echo "Variable 'array[$i]' is defined"
else
echo "Variable 'array[$i]' not exist"
fi
done
Variable 'array[10]' not exist
Variable 'array[12]' is defined
Variable 'array[30]' not exist
Variable 'array[50]' not exist
Variable 'array[51]' is defined
Variable 'array[52]' not exist
Variable 'array[128]' not exist
Variable 'array[129]' is defined
Variable 'array[130]' not exist
Variable 'array[131]' not exist
Note: regarding ssc's comment, I've single quoted 'array[i]' in -v test, in order to satisfy shellcheck's error SC2208. This seem not really required here, because there is no glob character in array[i], anyway...
This work with associative arrays in same way:
declare -A aArray=([foo]="bar" [bar]="baz" [baz]=$'Hello world\041')
for i in alpha bar baz dummy foo test;do
if [ -v 'aArray[$i]' ];then
echo "Variable 'aArray[$i]' is defined"
else
echo "Variable 'aArray[$i]' not exist"
fi
done
Variable 'aArray[alpha]' not exist
Variable 'aArray[bar]' is defined
Variable 'aArray[baz]' is defined
Variable 'aArray[dummy]' not exist
Variable 'aArray[foo]' is defined
Variable 'aArray[test]' not exist
With a little difference:In regular arrays, variable between brackets ([i]) is integer, so dollar symbol ($) is not required, but for associative array, as key is a word, $ is required ([$i])!
Old answer for bash prior to V4.2
Unfortunately, bash give no way to make difference betwen empty and undefined variable.
But there is some ways:
$ array=()
$ array[12]="red"
$ array[51]="black"
$ array[129]="blue"
$ echo ${array[#]}
red black blue
$ echo ${!array[#]}
12 51 129
$ echo "${#array[#]}"
3
$ printf "%s\n" ${!array[#]}|grep -q ^51$ && echo 51 exist
51 exist
$ printf "%s\n" ${!array[#]}|grep -q ^52$ && echo 52 exist
(give no answer)
And for associative array, you could use the same:
$ unset array
$ declare -A array
$ array["key1"]="red"
$ array["key2"]="black"
$ array["key3"]="blue"
$ echo ${array[#]}
blue black red
$ echo ${!array[#]}
key3 key2 key1
$ echo ${#array[#]}
3
$ set | grep ^array=
array=([key3]="blue" [key2]="black" [key1]="red" )
$ printf "%s\n" ${!array[#]}|grep -q ^key2$ && echo key2 exist || echo key2 not exist
key2 exist
$ printf "%s\n" ${!array[#]}|grep -q ^key5$ && echo key5 exist || echo key5 not exist
key5 not exist
You could do the job without the need of externals tools (no printf|grep as pure bash), and why not, build checkIfExist() as a new bash function:
$ checkIfExist() {
eval 'local keys=${!'$1'[#]}';
eval "case '$2' in
${keys// /|}) return 0 ;;
* ) return 1 ;;
esac";
}
$ checkIfExist array key2 && echo exist || echo don\'t
exist
$ checkIfExist array key5 && echo exist || echo don\'t
don't
or even create a new getIfExist bash function that return the desired value and exit with false result-code if desired value not exist:
$ getIfExist() {
eval 'local keys=${!'$1'[#]}';
eval "case '$2' in
${keys// /|}) echo \${$1[$2]};return 0 ;;
* ) return 1 ;;
esac";
}
$ getIfExist array key1
red
$ echo $?
0
$ # now with an empty defined value
$ array["key4"]=""
$ getIfExist array key4
$ echo $?
0
$ getIfExist array key5
$ echo $?
1
What about a -n test and the :- operator?
For example, this script:
#!/usr/bin/env bash
set -e
set -u
declare -A sample
sample["ABC"]=2
sample["DEF"]=3
if [[ -n "${sample['ABC']:-}" ]]; then
echo "ABC is set"
fi
if [[ -n "${sample['DEF']:-}" ]]; then
echo "DEF is set"
fi
if [[ -n "${sample['GHI']:-}" ]]; then
echo "GHI is set"
fi
Prints:
ABC is set
DEF is set
tested in bash 4.3.39(1)-release
declare -A fmap
fmap['foo']="boo"
key='foo'
# should echo foo is set to 'boo'
if [[ -z "${fmap[${key}]}" ]]; then echo "$key is unset in fmap"; else echo "${key} is set to '${fmap[${key}]}'"; fi
key='blah'
# should echo blah is unset in fmap
if [[ -z "${fmap[${key}]}" ]]; then echo "$key is unset in fmap"; else echo "${key} is set to '${fmap[${key}]}'"; fi
Reiterating this from Thamme:
[[ ${array[key]+Y} ]] && echo Y || echo N
This tests if the variable/array element exists, including if it is set to a null value. This works with a wider range of bash versions than -v and doesn't appear sensitive to things like set -u. If you see a "bad array subscript" using this method please post an example.
This is the easiest way I found for scripts.
<search> is the string you want to find, ASSOC_ARRAY the name of the variable holding your associative array.
Dependign on what you want to achieve:
key exists:
if grep -qe "<search>" <(echo "${!ASSOC_ARRAY[#]}"); then echo key is present; fi
key exists not:
if ! grep -qe "<search>" <(echo "${!ASSOC_ARRAY[#]}"); then echo key not present; fi
value exists:
if grep -qe "<search>" <(echo "${ASSOC_ARRAY[#]}"); then echo value is present; fi
value exists not:
if ! grep -qe "<search>" <(echo "${ASSOC_ARRAY[#]}"); then echo value not present; fi
I wrote a function to check if a key exists in an array in Bash:
# Check if array key exists
# Usage: array_key_exists $array_name $key
# Returns: 0 = key exists, 1 = key does NOT exist
function array_key_exists() {
local _array_name="$1"
local _key="$2"
local _cmd='echo ${!'$_array_name'[#]}'
local _array_keys=($(eval $_cmd))
local _key_exists=$(echo " ${_array_keys[#]} " | grep " $_key " &>/dev/null; echo $?)
[[ "$_key_exists" = "0" ]] && return 0 || return 1
}
Example
declare -A my_array
my_array['foo']="bar"
if [[ "$(array_key_exists 'my_array' 'foo'; echo $?)" = "0" ]]; then
echo "OK"
else
echo "ERROR"
fi
Tested with GNU bash, version 4.1.5(1)-release (i486-pc-linux-gnu)
For all time people, once and for all.
There's a "clean code" long way, and there is a shorter, more concise, bash centered way.
$1 = The index or key you are looking for.
$2 = The array / map passed in by reference.
function hasKey ()
{
local -r needle="${1:?}"
local -nr haystack=${2:?}
for key in "${!haystack[#]}"; do
if [[ $key == $needle ]] ;
return 0
fi
done
return 1
}
A linear search can be replaced by a binary search, which would perform better with larger data sets. Simply count and sort the keys first, then do a classic binary halving of of the haystack as you get closer and closer to the answer.
Now, for the purist out there that is like "No, I want the more performant version because I may have to deal with large arrays in bash," lets look at a more bash centered solution, but one that maintains clean code and the flexibility to deal with arrays or maps.
function hasKey ()
{
local -r needle="${1:?}"
local -nr haystack=${2:?}
[ -n ${haystack["$needle"]+found} ]
}
The line [ -n ${haystack["$needle"]+found} ]uses the ${parameter+word} form of bash variable expansion, not the ${parameter:+word} form, which attempts to test the value of a key, too, which is not the matter at hand.
Usage
local -A person=(firstname Anthony lastname Rutledge)
if hasMapKey "firstname" person; then
# Do something
fi
When not performing substring expansion, using the form described
below (e.g., ‘:-’), Bash tests for a parameter that is unset or null.
Omitting the colon results in a test only for a parameter that is
unset. Put another way, if the colon is included, the operator tests
for both parameter’s existence and that its value is not null; if the
colon is omitted, the operator tests only for existence.
${parameter:-word}
If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.
${parameter:=word}
If parameter is unset or null, the expansion of word is assigned to parameter. The value of parameter is then substituted. Positional
parameters and special parameters may not be assigned to in this way.
${parameter:?word}
If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard
error and the shell, if it is not interactive, exits. Otherwise, the
value of parameter is substituted. ${parameter:+word}
If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.
https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Parameter-Expansion
If $needle does not exist expand to nothing, otherwise expand to the non-zero length string, "found". This will make the -n test succeed if the $needle in fact does exist (as I say "found"), and fail otherwise.
Both in the case of arrays and hash maps I find the easiest and more straightforward solution is to use the matching operator =~.
For arrays:
myArray=("red" "black" "blue")
if [[ " ${myArray[#]} " =~ " blue " ]]; then
echo "blue exists in myArray"
else
echo "blue does not exist in myArray"
fi
NOTE: The spaces around the array guarantee the first and last element can match. The spaces around the value guarantee an exact match.
For hash maps, it's actually the same solution since printing a hash map as a string gives you a list of its values.
declare -A myMap
myMap=(
["key1"]="red"
["key2"]="black"
["key3"]="blue"
)
if [[ " ${myMap[#]} " =~ " blue " ]]; then
echo "blue exists in myMap"
else
echo "blue does not exist in myMap"
fi
But what if you would like to check whether a key exists in a hash map? In the case you can use the ! operator which gives you the list of keys in a hash map.
if [[ " ${!myMap[#]} " =~ " key3 " ]]; then
echo "key3 exists in myMap"
else
echo "key3 does not exist in myMap"
fi
I get bad array subscript error when the key I'm checking is not set. So, I wrote a function that loops over the keys:
#!/usr/bin/env bash
declare -A helpList
function get_help(){
target="$1"
for key in "${!helpList[#]}";do
if [[ "$key" == "$target" ]];then
echo "${helpList["$target"]}"
return;
fi
done
}
targetValue="$(get_help command_name)"
if [[ -z "$targetvalue" ]];then
echo "command_name is not set"
fi
It echos the value when it is found & echos nothing when not found. All the other solutions I tried gave me that error.

Bash dynamic var name assignment and access

In the following bash function I would like a new variable called PASS to be created on the first $2 occurrence and then have the new $PASS variable to be tested in the second $2 occurrence.
function ask() {
while read -s -p "Type your $1 and press enter: " $2 && [[ -z "${$2// }" ]]; do
echoboldred -e "\n${1^} can't be blank."
done
}
ask password PASS
The problem is the ${$2// }.
To perform the // on the variable whose name is in $2,
the correct syntax is ${!2// }.
while read -s -p "Type your $1 and press enter: " "$2" && [[ -z "${!2// }" ]]; do
You can also use nameref declared with local -n instead of parameter indirection, it might make your code more readable:
ask() {
local -n foo=$2
while read -srp "Type your $1 and press enter: " foo && ! [[ $foo ]]; do
printf -- "\n%s can't be blank.\n" "${1^}"
done
}
ask password pass
Don't declare your functions with the function keyword and it's also advisable to use -r option with read in case your password has backslashes in it:
-r do not allow backslashes to escape any characters

Create dynamic variable name bash and get value

I have a function in bash that get param a string, for example:
MYSQL_DATABASE then I want in my file to create a var named VAR_MYSQL_DATABASE but I can`t get the value.
create_var()
{
read -p "Enter $1 : " VAR_$1
printf VAR_$1 // print VAR_MYSQL_DATABASE_NAME instead what I typed, so how do I get the value?
if [[ -z VAR_$1 ]]; then
printf '%s\n' "No input entered. Enter $1"
create_entry $1
fi
}
create_var "MYSQL_DATABASE_NAME"
Use the declare built-in in bash,
name="MYSQL_DATABASE"
declare VAR_${name}="Some junk string"
printf "%s\n" "${VAR_MYSQL_DATABASE}"
Some junk string
With the above logic, you can modify how your name variable is controlled, either present locally. If it is passed as argument from a function/command-line do
declare VAR_${1}="Your string value here"
Perhaps you want to achieve something like this,
create_var()
{
read -p "Enter value: " value
declare -g VAR_$1="$value"
dynamVar="VAR_$1"
if [[ -z "${!dynamVar}" ]]; then
printf '%s\n' "No input entered. Enter $1"
create_entry $1
fi
}
So, here we are creating the dynamic variable using the declare built-in and with the dynamic part VAR_$1 cannot be referenced directly as normal variables, hence using indirect expansion {!variable} to see if the created variable is available.
Try the following :
#!/bin/bash
create_var() {
declare -g VAR_$1
ref=VAR_$1
read -p "Enter $1: " "VAR_$1"
echo "inside : ${!ref}"
}
create_var "MYSQL_DATABASE_NAME"
echo "output : $VAR_MYSQL_DATABASE_NAME"
declare -g will make sure the variable exists outside of the function scope, and "VAR_$1" is used to dynamically create the variable names.
Output :
Enter MYSQL_DATABASE_NAME: Apple
inside : Apple
output : Apple
You can use the printf function with the -v option to "print" to a variable name.
create_var()
{
while : ; do
IFS= read -r -p "Enter $1 : " value
if [[ -n $value ]]; then
printf -v "VAR_$1" '%s' "$value"
return
fi
printf 'No input entered. Enter %s\n' "$1"
done
}
(I've rewritten your function with a loop to avoid what appeared to be an attempt at recursion.)

how to write a Bash function that confirms the value of an existing variable with a user

I have a large number of configuration variables for which I want users to issue confirmation of the values. So, there could be some variable specifying a run number in existence and I want the script to ask the user if the current value of the variable is ok. If the user responds that the value is not ok, the script requests a new value and assigns it to the variable.
I have made an initial attempt at a function for doing this, but there is some difficulty with its running; it stalls. I would value some assistance in solving the problem and also any criticisms of the approach I'm using. The code is as follows:
confirmVariableValue(){
variableName="${1}"
variableValue="${!variableName}"
while [[ "${userInput}" != "n" && "${userInput}" != "y" ]]; do
echo "variable "${variableName}" value: "${variableValue}""
echo "Is this correct? (y: continue / n: change it / other: exit)"
read userInput
# Make the user input lowercase.
userInput="$(echo "${userInput}" | sed 's/\(.*\)/\L\1/')"
# If the user input is "n", request a new value for the variable. If the
# user input is anything other than "y" or "n", exit. If the user input
# is "y", then the user confirmation loop ends.
if [[ "${userInput}" == "n" ]]; then
echo "enter variable "${variableName}" value:"
read variableValue
elif [[ "${userInput}" != "y" && "${userInput}" != "n" ]]; then
echo "terminating"
exit 0
fi
done
echo "${variableValue}"
}
myVariable="run_2014-09-23T1909"
echo "--------------------------------------------------------------------------------"
echo "initial variable value: "${myVariable}""
myVariable="$(confirmVariableValue "myVariable")"
echo "final variable value: "${myVariable}""
echo "--------------------------------------------------------------------------------"
The problem is here:
myVariable="$(confirmVariableValue "myVariable")"
your questions, like
echo "Is this correct? (y: continue / n: change it / other: exit)"
are going into the myVariable and not to the screen.
Try print questions to STDERR, or any other file-descriptor but STDOUT.
Opinion based comment: I would be unhappy with such config-script. It is way too chatty. For me is better:
print out the description and the default value
and ask Press Enter for confirm or enter a new value or <something> for exit>
You can also, use the following technique:
use the bash readline library for the read command with -e
use the -i value for set the default value for the editing
use the printf -v variable to print into variable, so you don't need to use var=$(...) nor any (potentially) dangerous eval...
example:
err() { echo "$#" >&2; return 1; }
getval() {
while :
do
read -e -i "${!1}" -p "$1>" inp
case "$inp" in
Q|q) err "Quitting...." || return 1 ;;
"") err "Must enter some value" ;;
*)
#validate the input here
#and print the new value into the variable
printf -v "$1" "%s" "$inp"
return 0
;;
esac
done
}
somevariable=val1
anotherone=val2
x=val3
for var in somevariable anotherone x
do
getval "$var" || exit
echo "new value for $var is: =${!var}="
done
I would not have them answer "Yes" then type in the new value. Just have them type in the new value if they want one, or leave it blank to accept the default.
This little function lets you set multiple variables in one call:
function confirm() {
echo "Confirming values for several variables."
for var; do
read -p "$var = ${!var} ... leave blank to accept or enter a new value: "
case $REPLY in
"") # empty use default
;;
*) # not empty, set the variable using printf -v
printf -v "$var" "$REPLY"
;;
esac
done
}
Used like so:
$ foo='foo_default_value'
$ bar='default_for_bar'
$ confirm foo bar
Confirming values for several variables.
foo = foo_default_value ... leave blank to accept or enter a new value: bar
bar = default_for_bar ... leave blank to accept or enter a new value:
foo=[bar], bar=[default_for_bar]
Of course, if blank can be a default, then you would need to account for that, like #jm666 use of read -i.

Resources