Nested arrays in Bash from a collection of separated array values - arrays

I have multiple projects and one of them is a purposed deployment configuration for setting up multiple repositories, configuring a server structure from files and folders, and finally deploying some services (Docker based).
And with that one project, I want to figure out on how to solve the following situation:
Start with a collection of ( I call this tags)
REPOSITORY_COLLECTION=(
"folder_a:repository_a:branch_a"
"folder_b:repository_b:branch_b"
"folder_c:repository_c:branch_c"
)
folder stands for the source location of where repositories are going to be cloned from Git (I have pairs of repository names separated in different kinds of 'folders')
repository is the name of a repository from Git
branch is the remote branch from Git that is going to be used within the script
So, in order to use the values from within the array, they need to get separated. This is how I've done that:
for collection in ${REPOSITORY_COLLECTION[#]}; do
folder="${collection%%:*}" # only take the first part
result="${collection##"$folder:"}" # remove folder from collection value
repository="${result%%:*}" # only take the second part
branch="${result#"$repository:"}" # only take the third part
# then, add it to new arrays to keep them separated and for the requirement that it is going to be used within the other functionalities of the script
repository_tags+=("$collection")
repository_types+=("$folder")
repositories+=("$repository")
repository_branches+=("$branch")
done
After that, I will need both '$repository_types' and '$repositories' to check if that repository already exists or not. So I need to shape a path; but if I'm not wrong, it will require me to still compare all separated values with the source of $collection "$repository_tags" to check whether the combination of the two is correct. Values are from the same source array, but are randomly chosen according to each other. So:
for tags in ${repository_tags[#]}; do
for repository_type in ${repository_types[#]}; do
# beginning with the source
source_path="/path_to_server/repositories/$repository_type"
if [ -d "$source_path" ]; then
echo && echo "$source_path exist"
else
echo && echo "$source_path doesn't exist, creating it.."
echo "mkdir -p $source_path"
mkdir -p $source_path
fi
for repository in ${repositories[#]}; do
if [[ "$tags" == "$repository_type"* ]] && [[ "$tags" == *"$repository"* ]]; then
# adding the repository value to it, and check if it exists
if [ ! -d "$source_path/$repository" ]; then
# clone from Git
echo "Source '$source_path': Ready to clone $repository"
else
# pull changes from Git
echo "Source '$source_path': Ready to pull changes from $repository"
fi
fi
done
done
done
According to the result when running this:
/path_to_server/repositories/folder_a doesn't exist, creating it..
mkdir -p /path_to_server/repositories/folder_a
Source '/path_to_server/repositories/folder_a': Ready to clone repository_a
/path_to_server/repositories/folder_b doesn't exist, creating it..
mkdir -p /path_to_server/repositories/folder_b
/path_to_server/repositories/folder_c doesn't exist, creating it..
mkdir -p /path_to_server/repositories/folder_c
/path_to_server/repositories/folder_a exist
/path_to_server/repositories/folder_b exist
Source '/path_to_server/repositories/folder_b': Ready to clone repository_b
/path_to_server/repositories/folder_c exist
/path_to_server/repositories/folder_a exist
/path_to_server/repositories/folder_b exist
/path_to_server/repositories/folder_c exist
Source '/path_to_server/repositories/folder_c': Ready to clone repository_c
I like and want the first lines to repeat for every other values such as this;
/path_to_server/repositories/folder_a doesn't exist, creating it..
mkdir -p /path_to_server/repositories/folder_a
Source '/path_to_server/repositories/folder_a': Ready to clone repository_a
Does anyone know what I can do better in this situation, or on how this can be solved?

IFS=: read -r folder repo branch <<< "$collection"
is the simpler way to split a string on colons and assign the fields to variables.
Beyond that, I don't follow what you're doing at all. You may have 1 or more repositories in each folder, and you may have 1 or more branches in each repository. It smells like you need to switch to a language that can do arbitrarily deeply nested data structures.

Related

Is there a way to replace an element in an array with Bash?

I'm trying to create an array and use Homebrew to install apps. But before I install the app I want to check to see if it's installed. I know it's already there in Brew, but I was looking at something like this:
declare -a applications=(Spotify Discord Franz Rectangle visual-studio-code VLC microsoft-excel)
for i in "${applications[#]}"
do
#check for app installer
if [ -d "/Applications/$i.app" ]; then
echo " $i is installed"
appstatus="Installed"
else
echo "/Applications/$i.app"
appstatus=" $i, not installed - installing now"
brew install cask "$i"
fi
echo $appstatus
done`
However what's happening is the array of applications will always fail on VSC and Excel due to the -'s not being in the name in the application folder.
Either I was going to create another array with the correct names underneath - or I was wondering if I can parse the array and remove the -'s for when we check to see if the app is installed.
Hope this makes sense.
To modify your array, replacing dashes with spaces all at once:
applications=( "${applications[#]//-/ }" )
To do it one-at-a-time:
for idx in "${!applications[#]}"; do # iterate over array indices
application=${applications[$idx]} # look up item at index
application=${application//-/ } # transform to new value
applications[$idx]=$application # store new value
done

Using declare for referencing variables from an array in bash

I am trying to loop through an array of directories using a bash script so I can list directories with their timestamp, ownership etc using ls -arlt. I am reviewing bash so would like some feedback.
It works with declare -a for those indirect references but for each directory it outputs and extra directory from the /home/user.
I tried to use declare -n and declare -r for each directory and doesn't work.
#!/bin/bash
# Bash variables
acpi=/etc/acpi
apm=/etc/apm
xml=/etc/xml
array=( acpi apm xml )
# Function to display timestamp, ownership ...
displayInfo()
{
for i in "${array[#]}"; do
declare -n curArray=$i
if [[ -d ${curArray} ]]; then
declare -a _acpi=${curArray[0]} _apm=${curArray[1]} _xml=${curArray[2]}
echo "Displaying folder apci: "
cd $_acpi
ls -alrt
read -p "Press enter to continue"
echo "Displaying folder apm: "
cd $_apm
ls -alrt
read -p "Press enter to continue"
echo "Displaying folder xml: "
cd $_xml
ls -alrt
read -p "Press enter to continue"
else
echo "Displayed Failed" >&2
exit 1
fi
done
}
displayInfo
exit 0
It outputs an extra directory listing the /home/user and don't want that output.
There are a lot of complex and powerful shell features being used here, but in ways that don't fit together or make sense. I'll go over the mistakes in a minute, first let me just give how I'd do it. One thing I will use that you might not be familiar with is indirect variable references with ${!var} -- this is like using a nameref variable, but IMO it's clearer what's going on.
acpi=/etc/acpi
apm=/etc/apm
xml=/etc/xml
array=( acpi apm xml )
displayInfo()
{
for curDirectory in "${array[#]}"; do
if [[ -d ${!curDirectory} ]]; then
echo "Displaying folder $curDirectory:"
ls -alrt "${!curDirectory}"
read -p "Press enter to continue"
else
echo "Error: ${!curDirectory} does not exist or is not a directory" >&2
exit 1
fi
done
}
displayInfo
(One problem with this is that it does the "Press enter to continue" thing after each directory, rather than just between them. This can be fixed, but it's a little more work.)
Ok, now for what went wrong with the original. My main recommendation for you would be to try mentally stepping through your code to see what it's doing. It can help to put set -x before it, so the shell will print its interpretation of what it's doing as it runs, and see how it compares to what you expected. Let's do a short walkthrough of the displayInfo function:
for i in "${array[#]}"; do
This will loop over the contents of array, so on the first pass through the loop i will be set to "acpi". Good so far.
declare -n curArray=$i
This creates a nameref variable pointing to the other variable acpi -- this is similar to what I did with ${! }, and basically reasonable so far. Well, with one exception: the name suggests it's an array, but acpi is a plain variable, not an array.
if [[ -d ${curArray} ]]; then
This checks whether the contents of the acpi variable, "/etc/acpi" is the path of an existing directory (which it is). Still doing good.
declare -a _acpi=${curArray[0]} _apm=${curArray[1]} _xml=${curArray[2]}
Here's where things go completely off the rails. curArray points to the variable acpi, so ${curArray[0]} etc are equivalent to ${acpi[0]} etc. But acpi isn't an array, it's a plain variable, so ${acpi[0]} gets its value, and ${acpi[1]} and ${acpi[2]} get nothing. Furthermore, you're using declare -a (declare arrays), but you're just assigning single values to _acpi, _apm, and _xml. They're declared as arrays, but you're just using them as plain variables (basically the reverse of how you're using curArray -> acpi).
There's a deeper confusion here as well. The for loop above is iterating over "acpi", "apm", and "xml", and we're currently working on "acpi". During this pass through the loop, you should only be working on acpi, not also trying to work on apm and xml. That's the point of having a for loop there.
Ok, that's the main problem here, but let me just point out a couple of other things I'd consider bad practice:
cd $_apm
ls -alrt
Using a variable reference without double-quotes around it like this invites parsing confusion; you should almost always put double-quotes, like cd "$_apm". Also, using cd in a script is dangerous because if it fails the rest of the script will execute in the wrong place. In this case, _apm is empty, so without double-quotes it's equivalent to just cd, which moves to your home directory. This is why you're getting that result. If you used cd "$_apm" it would get an error instead... but since you don't check for that it'll go ahead and still list an irrelevant location.
It's almost always better to avoid cd and its complications entirely, and just use explicit paths, like ls -alrt "$_apm".
echo "Displayed Failed" >&2
exit 1
Do you actually want to exit the entire script if one of the directories doesn't exist? It'd make more sense to me to just return 1 (which exits just the function, not the entire script), or better yet continue (which just goes on to the next iteration of the loop -- i.e. the next directory on the list). I left the exit in my version, but I'd recommend changing it.
One more similar thing:
acpi=/etc/acpi
apm=/etc/apm
xml=/etc/xml
array=( acpi apm xml )
Is there any actual reason to use this array -> variable name -> actual directory path system (and resulting indirect expansion or nameref complications), rather than just having an array of directory paths, like this?
array=( /etc/acpi /etc/apm /etc/xml )
I left the indirection in my version above, but really if there's no reason for it I'd remove the complication.

Adding value to an associative array named after a variable

I need your help with a bash >= 4 script I'm writing.
I am retrieving some files from remote hosts to back them up.
I have a for loop that iterate through the hosts and for each one tests connection and the start a function that retrieves the various files.
My problem is that I need to know what gone wrong (and if), so I am trying to store OK or KO values in an array and parse it later.
This is the code:
...
for remote_host in $hosts ; do
short_host=$(echo "$remote_host" | grep -o '^[^.]\+')
declare -A cluster
printf "INFO: Testing connectivity to %s... " "$remote_host"
if ssh -q "$remote_host" exit ; then
printf "OK!\n"
cluster[$short_host]="Reacheable"
mkdir "$short_host"
echo "INFO: Collecting files ..."
declare -A ${short_host}
objects1="/etc/krb5.conf /etc/passwd /etc/group /etc/fstab /etc/sudoers /etc/shadow"
for obj in ${objects1} ; do
if file_retrieve "$user" "$remote_host" "$obj" ; then
-> ${short_host}=["$obj"]=OK
else
${short_host}=["$obj"]=KO
fi
done
...
So I'm using an array named cluster to list if the nodes were reacheable, and another array - named after the short name of the node - to list OK or KO for single files.
On execution, I got the following error (line 130 is the line I marked with the arrow above):
./test.sh: line 130: ubuntu01=[/etc/krb5.conf]=OK: command not found
I think this is a synthax error for sure, but I can't fix it. I tried a bunch of combinations without success.
Thanks for your help.
Since the array name is contained in a variable short_list, you need eval to make the assignment work:
${short_host}=["$obj"]=OK
Change it to:
eval ${short_host}=["$obj"]=OK
eval ${short_host}=["$obj"]=OK
Similar posts:
Single line while loop updating array

Compare strings inside in the two different directories using array

I don't get the scenario of this given code. All I wanted is to compare the files that is given below. But, in this script nothings happen. I assume that this given code can executed wherever like in /root and it will run. Please check this out.
#!/bin/bash
for file in /var/files/sub/old/*
do
# Strip path from file name
file="${file##*/}"
# Strip everything after the first hyphen
prefix="${file%%-*}-"
# Strip everything before the second-to-last dot
suffix="$(echo $file | awk -F. '{ print "."$(NF-1)"."$NF }')"
# Create new file name from $prefix and $suffix, and any version number
new=$(echo "/var/files/new/${prefix}"*"${suffix}")
# If file exists in the 'new' folder:
if test -f "${new}"
then
# Do string comparison to see if new file is lexicographically "greater than" old
if [[ "${new##*/}" > "${file}" ]]
then
# If so, delete the old version.
rm /var/sub/files/old/"${file}"
else
# 'new' file is NOT newer, delete it instead.
rm "${new}"
fi
fi
done
# Move all new files into the old folder.
mv /var/files/new/* /var/files/sub/old/
Example files inside of each sub- directories ..
/var/files/sub/old/
firefox-24.5.0-1.el5_10.i386.rpm
firefox-24.5.0-1.el5_10.x86_64.rpm
google-1.6.0-openjdk-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
google-1.6.0-openjdk-demo-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
/var/files/new/
firefox-25.5.0-1.el5_10.i386.rpm
firefox-25.5.0-1.el5_10.x86_64.rpm
ie-1.6.0-openjdk-devel-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
ie-1.6.0-openjdk-javadoc-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
ie-1.6.0-openjdk-src-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
google-2.6.0-openjdk-demo-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
In this instance, I want to get the files that are the same. So the files that are the same in the given example are:
firefox-24.5.0-1.el5_10.i386.rpm
firefox-24.5.0-1.el5_10.x86_64.rpm
google-1.6.0-openjdk-demo-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
in the old/ directory and for the new/ directory the equivalents are:
firefox-25.5.0-1.el5_10.i386.rpm
firefox-25.5.0-1.el5_10.x86_64.rpm
google-2.6.0-openjdk-demo-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
The files have similarity for their first characters. It will display in the terminal. After that, there will be another comparing again of the files and the comparison is about which file is more updated one by the number after the name of the file like: firefox-24.5.0-1.el5_10.i386.rpm compared with firefox-25.5.0-1.el5_10.i386.rpm. So in that instance the firefox-24.5.0-1.el5_10.i386.rpm will be replaced by firefox-25.5.0-1.el5_10.i386.rpm because it has a greater value and more updated one and same as other files that are similar. And if the old one is removed and the new will take replacement of it.
So at this moment after the script has been executed the output will be like this.
/var/files/sub/old/
google-1.6.0-openjdk-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
firefox-25.5.0-1.el5_10.i386.rpm
firefox-25.5.0-1.el5_10.x86_64.rpm
ie-1.6.0-openjdk-devel-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
ie-1.6.0-openjdk-javadoc-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
ie-1.6.0-openjdk-src-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
google-2.6.0-openjdk-demo-1.6.0.0-5.1.13.3.el5_10.x86_64.rpm
/var/files/new/
<<empty all files here must to moved to other directory take as a replacement>>
Can anyone help me to make a script for this ? above is just an example. Let's assume that there are lots of files to considered as similar and need to removed and moved.
You can use rpm to get the name of the package without version or architecture strings:
rpm -qi -p /firefox-25.5.0-1.el5_10.i386.rpm
Gives:
Name : firefox
Version : 25.5.0
Release : 1.el5_10
Architecture: i386
....
So you can compare the Names to find related packages.
If the goal here is to have the newrpms directory have only the newest version of each RPM from a combination of sources then you most likely want to simply combine all the files in a single directory and then use the repomanage tool (from the yum-utils package, at least on CentOS) to have it inform you which of the RPMS are old and remove them.
Something like:
repomanage --old combined_rpms_directory | xargs -r rm
As to your initial script
for i in $(\ls -d ./new/*);
do
diff ${i} newrpms/;
rm ${i}
done
You generally don't want to "parse" the output from ls, especially when a glob will do what you want just as easily (for i in ./new/* in this case).
diff ${i} newrpms/ is attempting to diff a file and a directory (or two directories if your ls/glob happened to catch a directory) but in neither case will diff do what you want there. That being said what diff does doesn't really matter because, as Barmar said in his comment
your script is removing them without testing the result of diff
A bash script that does the checking. Here's how it works:
Traverse over each file in the old files directory. Get the prefix (package name with no version, architecture, etc), eg. firefox-; get the suffix (architecture.rpm), eg. .i386.rpm.
Attempt to match prefix and suffix with any version number within the new files directory, ie. firefox-*.i386.rpm. If there is a match, $new will contain the file name, eg. firefox-25.5.0-1.el5_10.i386.rpm; if no match, $new will equal the literal string firefox-*.i386.rpm which is not a file.
Check new files directory for existence of $new.
If it exists, check that $new is indeed newer than the old version. This is done by lexicographical string comparison, ie. firefox-24.5.0-1.el5_10.i386.rpm is less than firefox-25.5.0-1.el5_10.i386.rpm because it comes earlier in the alphabet. Conveniently, sane versioning schemes also happen to be alphabetical. NB: this may fail, for example, when comparing version 2 to version 10.
A new version of a file in the old files directory has been found! In this case, get rid of the old file with rm. If the file in the new directory is not newer, then delete it instead.
Done removing old versions. Old files directory has only files without newer versions.
Move all new files into old directory, leaving newest files in old directory, and new directory empty.
#!/bin/bash
for file in /var/files/sub/old/*
do
# Strip path from file name
file="${file##*/}"
# Strip everything after the first hyphen
prefix="${file%%-*}-"
# Strip everything before the second-to-last dot
suffix="$(echo $file | awk -F. '{ print "."$(NF-1)"."$NF }')"
# Create new file name from $prefix and $suffix, and any version number
new=$(echo "/var/files/new/${prefix}"*"${suffix}")
# If file exists in the 'new' folder:
if test -f "${new}"
then
# Do string comparison to see if new file is lexicographically "greater than" old
if [[ "${new##*/}" > "${file}" ]]
then
# If so, delete the old version.
rm /var/sub/files/old/"${file}"
else
# 'new' file is NOT newer, delete it instead.
rm "${new}"
fi
fi
done
# Move all new files into the old folder.
mv /var/files/new/* /var/files/sub/old/

How to create patch for a new file?

I know to create a patch for an existing file is easy:
diff -aru oldFile newFile 2>&1 | tee myPatch.patch
But what to do, if i want to create a patch for a totally new file? Assume my file is residing in a folder called TestDir. Earlier TestDir did not have a file called entirelyNewfile.c, but now it is having the same.
How to create a patch for entirelyNewfile.c? The idea is, the patch should get properly applied to the specs and generate the RPM build. With BUILD dir having this new file.
Just to add: if i try to take diff between the two directories, one having the new file and the other missing the same, to create the patch, it generates an error saying that file is only present in one folder
Add -N to the diff arguments.
diff /dev/null <newfile>
Will create a patch for your newfile.
The easiest way to do this that I know is to put all the files under version control (if they aren't already). I prefer Git, but something similar could be done in any other version control system:
git init
git add .
git commit -m "initial state"
<do your edits here>
git add .
git commit -m "new state"
git diff HEAD^1

Resources