BASH array of variable elements: how to print name of variable - arrays

I have a bash array comprised of names of files, say:
foo = "foo.file"
bar = "bar.file"
declare -a foobar=( $foo $bar )
foo and bar are organized in a directory structure as subdirectories:
/maindirectory/foo/foo.file
/maindirectory/bar/bar.file
I would like to loop through the array, generating link targets to the complete path, for example:
maindirectory="/maindirectory/"
target = $maindirectory(something referential to the name of the variable here)$foo
link = foo
ln -s target link
So the goal is to encode the path (and name of the link) as the variable name and the name of the file as the value of the variable.
currently I do this in a harder-to-keep-track-of-way in which I have two arrays declared: one with the path to the file and the other with the names of links. I can loop through:
for ((i=0; i<${#foobarname[#]}; i++)); do
link=$workingdirectory${foobarname[i]};
target=$maindirectory${foobarpath[i]};
ln -s "$target" "$link";
done
but as I said before, I would rather do this with only one array.
Is there a way to encode an array of variable elements, and then spit out the name of the variable element instead of the value of the variable element?

If every file X.file is in folder X, you can use ${X%.file}/$X to find the full path.

First of all, even if we store the file paths to variables, you still need instruct bash which variables to loop through, right? Thus, you need a list of variables.
I am not quite sure this is what you want, but, for example:
#!/bin/bash
maindirectory="/MyFolder"
foo=file1
bar=file2
foobar=file3
barfoo=file4
# need a list so that we can loop thru
namelist="foo bar foobar barfoo"
for link in $namelist; do
file=`eval echo \\$$link`
target=$maindirectory/$link/$file
echo ln -s $target $link
done
It's a test code. When you have done the test, remove echo, so that it does the real task to do a symbolic link.
Or, if your file name is exactly the same as its variable, except for the extension ".file", there is a more simpler version:
#!/bin/bash
maindirectory="/MyFolder"
for link in "foo bar foobar barfoo"; do
target=$maindirectory/${link}/${link}.file
echo ln -s $target $link
done

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.

How to copy an array to a new array with dynamic name?

I have a complex data structure in Bash like this:
Test1_Name="test 1"
Test1_Files=(
file01.txt
file02.txt
)
Test2_Name="test 2"
Test2_Files=(
file11.txt
file12.txt
)
TestNames="Test1 Test2"
In my improved script, I would like read the files from disk. Each test resides in a directory.
So I have a Bash snippet reading directories and reading all the file names. The result is present in an array: $Files[*].
How can I copy that array to an array prefixed with the test's name. Let's assume $TestName holds the test's name.
I know how to create a dynamically named array:
for $Name in $TestNames; do
declare -a "${TestName}_Files"
done
Will create e.g. Test1_Files and Test2_Files. The variables are of type array, because I used -a in declare.
But how can I copy $Files[*] to "${TestName}_Files" in such a loop?
I tried this:
declare -a "${TestName}_Files"=${Files[*]}
But it gives an error that =file01.txt is not a valid identifier.
You'll want to use newarray=( "${oldarray[#]}" ) to keep the array elements intact. ${oldarray[*]} will involve word splitting, which will break at least with elements containing white space.
However, the obvious declare -a "$name"=("${oldarray[#]}") doesn't work, with the parenthesis quoted or not. One way to do it seems to be to use a name ref. This would copy the contents of old to new, where the name of new can be given dynamically:
#!/bin/bash
old=("abc" "white space")
name=new
declare -a "$name" # declare the new array, make 'ref' point to it
declare -n ref="$name"
ref=( "${old[#]}" ) # copy
#unset -n ref # unset the nameref, if required
declare -p "$name" # verify results

How to store a directory's contents inside of an array

I want to be able to store a directory's contents inside of an array. I know that you can use:
#!/bin/bash
declare -A array
for i in Directory; do
array[$i]=$i
done
to store directory contents in an associative array. But if there are subdirectories inside of a directory, I want to be able to store them and their contents inside of the same array. I also tried using:
declare -A arr1
find Directory -print0 | while read -d $'\0' file; do
arr1[$file]=$file
echo "${arr1[$file]}"
done
but this just runs into the problem where the array contents vanish once the while loop ends due to the subshell being discarded from the pipeline (not sure if I'm describing this correctly).
I even tried the following:
for i in $(find Directory/*); do
arr2[$i]="$i"
echo $i
done
but the output is a total disaster for files containing any spaces.
How can I store both a directory and all of its subdirectories (and their subdirectories if need be) inside of a single array?
So you know, you don't need associative arrays. A simpler way to add an element to a regular indexed array is:
array+=("$value")
Your find|while approach is on the right track. As you've surmised, you need to get rid of the pipeline. You can do that with process substitution:
while read -d $'\0' file; do
arr1+=("$file")
done < <(find Directory -print0)
Another way to do this without find is with globbing. If you just want the files under Directory it's as simple as:
array=(Directory/*)
If you want to recurse through all of its subdirectories as well, you can enable globstar and use **:
shopt -s globstar
array=(Directory/**)
The globbing methods are really nice because they automatically handle file names with whitespace and other special characters.

bash scripting: how to store the source code file's name in a variable?

I want the name of the file where the code is written
for example if the script is written in a file called "name.sh"
then i want to have a variable for example
var=name.sh
where var will hold the file's name
but suppose that i don't know the name of the file, how can I do it?
You can use basename.
basename - strip directory and suffix from filenames
var=$(basename $0)
You can just use the bash builtin variable $0 for this.
#!/bin/bash
echo This script file is named `basename $0`
If you want to store it in another variable, you can do:
#!/bin/bash
THESCRIPT=$0
echo This script file is named $THESCRIPT
Looks like you want either $0 or $_, which contain the name of the current script. See here: http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_02.html#sect_03_02_04
You can use $0 which is equivalent argv[0] in some other programming languages.
#!/bin/bash
echo $0
echo $1
If you run the bash script above with $ sh file.sh one two you would get the following output.
test.sh
one
two

Resources