'+=: Command not found' when trying to add a value to an array - Shell Script - arrays

I am trying to add folder paths to an array. I looked up on the internet and saw this solution. I gave it a try but I get an error message.
My code:
LOCALSITES=()
for d in "$DIRLOC"/*; do
${LOCALSITES}+='foo' #doesnt work
done
echo "${LOCALSITES[*]}"
Error message:
showSites.sh: line 35: +=: command not found

${LOCALSITES}+='foo'
will interpret the current value of that variable, giving you an empty string and therefore making the command read as:
+='foo'
You can see this if you do set -x beforehand:
pax:~$ set -x ; ${xx}+='foo' ; set +x
+ set -x
+ +=foo
+ '[' -x /usr/lib/command-not-found ']'
+ /usr/lib/command-not-found -- +=foo
+=foo: command not found
+ return 127
+ set +x
It's no real different to:
pax:~$ xx=1
pax:~$ ${xx}=2 # should be xx=2
1=2: command not found
To properly append to the array, you need to do:
LOCALSITES+=('foo')
Other things you may want to consider, courtesy of one Gordon Davisson in a comment:
It's probably best to use lower- or mixed-case variable names to avoid conflicts with the many all-caps variables with special functions: localsites+=('foo').
Make sure you select the correct quotes for whether you wish to interpret variables or not: localsites+=('foo') ; localsites+=("${newSite}").
You almost never want [*]. Use [#] instead, and put double-quotes around it: echo "${localsites[#]}".

Related

Why does "echo $array" print all members of the array in this specific case instead of only the first member like in any other case?

I have encountered a very curious problem, while trying to learn bash.
Usually trying to print an echo by simply parsing the variable name like this only outputs the first member Hello.
#!/bin/bash
declare -a test
test[0]="Hello"
test[1]="World"
echo $test # Only prints "Hello"
BUT, for some reason this piece of code prints out ALL members of the given array.
#!/bin/bash
declare -a files
counter=0
for file in "./*"
do
files[$counter]=$file
let $((counter++))
done
echo $files # prints "./file1 ./file2 ./file3" and so on
And I can't seem to wrap my head around it on why it outputs the whole array instead of only the first member. I think it has something to do with my usage of the foreach-loop, but I was unable to find any concrete answer. It's driving me crazy!
Please send help!
When you quoted the pattern, you only created a single entry in your array:
$ declare -p files
declare -a files=([0]="./*")
If you had quoted the parameter expansion, you would see
$ echo "$files"
./*
Without the quotes, the expansion is subject to pathname generation, so echo receives multiple arguments, each of which is printed.
To build the array you expected, drop the quotes around the pattern. The results of pathname generation are not subject to further word-splitting (or recursive pathname generation), so no quotes would be needed.
for file in ./*
do
...
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

Makefile error: build.make:3687: *** missing separator. Stop

I am getting the following error running cmake
/build.make:3687: *** missing separator. Stop.
Line 3687 that has the error:
game_rUnversioned directory_32_OBJECTS = \
What is wrong there?
The immediate answer is, you cannot create variable names that contain whitespace. That's not valid in (newer versions of) make. So this:
game_rUnversioned directory_32_OBJECTS = \
is not a valid variable assignment because of the space (it has nothing to do with the backslash).
The longer answer is that your script ${CMAKE_CURRENT_SOURCE_DIR}/svn_version.sh which is apparently supposed to print the SVN version, is instead printing the string Unversioned directory. You'll have to figure out why that is and get that script to print the right value, or at least ensure that whatever value it prints is a single word and does NOT contain whitespace, before this will work.
ETA:
If you want to make this work in a directory which is not an SVN workspace, you'll need to fix the svn_version.sh script so it can handle the case where it can't find a version. Rewrite that script something like this:
#!/bin/sh
ver=$(svnversion -n -c game | cut -d':' -f2)
case $ver in
(*\ *) echo unknown ;;
(*) echo "$ver" ;;
esac
exit 0
This ensures that if the game directory isn't an SVN directory, it will print a value that doesn't contain any spaces (unknown) and this means your makefiles won't break.
That bare backslash looks very suspicious. That will be interpreted by Make as an attempt to continue that line on the next (physical) line, so what does that line contain?

Parameter Substitution on Left Side of Variable Assignment - BASH and Arrays

I am processing some folders that each represent a page of a book. E.g. "Iliad-001" would be Book=Iliad, Page=001.
I want to iterate through all of the folders, create an array for each book and add an entry to that array for each page that is found, so that I can echo ${Iliad[#]} at the end of my script and it will give me a nice list of all the pages it found.
The catch I'm having is adding values to an array with a dynamic name. Here's the code that I think is intuitive (but clearly not right):
for j in */; do
vol_name=$(basename "$j" | sed 's/\(.*\)-[0-9]*/\1/')
page_name=$(basename "$j" | sed 's/.*-\([0-9]*\)/\1/')
$vol_name+=( "$page_name" )
done
This returns:
syntax error near unexpected token `"$page_name"'
If I change the variable assignment to this $vol_name+="( "$page_name" )" I get a little closer:
Iliad+=( 001 ): command not found
I was able to make it work using eval.
BTW, you do not need to run sed.
#! /bin/bash
for j in */; do
j=$(basename "$j")
vol_name=${j%-*}
page_name=${j#*-}
eval "$vol_name+=('$page_name')"
done
echo ${Iliad[#]}
try this
declare $vol_name+=( "$page_name" )

Resources