Best way to make variables local in a source'd bash script?

I have a bash script that generates a report on the progress of some long-running jobs on the machine. Basically, this parent script loops through a list of child scripts (calling them all with source). The child scripts are expected to set a couple of specific variables, which the parent script will then make use of.

Today, I discovered a bug where, a variable set by the first child script accidentally got used by the second child script, causing incorrect output. Is there a clean way to prevent these types of bugs from happening?

Basically, when I source a child script, there are a couple of specific variables that I want to persist back to the parent script. My parent script resets these specific variables before it sources each new child script, so there are no issues with them. However, some child scripts may have additional arbitrary variables that it uses locally that should not persist back to the parent script.

Obviously I could manually unset each of these at the end of the child script, but these seems prone to error if I forget one. Is there a more proper way of sourcing a script, and having only certain variables persist to the script that called source?

edit: For sake of clarity, here’s a sort of dumbed down version of my parent script:

echo "<html><body><h1>My jobs</h1>"

FILES=~/confs/*.sh
for f in $FILES; do
  # reset variables
  name="Unnamed job"
  secsSinceActive="Unknown"
  statusText="Unknown"

  # run the script that checks on the job
  source "$f"

  # print bit of report
  echo "<h3>$name</h3>"
  echo "<p>Last active: $secsSinceActive seconds ago</p>"
  echo "<p>Status: $statusText</p>"

echo "</body></html>"

And here’s what one of the child scripts might look like:

name="My awesome job"

nowTime=`expr $(date +%s) `
lastActiveTime=`expr $(date +%s -r ~/blah.log)`
secsSinceActive=`expr $nowTime - $lastActiveTime`

currentRaw=$(cat ~/blah.log | grep "Progress" | tail -n 1)
if [ -z "$currentRaw" ]; then
  statusText="Not running"
else
  statusText="In progress"
fi

The variables $name, $secsSinceActive, and $statusText need to persist back to the parent script, but all the other variables should disappear when the child script terminates.

Asked By: Hayden Schiff

||

I believe the syntax for making a variable local in bash is:

local variable_name=

I know this works for functions and am not sure how it works with multiple scripts.

Answered By: DannyK

Wrap the whole script you want to source into a function, add local before the declarations you want to only use in the function, and call the function at the end of the script.

func () {
    local name="My awesome job"

    nowTime=`expr $(date +%s) `
    lastActiveTime=`expr $(date +%s -r ~/blah.log)`
    local secsSinceActive=`expr $nowTime - $lastActiveTime`

    currentRaw=$(cat ~/blah.log | grep "Progress" | tail -n 1)
    if [ -z "$currentRaw" ]; then
      local statusText="Not running"
    else
      local statusText="In progress"
    fi
}
func
Answered By: MichalH

A variation on wrapping your whole script in a function is to let the sourced script source itself through a function:

Let bleh.sh be the source-ing script

#!/bin/bash

echo -e "x1b[34mI am  $(basename ${BASH_SOURCE[0]})x1b[0m"

source blah.sh

echo -e "x1b[34mStill $(basename ${BASH_SOURCE[0]})x1b[0m"

echo -e "x1b[34mA=$Ax1b[0m"
echo -e "x1b[34mB=$Bx1b[0m"

And blah.sh the sourced script:

#!/bin/bash

echo -e "x1b[33mI am  $(basename ${BASH_SOURCE[0]})x1b[0m"

fun() {
    local A
    local HACK_ACTIVATED=yes
    source "$1"
}

(return 0 2>/dev/null) && [ -z "$HACK_ACTIVATED" ] && {
    fun "${BASH_SOURCE[0]}"
    return
}

echo -e "x1b[33mStill $(basename ${BASH_SOURCE[0]})x1b[0m"

A=1
export B=1
echo -e "x1b[33mA=$Ax1b[0m"
echo -e "x1b[33mB=$Bx1b[0m"

When bleh.sh is run, it outputs:

enter image description here

How this works is:

  1. When bleh sources blah.sh, blah.sh sees Ⓐ that it is sourced and Ⓑ that it has not sourced itself yet through fun(), as is evidenced by fun()‘s local HACK_ACTIVATED variable not having been set yet.
  2. blah.sh than invokes fun() and passes the current script to the function.
  3. fun() makes those variables local that you wish to keep local and then recursively sources its containing script (blah.sh) again.

(The reason that I make blah.sh check if it is sourced at all is because, in my case, that script can also run stand-alone.)

Answered By: BigSmoke
Categories: Answers Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.