Why are "declare -f" and "declare -a" needed in bash scripts? - bash

Sorry for the innocent question - I'm just trying to understand...
For example - I have:
$ cat test.sh
#!/bin/bash
declare -f testfunct
testfunct () {
echo "I'm function"
}
testfunct
declare -a testarr
testarr=([1]=arr1 [2]=arr2 [3]=arr3)
echo ${testarr[#]}
And when I run it I get:
$ ./test.sh
I'm function
arr1 arr2 arr3
So here is a question - why do I have to (if I have to ...) insert declare here?
With it - or without it it works the same...
I can understand for example declare -i var or declare -r var. But for what is -f (declare function) and -a (declare array)?

declare -f functionname is used to output the definition of the function functionname, if it exists, and absolutely not to declare that functionname is/will be a function. Look:
$ unset -f a # unsetting the function a, if it existed
$ declare -f a
$ # nothing output and look at the exit code:
$ echo $?
1
$ # that was an "error" because the function didn't exist
$ a() { echo 'Hello, world!'; }
$ declare -f a
a ()
{
echo 'Hello, world!'
}
$ # ok? and look at the exit code:
$ echo $?
0
$ # cool :)
So in your case, declare -f testfunct will do nothing, except possibly if testfunct exists, it will output its definition on stdout.

As far as I know, the -a option alone does not have any practical relevance, but I think it's a plus for readability when declaring arrays. It becomes more interesting when it is combined with other options to generate arrays of a special type.
For example:
# Declare an array of integers
declare -ai int_array
int_array=(1 2 3)
# Setting a string as array value fails
int_array[0]="I am a string"
# Convert array values to lower case (or upper case with -u)
declare -al lowercase_array
lowercase_array[0]="I AM A STRING"
lowercase_array[1]="ANOTHER STRING"
echo "${lowercase_array[0]}"
echo "${lowercase_array[1]}"
# Make a read only array
declare -ar readonly_array=(42 "A String")
# Setting a new value fails
readonly_array[0]=23

declare -f allows you to list all defined functions (or sourced) and their contents.
Example of use:
[ ~]$ cat test.sh
#!/bin/bash
f(){
echo "Hello world"
}
# print 0 if is defined (success)
# print 1 if isn't defined (failure)
isDefined(){
declare -f "$1" >/dev/null && echo 0 || echo 1
}
isDefined f
isDefined g
[ ~]$ ./test.sh
0
1
[ ~]$ declare -f
existFunction ()
{
declare -f "$1" > /dev/null && echo 0 || echo 1
}
f ()
{
echo "Hello world"
}
However as smartly said gniourf_gniourf below : it's better to use declare -F to test the existence of a function.

Related

Bash Script Variable Scope inside Function

THIS CODE ...
#!/usr/local/bin/bash
getDeclared() {
local v
v=$( set -o posix ; set )
IFS=$'\n' read -d '\034' -r -a v <<<"${v}\034"
echo "${v[*]}"
}
declare -a _hints=()
declare _hint="start.hint"
declare _paths
declare _basepath="root/devops/cloud"
declare _base="/"
declare _hintmaxdepth=0
echo "${BASH_VERSION}"
x=$(getDeclared)
x=($x)
for ((y=0;y<${#x[#]};y++)); do
[ "${x[y]::1}" == "_" ] && echo "$y: ${x[y]}"$'\n'
done
HAS THIS OUTPUT:
➜ sometest bash working.sh
5.1.16(1)-release
66: _=posix
67: __CFBundleIdentifier=com.sublimetext.4
68: __CF_USER_TEXT_ENCODING=0x1F6:0x0:0x0
69: _base=/
70: _basepath=root/devops/cloud
71: _hint=start.hint
72: _hintmaxdepth=0
73: _hints=()
SO FAR SO GOOD
However, if I change the function to include the display logic, as seen in ...
THIS CODE ...
#!/usr/local/bin/bash
getDeclared() {
local v
local x
v=$( set -o posix ; set )
IFS=$'\n' read -d '\034' -r -a v <<<"${v}\034"
x=($v)
for ((y=0;y<${#x[#]};y++)); do
[ "${x[y]::1}" == "_" ] && echo "$y: ${x[y]}"$'\n'
done
}
declare -a _hints=()
declare _hint="start.hint"
declare _paths
declare _basepath="root/devops/cloud"
declare _base="/"
declare _hintmaxdepth=0
echo "${BASH_VERSION}"
getDeclared
THIS OUTPUT
➜ sometest bash working1.sh
5.1.16(1)-release
0: BASH=/usr/local/bin/bash
Not what I expected. If I change it to just echo everything (not filtering on the "_"), same result...
THIS CODE ...
#!/usr/local/bin/bash
getDeclared() {
local v
local x
v=$( set -o posix ; set )
IFS=$'\n' read -d '\034' -r -a v <<<"${v}\034"
x=($v)
for ((y=0;y<${#x[#]};y++)); do
echo "$y: ${x[y]}"$'\n'
done
}
declare -a _hints=()
declare _hint="start.hint"
declare _paths
declare _basepath="root/devops/cloud"
declare _base="/"
declare _hintmaxdepth=0
echo "${BASH_VERSION}"
getDeclared
HAS SAME OUTPUT
➜ sometest bash working1.sh
5.1.16(1)-release
0: BASH=/usr/local/bin/bash
Shellcheck just shows one warning on the x=($v) inside the function, saying to quote to prevent splitting. But I don't see any difference when I do x="($v)" it just puts parens around the result as so:
➜ sometest bash working1.sh
5.1.16(1)-release
0: (BASH=/usr/local/bin/bash)
What am I missing here? Help! :)
[Why do I get discrepencies between two code snippets?]
Because $v is only the first array element, so there is only one element inside x. If you want to copy an array, you should have used x=("${v[#]}").
Just -d '' and prefer mapfile.

Bash how to parse asscoiative array from argument?

I am trying to read a string into an associative array.
The string is being properly escaped and read into .sh file:
./myScript.sh "[\"el1\"]=\"val1\" [\"el2\"]=\"val2\""
within the script
#!/bin/bash -e
declare -A myArr=( "${1}" ) #it doesn't work either with or without quotes
All I get is:
line 2: myArr: "${1}": must use subscript when assigning associative array
Googling the error only results in "your array is not properly formatted" results.
You could read key/value pairs from variable inputs in series:
$ cat > example.bash <<'EOF'
#!/usr/bin/env bash
declare -A key_values
while true
do
key_values+=(["$1"]="$2")
shift 2
if [[ $# -eq 0 ]]
then
break
fi
done
for key in "${!key_values[#]}"
do
echo "${key} = ${key_values[$key]}"
done
EOF
$ chmod u+x example.bash
$ ./example.bash el1 val1 el2 val2
el2 = val2
el1 = val1
This should be safe no matter what the keys and values are.
Instead of caring about proper double escaping, set the variables on caller side, and use bash declare -p. That way you will always get properly escaped string.
declare -A temp=([el1]=val1 [el2]=val2)
./script.sh "$(declare -p temp)"
Then do:
# ./script.sh
# a safer version of `eval "$1"`
declare -A myarr="${1#*=}"
solution:
./test.sh "( [\"el1\"]=\"val1\" [\"el2\"]=\"val2\" )"
and within the script:
#!/bin/bash -e
declare -A myArr="${1}"
declare -p myArr
for i in "${!myArr[#]}"
do
echo "key : $i"
echo "value: ${myArr[$i]}"
done
Returns:
> ./test.sh "( [\"el1\"]=\"val1\" [\"el2\"]=\"val2\" )"
declare -A myArr=([el2]="val2" [el1]="val1" )
key : el2
value: val2
key : el1
value: val1
I can't explain why this works, or why the order changes though.
Test it yourself here

BASH: How to echo output from function that edits global variable

In general, how do one echo the output from a function while that function is editing a global variable? What's the best practice here?
I've tried calling the function in a subshell as an argument to echo. But then the global variable is not edited for the parent environment.
declare -a ARR
f () {
ARR+=("$1")
echo "added $1"
}
echo $(f a)
f b
f c
echo "${ARR[#]}"
I expect the output:
added a
added b
added c
a b c
But I'm getting
added a
added b
added c
b c
how do one echo the output from a function while that function is editing a global variable?
You can use a temporary file:
tmp=$(mktemp)
f > "$tmp"
output="$(<"$tmp")"
rm "$output"
echo "$output"
What's the best practice here?
This is subjective and opinion based. Don't grab the output of a function. If you have a function that modifies global state, why not pass the output using some global variable, too?
f() {
declare -g -a ARR
ARR+=("$1")
declare -g f_log
f_log+="added $1"
}
f
echo "$f_log"
Leave the stdout of a function for some logging and error reporting. There is no reason to grab the function output, it's already a function, it can modify the state of the program in any way.
Side note:
I recently tried to interleave stdout of a function for data and output and I did this:
f() {
source /dev/stdin <<<"$1"
shift 1
ARR+=("$1")
declare -p ARR
echo
echo "added $1"
}
tmp=$(f "$(declare -p ARR)" a)
source /dev/stdin < <(<<<"$tmp" sed -n '0,/^$/p')
echo "$(<<<"$tmp" sed '0,/^$/d')"
which actually adds more hassle then it's worth.

Change function arguments in Bash

I'd like to change function arguments before passing them to a next function.
firstfunction() {
# change "-f" to "--format" in arguments
secondfunction "$#"
}
I tried to convert to an array, change the array and convert back to arguments. But it looks so complicated. Is it possible to make it simpler?
UPDATE: to be more specific...
firstfunction data.txt -f "\d+"
should call
secondfunction data.txt --format "\d+"
This is a surprisingly tough problem. Bash is simply not very good at working with (slightly) complex data structures like arrays.
I think the only conceivable robust solution will require a loop. This is the easiest way I can think of:
function firstfunction {
local -A map=(['-f']='--format');
local -a args=();
local arg;
for arg in "$#"; do
if [[ -v map[$arg] ]]; then
args+=("${map[$arg]}");
else
args+=("$arg");
fi;
done;
echo ${args[#]+"${args[#]}"}; ## replace echo with secondfunction to run
};
firstfunction;
##
firstfunction a b;
## a b
firstfunction x -f -fff -f-f -fxy x-f \ -f -f\ -f;
## x --format -fff -f-f -fxy x-f -f -f --format
Using ${args[#]+"${args[#]}"} instead of just "${args[#]}" for the final expansion works around the ill-advised design decision the bash developers made to reject an empty array as an "unbound variable" if you have the nounset set option (set -u) enabled. See Bash empty array expansion with `set -u`.
Alternative:
function firstfunction {
local -A map=(['-f']='--format');
local -a args=("$#");
local -i i;
for ((i = 0; i < ${#args[#]}; ++i)); do
if [[ -v map[${args[i]}] ]]; then
args[i]="${map[${args[i]}]}";
fi;
done;
echo ${args[#]+"${args[#]}"}; ## replace echo with secondfunction to run
};
You can use getopts to reliable parse and process optional arguments like this:
firstfunction() {
OPTIND=1
local arr=()
while getopts "f:x:" opt; do
case $opt in
f) arr+=("--format $OPTARG");;
x) arr+=("--execute $OPTARG");;
esac
done
echo "${arr[#]}"; # or call second function here
}
firstfunction -fabc -x foobar
--format abc --execute foobar
firstfunction -fabc -xfoobar
--format abc --execute foobar
firstfunction -f abc -xfoobar
--format abc --execute foobar

In Bash test if associative array is declared

How can I test if an associative array is declared in Bash? I can test for a variable like:
[ -z $FOO ] && echo nope
but I doesn't seem to work for associative arrays:
$ unset FOO
$ declare -A FOO
$ [ -z $FOO ] && echo nope
nope
$ FOO=([1]=foo)
$ [ -z $FOO ] && echo nope
nope
$ echo ${FOO[#]}
foo
EDIT:
Thank you for your answers, both seem to work so I let the speed decide:
$ cat test1.sh
#!/bin/bash
for i in {1..100000}; do
size=${#array[#]}
[ "$size" -lt 1 ] && :
done
$ time bash test1.sh #best of five
real 0m1.377s
user 0m1.357s
sys 0m0.020s
and the other:
$ cat test2.sh
#!/bin/bash
for i in {1..100000}; do
declare -p FOO >/dev/null 2>&1 && :
done
$ time bash test2.sh #again, the best of five
real 0m2.214s
user 0m1.587s
sys 0m0.617s
EDIT 2:
Let's speed compare Chepner's solution against the previous ones:
#!/bin/bash
for i in {1..100000}; do
[[ -v FOO[#] ]] && :
done
$ time bash test3.sh #again, the best of five
real 0m0.409s
user 0m0.383s
sys 0m0.023s
Well that was fast.
Thanks again, guys.
In bash 4.2 or later, you can use the -v option:
[[ -v FOO[#] ]] && echo "FOO set"
Note that in any version, using
declare -A FOO
doesn't actually create an associative array immediately; it just sets an attribute on the name FOO which allows you to assign to the name as an associative array. The array itself doesn't exist until the first assignment.
You can use declare -p to check if a variable has been declared:
declare -p FOO >/dev/null 2>&1 && echo "exists" || echo "nope"
And to check specifically associative array:
[[ "$(declare -p FOO 2>/dev/null)" == "declare -A"* ]] &&
echo "array exists" || echo "nope"
This is a Community Wiki version of an excellent answer by #user15483624 on a question which is now closed as duplicate. Should that user choose to add their own answer here, this should be deleted in favor of the one with their name on it.
The prior answers on this question should be used only when compatibility with bash 4.x and prior is required. With bash 5.0 and later, an expansion to check variable type is directly available; its use is far more efficient than parsing the output of declare -p, and it avoids some of the unintended side effects of other proposals as well..
The following can be used to test whether a bash variable is an associative array.
[[ ${x#a} = A ]]
${x#a} can be used to test whether it is a variable and an array as well.
$ declare x; echo "${x#a}"
$ declare -a y; echo "${y#a}"
a
$ declare -A z; echo "${z#a}"
A
One of the easiest ways is to the check the size of the array:
size=${#array[#]}
[ "$size" -lt 1 ] && echo "array is empty or undeclared"
You can easily test this on the command line:
$ declare -A ar=( [key1]=val1 [key2]=val2 ); echo "szar: ${#ar[#]}"
szar: 2
This method allow you to test whether the array is declared and empty or undeclared altogether. Both the empty array and undeclared array will return 0 size.

Resources