How to iterate through a set of variables, then expand the variables in Bash - bash

I'm trying to set up a "check if machines are online" script with Bash, but running into an issue of when and where to define the variables so they're expanded properly. Something like:
#!/bin/bash
rm01="c01 c02 c03"
rm02="d01 d02 d03"
rm10="e11 e22 e33"
for room in rm01 rm02 rm03; do
echo $room
for computer in $room; do
#run various nslookup/ping tests and report
done
done
exit 0
I'm running into issues because I can't find a way to expand $room for its corresponding set of computers (in $rm01, $rm02, $rm10) listed at the beginning.
What am I doing wrong?

The quick fix is to use variable indirection:
for computer in ${!room}; do
Relying on word splitting is rarely the best idea, though. You could use arrays and namerefs instead (requires Bash 4.3 or newer):
#!/usr/bin/env bash
# Declare arrays
rm01=(c01 c02 c03)
rm02=(d01 d02 d03)
rm03=(e11 e22 e33)
# Declare room as nameref
declare -n room
# Using nameref as control variable sets room as reference to each variable in turn
for room in rm{01..03}; do
# Properly quoted array expansion
for computer in "${room[#]}"; do
echo "$computer" # or whatever needs to be done
done
done
exit 0

Related

Using a string value with the name of an existing variable to get the value of the existing variable

I am using a bash script in an azure devops pipeline where a variable is created dynamically from one of the pipeline tasks.
I need to use the variable's value in subsequent scripts, I can formulate the string which is used for the variable name, however cannot get the value of the variable using this string. I hope the below example makes it clear on what I need.
Thanks in advance for your help.
PartAPartB="This is my text"
echo "$PartAPartB" #Shows "This is my text" as expected
#HOW DO I GET BELOW TO PRINT "This is my text"
#without using the PartAPartB variable and
#using VarAB value to become the variable name
VarAB="PartAPartB"
VARNAME="$VarAB"
echo $("$VARNAME") #INCORRECT
You can use eval to do this
$ PartAPartB="This is my text"
$ VarAB="PartAPartB"
$ eval "echo \${${VarAB}}"
This is my text
You have two choices with bash. Using a nameref with declare -n (which is the preferred approach for Bash >= 4.26) or using variable indirection. In your case, examples of both would be:
#!/bin/bash
VarAB="PartAPartB"
## using a nameref
declare -n VARNAME=VarAB
echo "$VARNAME" # CORRECT
## using indirection
othervar=VarAB
echo "${!othervar}" # Also Correct
(note: do not use ALLCAPS variable names, those a generally reserved for environment variables or system variables)

Use a set of variables that start with the same string in bash

I know something like this is possible with DOS but I am not sure how to do it within bash.
I am writing a script that takes some configuration data: source, name, and destination. There will be a variable number of these in the configuration. I need to iterate over each set.
So, for example:
#!/bin/bash
FOLDER_1_SOURCE="/path/one"
FOLDER_1_NAME="one"
FOLDER_1_DESTINATION="one"
FOLDER_2_SOURCE="/path/two two"
FOLDER_2_NAME="two"
FOLDER_2_DESTINATION="here"
FOLDER_3_SOURCE="/something/random"
FOLDER_3_NAME="bravo"
FOLDER_3_DESTINATION="there"
FOLDER_..._SOURCE="/something/random"
FOLDER_..._NAME="bravo"
FOLDER_..._DESTINATION=""
FOLDER_X_SOURCE="/something/random"
FOLDER_X_NAME="bravo"
FOLDER_X_DESTINATION=""
Then I want to iterate over each set and get the SOURCE and NAME values for each set.
I am not stuck on this format. I just don't know how else to do this. The end goal is that I have 1 or more set of variables with source, name, and destination and then I need to iterate over them.
The answer to this type of question is nearly always "use arrays".
declare -a folder_source folder_name folder_dest
folder_source[1]="/path/one"
folder_name[1]="one"
folder_dest[1]="one"
folder_source[2]="/path/two two"
folder_name[2]="two"
folder_dest[2]="here"
folder_source[3]="/something/random"
folder_name[3]="bravo"
folder_dest[3]="there"
folder_source[4]="/something/random"
folder_name[4]="bravo"
folder_dest[4]=""
for((i=1; i<=${#folder_source[#]}; ++i)); do
echo "$i source:" "${folder_source[$i]}"
echo "$i name:" "${folder_name[$i]}"
echo "$i destination:" "${folder_dest[$i]}"
done
Demo: https://ideone.com/gZn0wH
Bash array indices are zero-based, but we just leave the zeroth slot unused here for convenience.
Tangentially, avoid upper case for your private variables.
AFIK bash does not have a facility to list all variables. A workaround - which also would mimic what is going on in DOS - is to use environment variables and restrict your search to those. In this case, you could do a
printenv|grep ^FOLDER||cut -d = -f 1
This is the same as doing in Windows CMD shell a
SET FOLDER

associative array gives no output [duplicate]

This question already has an answer here:
Bash variables: case sensitive or not?
(1 answer)
Closed 2 years ago.
I have tried messing around with a script that sets my display resolutions as i use my laptop with different setups of external monitors. also to learn bash i suppose.
now, i have a associative array with all my monitors configured as keys, and expected resolution as value.
declare -A known_monitor
known_monitor[Virtual1]=1920x1200
known_monitor[Virtual2]=1400x1050
Known_monitor[eDP-1]=2560x1440
when i try to access the monitors like this:
for monitor in "${monitors[#]}"
do
echo ------------------
echo $monitor
echo ${known_monitor[$monitor]}
echo ------------------
done
I see the $monitor value, but the "known_monitor" is empty.
------------------
eDP-1
------------------
I have tried moving parantheses around and adding citation marks, but nothing seems to work.
Any advice would be helpful, Thanks in advance!
Corrected a few errors in your script and it works:
#!/usr/bin/env bash
declare -A known_monitor
known_monitor[Virtual1]=1920x1200
known_monitor[Virtual2]=1400x1050
known_monitor[eDP-1]=2560x1440
for monitor in "${!known_monitor[#]}"
do
echo ------------------
echo "$monitor"
echo "${known_monitor[$monitor]}"
echo ------------------
done
Alternate declaration for the known_monitor associative array:
declare -A known_monitor=(
[Virtual1]=1920x1200
[Virtual2]=1400x1050
[eDP-1]=2560x1440
)
What went wrong with your initial script:
Problem1:
Known_monitor[eDP-1]=2560x1440 with an upper-case K refers to a different variable name as in shell, variable names are case-sensitive. This one has not been declared as an associative array.
Problem2:
for monitor in "${monitors[#]}" would iterate the values of a monitors array. But nowhere in your script you have defined and populated this monitors array. You have an associative array named known_monitor.
When you iterate the values of an array, you cannot index this array by its values. You need to iterate the index or keys of the array by prepending the name of the array with an exclamation mark !. Like this: for monitor in "${!monitors[#]}"
Your last known_monitor uses a capital key, it shouldn't :
declare -A known_monitor
known_monitor[Virtual1]=1920x1200
known_monitor[Virtual2]=1400x1050
known_monitor[eDP-1]=2560x1440
monitors=(Virtual1 Virtual2 eDP-1)
for monitor in "${monitors[#]}"
do
echo ------------------
echo $monitor
echo ${known_monitor[$monitor]}
echo ------------------
done

Why would I use declare / typeset in a shell script instead of just X=y?

I've recently come across a shell script that uses
declare -- FOO="" which apparently is spelled typeset -- FOO="" in non-bash shells.
Why might I want to do that instead of plain FOO="" or export FOO?
The most important purpose of using declare is to control scope, or to use array types that aren't otherwise accessible.
Using Function-Local Variables
To give you an example:
print_dashes() { for (( i=0; i<10; i++; do printf '-'; done; echo; }
while read -p "Enter a number: " i; do
print_dashes
echo "You entered: $i"
done
You'd expect that to print the number the user entered, right? But instead, it'll always print the value of i that print_dashes leaves when it's complete.
Consider instead:
print_dashes() {
declare i # ''local i'' would also have the desired effect
for (( i=0; i<10; i++; do printf '-'; done; echo;
}
...now i is local, so the newly-assigned value doesn't last beyond its invocation.
Declaring Explicitly Global Variables
Contrariwise, you sometimes want to declare a global variable, and make it clear to your code's readers that you're doing that by intent, or to do so while also declaring something as an array (or otherwise where declare would otherwise implicitly specify global state). You can do that too:
myfunc() {
declare arg # make arg local
declare -g -A myfunc_args_seen # make myfunc_args_seen a global associative array
for arg; do
myfunc_args_seen["$arg"]=1
done
echo "Across all invocations of myfunc, we have seen the following arguments:"
printf ' - %q\n' "${!myfunc_args_seen[#]}"
}
Declaring Associative Arrays
Normal shell arrays can just be assigned: my_arr=( one two three )
However, that's not the case for associative arrays, which are keyed as strings. For those, you need to declare them:
declare -A my_arr=( ["one"]=1 ["two"]=2 ["three"]=3 )
declare -i cnt=0
declares an integer-only variable, which is faster for math and always evaluates in arithmetic context.
declare -l lower="$1"
declares a variabl that automatically lowercases anything put in it, without any special syntax on access.
declare -r unchangeable="$constant"
declares a variable read-only.
Take a look at https://unix.stackexchange.com/questions/254367/in-bash-scripting-whats-the-different-between-declare-and-a-normal-variable for some useful discussion - you might not need these things often, but if you don't know what's available you're likely to work harder than you should.
A great reason to use declare, typeset, and/or readonly is code compartmentalization and reuse (i.e. encapsulation). You can write code in one script that can be sourced by others.
(Note declared/typeset/readonly constants/variables/functions lose their "readonly-ness" in a subshell, but they retain it when a child script sources their defining script since sourcing loads a script into the current shell, not a subshell.)
Since sourcing loads code from the script into the current shell though, the namespaces will overlap. To prevent a variable in a child script from being overwritten by its parent (or vice-versa, depending on where the script is sourced and the variable used), you can declare a variable readonly so it won't get overwritten.
You have to be careful with this because once you declare something readonly, you cannot unset it, so you do not want to declare something readonly that might naturally be redefined in another script. For example, if you're writing a library for general use that has logging functions, you might not want to use typeset -f on a function called warn, error, or info, since it is likely other scripts will create similar logging functions of their own with that name. In this case, it is actually standard practice to prefix the function, variable, and/or constant name with the name of the defining script and then make it readonly (e.g. my_script_warn, my_script_error, etc.). This preserves the values of the functions, variables, and/or constants as used in the logic in the code in the defining script so they don't get overwritten by sourcing scripts and accidentally fail.

how to access an automatically named variable in a Bash shell script

I have some code that creates a variable of some name automatically and assigns some value to it. The code is something like the following:
myVariableName="zappo"
eval "${myVariableName}=zappo_value"
How would I access the value of this variable using the automatically generated name of the variable? So, I'm looking for some code a bit like the following (but working):
eval "echo ${${myVariableName}}"
(... which may be used in something such as myVariableValue="$(eval "echo ${${myVariableName}}")"...).
Thanks muchly for any assistance
If you think this approach is madness and want offer more general advice, the general idea I'm working on is having variables defined in functions in a library with such names as ${usage} and ${prerequisiteFunctions}. These variables that are defined within functions would be accessed by an interrogation function that can, for instance, ensure that prerequisites etc. are installed. So a loop within this interrogation function is something like this:
for currentFunction in ${functionList}; do
echo "function: ${currentFunction}"
${currentFunction} -interrogate # (This puts the function variables into memory.)
currentInterrogationVariables="${interrogationVariables}" # The variable interrogationVariables contains a list of all function variables available for interrogation.
for currentInterrogationVariable in ${currentInterrogationVariables}; do
echo "content of ${currentInterrogationVariable}:"
eval "echo ${${currentInterrogationVariable}}"
done
done
Thanks again for any ideas!
IIRC, indirection in bash is by !, so try ${!myVariableName}
Try:
echo ${!myVariableName}
It will echo the variable who's name is contained in $myVariableName
For example:
#!/bin/bash
VAR1="ONE"
VAR2="TWO"
VARx="VAR1"
echo ${VARx} # prints "VAR1"
echo ${!VARx} # prints "ONE"

Resources