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

8,881

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
Share:
8,881
Hayden Schiff
Author by

Hayden Schiff

Hey there! I do software and data things. You can find info about my other projects on my personal website or my GitHub, or read my incessant tweets over on my Twitter.

Updated on September 18, 2022

Comments

  • Hayden Schiff
    Hayden Schiff over 1 year

    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.

    • Eric Renouf
      Eric Renouf almost 9 years
      Could you invoke the source and checking parts to a subshell so they wouldn't actually affect the parent permanently?
    • Hayden Schiff
      Hayden Schiff almost 9 years
      @EricRenouf I added some example code for clarity; I need some specific variables to persist, but any variable outside of my list of specifically approved variables needs to die.
    • goldilocks
      goldilocks almost 9 years
      This is a quintessential pitfall of using global variables in programming. You should use a more robust mechanism for this, such as parsing values from the output of each subprocess or using temp files to store them.
  • Hayden Schiff
    Hayden Schiff almost 9 years
    It seems that it can only be used for functions; I get an error: local: can only be used in a function
  • yolenoyer
    yolenoyer about 6 years
    The best solution imo, but for those who read this answer, keep in mind that the above func() function will however stay present into the parent scope, and pollute it; to avoid conflicting names, the best solution I know is to give to it an ugly name, something like ___this_main___()
  • jwd
    jwd almost 5 years
    To clean up the function itself, you can use unset -f func just after calling it.