Change name of Variable while in a loop - bash

I have this idea in mind:
I have this number: CN=20
and a list=( "xa1-" "xa2-" "xb1-" "xb2-")
and this is my script:
for a in "${list[#]}"; do
let "CN=$(($CN+1))"
echo $CN
Output:
21
22
23
24
I am trying to create a loop where it creates the following variables, which will be referenced later in my script:
fxp0_$CN="fxp-$a$CN"
fxp0_21="fxp-xa1-21"
fxp0_22="fxp-xa2-22"
fxp0_23="fxp-xb1-23"
fxp0_24="fxp-xb2-24"
However, I have not been able to find a way to change the variable name within my loop. Instead, I was trying myself and I got this error when trying to change the variable name:
scripts/srx_file_check.sh: line 317: fxp0_21=fxp0-xa2-21: command not found

After playing around I found the solution!
for a in "${list[#]}"; do
let "CN=$(($CN+1))"
fxp_int="fxp0-$a$CN"
eval "fxp0_$CN=${fxp_int}"
done
echo $fxp0_21
echo $fxp0_22
echo $fxp0_23
echo $fxp0_24
echo $fxp0_25
echo $fxp0_26
echo $fxp0_27
echo $fxp0_28
Output:
fxp0-xa1-21
fxp0-xa2-22
fxp0-xb1-23
fxp0-xb2-24
fxp0-xc1-25
fxp0-xc2-26
fxp0-xd1-27
fxp0-xd2-28

One common method for maintaining a dynamically generated set of variables is via arrays.
When the variable names vary in spelling an associative array comes in handy whereby the variable 'name' acts as the array index.
In this case since the only thing changing in the variable names is a number we can use a normal (numerically indexed) array, eg:
CN=20
list=("xa1-" "xa2-" "xb1-" "xb2-")
declare -a fxp0=()
for a in "${list[#]}"
do
(( CN++ ))
fxp0[${CN}]="fxp-${a}${CN}"
done
This generates:
$ declare -p fxp0
declare -a fxp0=([21]="fxp-xa1-21" [22]="fxp-xa2-22" [23]="fxp-xb1-23" [24]="fxp-xb2-24")
$ for i in "${!fxp0[#]}"; do echo "fxp0[$i] = ${fxp0[$i]}"; done
fxp0[21] = fxp-xa1-21
fxp0[22] = fxp-xa2-22
fxp0[23] = fxp-xb1-23
fxp0[24] = fxp-xb2-24

As a general rule can I tell you that it's not a good idea to modify names of variables within loops.
There is, however, a way to do something like that, using the source command, as explained in this URL with some examples. It comes down to the fact that you treat a file as a piece of source code.
Good luck

Related

Variable in variablenames

i have a a couple of variables with a number in its names. e.g
SERVER_IP48_SUBNET
..
SERVER_IP60_SUBNET
And an additional variable
SERVER_IP
Im trying to expand/concatenate them in the following way:
ALLIPs=${SERVER_IP}
for i in {48..64}; do
ALLIPs=${ALLIPs},${SERVER_IP${i}_SUBNET}
done
as you can imagine this script fails saying:
Wrong substitution
Does anybody of you know a good solution for this problem?
Thanks so far
Use a nameref with bash version 4.3 +
ALLIPs=${SERVER_IP}
for i in {48..64}; do
declare -n tmp="SERVER_IP${i}_SUBNET"
ALLIPs+=",$tmp"
done
But you should really be using an array in the first place:
server_ip=0.0.0.0
subnet_ip=(
[48]=1.1.1.1
[49]=2.2.2.2
# ...
[64]=16.16.16.16
)
all_ips=( "$server_ip" )
for i in {48..64}; do
all_ips+=( "${subnet_ip[i]}" )
done
(
IFS=,
echo "ALLIPs = ${all_ips[*]}"
)
Get out of the habit of using ALLCAPS variable names, leave those as
reserved by the shell. One day you'll write PATH=something and then
wonder why
your script is broken.
I just noticed, if you just want a to join the IP addresses with commas, and you're using an array, you don't need a loop at all:
all_ips=$(
IFS=,
set -- "$server_ip" "${subnet_ip[#]}"
echo "$*"
)
You can use ${!varprefix#} or ${!varprefix*} to expand to all variables with that common prefix (the difference is the same as $# and $*):
SERVER_IP48_SUBNET=48sub
SERVER_IP49_SUBNET=49sub
SERVER_IP50_SUBNET=50sub
SERVER_IP=1.2.3.4
# set this as empty since !SERVER_IP# also matches SERVER_IP
ALLIPS=""
for var in "${!SERVER_IP#}"; do
ALLIPS=$ALLIPS,${!var}
done
This would probably be more practical if you could invert the names like this, since we can only match prefixes:
SERVER_IP_SUBNET_48=48sub
SERVER_IP_SUBNET_49=49sub
SERVER_IP_SUBNET_50=50sub
SERVER_IP=1.2.3.4
ALLIPS=$SERVER_IP
for var in "${!SERVER_IP_SUBNET_#}"; do
ALLIPS=$ALLIPS,${!var}
done
More info on this feature in the bash manual.
One idea:
SERVER_IP48_SUBNET=48sub
SERVER_IP49_SUBNET=49sub
SERVER_IP50_SUBNET=50sub
SERVER_IP=1.2.3.4
ALLIPs=${SERVER_IP}
for i in {48..50}
do
tmpvar="SERVER_IP${i}_SUBNET" # build the variable name
ALLIPs="${ALLIPs},${!tmpvar}" # indirect variable reference via tmpvar
done
echo "ALLIPs = $ALLIPs}"
This generates:
ALLIPs = 1.2.3.4,48sub,49sub,50sub

Conditional on non-instantiated variable

I am new to Bash scripting, having a lot more experience with C-type languages. I have written a few scripts with a conditional that checks the value of a non-instantiated variable and if it doesn't exist or match a value sets the variable. On top of that the whole thing is in a for loop. Something like this:
for i in ${!my_array[#]}; do
if [ "${my_array[i]}" = true ]
then
#do something
else
my_array[i]=true;
fi
done
This would fail through a null pointer in Java since my_array[i] is not instantiated until after it is checked. Is this good practice in Bash? My script is working the way I designed, but I have learned that just because a kluge works now doesn't mean it will work in the future.
Thanks!
You will find this page on parameter expansion helpful, as well as this one on conditionals.
An easy way to test a variable is to check it for nonzero length.
if [[ -n "$var" ]]
then : do stuff ...
I also like to make it fatal to access a nonexisting variable; this means extra work, but better safety.
set -u # unset vars are fatal to access without exception handling
if [[ -n "${var:-}" ]] # handles unset during check
then : do stuff ...
By default, referencing undefined (or "unset") variable names in shell scripts just gives the empty string. But is an exception: if the shell is run with the -u option or set -u has been run in it, expansions of unset variables are treated as errors and (if the shell is not interactive) cause the shell to exit. Bash applies this principle to array elements as well:
$ array=(zero one two)
$ echo "${array[3]}"
$ echo "array[3] = '${array[3]}'"
array[3] = ''
$ set -u
$ echo "array[3] = '${array[3]}'"
-bash: array[3]: unbound variable
There are also modifiers you can use to control what expansions do if a variable (or array element) is undefined and/or empty (defined as the empty string):
$ array=(zero one '')
$ echo "array[2] is ${array[2]-unset}, array[3] is ${array[3]-unset}"
array[2] is , array[3] is unset
$ echo "array[2] is ${array[2]:-unset or empty}, array[3] is ${array[3]:-unset or empty}"
array[2] is unset or empty, array[3] is unset or empty
There are a bunch of other variants, see the POSIX shell syntax standard, section 2.6.2 (Parameter Expansion).
BTW, you do need to use curly braces (as I did above) around anything other than a plain variable reference. $name[2] is a reference to the plain variable name (or element 0 if it's an array), followed by the string "[2]"; ${name[2]}, on the other hand, is a reference to element 2 of the array name. Also, you pretty much always want to wrap variable references in double-quotes (or include them in double-quoted strings), to prevent the shell from "helpfully" splitting them into words and/or expanding them into lists of matching files. For example, this test:
if [ $my_array[i] = true ]
is (mostly) equivalent to:
if [ ${my_array[0]}[i] = true ]
...which isn't what you want at all. But this one:
if [ ${my_array[i]} = true ]
still doesn't work, because if my_array[i] is unset (or empty) it'll expand to the equivalent of:
if [ = true ]
...which is bad test expression syntax. You want this:
if [ "${my_array[i]}" = true ]

Have unique values in each while loop iteration

What I try to achieve, is to define global variables in my script. These variables can be reused in a loop (preferably a while loop..) and with every iteration, the loop should get a new set a variables.
My script (so far):
PACKAGE_ASSET_ID=AUTO`date +%s`000001
TITLE_ASSET_ID=AUTO`date +%s`000002
MOVIE_ASSET_ID=AUTO`date +%s`000003
PREVIEW_ASSET_ID=AUTO`date +%s`000004
POSTER_ASSET_ID=AUTO`date +%s`000005
while read name; do
#DATE=`date +%s`
#PACKAGE_ASSET_ID="AUTO${DATE}000001"
#TITLE_ASSET_ID="AUTO${DATE}000002"
#MOVIE_ASSET_ID="AUTO${DATE}000003"
#PREVIEW_ASSET_ID="AUTO${DATE}000004"
#POSTER_ASSET_ID="AUTO${DATE}000005"
echo $PACKAGE_ASSET_ID
echo $TITLE_ASSET_ID
echo $MOVIE_ASSET_ID
echo $PREVIEW_ASSET_ID
echo $POSTER_ASSET_ID
done <names.txt
Within the file names.txt, there are 15 entries. For every entry, the while loop needs to process these sets of variables. Giving me something like
AUTO1521884581000001
AUTO1521884581000002
AUTO1521884581000003
AUTO1521884581000004
AUTO1521884581000005
AUTO1521884592000001
AUTO1521884592000002
AUTO1521884592000003
AUTO1521884592000004
AUTO1521884592000005
As you can see in the script, I tried putting it into the while loop and with different syntax but regretfully, without success. The results I get are always the same set of variables, for all the 15 entries.
Did you really expect that bash (even bash!) would need more than a second to read one line?? Try adding the nanoseconds.
while read name; do
DATE=$(date +%s%N)
PACKAGE_ASSET_ID="AUTO${DATE}000001"
TITLE_ASSET_ID="AUTO${DATE}000002"
MOVIE_ASSET_ID="AUTO${DATE}000003"
PREVIEW_ASSET_ID="AUTO${DATE}000004"
POSTER_ASSET_ID="AUTO${DATE}000005"
echo $PACKAGE_ASSET_ID
echo $TITLE_ASSET_ID
echo $MOVIE_ASSET_ID
echo $PREVIEW_ASSET_ID
echo $POSTER_ASSET_ID
done <names.txt

Evaluate variable in if statement

So I have an array like:
al_ap_version=('ap_version' '[[ $data -ne $version ]]')
And the condition gets evaluated inside a loop like:
for alert in alert_list; do
data=$(tail -1 somefile)
condition=$(eval echo \${$alert[1]})
if eval "$condition" ; then
echo SomeAlert
fi
done
Whilst this generally works with many scenarios, if $data returns something like "-/-" or "4.2.9", I get errors as it doesn't seem to like complex strings in the variable.
Obviously I can't enclose the variable in single quotes as it won't expand so I'm after any ideas to expand the $data variable (or indeed the $version var which suffers the same possible fate) in a way that the evaluation can handle?
Ignoring the fact that eval is probably super dangerous to use here (unless the data in somefile is controlled by you and only you), there are a few issues to fix in your example code.
In your for loop, alert_list needs to be $alert_list.
Also, as pointed out by #choroba, you should be using != instead of -ne since your input isn't always an integer.
Finally, while debugging, you can add set -x to the top of your script, or add -x to the end of your shebang line to enable verbose output (helps to determine how bash is expanding your variables).
This works for me:
#!/bin/bash -x
data=2.2
version=1
al_ap_version=('ap_version' '[[ $data != $version ]]')
alert_list='al_ap_version'
for alert in $alert_list; do
condition=$(eval echo \${$alert[1]})
if eval "$condition"; then
echo "alert"
fi
done
You could try a more functional approach, even though bash is only just barely capable of such things. On the whole, it is usually a lot easier to pack an action to be executed into a bash function and refer to it with the name of the function, than to try to maintain the action as a string to be evaluated.
But first, the use of an array of names of arrays is awkward. Let's get rid of it.
It's not clear to me the point of element 0, ap_version, in the array al_ap_version but I suppose it has something to do with error messages. If the order of alert processing isn't important, you could replace the list of names of arrays with a single associative array:
declare -A alert_list
alert_list[ap_version]=... # see below
alert_list[os_dsk]=...
and then process them with:
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
...
done
Having done that, we can get rid of the eval, with its consequent ugly necessity for juggling quotes, by creating a bash function for each alert:
check_ap_version() {
(($version != $1))
}
Edit: It seems that $1 is not necessarily numeric, so it would be better to use a non-numeric comparison, although exact version match might not be what you're after either. So perhaps it would be better to use:
check_ap_version() {
[[ $version != $1 ]]
}
Note the convention that the first argument of the function is the data value.
Now we can insert the name of the function into the alert array, and call it indirectly in the loop:
declare -A alert_list
alert_list[ap_version]=check_ap_version
alert_list[os_dsk]=check_op_dsk
check_alerts() {
local alert_name alert
local data=$(tail -1 somefile)
for alert_name in ${!alert_list[#]}; do
alert=${alert_list[$alert_name]}
if $alert "$data"; then
signal_alert $alert_name
fi
done
}
If you're prepared to be more disciplined about the function names, you can avoid the associative array, and thereby process the alerts in order. Suppose, for example, that every function has the name check_<alert_name>. Then the above could be:
alert_list=(ap_version os_dsk)
check_alerts() {
local alert_name
local data=$(tail -1 somefile)
for alert_name in $alert_list[#]; do
if check_$alert_name "$data"; then
signal_alert $alert_name
fi
done
}

Iterating over variable name in bash script

I needed to run a script over a bunch of files, which paths were assigned to train1, train2, ... , train20, and I thought 'why not make it automatic with a bash script?'.
So I did something like:
train1=path/to/first/file
train2=path/to/second/file
...
train20=path/to/third/file
for i in {1..20}
do
python something.py train$i
done
which didn't work because train$i echoes train1's name, but not its value.
So I tried unsuccessfully things like $(train$i) or ${train$i} or ${!train$i}.
Does anyone know how to catch the correct value of these variables?
Use an array.
Bash does have variable indirection, so you can say
for varname in train{1..20}
do
python something.py "${!varname}"
done
The ! introduces the indirection, so "get the value of the variable named by the value of varname"
But use an array. You can make the definition very readable:
trains=(
path/to/first/file
path/to/second/file
...
path/to/third/file
)
Note that this array's first index is at position zero, so:
for ((i=0; i<${#trains[#]}; i++)); do
echo "train $i is ${trains[$i]}"
done
or
for idx in "${!trains[#]}"; do
echo "train $idx is ${trains[$idx]}"
done
You can use array:
train[1]=path/to/first/file
train[2]=path/to/second/file
...
train[20]=path/to/third/file
for i in {1..20}
do
python something.py ${train[$i]}
done
Or eval, but it awfull way:
train1=path/to/first/file
train2=path/to/second/file
...
train20=path/to/third/file
for i in {1..20}
do
eval "python something.py $train$i"
done

Resources