Skip to content

Discover unintended variable shadowing in your bash code.

License

Notifications You must be signed in to change notification settings

slowpeek/bash-varr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 

Repository files navigation

Index

What

This is a bash function to catch name clashes early when passing vars to a function by reference i.e. by var name.

Homepage: https://github.com/slowpeek/bash-varr

Why

Lets create a function to find difference between the min and the max values among a list of numbers. The first arg is a var name for the function to store the result to. All the rest are the numbers.

range () {
    [[ $1 == result ]] || local -n result=$1

    local min=$2 max=$2 arg
    shift 2

    for arg; do
        if ((arg < min)); then
            min=$arg
        elif ((arg > max)); then
            max=$arg
        fi
    done

    ((result = max-min))
}

Lets test it with some result var names:

nums=(22 17 3 7 19)

for var in my_result result min max arg i j n; do
    range "$var" "${nums[@]}"
    declare -p "$var"
done

Test output running ./test.sh:

declare -- my_result="19"
declare -- result="19"
./test.sh: line 24: declare: min: not found
./test.sh: line 24: declare: max: not found
./test.sh: line 24: declare: arg: not found
declare -- i="19"
declare -- j="19"
declare -- n="19"

Evidently, there is a problem with some var names. The issue is variable shadowing: when we declare a local var with the same name as an already existing var, the local one takes over it until we leave the function. In range() we used such extra local vars: min, max, arg. Running range() with any of these names as the first arg we encounter shadowing: the result var becomes a reference to a local var instead of the upper level var.

Since the result var name can be anything, there is no easy way to prevent shadowing.

Lets rewrite some parts of the code so that VARR can work with it (for details see Usage):

range () {
    varr "$1"

    [[ $1 == result ]] || {
        local -n result
        result=$1
    }

    local min max arg
    min=$2 max=$2
    shift 2

    for arg; do
        if ((arg < min)); then
            min=$arg
        elif ((arg > max)); then
            max=$arg
        fi
    done

    ((result = max-min))
}

Here is the patch:

 range () {
-    [[ $1 == result ]] || local -n result=$1
+    varr "$1"

-    local min=$2 max=$2 arg
+    [[ $1 == result ]] || {
+        local -n result
+        result=$1
+    }
+
+    local min max arg
+    min=$2 max=$2
     shift 2

     for arg; do

With that VARR would check every local statement to be executed if it contains any protected var names. In such case it would emit an error and terminate the script.

Test output running VARR_ENABLED=y ./test.sh:

declare -- my_result="19"
declare -- result="19"
varr on 13: 'min' could be shadowed; call chain: range

This way we can catch and fix all name clashes while developing the script.

Usage

  • source varr.sh in your script.
  • follow such rules in functions to be protected:
    • use varr command to mark some var names protected from shadowing ahead of any local statements. It accepts multiple names.
    • only use local statements to declare local vars. Feel free to use declare for other purposes. typeset is obsolete, just dont use it.
    • do not assign values in local statements. VARR checks for this and emits an error in the case.
    • only list static var names in local statements. Do not use stuff like local $a or local $(echo a). VARR checks for this and emits an error in the case.
  • run your script with VARR_ENABLED=y env var.

By default VARR is disabled. In the case all it does is declaring a do-nothing stub for varr command. Hence there is no need in removing varr stuff from your production code.

Env vars

VARR_ENABLED
VARR status. Default: n. Enable: y.
VARR_ERROR
exit code in case of errors. Default: 1.

FAQ

Which variable to rename in case of a name clash?
The upper one. Otherwise it is possible to create another clash in another point of the code which calls the same function.

Internals

When enabled VARR does this:

  • enable aliases expansion. varr is an alias to varr_add function. The alias is used to inject essential local vars ahead of the function call.
  • enable functions tracing.
  • set a DEBUG trap to intercept local statements.

There is definitely a performance penalty, do not enable it in production code.

About

Discover unintended variable shadowing in your bash code.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages