Bash shellscript array elements are not ordered - bash

I had this code, when I stored hostname to CustomHostName array, I expected it should store in order, it seems not stored correctly when i tried to print out or use that array
declare -A CustHostName
for i in "${UdmPodsList[#]}" ; do
hostname=`kubectl get pods -n $NameSpace -o wide|grep $i |awk '{print $7}'`
CustHostName["$i"]="$hostname"
echo "$hostname"
done
echo "${CustHostName[#]}"
echo $hostname, will print out as in this order
vudmvzcl00-worker-02
vudmvzcl00-worker-03
echo "${CustHostName[#]}", or used it in for loop, it had worker-03 as first element
vudmvzcl00-worker-03 vudmvzcl00-worker-02
I am trying to sort the array but this is not what I wanted to do.
I want the vudmvzcl00-worker-02 to be first element to on the array list when I used.
Thanks.

replace
echo "${CustHostName[#]}"
with
for key in $(sort <<<"${!CustHostName[#]}"); do
echo "${CustHostName[$key]}"
done
explanation
${!CustHostName[#]}" --> get the keys, not the values

I tried this way, it seems working for me.
CustomHostName=()
n=0
for i in "${UdmPodsList[#]}" ; do
((n++))
hostname=`kubectl get pods -n $NameSpace -o wide|grep $i |awk '{print $7}'`
CustHostName[$n]="$hostname"
echo "$hostname"
done
output as I expected. no sorting needed.
echo "${CustHostName[#]}"
vudmvzcl00-worker-02 vudmvzcl00-worker-03
Or print in the array.
for HOST in "${!CustHostName[#]}" ; do
echo "${CustHostName[$HOST]}"
done
output(ArrayPrint):
vudmvzcl00-worker-02
vudmvzcl00-worker-03
Thanks.

Related

How to check if a string is equal to another string based in a substring in Bash

I'm trying to do the following:
I have this file called testing.txt that I want to update each time the ip address or addresses change based on the name (test1ip, test2ip):
127.0.0.1 localhost
somotherrandomip testing
192.168.0.36 test1ip
192.168.0.37 test2ip
This is what I've tried.
#!/bin/bash
array=(
"192.168.0.34 test1ip"
"192.168.0.35 test2ip"
)
for i in "${array[#]}"; do
if ! grep -Fxq "$i" testing.txt
then
echo "ip-name=$i is not present, so adding it in testing.txt file"
echo "$i" >> testing.txt
else
echo "ip-name=$i is present in file, so nothing to do"
fi
done
However, this script appends a completely new line if the line is not found. What I would like to achieve is to overwrite the line if test1ip or test2ip is found but the ip address change.
Expected result:
127.0.0.1 localhost
somotherrandomip testing
192.168.0.34 test1ip
192.168.0.35 test2ip
I've also read this How to check if a string contains a substring in Bash but it seems i can't figure it out.
Any help is greatly appreciated!
The following works on my machine. I changed the array to an associative array, removed the -x option to grep, and used sed to edit the file in place.
#!/bin/bash
#associative array
declare -A array=(
[test1ip]="192.168.0.34"
[test2ip]="192.168.0.35"
)
#Loop over keys of the array
#See parameter expansion in bash manpage
for i in "${!array[#]}"; do
if ! grep -Fq "$i" testing.txt
then
echo "ip-name=$i is not present, so adding it in testing.txt file"
echo "${array[$i]} $i" >> testing.txt
else
echo "ip-name=$i is present in file so running sed"
#Escape sed left hand side
#Adapted for extended regular expressions from https://unix.stackexchange.com/a/129063/309759
i=$(printf '%s\n' "$i" | sed 's:[][\\/.^$*+?(){}|]:\\&:g')
#Excape right hand side
ipaddr=$(printf '%s\n' "${array[$i]}" | sed 's:[\\/&]:\\&:g;$!s/$/\\/')
#Replace old IP vith new IP
sed -Ei "s/[0-9]+(\.[0-9]+){3} +$i/$ipaddr $i/" testing.txt
fi
done
Here's a bash + awk solution that'll do the job efficiently:
#!/bin/bash
array=(
"192.168.0.34 test1ip"
"192.168.0.35 test2ip"
"192.168.0.33 test3ip"
)
awk '
FNR == NR {
aarr[$2] = $1
next
}
! ($2 in aarr)
END {
for (host in aarr)
print aarr[host], host
}
' <(printf '%s\n' "${array[#]}") testing.txt
127.0.0.1 localhost
somotherrandomip testing
192.168.0.33 test3ip
192.168.0.34 test1ip
192.168.0.35 test2ip
notes:
bash's <( commands ) is called process-subtitution. It creates a file containing the output of commands that you can use as argument.
awk's FNR == NR is a condition for selecting the first file argument. In this block I create an associative array that translates a hostname to its new IP address.
! ($2 in aarr) means to print the records for which the hostname is not in the translation array.
The END is for printing the translation array (new IPs of hostnames).

How I prepend a string on each item of an array?

I have the following bash script:
#!/bin/bash
items=('mysql_apache','postgresql_apache','maria_apache')
string=""
for i in "${array[#]}"; do
string=$string" -t"$i
done
echo $string
But if I output the string I won't get the expected result:
-t 'mysql_apache' -t 'postgresql_apache' -t 'maria_apache'
DO you have any Idea how I can do this?
Edit 1
I tried the following:
#!/bin/bash
items=('mysql_apache' 'postgresql_apache' 'maria_apache')
string=""
for i in "${array[#]}"; do
string=$string" -t"$i
done
echo $string
But I still do not get the expected output.
Array elements are separated by whitespace, not commas. Also, items != array.
#! /bin/bash
items=(mysql_apache postgresql_apache maria_apache)
string=""
for i in "${items[#]}"; do
string+=" -t $i"
done
echo $string
But you don't need a loop at all:
items=(mysql_apache postgresql_apache maria_apache)
echo ${items[#]/#/-t }
The substitution can be applied to every element of an array. The /# matches at the start of each string.
You're close. Your forgot to change ${array[#]} in the for loop to what your array was named: items or specifically ${items[#]} You also needed a few other little changes, see below:
#!/bin/bash
declare -a items=('mysql_apache' 'postgresql_apache' 'maria_apache')
string=""
for i in "${items[#]}"; do
string=${string}" -t "$i
done
echo $string
Lastly if you want to see what is happening you can add temporary echo statements to see what if anything is changing:
for i in "${items[#]}"; do
string=${string}" -t "$i
echo >>>$string<<<
done

Using a FOR loop to compare items in a list with items in an ARRAY

Without too much fluff, basically I'm creating an array of IP addresses from a user provided file. Then I have another file with three columns of data and multiple lines, the first column is IP addresses.
What I'm trying to do is loop through the file with 3 columns of data and compare the IP addresses with the values in the arrary, and if a value is present from file in the array, to then print some text as well as the 3rd column from that line of the file.
I have a feeling I'm taking a really wrong approach and making things a lot harder than what they need to be!
Semi-Pseudo code below
#!/bin/bash
scopeFile=$1
data=$2
scopeArray=()
while IFS= read -r line; do
scopeArray+=("$line")
done <$1
for line in $2; do
if [[ $line == scopeArray ]]; then
awk '{print $3 " is in scope!"}' $2;
else
echo "$line is NOT in scope!"
fi;
done
EDIT: Added example files for visulisation for context, data.txt
file is dynamically generated elsewhere but the format is always the same.
scope.txt=$1
192.168.0.14
192.168.0.15
192.168.0.16
data.txt=$2
192.168.0.14 : example.com
192.168.0.15 : foobar.com
192.168.0.19 : test.com
Here is one way of doing what you wanted.
#!/usr/bin/env bash
mapfile -t scopeArray < "$1"
while read -r col1 col2 col3; do
for item in "${!scopeArray[#]}"; do
if [[ $col1 == "${scopeArray[item]}" ]]; then
printf '%s is in scope!\n' "$col3"
unset 'scopeArray[item]' && break
else
printf '%s is not is scope!\n' "$col1" >&2
fi
done
done < "$2"
The shell is not the best if not the right tool for comparing files, but it will get you there slowly but surely.
mapfile is a bash4+ feature jyfi.

Read a config file in BASH without using "source"

I'm attempting to read a config file that is formatted as follows:
USER = username
TARGET = arrows
I realize that if I got rid of the spaces, I could simply source the config file, but for security reasons I'm trying to avoid that. I know there is a way to read the config file line by line. I think the process is something like:
Read lines into an array
Filter out all of the lines that start with #
search for the variable names in the array
After that I'm lost. Any and all help would be greatly appreciated. I've tried something like this with no success:
backup2.config>cat ~/1
grep '^[^#].*' | while read one two;do
echo $two
done
I pulled that from a forum post I found, just not sure how to modify it to fit my needs since I'm so new to shell scripting.
http://www.linuxquestions.org/questions/programming-9/bash-shell-program-read-a-configuration-file-276852/
Would it be possible to automatically assign a variable by looping through both arrays?
for (( i = 0 ; i < ${#VALUE[#]} ; i++ ))
do
"${NAME[i]}"=VALUE[i]
done
echo $USER
Such that calling $USER would output "username"? The above code isn't working but I know the solution is something similar to that.
The following script iterates over each line in your input file (vars in my case) and does a pattern match against =. If the equal sign is found it will use Parameter Expansion to parse out the variable name from the value. It then stores each part in it's own array, name and value respectively.
#!/bin/bash
i=0
while read line; do
if [[ "$line" =~ ^[^#]*= ]]; then
name[i]=${line%% =*}
value[i]=${line#*= }
((i++))
fi
done < vars
echo "total array elements: ${#name[#]}"
echo "name[0]: ${name[0]}"
echo "value[0]: ${value[0]}"
echo "name[1]: ${name[1]}"
echo "value[1]: ${value[1]}"
echo "name array: ${name[#]}"
echo "value array: ${value[#]}"
Input
$ cat vars
sdf
USER = username
TARGET = arrows
asdf
as23
Output
$ ./varscript
total array elements: 2
name[0]: USER
value[0]: username
name[1]: TARGET
value[1]: arrows
name array: USER TARGET
value array: username arrows
First, USER is a shell environment variable, so it might be better if you used something else. Using lowercase or mixed case variable names is a way to avoid name collisions.
#!/bin/bash
configfile="/path/to/file"
shopt -s extglob
while IFS='= ' read lhs rhs
do
if [[ $lhs != *( )#* ]]
then
# you can test for variables to accept or other conditions here
declare $lhs=$rhs
fi
done < "$configfile"
This sets the vars in your file to the value associated with it.
echo "Username: $USER, Target: $TARGET"
would output
Username: username, Target: arrows
Another way to do this using keys and values is with an associative array:
Add this line before the while loop:
declare -A settings
Remove the declare line inside the while loop and replace it with:
settings[$lhs]=$rhs
Then:
# set keys
user=USER
target=TARGET
# access values
echo "Username: ${settings[$user]}, Target: ${settings[$target]}"
would output
Username: username, Target: arrows
I have a script which only takes a very limited number of settings, and processes them one at a time, so I've adapted SiegeX's answer to whitelist the settings I care about and act on them as it comes to them.
I've also removed the requirement for spaces around the = in favour of ignoring any that exist using the trim function from another answer.
function trim()
{
local var=$1;
var="${var#"${var%%[![:space:]]*}"}"; # remove leading whitespace characters
var="${var%"${var##*[![:space:]]}"}"; # remove trailing whitespace characters
echo -n "$var";
}
while read line; do
if [[ "$line" =~ ^[^#]*= ]]; then
setting_name=$(trim "${line%%=*}");
setting_value=$(trim "${line#*=}");
case "$setting_name" in
max_foos)
prune_foos $setting_value;
;;
max_bars)
prune_bars $setting_value;
;;
*)
echo "Unrecognised setting: $setting_name";
;;
esac;
fi
done <"$config_file";
Thanks SiegeX. I think the later updates you mentioned does not reflect in this URL.
I had to edit the regex to remove the quotes to get it working. With quotes, array returned is empty.
i=0
while read line; do
if [[ "$line" =~ ^[^#]*= ]]; then
name[i]=${line%% =*}
value[i]=${line##*= }
((i++))
fi
done < vars
A still better version is .
i=0
while read line; do
if [[ "$line" =~ ^[^#]*= ]]; then
name[i]=`echo $line | cut -d'=' -f 1`
value[i]=`echo $line | cut -d'=' -f 2`
((i++))
fi
done < vars
The first version is seen to have issues if there is no space before and after "=" in the config file. Also if the value is missing, i see that the name and value are populated as same. The second version does not have any of these. In addition it trims out unwanted leading and trailing spaces.
This version reads values that can have = within it. Earlier version splits at first occurance of =.
i=0
while read line; do
if [[ "$line" =~ ^[^#]*= ]]; then
name[i]=`echo $line | cut -d'=' -f 1`
value[i]=`echo $line | cut -d'=' -f 2-`
((i++))
fi
done < vars

Loading variables from a text file into bash script

Is it possible to load new lines from a text file to variables in bash?
Text file looks like?
EXAMPLEfoo
EXAMPLEbar
EXAMPLE1
EXAMPLE2
EXAMPLE3
EXAMPLE4
Variables become
$1 = EXAMPLEfoo
$2 = EXAMPLEbar
ans so on?
$ s=$(<file)
$ set -- $s
$ echo $1
EXAMPLEfoo
$ echo $2
EXAMPLEbar
$ echo $#
EXAMPLEfoo EXAMPLEbar EXAMPLE1 EXAMPLE2 EXAMPLE3 EXAMPLE4
I would improve the above by getting rid of temporary variable s:
$ set -- $(<file)
And if you have as input a file like this
variable1 = value
variable2 = value
You can use following construct to get named variables.
input=`cat filename|grep -v "^#"|grep "\c"`
set -- $input
while [ $1 ]
do
eval $1=$3
shift 3
done
cat somefile.txt| xargs bash_command.sh
bash_command.sh will receive these lines as arguments
saveIFS="$IFS"
IFS=$'\n'
array=($(<file))
IFS="$saveIFS"
echo ${array[0]} # output: EXAMPLEfoo
echo ${array[1]} # output: EXAMPLEbar
for i in "${array[#]}"; do echo "$i"; done # iterate over the array
Edit:
The loop in your pastebin has a few problems. Here it is as you've posted it:
for i in "${array[#]}"; do echo " "AD"$count = "$i""; $((count=count+1)); done
Here it is as it should be:
for i in "${array[#]}"; do declare AD$count="$i"; ((count=count+1)); done
or
for i in "${array[#]}"; do declare AD$count="$i"; ((count++)); done
But why not use the array directly? You could call it AD instead of array and instead of accessing a variable called "AD4" you'd access an array element "${AD[4]}".
echo "${AD[4]}"
if [[ ${AD[9]} == "EXAMPLE value" ]]; then do_something; fi
This can be done be with an array if you don't require these variables as inputs to a script. push() function lifted from the Advanced Scripting Guide
push() # Push item on stack.
{
if [ -z "$1" ] # Nothing to push?
then
return
fi
let "SP += 1" # Bump stack pointer.
stack[$SP]=$1
return
}
The contents of /tmp/test
[root#x~]# cat /tmp/test
EXAMPLEfoo
EXAMPLEbar
EXAMPLE1
EXAMPLE2
EXAMPLE3
EXAMPLE4
SP=0; for i in `cat /tmp/test`; do push $i ; done
Then
[root#x~]# echo ${stack[3]}
EXAMPLE1
None of the above will work, if your values are quoted with spaces.
However, not everythinf is lost.
Try this:
eval "$(VBoxManage showvminfo "$VMname" --details --machinereadable | egrep "^(name|UUID|CfgFile|VMState)")"
echo "$name {$UUID} $VMState ($VMStateChangeTime) CfgFile=$CfgFile"
P.S.
Nothing will ever work, if your names are quoted or contain dashes.
If you have something like that, as is the case with VBoxManage output ("IDE-1-0"="emptydrive" and so on), either egrep only specific values, as shown in my example, or silence the errors.
However, silencing erors is always dangerous. You never know, when the next value will have unquoted "*" in it, thus you must treat values loaded this way very careful, with all due precaution.

Resources