Bash Script Variable Scope inside Function - bash

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.

Related

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

Make Variable available outside of function

I had some help previously on generating some Variables from a file which has worked a treat.
The problem I have now is that this procedure is held as a function and the declare is within that function.
Is there a way to use the variable declared outside of the function? I just get a blank row returned
getVARS () {
while IFS== read -r name value; do
if [[ $name == \#* ]] || [[ $name == \-\-* ]]; then
echo $name" ignored"
else
declare -x "$name=$value"
fi
done < /home/admin/file.dat
unset IFS
}
The function above is held in a separate script and called in this way...
. /home/admin/functions
getVARS
echo "$VarA"
The problem is using declare
In man of declare you can read:
When used in a function, declare makes each name local, as with the
local command.
So if you want to use outside of your function you can use:
eval $name=$value
or
export $name=$value
Starting in bash 4.2, you can add the -g option to declare to make the variable global instead of local
getVARS () {
while IFS== read -r name value; do
if [[ $name == \#* ]] || [[ $name == \-\-* ]]; then
echo $name" ignored"
else
declare -gx "$name=$value"
fi
done < /home/admin/file.dat
}
Note that you don't need to unset IFS; the purpose of putting the assignment just prior to read is to localize the change to that command.

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

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

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.

Listing defined functions in Bash

I'm trying to write some code in bash which uses introspection to select the appropriate function to call.
Determining the candidates requires knowing which functions are defined. It's easy to list defined variables in bash using only parameter expansion:
$ prefix_foo="one"
$ prefix_bar="two"
$ echo "${!prefix_*}"
prefix_bar prefix_foo
However, doing this for functions appears to require filtering the output of set -- a much more haphazard approach.
Is there a Right Way?
How about compgen:
compgen -A function # compgen is a shell builtin
$ declare -F
declare -f ::
declare -f _get_longopts
declare -f _longopts_func
declare -f _onexit
...
So, Jed Daniel's alias,
declare -F | cut -d" " -f3
cuts on a space and echos the 3rd field:
$ declare -F | cut -d" " -f3
::
_get_longopts
_longopts_func
_onexit
I have an entry in my .bashrc that says:
alias list='declare -F |cut -d" " -f3'
Which allows me to type list and get a list of functions. When I added it, I probably understood what was happening, but I can't remember to save my life at the moment.
Good luck,
--jed
zsh only (not what was asked for, but all the more generic questions have been closed as a duplicate of this):
typeset -f +
From man zshbuiltins:
-f The names refer to functions rather than parameters.
+ If `+' appears by itself in a separate word as the last
option, then the names of all parameters (functions with -f)
are printed, but the values (function bodies) are not.
Example:
martin#martin ~ % cat test.zsh
#!/bin/zsh
foobar()
{
echo foobar
}
barfoo()
{
echo barfoo
}
typeset -f +
Output:
martin#martin ~ % ./test.zsh
barfoo
foobar
Use the declare builtin to list currently defined functions:
declare -F
This has no issues with IFS nor globbing:
readarray -t funcs < <(declare -F)
printf '%s\n' "${funcs[#]##* }"
Of course, that needs bash 4.0.
For bash since 2.04 use (a little trickier but equivalent):
IFS=$'\n' read -d '' -a funcs < <(declare -F)
If you need that the exit code of this option is zero, use this:
IFS=$'\n' read -d '' -a funcs < <( declare -F && printf '\0' )
It will exit unsuccesful (not 0) if either declare or read fail. (Thanks to #CharlesDuffy)
One (ugly) approach is to grep through the output of set:
set \
| egrep '^[^[:space:]]+ [(][)][[:space:]]*$' \
| sed -r -e 's/ [(][)][[:space:]]*$//'
Better approaches would be welcome.
Pure Bash:
saveIFS="$IFS"
IFS=$'\n'
funcs=($(declare -F)) # create an array
IFS="$saveIFS"
funcs=(${funcs[#]##* }) # keep only what's after the last space
Then, run at the Bash prompt as an example displaying bash-completion functions:
$ for i in ${funcs[#]}; do echo "$i"; done
__ack_filedir
__gvfs_multiple_uris
_a2dismod
. . .
$ echo ${funcs[42]}
_command
This collects a list of function names matching any of a list of patterns:
functions=$(for c in $patterns; do compgen -A function | grep "^$c\$")
The grep limits the output to only exact matches for the patterns.
Check out the bash command type as a better alternative to the following. Thanks to Charles Duffy for the clue.
The following uses that to answer the title question for humans rather than shell scripts: it adds a list of function names matching the given patterns, to the regular which list of shell scripts, to answer, "What code runs when I type a command?"
which() {
for c in "$#"; do
compgen -A function |grep "^$c\$" | while read line; do
echo "shell function $line" 1>&2
done
/usr/bin/which "$c"
done
}
So,
(xkcd)Sandy$ which deactivate
shell function deactivate
(xkcd)Sandy$ which ls
/bin/ls
(xkcd)Sandy$ which .\*run_hook
shell function virtualenvwrapper_run_hook
This is arguably a violation of the Unix "do one thing" philosophy, but I've more than once been desperate because which wasn't finding a command that some package was supposed to contain, me forgetting about shell functions, so I've put this in my .profile.
#!/bin/bash
# list-defined-functions.sh
# Lists functions defined in this script.
#
# Using `compgen -A function`,
# We can save the list of functions defined before running out script,
# the compare that to a new list at the end,
# resulting in the list of newly added functions.
#
# Usage:
# bash list-defined-functions.sh # Run in new shell with no predefined functions
# list-defined-functions.sh # Run in current shell with plenty of predefined functions
#
# Example predefined function
foo() { echo 'y'; }
# Retain original function list
# If this script is run a second time, keep the list from last time
[[ $original_function_list ]] || original_function_list=$(compgen -A function)
# Create some new functions...
myfunc() { echo "myfunc is the best func"; }
function another_func() { echo "another_func is better"; }
function superfunction { echo "hey another way to define functions"; }
# ...
# function goo() { echo ok; }
[[ $new_function_list ]] || new_function_list=$(comm -13 \
<(echo $original_function_list) \
<(compgen -A function))
echo "Original functions were:"
echo "$original_function_list"
echo
echo "New Functions defined in this script:"
echo "$new_function_list"

Resources