1

I have an ever-expanding .zshrc file that was getting out of hand so I decided to try to fix it by breaking it into modular files and sourcing each of them. This works for the most part, but I seem to have some scoping issues related to variables.

Each module file contains a function matching corresponding with the name of the file. For instance: nvm..zshrc contains the function (called by the main .zshrc file) named zshrc_config_load_nvm {}.

Below is a simplified form of the setup I'm using.

The main .zshrc file

Note that I've thoroughly tested this file and everything in it works just fine.

# Load all config files 
# This method basically copied directly from the .oh-my-zsh.sh file.
for config_file ($ZSH_CUSTOM/*.zshrc(N)); do

    # Skip self-inclusion
    if [[ $config_file==".zshrc" || $config_file==".zshenv" ]]; then
        continue
    fi
    
    # The (D) modifier will escape the path so it can be passed to `basename`
    # without quotes. the second param in `basename` removes the file extension.
    local handle=$(basename ${(D)config_file} .zshrc)

    # If the file contains the expected callback function name ...
    # The :t filter will remove the path and file extension.
    if [[ $(command -v "zshrc_config_load_${handle}") ]]; then
        
        # ... Add it to the list of functions to call.
        ZSH_CONFIG_HANDLES+=($handle)
    fi
    # Each config file will call the register function above.
    source $config_file
done

for handle ($ZSH_CONFIG_HANDLES); do

    func="zshrc_config_load_${handle}"

    # Call the function
    $func
done
unset config_file handle func

nvm.zshrc

# This line is copied directly from the documentation. 
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"



# The callback function
function zshrc_config_load_nvm {
    
    # Unable to access the $NVM_DIR variable in this scope. 
    source $NVM_DIR/nvm.sh --fast-reuse
    # Error: /nvm.sh does not exist.
}

The Question

How do I make exported global variables accessible from within the scope of another function, but without passing those values in as arguments?

According to this answer by Gilles 'SO- stop being evil'...

export [in] zsh is shorthand for typeset -gx, where the attribute g means “global” (as opposed to local to a function) and the attribute x means “exported” (i.e. in the environment).

... Therefore I assume that the $NVM_DIR variable should be available from within another function.

1 Answer 1

2

Your functions will see the variable that you have set earlier (and exported or not) provided they haven't been set in a subshell or not declared local to some function that has since returned.

Your problem comes from unrelated mistakes in your code:

for config_file ($ZSH_CUSTOM/*.zshrc(N)); do

    # Skip self-inclusion
    if [[ $config_file==".zshrc" || $config_file==".zshenv" ]]; then
        continue
    fi

[[ somestring ]] returns true if somestring is not the empty string. $config_file==".zshrc" is obviously not the empty string, so that test will return true regardless of the value of $config_file. So your loop won't do anything.

You probably meant something like [[ $config_file = .zsh(rc|env) ]], though that's a bit pointless given that $config_file will never be either .zshrc nor .zshenv as those are hidden files, so not included by default in glob expansions and anyway $config_file also starts with /path/to/zsh_custome/ (and .zshenv doesn't end in .zshrc).

if [[ $config_file -ef $0 ]] continue to check that $config_file is not the script currently being sourced may make more sense.

Other notes:

 export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"

Would better be written as:

if (( $+XDG_CONFIG_HOME )); then
  export NVM_DIR=$XDG_CONFIG_HOME/nvm
else
  export NVM_DIR=~/.nvm
fi

In:

  local handle=$(basename ${(D)config_file} .zshrc)

The D parameter expansion flag (which turns /home/you/file to ~/file for instance) doesn't make sense.

Sounds like you want local handle=$config_file:t:r (root of tail of the file).

  if [[ $(command -v "zshrc_config_load_${handle}") ]];

Is better written as:

if command -v "zshrc_config_load_${handle}" >& /dev/null;

Which avoids the fork. Or possibly if (( $+functions[zshrc_config_load_${handle}] ));.

Instead of doing var=... ... unset var, you can use local in a function or anonymous function:

function {
  local var
  ...
}

If there was a $var from an outer scope (or coming from the environment), that would then leave it alone instead of ending up unsetting it.

2
  • After implementing these changes, I'm still not finding success. But this has helped me realize I have a lot more areas to explore for potential problems – and a lot to learn on this subject. You've pointed out issues that didn't come up in hundreds of searches. So, I have to thank you for that.
    – undefined
    Apr 28, 2022 at 20:59
  • Thanks for this reply! Taught me a bunch. Really appreciate the thorough line by line explanation of shortcomings and possible improvement. I've also been doing things like if [[ $(command ... all the time, and didn't realize how much cleaner a simple if command -v is. Similarly I've been doing [[ ! -z $VAR ]] instead of (( $+VAR )). Appreciate it!
    – Chris
    Jun 7, 2022 at 2:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .