BASH: Duplicate array and pass array by value - arrays

Suppose I have an array foo that looks like the below, notice the third element.
foo=("a" "b" "c c")
echo "${foo[2]}" # "c c"
echo "${#foo[#]}" # "3"
How might I create an exact duplicate of that array into a variable bar?
And then if you want to pass that array (by value) into a function baz?
Edit: Moved answer to answer.

If you want to pass an array by reference (which is what your code actually does), bash 4.3 allows this to be done with namevars:
foo=( hello "cruel world" )
print_contents() {
local -n array=$1
printf '%q\n' "${array[#]}"
}
print_contents foo
If you want to pass by value, even easier (and functional even with ancient releases):
foo=( hello "cruel world" )
print_contents() {
printf '%q\n' "$#"
}
print_contents "${foo[#]}"
If you want to pass multiple arrays by value, by contrast:
foo=( hello "cruel world" )
bar=( more "stuff here" )
print_arrays() {
while (( $# )); do
printf '%s\n' "Started a new array:"
local -a contents=()
local array_len
array_len=$1; shift
for ((n=0; n<array_len; n++)); do
contents+=( "$1" ); shift
done
printf ' %q\n' "${contents[#]}"
done
}
print_arrays "${#foo[#]}" "${foo[#]}" \
"${#bar[#]}" "${bar[#]}"

I ended up finding my answer as I was writing the question. Here's the train of thought.
Several attempts below
bar=foo # String "foo"
bar=$foo # First element of array foo "a"
bar=${foo[#]}
echo "${#bar[#]}" # "1"
echo "${bar[0]}" # "a b c c"
bar=(${foo[#]})
echo "${#bar[#]}" # "4"
echo "${bar[2]}" # "c"
echo "${bar[3]}" # "c"
bar=("${foo[#]}")
echo "${#bar[#]}" # "3"
echo "${bar[2]}" # "c c"
baz () {
eval "bar=(\"\${$1[#]}\")"
for item in "${bar[#]}"
do
echo "$item"
done
# a
# b
# c c
}

function baz {
set $1[#]
printf '%s\n' "${!1}"
}
foo=(a b 'c c')
bar=("${foo[#]}")
baz bar

Related

Bash Global Array

I want to have a global array, each time I call the function cal on Main, I can add a new element on array arr_var
function cal () {
# Some operation
while true; do
read -p "would you like asignment ? on ${var} " yn
case $yn in
[Yy]* ) arr_var+=$var
[Nn]* ) break;;
* ) echo "Please answer yes or no.";;
esac
# Some operation
done
}
# Main
eval arr_var=()
cal "aa"
cal "bb"
cal "cc"
That I want
printf '%s\n' "${arr_var[#]}"
aa bb cc
but I get
aabbcc
Don't use eval for that...
arr_var=()
or
declare -ag arr_var
eval is evil : Why should eval be avoided in Bash, and what should I use instead?
And you have you answer in the comments :
var=()
var+=(a)
var+=(b)
declare -p var

Pass multiple arrays as arguments to a Bash script?

I've looked, but have only seen answers to one array being passed in a script.
I want to pass multiple arrays to a bash script that assigns them as individual variables as follows:
./myScript.sh ${array1[#]} ${array2[#]} ${array3[#]}
such that: var1=array1 and var2=array2 and var3=array3
I've tried multiple options, but doing variableName=("$#") combines all arrays together into each variable. I hope to have in my bash script a variable that represents each array.
The shell passes a single argument vector (that is to say, a simple C array of strings) off to a program being run. This is an OS-level limitation: There exists no method to pass structured data between two programs (any two programs, written in any language!) in an argument list, except by encoding that structure in the contents of the members of this array of C strings.
Approach: Length Prefixes
If efficiency is a goal (both in terms of ease-of-parsing and amount of space used out of the ARG_MAX limit on command-line and environment storage), one approach to consider is prefixing each array with an argument describing its length.
By providing length arguments, however, you can indicate which sections of that argument list are supposed to be part of a given array:
./myScript \
"${#array1[#]}" "${array1[#]}" \
"${#array2[#]}" "${array2[#]}" \
"${#array3[#]}" "${array3[#]}"
...then, inside the script, you can use the length arguments to split content back into arrays:
#!/usr/bin/env bash
array1=( "${#:2:$1}" ); shift "$(( $1 + 1 ))"
array2=( "${#:2:$1}" ); shift "$(( $1 + 1 ))"
array3=( "${#:2:$1}" ); shift "$(( $1 + 1 ))"
declare -p array1 array2 array3
If run as ./myScript 3 a b c 2 X Y 1 z, this has the output:
declare -a array1='([0]="a" [1]="b" [2]="c")'
declare -a array2='([0]="X" [1]="Y")'
declare -a array3='([0]="z")'
Approach: Per-Argument Array Name Prefixes
Incidentally, a practice common in the Python world (particularly with users of the argparse library) is to allow an argument to be passed more than once to amend to a given array. In shell, this would look like:
./myScript \
"${array1[#]/#/--array1=}" \
"${array2[#]/#/--array2=}" \
"${array3[#]/#/--array3=}"
and then the code to parse it might look like:
#!/usr/bin/env bash
declare -a args array1 array2 array3
while (( $# )); do
case $1 in
--array1=*) array1+=( "${1#*=}" );;
--array2=*) array2+=( "${1#*=}" );;
--array3=*) array3+=( "${1#*=}" );;
*) args+=( "$1" );;
esac
shift
done
Thus, if your original value were array1=( one two three ) array2=( aye bee ) array3=( "hello world" ), the calling convention would be:
./myScript --array1=one --array1=two --array1=three \
--array2=aye --array2=bee \
--array3="hello world"
Approach: NUL-Delimited Streams
Another approach is to pass a filename for each array from which a NUL-delimited list of its contents can be read. One chief advantage of this approach is that the size of array contents does not count against ARG_MAX, the OS-enforced command-line length limit. Moreover, with an operating system where such is available, the below does not create real on-disk files but instead creates /dev/fd-style links to FIFOs written to by subshells writing the contents of each array.
./myScript \
<( (( ${#array1[#]} )) && printf '%s\0' "${array1[#]}") \
<( (( ${#array2[#]} )) && printf '%s\0' "${array2[#]}") \
<( (( ${#array3[#]} )) && printf '%s\0' "${array3[#]}")
...and, to read (with bash 4.4 or newer, providing mapfile -d):
#!/usr/bin/env bash
mapfile -d '' array1 <"$1"
mapfile -d '' array2 <"$2"
mapfile -d '' array3 <"$3"
...or, to support older bash releases:
#!/usr/bin/env bash
declare -a array1 array2 array3
while IFS= read -r -d '' entry; do array1+=( "$entry" ); done <"$1"
while IFS= read -r -d '' entry; do array2+=( "$entry" ); done <"$2"
while IFS= read -r -d '' entry; do array3+=( "$entry" ); done <"$3"
Charles Duffy's response works perfectly well, but I would go about it a different way that makes it simpler to initialize var1, var2 and var3 in your script:
./myScript.sh "${#array1[#]} ${#array2[#]} ${#array3[#]}" \
"${array1[#]}" "${array2[#]}" "${array3[#]}"
Then in myScript.sh
#!/bin/bash
declare -ai lens=($1);
declare -a var1=("${#:2:lens[0]}") var2=("${#:2+lens[0]:lens[1]}") var3=("${#:2+lens[0]+lens[1]:lens[2]}");
Edit: Since Charles has simplified his solution, it is probably a better and more clear solution than mine.
Here is a code sample, which shows how to pass 2 arrays to a function. There is nothing more than in previous answers except it provides a full code example.
This is coded in bash 4.4.12, i.e. after bash 4.3 which would require a different coding approach. One array contains the texts to be colorized, and the other array contains the colors to be used for each of the text elements :
function cecho_multitext () {
# usage : cecho_multitext message_array color_array
# what it does : Multiple Colored-echo.
local -n array_msgs=$1
local -n array_colors=$2
# printf '1: %q\n' "${array_msgs[#]}"
# printf '2: %q\n' "${array_colors[#]}"
local i=0
local coloredstring=""
local normalcoloredstring=""
# check array counts
# echo "msg size : "${#array_msgs[#]}
# echo "col size : "${#array_colors[#]}
[[ "${#array_msgs[#]}" -ne "${#array_colors[#]}" ]] && exit 2
# build the colored string
for msg in "${array_msgs[#]}"
do
color=${array_colors[$i]}
coloredstring="$coloredstring $color $msg "
normalcoloredstring="$normalcoloredstring $msg"
# echo -e "coloredstring ($i): $coloredstring"
i=$((i+1))
done
# DEBUG
# echo -e "colored string : $coloredstring"
# echo -e "normal color string : $normal $normalcoloredstring"
# use either echo or printf as follows :
# echo -e "$coloredstring"
printf '%b\n' "${coloredstring}"
return
}
Calling the function :
#!/bin/bash
green='\E[32m'
cyan='\E[36m'
white='\E[37m'
normal=$(tput sgr0)
declare -a text=("one" "two" "three" )
declare -a color=("$white" "$green" "$cyan")
cecho_multitext text color
Job done :-)
I do prefer using base64 to encode and decode arrays like:
encode_array(){
local array=($#)
echo -n "${array[#]}" | base64
}
decode_array(){
echo -n "$#" | base64 -d
}
some_func(){
local arr1=($(decode_array $1))
local arr2=($(decode_array $2))
local arr3=($(decode_array $3))
echo arr1 has ${#arr1[#]} items, the second item is ${arr1[2]}
echo arr2 has ${#arr2[#]} items, the third item is ${arr2[3]}
echo arr3 has ${#arr3[#]} items, the here the contents ${arr3[#]}
}
a1=(ab cd ef)
a2=(gh ij kl nm)
a3=(op ql)
some_func "$(encode_array "${a1[#]}")" "$(encode_array "${a2[#]}")" "$(encode_array "${a3[#]}")"
The output is
arr1 has 3 items, the second item is cd
arr2 has 4 items, the third item is kl
arr3 has 2 items, the here the contents op ql
Anyway, that will not work with values that have tabs or spaces. If required, we need a more elaborated solution. something like:
encode_array()
{
for item in "$#";
do
echo -n "$item" | base64
done | paste -s -d , -
}
decode_array()
{
local IFS=$'\2'
local -a arr=($(echo "$1" | tr , "\n" |
while read encoded_array_item;
do
echo "$encoded_array_item" | base64 -d;
echo "$IFS"
done))
echo "${arr[*]}";
}
test_arrays_step1()
{
local IFS=$'\2'
local -a arr1=($(decode_array $1))
local -a arr2=($(decode_array $2))
local -a arr3=($(decode_array $3))
unset IFS
echo arr1 has ${#arr1[#]} items, the second item is ${arr1[1]}
echo arr2 has ${#arr2[#]} items, the third item is ${arr2[2]}
echo arr3 has ${#arr3[#]} items, the here the contents ${arr3[#]}
}
test_arrays()
{
local a1_2="$(echo -en "c\td")";
local a1=("a b" "$a1_2" "e f");
local a2=(gh ij kl nm);
local a3=(op ql );
a1_size=${#a1[#])};
resp=$(test_arrays_step1 "$(encode_array "${a1[#]}")" "$(encode_array "${a2[#]}")" "$(encode_array "${a3[#]}")");
echo -e "$resp" | grep arr1 | grep "arr1 has $a1_size, the second item is $a1_2" || echo but it should have only $a1_size items, with the second item as $a1_2
echo "$resp"
}
Based on the answers to this question you could try the following.
Define the arrays as variable on the shell:
array1=(1 2 3)
array2=(3 4 5)
array3=(6 7 8)
Have a script like this:
arg1=("${!1}")
arg2=("${!2}")
arg3=("${!3}")
echo "arg1 array=${arg1[#]}"
echo "arg1 #elem=${#arg1[#]}"
echo "arg2 array=${arg2[#]}"
echo "arg2 #elem=${#arg2[#]}"
echo "arg3 array=${arg3[#]}"
echo "arg3 #elem=${#arg3[#]}"
And call it like this:
. ./test.sh "array1[#]" "array2[#]" "array3[#]"
Note that the script will need to be sourced (. or source) so that it is executed in the current shell environment and not a sub shell.

Update array passed by reference with BASH

I would like to write a function that takes an array variable name and updates the contents. For example:
ARRAY1=("test 1" "test 2" "test 3")
toUpper ARRAY1
for arg in "${ARRAY1[#]}"; do
echo "arg=$arg"
done
# output
arg=TEST 1
arg=TEST 2
arg=TEST 3
I have a crude attempt at doing this which requires a copy of the input array. Using indirect references, I am able to create a copy of the input variable. The copy of the array is used to get the count of the elements. If there is a better way to do this please let me know.
function toUpper() {
local ARRAY_NAME=$1
local ARRAY_REF="$ARRAY_NAME[#]"
# use an indirect reference to copy the array so we can get the count
declare -a ARRAY=("${!ARRAY_REF}")
local COUNT=${#ARRAY[#]}
for ((i=0; i<$COUNT; i++)); do
local VAL="${ARRAY[$i]}"
VAL=$(echo $VAL | tr [:lower:] [:upper:])
echo "ARRAY[$i]=\"$VAL\""
eval "$ARRAY_NAME[$i]=\"$VAL\""
done
}
ARRAY1=( "test" "test 1" "test 3" )
toUpper ARRAY1
echo
echo "Printing array contents"
for arg in "${ARRAY1[#]}"; do
echo "arg=$arg"
done
Using BASH 4.3+ you can do
arr=( "test" "test 1" "test 3" )
toUpper() { declare -n tmp="$1"; printf "%s\n" "${tmp[#]^^}"; }
toUpper arr
TEST
TEST 1
TEST 3
Update: To reflect the changes in original array:
toUpper() {
declare -n tmp="$1";
for ((i=0; i<"${#tmp[#]}"; i++)); do
tmp[i]="${tmp[i]^^}"
done;
}
arr=( "test" "test 1" "test 3" )
toUpper arr
printf "%s\n" "${arr[#]}"
TEST
TEST 1
TEST 3
Update2: Here is a way to make it work in older BASH (prior to 4) versions without eval:
upper() {
len=$2
for ((i=0; i<len; i++)); do
elem="${1}[$i]"
val=$(tr '[:lower:]' '[:upper:]' <<< "${!elem}")
IFS= read -d '' -r "${1}[$i]" < <(printf '%s\0' "$val")
done;
}
arr=( "test" "test 1" "test 3" )
upper arr ${#arr[#]}
printf "%s\n" "${arr[#]}"
TEST
TEST 1
TEST 3
anubhava's answer is ideal for bash 4.3 or newer. To support bash 3, one can use eval (very cautiously, with strings generated with printf %q) to replace the use of namevars, and tr to replace the ${foo^^} expansion for upper case:
toUpper() {
declare -a indexes
local cmd idx item result
printf -v cmd 'indexes=( "${!%q[#]}" )' "$1"; eval "$cmd"
for idx in "${indexes[#]}"; do
printf -v cmd 'item=${%q[%q]}' "$1" "$idx"; eval "$cmd"
result=$(tr '[:lower:]' '[:upper:]' <<<"$item")
printf -v cmd '%q[%q]=%q' "$1" "$idx" "$result"; eval "$cmd"
done
}

How to pass array as an argument to a function in Bash

As we know, in bash programming the way to pass arguments is $1, ..., $N. However, I found it not easy to pass an array as an argument to a function which receives more than one argument. Here is one example:
f(){
x=($1)
y=$2
for i in "${x[#]}"
do
echo $i
done
....
}
a=("jfaldsj jflajds" "LAST")
b=NOEFLDJF
f "${a[#]}" $b
f "${a[*]}" $b
As described, function freceives two arguments: the first is assigned to x which is an array, the second to y.
f can be called in two ways. The first way use the "${a[#]}" as the first argument, and the result is:
jfaldsj
jflajds
The second way use the "${a[*]}" as the first argument, and the result is:
jfaldsj
jflajds
LAST
Neither result is as I wished. So, is there anyone having any idea about how to pass array between functions correctly?
You cannot pass an array, you can only pass its elements (i.e. the expanded array).
#!/bin/bash
function f() {
a=("$#")
((last_idx=${#a[#]} - 1))
b=${a[last_idx]}
unset a[last_idx]
for i in "${a[#]}" ; do
echo "$i"
done
echo "b: $b"
}
x=("one two" "LAST")
b='even more'
f "${x[#]}" "$b"
echo ===============
f "${x[*]}" "$b"
The other possibility would be to pass the array by name:
#!/bin/bash
function f() {
name=$1[#]
b=$2
a=("${!name}")
for i in "${a[#]}" ; do
echo "$i"
done
echo "b: $b"
}
x=("one two" "LAST")
b='even more'
f x "$b"
You can pass an array by name reference to a function in bash (since version 4.3+), by setting the -n attribute:
show_value () # array index
{
local -n myarray=$1
local idx=$2
echo "${myarray[$idx]}"
}
This works for indexed arrays:
$ shadock=(ga bu zo meu)
$ show_value shadock 2
zo
It also works for associative arrays:
$ declare -A days=([monday]=eggs [tuesday]=bread [sunday]=jam)
$ show_value days sunday
jam
See also nameref or declare -n in the man page.
You could pass the "scalar" value first. That would simplify things:
f(){
b=$1
shift
a=("$#")
for i in "${a[#]}"
do
echo $i
done
....
}
a=("jfaldsj jflajds" "LAST")
b=NOEFLDJF
f "$b" "${a[#]}"
At this point, you might as well use the array-ish positional params directly
f(){
b=$1
shift
for i in "$#" # or simply "for i; do"
do
echo $i
done
....
}
f "$b" "${a[#]}"
This will solve the issue of passing array to function:
#!/bin/bash
foo() {
string=$1
array=($#)
echo "array is ${array[#]}"
echo "array is ${array[1]}"
return
}
array=( one two three )
foo ${array[#]}
colors=( red green blue )
foo ${colors[#]}
Try like this
function parseArray {
array=("$#")
for data in "${array[#]}"
do
echo ${data}
done
}
array=("value" "value1")
parseArray "${array[#]}"
Pass the array as a function
array() {
echo "apple pear"
}
printArray() {
local argArray="${1}"
local array=($($argArray)) # where the magic happens. careful of the surrounding brackets.
for arrElement in "${array[#]}"; do
echo "${arrElement}"
done
}
printArray array
Here is an example where I receive 2 bash arrays into a function, as well as additional arguments after them. This pattern can be continued indefinitely for any number of bash arrays and any number of additional arguments, accommodating any input argument order, so long as the length of each bash array comes just before the elements of that array.
Function definition for print_two_arrays_plus_extra_args:
# Print all elements of a bash array.
# General form:
# print_one_array array1
# Example usage:
# print_one_array "${array1[#]}"
print_one_array() {
for element in "$#"; do
printf " %s\n" "$element"
done
}
# Print all elements of two bash arrays, plus two extra args at the end.
# General form (notice length MUST come before the array in order
# to be able to parse the args!):
# print_two_arrays_plus_extra_args array1_len array1 array2_len array2 \
# extra_arg1 extra_arg2
# Example usage:
# print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
# "${#array2[#]}" "${array2[#]}" "hello" "world"
print_two_arrays_plus_extra_args() {
i=1
# Read array1_len into a variable
array1_len="${#:$i:1}"
((i++))
# Read array1 into a new array
array1=("${#:$i:$array1_len}")
((i += $array1_len))
# Read array2_len into a variable
array2_len="${#:$i:1}"
((i++))
# Read array2 into a new array
array2=("${#:$i:$array2_len}")
((i += $array2_len))
# You can now read the extra arguments all at once and gather them into a
# new array like this:
extra_args_array=("${#:$i}")
# OR you can read the extra arguments individually into their own variables
# one-by-one like this
extra_arg1="${#:$i:1}"
((i++))
extra_arg2="${#:$i:1}"
((i++))
# Print the output
echo "array1:"
print_one_array "${array1[#]}"
echo "array2:"
print_one_array "${array2[#]}"
echo "extra_arg1 = $extra_arg1"
echo "extra_arg2 = $extra_arg2"
echo "extra_args_array:"
print_one_array "${extra_args_array[#]}"
}
Example usage:
array1=()
array1+=("one")
array1+=("two")
array1+=("three")
array2=("four" "five" "six" "seven" "eight")
echo "Printing array1 and array2 plus some extra args"
# Note that `"${#array1[#]}"` is the array length (number of elements
# in the array), and `"${array1[#]}"` is the array (all of the elements
# in the array)
print_two_arrays_plus_extra_args "${#array1[#]}" "${array1[#]}" \
"${#array2[#]}" "${array2[#]}" "hello" "world"
Example Output:
Printing array1 and array2 plus some extra args
array1:
one
two
three
array2:
four
five
six
seven
eight
extra_arg1 = hello
extra_arg2 = world
extra_args_array:
hello
world
For further examples and detailed explanations of how this works, see my longer answer on this topic here: Passing arrays as parameters in bash
You can also create a json file with an array, and then parse that json file with jq
For example:
my-array.json:
{
"array": ["item1","item2"]
}
script.sh:
ARRAY=$(jq -r '."array"' $1 | tr -d '[],"')
And then call the script like:
script.sh ./path-to-json/my-array.json

Outputting array with elements containing spaces

I have successfully made a join function, which joins an array to a string using the delimeter:
function join() # Usage: string=$(join "delimeter" "${array[#]}" )
{
local array=( "${#:2}" )
OLD_IFS="$IFS"
IFS="$1"
local string="${array[*]}"
IFS="$OLD_IFS"
echo "$string"
}
I have also tried to make a split function which should do the opposite:
function split() # Usage: array=( $(split "delimeter" "$string") )
{
OLD_IFS="$IFS"
IFS="$1"
local array=( $2 )
IFS="$OLD_IFS"
echo "${array[#]}"
}
But, when I use the split command and the result contains spaces, it will not work as expected.
Example:
array=( "foo" "bar" "baz" "foo bar" )
string=$(join "|" "${array[#]}")
echo $string
array=( $(split "|" "$string") )
for i in ${array[#]}
do
echo $i
done
The last element "foo bar" has been split too.
I think the solution is that you have to do array=( "$(split '|' "$string")" ), but I do not know how to nest the quotes probably.
Note that IFS can be a local variable in a function, so you don't have to backup and restore its value:
join () { local IFS="$1"; local s="${#:2}"; printf "%s" "$s"; }
Here's an implementation of split that requires an eval:
# usage: split varName separator stringToSplit
split () {
local var="$1"
local IFS="$2"
set -- $3
echo "declare -a $var=( $(printf "\"%s\" " "$#") )"
}
Demonstration
$ a=( foo bar "hello world" baz )
$ s=$(join , "${a[#]}")
$ echo $s
foo,bar,hello world,baz
$ split b , "$s"
declare -a b=( "foo" "bar" "hello world" "baz" )
$ eval $(split b , "$s")
$ for x in "${b[#]}"; do echo "$x"; done
foo
bar
hello world
baz
The problem is at the end of your split () function. You serialize array back to string! So it is again string separated by spaces, which does not recognize if the space was within the items or not. So, I know it is not very clean, but you'll have to return the value from split() function using a global variable (omit the keyword local in the example, and simplify the call, and and double quotes to the for command and some other cosmetic changes and it works...:
function split() # Usage: array=( $(split "delimeter" "$string") )
{
OLD_IFS="$IFS"
IFS="$1"
array2=( $2 )
IFS="$OLD_IFS"
}
array=( "foo" "bar" "baz" "foo bar" )
string=$(join "|" "${array[#]}")
echo $string
split "|" "$string"
for i in "${array2[#]}"
do
echo $i
done
Don't know if there is cleaner way, to avoid the global variable, but I doubt.

Resources