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

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

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

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

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

How to pass one argument into multiple arrays

I want my bash script to process one or multiple attachments. They're passed to the script by the following parameter:
--attachment "filename(1),pathtofile(1),fileextension(1);[...];filename(n),pathtofile(n),fileextesion(n)"
--attachment "hello,/dir/hello.pdf,pdf;image,/dir/image.png,png"
This argument is stored in inputAttachments.
How can I store them in the following array?
attachmentFilename=("hello" "image")
attachmentPath=("/dir/hello.pdf" "/dir/image.png")
attachmentType=("pdf" "png")
My thought: I need to seperate inputAttachments at every semicolon and split those parts again at the comma. But how is this done and passed into the array.
I'm not exactly sure, how you are capturing the string after --attachment argument. But once you have it in a shell variable, all you need is a two-pass split which you can accomplish in the bash shell using read and a custom de-limiter to split on by setting the IFS value
IFS=\; read -r -a attachments <<<"hello,/dir/hello.pdf,pdf;image,/dir/image.png,png"
With this the string is split on ; and stored in the array attachments as individual elements which you can visualize by doing declare -p on the array name
declare -p attachments
declare -a attachments='([0]="hello,/dir/hello.pdf,pdf" [1]="image,/dir/image.png,png")'
Now loop over the array and re-split the elements on , and append to individual arrays
for attachment in "${attachments[#]}"; do
IFS=, read -r name path type <<<"$attachment"
attachmentFilename+=( "$name" )
attachmentPath+=( "$path" )
attachmentType+=( "$type" )
done

Iterating over list of arrays in bash

I need to iterate over couple of key-value arrays (associative arrays) in bash. Here's my last attempt:
declare -A ARR1
ARR1[foo]=bar
declare -A ARR2
ARR2[foo]=baz
arrays=(ARR1 ARR2)
for idx in "${arrays[#]}"; do
echo ${${idx}[foo]};
done
which is wrong of course (syntax error), but at this moment, I have no other ideas how to deal with it. Whereas in the following example there are no errors, but the output is just a name of an array.
for idx in "${array[#]}"; do
echo "${idx[foo]}";
done
------- OUTPUT -------
ARR1
ARR2
EDIT
Ok, I was able to do it by using eval:
eval echo \${${idx}[foo]};
However, I read that using eval in bash scripts is not such a good idea. Is there any better way?
Bash 4.3-alpha introduced the nameref attribute, which could be used in this case:
declare -A arr1=([foo]=bar)
declare -A arr2=([foo]=baz)
arrays=(arr1 arr2)
for idx in "${arrays[#]}"; do
declare -n temp="$idx"
echo "${temp[foo]}"
done
gives the output
bar
baz
As pointed out by kojiro in his comment, storing the array names in an array to iterate over is not actually required as long as the names have a shared prefix.
arrays=(arr1 arr2)
for idx in "${arrays[#]}"; do
could be replaced by
for idx in "${!arr#}"; do
Notice that despite the exclamation mark, this has nothing to do with indirect expansion.
Relevant excerpts from the reference manual
Section "Shell Parameters":
A variable can be assigned the nameref attribute using the -n
option to the declare or local builtin commands (see Bash
Builtins)
to create a nameref, or a reference to another variable. This allows
variables to be manipulated indirectly. Whenever the nameref variable
is referenced or assigned to, the operation is actually performed on
the variable specified by the nameref variable's value. A nameref is
commonly used within shell functions to refer to a variable whose name
is passed as an argument to the function. For instance, if a variable
name is passed to a shell function as its first argument, running
declare -n ref=$1
inside the function creates a nameref variable ref whose value is
the variable name passed as the first argument. References and
assignments to ref are treated as references and assignments to the
variable whose name was passed as $1.
If the control variable in a for loop has the nameref attribute, the
list of words can be a list of shell variables, and a name reference
will be established for each word in the list, in turn, when the loop
is executed. Array variables cannot be given the -n attribute.
However, nameref variables can reference array variables and
subscripted array variables. Namerefs can be unset using the -n
option to the unset builtin (see Bourne Shell
Builtins).
Otherwise, if unset is executed with the name of a nameref variable
as an argument, the variable referenced by the nameref variable will
be unset.
Section "Shell Parameter Expansion":
${!prefix*}${!prefix#}
Expands to the names of variables whose names begin with prefix,
separated by the first character of the IFS special variable. When
# is used and the expansion appears within double quotes, each
variable name expands to a separate word.
You can do this like with indirect substitution, but ideally you don't want to use bash for something like this if it's anything but a one-off script.
#!/bin/bash
set -ex
declare -A ARR1
ARR1[foo]=bar
declare -A ARR2
ARR2[foo]=baz
arrays=(ARR1 ARR2)
for idx in "${arrays[#]}"; do
x="${idx}[foo]" # Store as a string in a variable
echo ${!x} # Reference using indirection.
done
See http://mywiki.wooledge.org/BashFAQ/006 for more info.

Storing Bash associative arrays

I want to store (and retrieve, of course) Bash's associative arrays and am looking for a simple way to do that.
I know that it is possible to do it using a look over all keys:
for key in "${!arr[#]}"
do
echo "$key ${arr[$key]}"
done
Retrieving it could also be done in a loop:
declare -A arr
while read key value
do
arr[$key]=$value
done < store
But I also see that set will print a version of the array in this style:
arr=([key1]="value1" [key2]="value2" )
(Unfortunately along with all other shell variables.)
Is there a simpler way for storing and retrieving an associative array than my proposed loop?
To save to a file:
declare -p arr > saved.sh
(You can also use typeset instead of declare if you prefer.)
To load from the file:
source saved.sh

Resources