Creating array of objects in bash - bash

Is it possible to create an array of objects in bash?
That's how I'm trying:
declare -a identifications=(
{
email = '...',
password = '...'
}
)
declare -a years=(
'2011'
'2012'
'2013'
'2014'
'2015'
'2016'
)
for identification in "${identifications[#]}"
do
for year in "${years[#]}"
do
my_program --type=CNPJ --format=XLS --identification=${identification.email} --password=${identication.password} --competence=${year} --output="$identification - $year"
done
done
Obviously, this doesn't work, and I'm not finding how to achieve that, since I'm not finding bash objects.

You could do some trickery with associative arrays (introduced in Bash 4.0) and namerefs (see manual for declare and the first paragraph of Shell Parameters โ€“ introduced in Bash 4.3):
#!/usr/bin/env bash
declare -A identification0=(
[email]='test#abc.com'
[password]='admin123'
)
declare -A identification1=(
[email]='test#xyz.org'
[password]='passwd1!'
)
declare -n identification
for identification in ${!identification#}; do
echo "Email: ${identification[email]}"
echo "Password: ${identification[password]}"
done
This prints
Email: test#abc.com
Password: admin123
Email: test#xyz.org
Password: passwd1!
declare -A declares an associative array.
The trick is to assign all your "objects" (associative arrays) variable names starting with the same prefix, like identification. The ${!prefix#} notation expands to all variable names starting with prefix:
$ var1=
$ var2=
$ var3=
$ echo "${!var#}"
var1 var2 var3
Then, to access the key-value pairs of the associative array, we declare the control variable for the for loop with the nameref attribute:
declare -n identification
so that the loop
for identification in ${!identification#}; do
makes identification behave as if it were the actual variable from the expansion of ${!identification#}.
In all likelihood, it'll be easier to do something like the following, though:
emails=('test#abc.com' 'test#xyz.org')
passwords=('admin123' 'passwd1!')
for (( i = 0; i < ${#emails[#]}; ++i )); do
echo "Email: ${emails[i]}"
echo "Password: ${passwords[i]}"
done
I.e., just loop over two arrays containing your information.

I tend to use json to create objects. For me it makes it really easy and flexible.
Here is a oversimplified example.
I create a json file: devices.json
{
"backup" : [{
"addr":"192.168.1.1",
"username":"backuper",
"dstfile":"firewallconfig",
"ext":".cfg",
"method":"ssh",
"rotate":"no",
"enabled":"yes"
}, {
"addr":"192.168.1.2",
"username":"backuper",
"dstfile":"routerconfig",
"ext":".cfg",
"method":"ssh",
"rotate":"no",
"enabled":"yes"
}]
}
Bash script: task.sh
# read the devices.json file and store it in the variable jsonlist
jsonlist=$(jq -r '.backup' "devices.json")
# inside the loop, you cant use the fuction _jq() to get values from each object.
for row in $(echo "${jsonlist}" | jq -r '.[] | #base64'); do
_jq()
{
echo ${row} | base64 --decode | jq -r ${1}
}
echo "backing up: $(_jq '.addr')"
echo "using method: $(_jq '.method')"
Done
The guy who posted the original post can be found by googling "using json with bash" or something.

Related

How do i add whitepsaces to a String while filling it up in a for-loop in Bash?

Have a string as follows:
files="applications/dbt/Dockerfile applications/dbt/cloudbuild.yaml applications/dataform/Dockerfile applications/dataform/cloudbuild.yaml"
Want to extract the first two directories and save it as another string like this:
"applications/dbt applications/dbt applications/dataform pplications/dataform"
But while filling up the second string, its being saved as
applications/dbtapplications/dbtapplications/dataformapplications/dataform
What i tried:
files="applications/dbt/Dockerfile applications/dbt/cloudbuild.yaml applications/dataform/Dockerfile applications/dataform/cloudbuild.yaml"
arr=($files)
#extracting the first two directories and saving it to a new string
for i in ${arr[#]}; do files2+=$(echo "$i" | cut -d/ -f 1-2); done
echo $files2
files2 echoes the following
applications/dbtapplications/dbtapplications/dataformapplications/dataform
Reusing your code as much as possible:
(assuming to only remove the last right part):
arr=( applications/dbt/Dockerfile applications/dbt/cloudbuild.yaml applications/dataform/Dockerfile applications/dataform/cloudbuild.yaml )
#extracting the first two directories and saving it to a new string
for file in "${arr[#]}"; do
files2+="${file%/*} "
done
echo "$files2"
applications/dbt applications/dbt applications/dataform
You could use a for loop as requested
for dir in ${files};
do file2+=$(printf '%s ' "${dir%/*}")
done
which will give output
$ echo "$file2"
applications/dbt applications/dbt applications/dataform applications/dataform
However, it would be much easier with sed
$ sed -E 's~([^/]*/[^/]*)[^ ]*~\1~g' <<< $files
applications/dbt applications/dbt applications/dataform applications/dataform
Convert the string in an array first. Assuming there are no white/blank/newline space embedded in your strings/path name. Something like
#!/usr/bin/env bash
files="applications/dbt/Dockerfile applications/dbt/cloudbuild.yaml applications/dataform/Dockerfile applications/dataform/cloudbuild.yaml"
mapfile -t array <<< "${files// /$'\n'}"
Now check the value of the array
declare -p array
Output
declare -a array=([0]="applications/dbt/Dockerfile" [1]="applications/dbt/cloudbuild.yaml" [2]="applications/dataform/Dockerfile" [3]="applications/dataform/cloudbuild.yaml")
Remove all the last / from the path name in the array.
new_array=("${array[#]%/*}")
Check the new value
declare -p new_array
Output
declare -a new_array=([0]="applications/dbt" [1]="applications/dbt" [2]="applications/dataform" [3]="applications/dataform")
Now the value is an array, assign it to a variable or do what ever you like with it. Like what was mentioned in the comment section. Use an array from the start.
Assign the first 2 directories/path in a variable (weird requirement)
new_var="${new_array[#]::2}"
declare -p new_var
Output
declare -- new_var="applications/dbt applications/dbt"

Bash: call array item by string

How can I access array elements by string?
What I want to achieve is the following: I have a number of json files in a directory. These should be read in one after the other. All key values โ€‹โ€‹("id", "test") should be configured / replaced. The changes should be saved in the json files.
#a json file example:
{
"id": "<fill-id>",
"test": "<fill-test>"
}
The first key "id" should call the function "create_id".
The second key "test" should call the function "create_test".
#!/bin/sh
declare -a FUNCTION
FUNCTION["id"]="create_id"
FUNCTION["test"]="create_test"
FUNCTION["secret"]="FUNCTION.C"
# read key/value from json file
for filename in *.json; do
while read -r key value; do
declare "$key=$value"
echo name: $key value: $value
${FUNCTION[$key]} $filename $value
done < <(jq -r 'to_entries[] | "\(.key) \(.value)"' $filename)
done
You'll need to use an associative array, declare -A FUNCTION
BUT associative arrays are not a sh feature: declare -A is bash syntax. Change your #! line.
You'll also need bash version 4.0 or greater: the default /bin/bash on MacOSX is too old.
You can also find associative arrays in ksh and zsh.

Adding to Bash associative arrays inside functions

I'm trying to use associative arrays as a work around for Bash's poor function parameter passing. I can declare a global associative array and read/write to that but I would like to have the variable name passed to the function since many times I want to use the same function with different parameter blocks.
Various Stack Overflow posts have approaches for reading a passed array within a function but not writing to it to allow return values. Pseudo Bash for what I'm trying to do is thus:
TestFunc() {
local __PARMBLOCK__=${1} # Tried ${!1} as well
# Do something with incoming array
__PARMBLOCK__[__rc__]+=1 # Error occured
__PARMBLOCK__[__error__]+="Error in TestFunc"
}
declare -A FUNCPARM
# Populate FUNCPARM
TestFunc FUNCPARM
if [[ ${FUNCPARM[__rc__]} -ne 0 ]]; then
echo "ERROR : ${FUNCPARM[__error__]}
fi
Is this kind of thing possible or do I really need to abandon Bash for something like Python?
EDIT: Found the duplicate. This is basically the same answer as this one.
You can use a reference variable for that, see help declare:
declare [-aAfFgilnrtux] [-p] [name[=value] ...]
[...]
-n make NAME a reference to the variable named by its value
[...]
When used in a function, declare makes NAMEs local, as with the local command.
f() {
declare -n paramblock="$1"
# example for reading (print all keys and entries)
paste <(printf %s\\n "${!paramblock[#]}") <(printf %s\\n "${paramblock[#]}")
# example for writing
paramblock["key 1"]="changed"
paramblock["new key"]="new output"
}
Example usage:
$ declare -A a=(["key 1"]="input 1" ["key 2"]="input 2")
$ f a
key 2 input 2
key 1 input 1
$ declare -p a
declare -A a=(["key 2"]="input 2" ["key 1"]="changed" ["new key"]="new output" )
This works very well. The only difference to an actual associative array I found so far is, that you cannot print the referenced array using declare -p as that will only show the reference.

How to create a dictionary from a text file in bash?

I want to create a dictionary in bash from a text file which looks like this:
H96400275|A
H96400276|B
H96400265|C
H96400286|D
Basically I want a dictionary like this from this file file.txt:
KEYS VALUES
H96400275 = A
H96400276 = B
H96400265 = C
H96400286 = D
I created following script:
#!/bin/bash
declare -a dictionary
while read line; do
key=$(echo $line | cut -d "|" -f1)
data=$(echo $line | cut -d "|" -f2)
dictionary[$key]="$data"
done < file.txt
echo ${dictionary[H96400275]}
However, this does not print A, rather it prints D. Can you please help ?
Associative arrays (dictionaries in your terms) are declared using -A, not -a. For references to indexed (ones declared with -a) arrays' elements, bash performs arithmetic expansion on the subscript ($key and H96400275 in this case); so you're basically overwriting dictionary[0] over and over, and then asking for its value; thus D is printed.
And to make this script more effective, you can use read in conjunction with a custom IFS to avoid cuts. E.g:
declare -A dict
while IFS='|' read -r key value; do
dict[$key]=$value
done < file
echo "${dict[H96400275]}"
See Bash Reference Manual ยง 6.7 Arrays.
the only problem is that you have to use -A instead of -a
-a Each name is an indexed array variable (see Arrays above).
-A Each name is an **associative** array variable (see Arrays above).
What you want to do is so named associative array. And to declare it you need to use command:
declare -A dictionary

Multidimensional associative arrays in Bash

I'm trying to create a multidimensional associative array but need some help. I have reviewed the page suggested in this SO answer but it confused me even more. So far here is what I have:
The script:
#!/bin/bash
declare -A PERSONS
declare -A PERSON
PERSON["FNAME"]='John'
PERSON["LNAME"]='Andrew'
PERSONS["1"]=${PERSON[#]}
PERSON["FNAME"]='Elen'
PERSON["LNAME"]='Murray'
PERSONS["2"]=${PERSON[#]}
for KEY in "${!PERSONS[#]}"; do
TMP="${PERSONS["$KEY"]}"
echo "$KEY - $TMP"
echo "${TMP["FNAME"]}"
echo "${TMP["LNAME"]}"
done
The output:
1 - John Andrew
John Andrew
John Andrew
2 - Elen Murray
Elen Murray
Elen Murray
As you can see trying to access a specific index of the $TMP array in the for loop returns the whole array.
[Q] What do I need to do in order to separately access the "FNAME" and "LNAME" indexes of the $TMP array inside the for loop?
Thanks.
You can't do what you're trying to do: bash arrays are one-dimensional
$ declare -A PERSONS
$ declare -A PERSON
$ PERSON["FNAME"]='John'
$ PERSON["LNAME"]='Andrew'
$ declare -p PERSON
declare -A PERSON='([FNAME]="John" [LNAME]="Andrew" )'
$ PERSONS[1]=([FNAME]="John" [LNAME]="Andrew" )
bash: PERSONS[1]: cannot assign list to array member
You can fake multidimensionality by composing a suitable array index string:
declare -A PERSONS
declare -A PERSON
PERSON["FNAME"]='John'
PERSON["LNAME"]='Andrew'
i=1
for key in "${!PERSON[#]}"; do
PERSONS[$i,$key]=${PERSON[$key]}
done
PERSON["FNAME"]='Elen'
PERSON["LNAME"]='Murray'
((i++))
for key in "${!PERSON[#]}"; do
PERSONS[$i,$key]=${PERSON[$key]}
done
declare -p PERSONS
# ==> declare -A PERSONS='([1,LNAME]="Andrew" [2,FNAME]="Elen" [1,FNAME]="John" [2,LNAME]="Murray" )'
I understand what you need. I also wanted the same for weeks.
I was confused whether to use Python or Bash.
Finally, exploring something else I found this
Bash: How to assign an associative array to another variable name (e.g. rename the variable)?
Here, I got to know how to assign some string and use it later as command.
Then with my creativity I found solution to your problem as below:-
#!/bin/bash
declare -A PERSONS
declare -A PERSON
PERSON["FNAME"]='John'
PERSON["LNAME"]='Andrew'
string=$(declare -p PERSON)
#printf "${string}\n"
PERSONS["1"]=${string}
#echo ${PERSONS["1"]}
PERSON["FNAME"]='Elen'
PERSON["LNAME"]='Murray'
string=$(declare -p PERSON)
#printf "${string}\n"
PERSONS["2"]=${string}
#echo ${PERSONS["2"]}
for KEY in "${!PERSONS[#]}"; do
printf "$KEY - ${PERSONS["$KEY"]}\n"
eval "${PERSONS["$KEY"]}"
printf "${PERSONS["$KEY"]}\n"
for KEY in "${!PERSON[#]}"; do
printf "INSIDE $KEY - ${PERSON["$KEY"]}\n"
done
done
OUTPUT:-
1 - declare -A PERSON='([FNAME]="John" [LNAME]="Andrew" )'
declare -A PERSON='([FNAME]="John" [LNAME]="Andrew" )'
INSIDE FNAME - John
INSIDE LNAME - Andrew
2 - declare -A PERSON='([FNAME]="Elen" [LNAME]="Murray" )'
declare -A PERSON='([FNAME]="Elen" [LNAME]="Murray" )'
INSIDE FNAME - Elen
INSIDE LNAME - Murray
The problem actually with multi dimensional arrays in bash and specifically in your approach is that you are assigning PERSON array values to the array element PERSONS[1] which is converted to a list and not an assoc array when you assigned it.
And so it no longer will take it as 2 elements of an array as you are not keeping any info about the array data structure in your value.
So, I found this hack to be sufficient with only 1 limitation that you will have to do this each time you want to do store/retrieve values. But it shall solve your purpose.

Resources